Project

General

Profile

New Model #10478 » uvk5.py

chirp driver for UV-K5, version 20230523 - Jacek Lipkowski SQ5BPF, 05/23/2023 05:52 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
import os
35
from chirp import chirp_common, directory, bitwise, memmap, errors, util
36
from chirp.settings import RadioSetting, RadioSettingGroup, \
37
        RadioSettingValueBoolean, RadioSettingValueList, \
38
        RadioSettingValueInteger, RadioSettingValueString, \
39
        RadioSettingValueFloat, RadioSettingValueMap, RadioSettings
40

    
41
LOG = logging.getLogger(__name__)
42

    
43
# Show the obfuscated version of commands. Not needed normally, but 
44
# might be useful for someone who is debugging a similar radio
45
DEBUG_SHOW_OBFUSCATED_COMMANDS=False
46

    
47
# Show the memory being written/received. Not needed normally, because
48
# this is the same information as in the packet hexdumps, but
49
# might be useful for someone debugging some obscure memory issue
50
DEBUG_SHOW_MEMORY_ACTIONS=False
51

    
52
DRIVER_VERSION="Quansheng UV-K5 driver v20230523 (c) Jacek Lipkowski SQ5BPF"
53
PRINT_CONSOLE=False
54

    
55
MEM_FORMAT = """
56
#seekto 0x0000;
57
struct {
58
  ul32 freq;
59
  ul32 offset;
60
  u8 rxcode;
61
  u8 txcode;
62
  u8 code_flag;
63
  u8 flags1;
64
  u8 flags2;
65
  u8 dtmf_flags;
66
  u8 step;
67
  u8 scrambler;
68
} channel[214];
69

    
70
#seekto 0x640;
71
ul16 fmfreq[20];
72

    
73
#seekto 0xe70;
74
u8 call_channel;
75
u8 squelch;
76
u8 max_talk_time;
77
u8 noaa_autoscan;
78
u8 unknown1;
79
u8 unknown2;
80
u8 vox_level;
81
u8 mic_gain;
82
u8 unknown3;
83
u8 channel_display_mode;
84
u8 crossband;
85
u8 battery_save;
86
u8 dual_watch;
87
u8 tail_note_elimination;
88
u8 vfo_open;
89

    
90
#seekto 0xe90;
91
u8 beep_control;
92
#seekto 0xe95;
93
u8 scan_resume_mode;
94
u8 auto_keypad_lock;
95
u8 power_on_dispmode;
96
u8 password[4];
97

    
98
#seekto 0xea0;
99
u8 keypad_tone;
100
u8 language;
101

    
102
#seekto 0xea8;
103
u8 alarm_mode;
104
u8 reminding_of_end_talk;
105
u8 repeater_tail_elimination;
106

    
107
#seekto 0xeb0;
108
char logo_line1[16];
109
char logo_line2[16];
110

    
111
#seekto 0xf40;
112
u8 int_flock;
113
u8 int_350tx;
114
u8 int_unknown1;
115
u8 int_200tx;
116
u8 int_500tx;
117
u8 int_350en;
118
u8 int_screen;
119

    
120
#seekto 0xf50;
121
struct {
122
char name[16];
123
} channelname[200];
124

    
125
"""
126
#flags1
127
FLAGS1_OFFSET=0b1
128
FLAGS1_ISSCANLIST=0b100
129
FLAGS1_ISAM=0b10000
130

    
131
#flags2
132
FLAGS2_BCLO=0b10000
133
FLAGS2_POWER_MASK=0b1100
134
FLAGS2_POWER_HIGH=0b1000
135
FLAGS2_POWER_MEDIUM=0b0100
136
FLAGS2_POWER_LOW=0b0000
137
FLAGS2_BANDWIDTH=0b10
138
FLAGS2_REVERSE=0b1
139

    
140
#dtmf_flags
141
PTTID_LIST = ["off", "BOT", "EOT", "BOTH"]
142
FLAGS_DTMF_PTTID_MASK=0b110 # PTTID: 00 - disabled, 01 - BOT, 10 - EOT, 11 - BOTH
143
FLAGS_DTMF_PTTID_DISABLED=0b000
144
FLAGS_DTMF_PTTID_BOT=0b010
145
FLAGS_DTMF_PTTID_EOT=0b100
146
FLAGS_DTMF_PTTID_BOTH=0b110
147
FLAGS_DTMF_DECODE=0b1
148

    
149
#power
150
UVK5_POWER_LEVELS = [ chirp_common.PowerLevel("Low",  watts=1.50),
151
                     chirp_common.PowerLevel("Med",  watts=3.00),
152
                     chirp_common.PowerLevel("High", watts=5.00),
153
                     ]
154

    
155
#scrambler
156
SCRAMBLER_LIST = [ "off", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10" ]
157

    
158
#channel display mode
159
CHANNELDISP_LIST = [ "Frequency", "Channel No", "Channel Name" ]
160
#battery save
161
BATSAVE_LIST = [ "OFF", "1:1", "1:2", "1:3", "1:4" ]
162

    
163
#Crossband receiving/transmitting
164
CROSSBAND_LIST = [ "Off", "Band A", "Band B" ]
165
DUALWATCH_LIST= CROSSBAND_LIST
166

    
167
#steps
168
STEPS = [ 2.5, 5.0, 6.25, 10.0, 12.5, 25.0 , 8.33 ]
169

    
170
#ctcss/dcs codes
171
TMODES = ["", "Tone", "DTCS", "DTCS"]
172
TONE_NONE=0
173
TONE_CTCSS=1
174
TONE_DCS=2
175
TONE_RDCS=3
176

    
177
#DTCS_CODES = sorted(chirp_common.DTCS_CODES)
178
CTCSS_TONES = [
179
        67.0, 69.3, 71.9, 74.4, 77.0, 79.7, 82.5, 85.4,
180
        88.5, 91.5, 94.8, 97.4, 100.0, 103.5, 107.2, 110.9,
181
        114.8, 118.8, 123.0, 127.3, 131.8, 136.5, 141.3, 146.2,
182
        151.4, 156.7, 159.8, 162.2, 165.5, 167.9, 171.3, 173.8,
183
        177.3, 179.9, 183.5, 186.2, 189.9, 192.8, 196.6, 199.5,
184
        203.5, 206.5, 210.7, 218.1, 225.7, 229.1, 233.6, 241.8,
185
        250.3, 254.1
186
        ]
187

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

    
202
FLOCK_LIST=[ "Off", "FCC", "CE", "GB", "430", "438" ]
203
SCANRESUME_LIST = [ "TO: Resume after 5 seconds", "CO: Resume after signal dissapears", "SE: Stop scanning after receiving a signal" ]
204
WELCOME_LIST = [ "Full Screen", "Welcome Info", "Voltage" ]
205
KEYPADTONE_LIST = [ "Off", "Chinese", "English" ]
206
LANGUAGE_LIST = [ "Chinese", "English" ]
207
ALARMMODE_LIST = [ "SITE", "TONE" ]
208
REMENDOFTALK_LIST = [ "Off" , "ROGER", "MDC" ]
209
RTE_LIST = [ "Off" , "100ms", "200ms", "300ms", "400ms", "500ms", "600ms", "700ms", "800ms", "900ms" ]
210

    
211
MEM_SIZE=0x2000 #size of all memory
212
PROG_SIZE=0x1d00 #size of the memory that we will write
213
MEM_BLOCK=0x80 #largest block of memory that we can reliably write
214

    
215
# the communication is obfuscated using this fine mechanism
216
def xorarr(data: bytes):
217
    tbl=[ 22, 108, 20, 230, 46, 145, 13, 64, 33, 53, 213, 64, 19, 3, 233, 128]
218
    x=b""
219
    r=0
220
    for byte in data:
221
        x+=bytes([byte^tbl[r]])
222
        r=(r+1)%len(tbl)
223
    return x
224

    
225
# if this crc was used for communication to AND from the radio, then it
226
# would be a measure to increase reliability.
227
# but it's only used towards the radio, so it's for further obfuscation
228
def calculate_crc16_xmodem(data: bytes):
229
    poly=0x1021
230
    crc = 0x0
231
    for byte in data:
232
        crc = crc ^ (byte << 8)
233
        for i in range(8):
234
            crc = crc << 1
235
            if (crc & 0x10000):
236
                crc = (crc ^ poly ) & 0xFFFF
237
    return crc & 0xFFFF
238

    
239

    
240
def _send_command(serial,data: bytes):
241
    """Send a command to UV-K5 radio"""
242
    LOG.debug("Sending command (unobfuscated) len=0x%4.4x:\n%s" % (len(data),util.hexprint(data)))
243

    
244
    crc=calculate_crc16_xmodem(data)
245
    data2=data+bytes([crc&0xff,(crc>>8)&0xff])
246

    
247
    command=b"\xAB\xCD"+bytes([len(data)])+b"\x00"+xorarr(data2)+b"\xDC\xBA"
248
    if DEBUG_SHOW_OBFUSCATED_COMMANDS:
249
        LOG.debug("Sending command (obfuscated):\n%s" % util.hexprint(command))
250
    serial.write(command)
251

    
252
def _receive_reply(serial):
253
    headed: bytes
254
    cmd: bytes
255
    footer: bytes
256

    
257
    header=serial.read(4)
258
    if header[0]!=0xAB or header[1]!=0xCD or header[3]!=0x00:
259
        LOG.warning("Bad response header: %s len=%i",(util.hexprint(header),len(header)))
260
        raise errors.RadioError( "Bad response header")
261
 
262
        return False
263

    
264
    cmd=serial.read(int(header[2]))
265
    footer=serial.read(4)
266

    
267
    if footer[2]!=0xDC or footer[3]!=0xBA:
268
        LOG.debug("Reply before bad response footer (obfuscated) len=0x%4.4x:\n%s" % (len(cmd),util.hexprint(cmd)))
269
        LOG.warning("Bad response footer: %s len=%i",(util.hexprint(footer),len(footer)))
270
        raise errors.RadioError( "Bad response footer")
271
        return False
272

    
273
    if DEBUG_SHOW_OBFUSCATED_COMMANDS:
274
        LOG.debug("Received reply (obfuscated) len=0x%4.4x:\n%s" % (len(cmd),util.hexprint(cmd)))
275

    
276
    cmd2=xorarr(cmd)
277
    
278
    LOG.debug("Received reply (unobfuscated) len=0x%4.4x:\n%s" % (len(cmd2),util.hexprint(cmd2)))
279

    
280
    return cmd2
281

    
282

    
283
def _getstring(data: bytes, begin, maxlen):
284
    s=""
285
    c=0
286
    for i in data:
287
        c+=1
288
        if c<begin:
289
            continue
290
        if i<ord(' ') or i>ord('~'):
291
            break
292
        s+=chr(i)
293
    return s
294

    
295

    
296
def _sayhello(serial):
297
    hellopacket=b"\x14\x05\x04\x00\x6a\x39\x57\x64"
298

    
299
    tries=5
300
    while (True):
301
        LOG.debug("Sending hello packet")
302
        _send_command(serial,hellopacket)
303
        o=_receive_reply(serial)
304
        if (o):
305
            break
306
        tries-=1
307
        if tries==0:
308
            LOG.warning("Failed to initialise radio")
309
            raise errors.RadioError( "Failed to initialize radio")
310
            return False
311
    firmware=_getstring(o,5,16)
312
    LOG.info("Found firmware: %s" % firmware)
313
    return firmware
314

    
315
def _readmem(serial,offset,length):
316
    LOG.debug("Sending readmem offset=0x%4.4x len=0x%4.4x" % (offset,length))
317

    
318
    readmem=b"\x1b\x05\x08\x00"+bytes([offset&0xff,(offset>>8)&0xff,length,0])+b"\x6a\x39\x57\x64"
319
    _send_command(serial,readmem)
320
    o=_receive_reply(serial)
321
    if DEBUG_SHOW_MEMORY_ACTIONS:
322
        LOG.debug("readmem Received data len=0x%4.4x:\n%s" % (len(o),util.hexprint(o)))
323
    return o[8:]
324

    
325

    
326
def _writemem(serial,data,offset):
327
    LOG.debug("Sending writemem offset=0x%4.4x len=0x%4.4x" % (offset,len(data)))
328

    
329
    if DEBUG_SHOW_MEMORY_ACTIONS:
330
        LOG.debug("writemem sent data offset=0x%4.4x len=0x%4.4x:\n%s" % (offset,len(data),util.hexprint(data)))
331

    
332
    dlen=len(data)
333
    writemem=b"\x1d\x05"+bytes([dlen+8])+b"\x00"+bytes([offset&0xff,(offset>>8)&0xff,dlen,1])+b"\x6a\x39\x57\x64"+data
334
    _send_command(serial,writemem)
335
    o=_receive_reply(serial)
336

    
337
    LOG.debug("writemem Received data: %s len=%i" % (util.hexprint(o),len(o)))
338

    
339
    if o[0]==0x1e and o[4]==(offset&0xff) and o[5]==(offset>>8)&0xff:
340
        return True
341
    else:
342
        LOG.warning("Bad data from writemem")
343
        raise errors.RadioError( "Bad response to writemem")
344
    return False
345

    
346

    
347
def _resetradio(serial):
348
    resetpacket=b"\xdd\x05\x00\x00"
349
    _send_command(serial,resetpacket)
350

    
351

    
352
def do_download(radio):
353
    serial=radio.pipe
354
    serial.timeout=0.5
355
    status = chirp_common.Status()
356
    status.cur = 0
357
    status.max = MEM_SIZE
358
    status.msg = "Downloading from radio"
359
    radio.status_fn(status)
360

    
361
    eeprom=b""
362
    f=_sayhello(serial)
363
    if f:
364
        radio.FIRMWARE_VERSION=f
365
    else:
366
        return False
367

    
368

    
369

    
370
    addr=0
371
    while addr<MEM_SIZE:
372
        o=_readmem(serial,addr,MEM_BLOCK)
373
        status.cur=addr
374
        radio.status_fn(status)
375

    
376
        if o and len(o)==MEM_BLOCK:
377
            eeprom+=o
378
            addr+=MEM_BLOCK
379
        else:
380
            raise errors.RadioError( "Memory download incomplete")
381

    
382
    return memmap.MemoryMapBytes(eeprom)
383

    
384

    
385

    
386
def do_upload(radio):
387
    serial=radio.pipe
388
    serial.timeout=0.5
389
    status = chirp_common.Status()
390
    status.cur = 0
391
    status.max = PROG_SIZE
392
    status.msg = "Uploading to radio"
393
    radio.status_fn(status)
394

    
395
    f=_sayhello(serial)
396
    if f:
397
        radio.FIRMWARE_VERSION=f
398
    else:
399
        return False
400

    
401
    addr=0
402
    while addr<PROG_SIZE:
403
        o = radio.get_mmap()[addr:addr+MEM_BLOCK]
404
        _writemem(serial,o,addr)
405
        status.cur=addr
406
        radio.status_fn(status)
407
        if o:
408
            addr+=MEM_BLOCK
409
        else:
410
            raise errors.RadioError( "Memory upload incomplete")
411
    status.msg = "Uploaded OK"
412

    
413
    _resetradio(serial)
414

    
415
    return True
416

    
417

    
418
@directory.register
419
class TemplateRadio(chirp_common.CloneModeRadio):
420
    """Quansheng UV-K5"""
421
    VENDOR = "Quansheng"
422
    MODEL = "UV-K5"
423
    BAUD_RATE = 38400
424

    
425
    NEEDS_COMPAT_SERIAL = False
426
    FIRMWARE_VERSION=""
427
    # If the user knows what he's doing, then he can expose this
428
    # functionality that goes against the spirit of chirp
429
    if os.getenv("UVK5_DATA_IN_COMMENTS"):
430
        COMMENT_HACK=True
431
    else:
432
        COMMENT_HACK=False
433

    
434

    
435

    
436
    def get_prompts(x=None):
437
        rp = chirp_common.RadioPrompts()
438
        rp.experimental = \
439
            ('This is an experimental driver for the Quanscheng UV-K5. '
440
             'It may harm your radio, or worse. Use at your own risk.\n\n'
441
             'Before attempting to do any changes please download'
442
             'the memory image from the radio with chirp or k5prog '
443
             'and keep it. This can be later used to recover the '
444
             'original settings. \n\n'
445
             'FM radio, DTMF settings and scanlists are not yet implemented' )
446
        rp.pre_download = _(
447
            "1. Turn radio on.\n"
448
            "2. Connect cable to mic/spkr connector.\n"
449
            "3. Make sure connector is firmly connected.\n"
450
            "4. Click OK to download image from device.\n\n"
451
            "It will may not work if you turn o the radio "
452
            "with the cable already attached\n")
453
        rp.pre_upload = _(
454
            "1. Turn radio on.\n"
455
            "2. Connect cable to mic/spkr connector.\n"
456
            "3. Make sure connector is firmly connected.\n"
457
            "4. Click OK to upload the image to device.\n\n"
458
            "It will may not work if you turn o the radio "
459
            "with the cable already attached")
460
        return rp
461

    
462
    # Return information about this radio's features, including
463
    # how many memories it has, what bands it supports, etc
464
    def get_features(self):
465
        rf = chirp_common.RadioFeatures()
466
        rf.has_bank = False
467
        rf.valid_dtcs_codes = DTCS_CODES
468
        rf.has_rx_dtcs = True
469
        rf.has_ctone = True
470
        rf.has_settings = True
471
        if self.COMMENT_HACK:
472
            rf.has_comment = True
473
        else:
474
            rf.has_comment = False
475
        rf.valid_name_length = 16
476
        rf.valid_power_levels = UVK5_POWER_LEVELS
477

    
478
        #hack so we can input any frequency, the 0.1 and 0.01 steps don't work unfortunately
479
        rf.valid_tuning_steps = [ 0.01, 0.1, 1.0 ] + STEPS 
480

    
481
        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"]
482
        rf.valid_cross_modes = ["Tone->Tone", "Tone->DTCS", "DTCS->Tone",
483
                                "->Tone", "->DTCS", "DTCS->", "DTCS->DTCS"]
484

    
485
        rf.valid_characters = chirp_common.CHARSET_ASCII
486
        rf.valid_modes = ["FM", "NFM", "AM" ]
487
        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "TSQL-R", "Cross"]
488

    
489
        # This radio supports memories 1-200, 201-214 are the VFO memories
490
        rf.memory_bounds = (1, 214) 
491

    
492
        # For now everything that the BK4819 chip supports
493
        # in the future this driver should read the service settings
494
        # and modify the valid_bands accordingly
495
        rf.valid_bands = [(18000000,  620000000), 
496
                          (840000000, 1300000000)
497
                          ]
498
        return rf
499

    
500
    # Do a download of the radio from the serial port
501
    def sync_in(self):
502
        self._mmap = do_download(self)
503
        self.process_mmap()
504

    
505
    # Do an upload of the radio to the serial port
506
    def sync_out(self):
507
        do_upload(self)
508

    
509
    # Convert the raw byte array into a memory object structure
510
    def process_mmap(self):
511
        self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
512

    
513
    # Return a raw representation of the memory object, which
514
    # is very helpful for development
515
    def get_raw_memory(self, number):
516
        return repr(self._memobj.channel[number-1])
517

    
518
    def validate_memory(self, mem):
519
        msgs = super().validate_memory(mem)
520
        return msgs
521

    
522

    
523

    
524
    def _set_tone(self, mem, _mem):
525
        ((txmode, txtone, txpol),
526
         (rxmode, rxtone, rxpol)) = chirp_common.split_tone_encode(mem)
527

    
528
        if txmode == "Tone":
529
            txtoval = CTCSS_TONES.index(txtone)
530
            txmoval = 0b01
531
        elif txmode == "DTCS":
532
            txmoval = txpol == "R" and 0b11 or 0b10
533
            txtoval = DTCS_CODES.index(txtone)
534
        else:
535
            txmoval=0
536
            txtoval = 0
537

    
538
        if rxmode == "Tone":
539
            rxtoval = CTCSS_TONES.index(rxtone)
540
            rxmoval = 0b01
541
        elif rxmode == "DTCS":
542
            rxmoval = rxpol == "R" and 0b11 or 0b10
543
            rxtoval = DTCS_CODES.index(rxtone)
544
        else:
545
            rxmoval=0
546
            rxtoval = 0
547

    
548
        _mem.code_flag = ( _mem.code_flag & 0b11001100 ) | (txmoval << 4) | rxmoval
549
        _mem.rxcode=rxtoval
550
        _mem.txcode=txtoval
551

    
552

    
553
    def _get_tone(self, mem, _mem):
554
        rxtype=_mem.code_flag&0x03
555
        txtype=(_mem.code_flag>>4)&0x03
556
        rx_tmode = TMODES[rxtype]
557
        tx_tmode = TMODES[txtype]
558

    
559
        rx_tone = tx_tone = None
560

    
561
        if tx_tmode == "Tone":
562
            if _mem.txcode<len(CTCSS_TONES):
563
                tx_tone = CTCSS_TONES[_mem.txcode]
564
            else:
565
                tx_tone=0
566
                tx_tmode = ""
567
        elif tx_tmode == "DTCS":
568
            if _mem.txcode<len(DTCS_CODES):
569
                tx_tone = DTCS_CODES[_mem.txcode]
570
            else:
571
                tx_tone = 0
572
                tx_tmode = ""
573

    
574
        if rx_tmode == "Tone":
575
            if _mem.rxcode<len(CTCSS_TONES):
576
                rx_tone = CTCSS_TONES[_mem.rxcode]
577
            else:
578
                rx_tone=0
579
                rx_tmode = ""
580
        elif rx_tmode == "DTCS":
581
            if _mem.rxcode<len(DTCS_CODES):
582
                rx_tone = DTCS_CODES[_mem.rxcode]
583
            else:
584
                rx_tone = 0
585
                rx_tmode = ""
586

    
587

    
588
        tx_pol = txtype == 0x03 and "R" or "N"
589
        rx_pol = rxtype == 0x03 and "R" or "N"
590

    
591
        chirp_common.split_tone_decode(mem, (tx_tmode, tx_tone, tx_pol),
592
                                       (rx_tmode, rx_tone, rx_pol))
593

    
594

    
595

    
596
    # Extract a high-level memory object from the low-level memory map
597
    # This is called to populate a memory in the UI
598
    def get_memory(self, number2):
599

    
600
        number=number2-1 #in the radio memories start with 0
601

    
602
        mem = chirp_common.Memory()
603

    
604
        # cutting and pasting configs from different radios might try to set channel 0
605
        if number2==0:
606
            LOG.warning("Attempt to set channel 0")
607
            return mem
608

    
609
        _mem = self._memobj.channel[number]
610
        tmpcomment="" 
611

    
612
        mem.number = number2
613

    
614
        if number>199:
615
            mem.name="VFO_"+str(number-199)
616
        else:
617
            _mem2 = self._memobj.channelname[number]
618
            for char in _mem2.name:
619
                if str(char) == "\xFF" or str(char) == "\x00":
620
                    break
621
                mem.name += str(char)
622
            mem.name = mem.name.rstrip()
623

    
624

    
625
        # Convert your low-level frequency to Hertz
626
        mem.freq = int(_mem.freq)*10
627
        mem.offset = int(_mem.offset)*10
628

    
629

    
630
        if (mem.offset==0):
631
            mem.duplex = ''
632
        else:
633
            if (_mem.flags1&FLAGS1_OFFSET):
634
                mem.duplex = '-'
635
            else:
636
                mem.duplex = '+'
637

    
638
        # tone data 
639
        self._get_tone(mem, _mem)
640

    
641
        # mode
642
        if (_mem.flags1 & FLAGS1_ISAM )>0 :
643
            # Actually not sure if internally there aren't "Narrow AM" 
644
            # and "Wide AM" modes. To be investigated.
645
            mem.mode="AM"
646
        else:
647
            if (_mem.flags2 & FLAGS2_BANDWIDTH )>0:
648
                mem.mode="NFM"
649
            else:
650
                mem.mode="FM"
651

    
652
        # tuning step
653
        tstep=(_mem.step>>1)&0x7
654
        if tstep < len(STEPS):
655
            mem.tuning_step = STEPS[tstep]
656
        else:
657
            mem.tuning_step = 2.5
658

    
659

    
660
        # power
661
        if (_mem.flags2 & FLAGS2_POWER_MASK ) == FLAGS2_POWER_HIGH:
662
            mem.power=UVK5_POWER_LEVELS[2]
663
        elif (_mem.flags2 & FLAGS2_POWER_MASK ) == FLAGS2_POWER_MEDIUM:
664
            mem.power=UVK5_POWER_LEVELS[1]
665
        else:
666
            mem.power=UVK5_POWER_LEVELS[0]
667

    
668
        # We'll consider any blank (i.e. 0MHz frequency) to be empty
669
        if (_mem.freq == 0xffffffff) or (_mem.freq == 0):
670
            mem.empty = True
671
        else:
672
            mem.empty = False
673

    
674
        mem.extra = RadioSettingGroup("Extra", "extra")
675

    
676
        # BCLO
677
        is_bclo=bool(_mem.flags2&FLAGS2_BCLO>0);
678
        rs = RadioSetting("bclo", "BCLO", RadioSettingValueBoolean(is_bclo))
679
        mem.extra.append(rs)
680
        tmpcomment+="BCLO:"+(is_bclo and "ON" or "off")+" "
681

    
682
        # Frequency reverse - whatever that means, don't see it in the manual
683
        is_frev=bool(_mem.flags2&FLAGS2_REVERSE>0);
684
        rs = RadioSetting("frev", "FreqRev", RadioSettingValueBoolean(is_frev))
685
        mem.extra.append(rs)
686
        tmpcomment+="FreqReverse:"+(is_frev and "ON" or "off")+" "
687

    
688
        # PTTID
689
        pttid=(_mem.dtmf_flags&FLAGS_DTMF_PTTID_MASK)>>1
690
        rs = RadioSetting("pttid", "PTTID", RadioSettingValueList(PTTID_LIST,PTTID_LIST[pttid]))
691
        mem.extra.append(rs)
692
        tmpcomment+="PTTid:"+PTTID_LIST[pttid]+" "
693

    
694
        # DTMF DECODE
695
        is_dtmf=bool(_mem.dtmf_flags&FLAGS_DTMF_DECODE>0);
696
        rs = RadioSetting("dtmfdecode", "DTMF decode", RadioSettingValueBoolean(is_dtmf))
697
        mem.extra.append(rs)
698
        tmpcomment+="DTMFdecode:"+(is_dtmf and "ON" or "off")+" "
699

    
700
        # Scrambler
701
        if _mem.scrambler&0x0f<len(SCRAMBLER_LIST):
702
            enc=_mem.scrambler&0x0f
703
        else:
704
            enc=0
705

    
706
        rs = RadioSetting("scrambler", "Scrambler", RadioSettingValueList(SCRAMBLER_LIST,SCRAMBLER_LIST[enc]))
707
        mem.extra.append(rs)
708
        tmpcomment+="Scrambler:"+SCRAMBLER_LIST[enc]+" "
709

    
710
        # Ugly hack: the extra parameters will be shown in the comments field, 
711
        # because there is no other way to show them without the user 
712
        # right-clicking every channel
713
        # Unfortunately this makes that field effectively read-only, so
714
        # it is only for those that can find it in the sources
715
        if self.COMMENT_HACK:
716
            mem.comment=tmpcomment
717

    
718
        return mem
719

    
720
    def set_settings(self, settings):
721
        _mem = self._memobj
722
        for element in settings:
723
            if not isinstance(element, RadioSetting):
724
                self.set_settings(element)
725
                continue
726

    
727
            ### basic settings
728

    
729
            # call channel
730
            if element.get_name()=="call_channel":
731
                _mem.call_channel=int(element.value)-1
732

    
733
            #squelch
734
            if element.get_name()=="squelch":
735
                _mem.squelch=int(element.value)
736
            #TOT
737
            if element.get_name()=="tot":
738
                _mem.max_talk_time=int(element.value)
739
            #NOAA autoscan
740
            if element.get_name()=="noaa_autoscan":
741
                _mem.noaa_autoscan=element.value and 1 or 0
742

    
743
            #vox level
744
            if element.get_name()=="vox_level":
745
                _mem.vox_level=int(element.value)-1
746

    
747
            #mic gain
748
            if element.get_name()=="mic_gain":
749
                _mem.mic_gain=int(element.value)
750

    
751
            #Channel display mode
752
            if element.get_name()=="channel_display_mode":
753
                _mem.channel_display_mode=CHANNELDISP_LIST.index(str(element.value))
754

    
755
            #Crossband receiving/transmitting
756
            if element.get_name()=="crossband":
757
                _mem.crossband=CROSSBAND_LIST.index(str(element.value))
758

    
759
            #Battery Save
760
            if element.get_name()=="battery_save":
761
                _mem.battery_save=BATSAVE_LIST.index(str(element.value))
762
            #Dual Watch
763
            if element.get_name()=="dualwatch":
764
                _mem.dual_watch=DUALWATCH_LIST.index(str(element.value))
765

    
766
            #Tail tone elimination
767
            if element.get_name()=="tail_note_elimination":
768
                _mem.tail_note_elimination=element.value and 1 or 0
769

    
770
            #VFO Open
771
            if element.get_name()=="vfo_open":
772
                _mem.vfo_open=element.value and 1 or 0
773

    
774
            #Beep control
775
            if element.get_name()=="beep_control":
776
                _mem.beep_control=element.value and 1 or 0
777

    
778
            #Scan resume mode
779
            if element.get_name()=="scan_resume_mode":
780
                _mem.scan_resume_mode=SCANRESUME_LIST.index(str(element.value))
781

    
782
            #Auto keypad lock
783
            if element.get_name()=="auto_keypad_lock":
784
                _mem.auto_keypad_lock=element.value and 1 or 0
785

    
786
            #Power on display mode
787
            if element.get_name()=="welcome_mode":
788
                _mem.power_on_dispmode=WELCOME_LIST.index(str(element.value))
789

    
790
            #Keypad Tone
791
            if element.get_name()=="keypad_tone":
792
                _mem.keypad_tone=KEYPADTONE_LIST.index(str(element.value))
793

    
794
            #Language
795
            if element.get_name()=="language":
796
                _mem.language=LANGUAGE_LIST.index(str(element.value))
797

    
798
            #Alarm mode
799
            if element.get_name()=="alarm_mode":
800
                _mem.alarm_mode=ALARMMODE_LIST.index(str(element.value))
801

    
802
            #Reminding of end of talk
803
            if element.get_name()=="reminding_of_end_talk":
804
                _mem.reminding_of_end_talk=REMENDOFTALK_LIST.index(str(element.value))
805

    
806
            #Repeater tail tone elimination
807
            if element.get_name()=="repeater_tail_elimination":
808
                _mem.repeater_tail_elimination=RTE_LIST.index(str(element.value))
809

    
810
            #Logo string 1
811
            if element.get_name()=="logo1":
812
                b=str(element.value).rstrip("\x20\xff\x00")+"\x00"*12
813
                _mem.logo_line1=b[0:12]+"\xff\xff\xff\xff"
814

    
815

    
816
            #Logo string 2
817
            if element.get_name()=="logo2":
818
                b=str(element.value).rstrip("\x20\xff\x00")+"\x00"*12
819
                _mem.logo_line2=b[0:12]+"\xff\xff\xff\xff"
820

    
821

    
822

    
823

    
824
            ### unlock settings
825

    
826
            #FLOCK
827
            if element.get_name()=="flock":
828
                _mem.int_flock=FLOCK_LIST.index(str(element.value))
829

    
830
            #350TX
831
            if element.get_name()=="350tx":
832
                _mem.int_350tx=element.value and 1 or 0
833

    
834
            #UNKNOWN1
835
            if element.get_name()=="unknown1":
836
                _mem.int_unknown1=element.value and 1 or 0
837

    
838

    
839
            #200TX
840
            if element.get_name()=="200tx":
841
                _mem.int_200tx=element.value and 1 or 0
842

    
843

    
844
            #500TX
845
            if element.get_name()=="500tx":
846
                _mem.int_500tx=element.value and 1 or 0
847

    
848

    
849
            #350EN
850
            if element.get_name()=="350en":
851
                _mem.int_350en=element.value and 1 or 0
852

    
853

    
854

    
855
    def get_settings(self):
856
        _mem = self._memobj
857
        basic = RadioSettingGroup("basic", "Basic Settings")
858
        unlock = RadioSettingGroup("unlock", "Unlock Settings")
859
        fmradio = RadioSettingGroup("fmradio", "FM Radio (unimplemented yet)")
860
        roinfo = RadioSettingGroup("roinfo","Driver information")
861

    
862
        top = RadioSettings(basic, unlock, fmradio,roinfo)
863

    
864
        ### basic settings
865

    
866
        # call channel
867
        tmpc=_mem.call_channel+1
868
        if tmpc>200:
869
            tmpc=1
870
        rs = RadioSetting("call_channel", "One key call channel",RadioSettingValueInteger(1, 200, tmpc))
871
        basic.append(rs)
872

    
873
        # squelch
874
        tmpsq=_mem.squelch
875
        if tmpsq>9:
876
            tmpsq=1
877
        rs = RadioSetting("squelch", "Squelch", RadioSettingValueInteger(0, 9, tmpsq))
878
        basic.append(rs)
879

    
880
        # TOT
881
        tmptot=_mem.max_talk_time
882
        if tmptot>10:
883
            tmptot=10
884
        rs = RadioSetting("tot", "Max talk time [min]", RadioSettingValueInteger(0, 10, tmptot))
885
        basic.append(rs)
886

    
887
        # NOAA autoscan
888
        rs = RadioSetting("noaa_autoscan", "NOAA Autoscan", RadioSettingValueBoolean(bool(_mem.noaa_autoscan>0)))
889
        basic.append(rs)
890

    
891
        # VOX Level
892
        tmpvox=_mem.vox_level+1
893
        if tmpvox>10:
894
            tmpvox=10
895
        rs = RadioSetting("vox_level", "VOX Level", RadioSettingValueInteger(1, 10, tmpvox))
896
        basic.append(rs)
897

    
898
        # Mic gain
899
        tmpmicgain=_mem.mic_gain
900
        if tmpmicgain>4:
901
            tmpmicgain=4
902
        rs = RadioSetting("mic_gain", "Mic Gain", RadioSettingValueInteger(0, 4, tmpmicgain))
903
        basic.append(rs)
904

    
905
        # Channel display mode
906
        tmpchdispmode=_mem.channel_display_mode
907
        if tmpchdispmode>=len(CHANNELDISP_LIST):
908
            tmpchdispmode=0
909
        rs = RadioSetting("channel_display_mode", "Channel display mode", RadioSettingValueList( CHANNELDISP_LIST, CHANNELDISP_LIST[tmpchdispmode]))
910
        basic.append(rs)
911

    
912
        #Crossband receiving/transmitting
913
        tmpcross=_mem.crossband
914
        if tmpcross>=len(CROSSBAND_LIST):
915
            tmpcross=0
916
        rs = RadioSetting("crossband", "Cross-band receiving/transmitting", RadioSettingValueList( CROSSBAND_LIST, CROSSBAND_LIST[tmpcross]))
917
        basic.append(rs)
918

    
919
        #Battery save
920
        rs = RadioSetting("battery_save", "Battery Save", RadioSettingValueList( BATSAVE_LIST, BATSAVE_LIST[_mem.battery_save]))
921
        basic.append(rs)
922

    
923
        #Dual watch
924
        tmpdual=_mem.dual_watch
925
        if tmpdual>=len(DUALWATCH_LIST):
926
            tmpdual=0
927
        rs = RadioSetting("dualwatch", "Dual Watch", RadioSettingValueList( DUALWATCH_LIST, DUALWATCH_LIST[tmpdual]))
928
        basic.append(rs)
929

    
930
        #Tail tone elimination
931
        rs = RadioSetting("tail_note_elimination", "Tail tone elimination", RadioSettingValueBoolean(bool(_mem.tail_note_elimination>0)))
932
        basic.append(rs)
933

    
934
        #VFO open
935
        rs = RadioSetting("vfo_open", "VFO open", RadioSettingValueBoolean(bool(_mem.vfo_open>0)))
936
        basic.append(rs)
937

    
938
        #Beep control
939
        rs = RadioSetting("beep_control", "Beep control", RadioSettingValueBoolean(bool(_mem.beep_control>0)))
940
        basic.append(rs)
941

    
942
        #Scan resume mode
943
        tmpscanres=_mem.scan_resume_mode
944
        if tmpscanres>=len(SCANRESUME_LIST):
945
            tmpscanres=0
946
        rs = RadioSetting("scan_resume_mode", "Scan resume mode", RadioSettingValueList( SCANRESUME_LIST, SCANRESUME_LIST[tmpscanres]))
947
        basic.append(rs)
948

    
949
        #Auto keypad lock
950
        rs = RadioSetting("auto_keypad_lock", "Auto keypad lock", RadioSettingValueBoolean(bool(_mem.auto_keypad_lock>0)))
951
        basic.append(rs)
952

    
953

    
954
        #Power on display mode
955
        tmpdispmode=_mem.power_on_dispmode
956
        if tmpdispmode>=len(WELCOME_LIST):
957
            tmpdispmode=0
958
        rs = RadioSetting("welcome_mode", "Power on display mode", RadioSettingValueList( WELCOME_LIST, WELCOME_LIST[tmpdispmode]))
959
        basic.append(rs)
960

    
961
        #Keypad Tone
962
        tmpkeypadtone=_mem.keypad_tone
963
        if tmpkeypadtone>=len(KEYPADTONE_LIST):
964
            tmpkeypadtone=0
965
        rs = RadioSetting("keypad_tone", "Keypad tone", RadioSettingValueList( KEYPADTONE_LIST, KEYPADTONE_LIST[tmpkeypadtone]))
966
        basic.append(rs)
967

    
968
        #Language
969
        tmplanguage=_mem.language
970
        if tmplanguage>=len(LANGUAGE_LIST):
971
            tmplanguage=0
972
        rs = RadioSetting("language", "Language", RadioSettingValueList( LANGUAGE_LIST, LANGUAGE_LIST[tmplanguage]))
973
        basic.append(rs)
974

    
975
        # Alarm mode
976
        tmpalarmmode=_mem.alarm_mode
977
        if tmpalarmmode>=len(ALARMMODE_LIST):
978
            tmpalarmmode=0
979
        rs = RadioSetting("alarm_mode", "Alarm mode", RadioSettingValueList( ALARMMODE_LIST, ALARMMODE_LIST[tmpalarmmode]))
980
        basic.append(rs)
981

    
982
        # Reminding of end of talk
983
        tmpalarmmode=_mem.reminding_of_end_talk
984
        if tmpalarmmode>=len(REMENDOFTALK_LIST):
985
            tmpalarmmode=0
986
        rs = RadioSetting("reminding_of_end_talk", "Reminding of end of talk", RadioSettingValueList( REMENDOFTALK_LIST, REMENDOFTALK_LIST[tmpalarmmode]))
987
        basic.append(rs)
988

    
989
        # Repeater tail tone elimination
990
        tmprte=_mem.repeater_tail_elimination
991
        if tmprte>=len(RTE_LIST):
992
            tmprte=0
993
        rs = RadioSetting("repeater_tail_elimination", "Repeater tail tone elimination", RadioSettingValueList( RTE_LIST, RTE_LIST[tmprte]))
994
        basic.append(rs)
995

    
996
        #Logo string 1
997
        logo1=str(_mem.logo_line1).strip("\x20\x00\xff") #+"\x20"*12
998
        logo1=logo1[0:12]
999
        rs = RadioSetting("logo1", "Logo string 1 (12 characters)", RadioSettingValueString(0,12,logo1))
1000
        basic.append(rs)
1001

    
1002

    
1003
        #Logo string 2
1004
        logo2=str(_mem.logo_line2).strip("\x20\x00\xff") #+"\x20"*12
1005
        logo2=logo2[0:12]
1006
        rs = RadioSetting("logo2", "Logo string 2 (12 characters)", RadioSettingValueString(0,12,logo2))
1007
        basic.append(rs)
1008

    
1009

    
1010

    
1011

    
1012

    
1013
        #### unlock settings
1014

    
1015
        # F-LOCK
1016
        tmpflock=_mem.int_flock
1017
        if tmpflock>=len(FLOCK_LIST):
1018
            tmpflock=0
1019
        rs = RadioSetting("flock", "F-LOCK", RadioSettingValueList( FLOCK_LIST, FLOCK_LIST[tmpflock]))
1020
        unlock.append(rs)
1021

    
1022
        #350TX
1023
        rs = RadioSetting("350tx", "350TX", RadioSettingValueBoolean(bool(_mem.int_350tx>0)))
1024
        unlock.append(rs)
1025

    
1026
        #unknown1
1027
        rs = RadioSetting("unknown11", "UNKNOWN1", RadioSettingValueBoolean(bool(_mem.int_unknown1>0)))
1028
        unlock.append(rs)
1029

    
1030
        #200TX
1031
        rs = RadioSetting("200tx", "200TX", RadioSettingValueBoolean(bool(_mem.int_200tx>0)))
1032
        unlock.append(rs)
1033

    
1034
        #500TX
1035
        rs = RadioSetting("500tx", "500TX", RadioSettingValueBoolean(bool(_mem.int_500tx>0)))
1036
        unlock.append(rs)
1037

    
1038
        #350EN
1039
        rs = RadioSetting("350en", "350EN", RadioSettingValueBoolean(bool(_mem.int_350en>0)))
1040
        unlock.append(rs)
1041

    
1042
        #SCREEN
1043
        rs = RadioSetting("screen", "SCREEN", RadioSettingValueBoolean(bool(_mem.int_screen>0)))
1044
        unlock.append(rs)
1045

    
1046
        ### readonly info
1047
        # Firmware
1048
        if self.FIRMWARE_VERSION=="":
1049
            firmware="To get the firmware version please download the image from the radio first"
1050
        else:
1051
            firmware=self.FIRMWARE_VERSION
1052
        rs = RadioSetting("fw_ver", "Firmware Version", RadioSettingValueString(0,128,firmware))
1053
        roinfo.append(rs)
1054

    
1055
        # Driver version
1056
        rs = RadioSetting("driver_ver", "Driver version", RadioSettingValueString(0,128,DRIVER_VERSION))
1057
        roinfo.append(rs)
1058

    
1059
        rs = RadioSetting("comment_hack", "Show extra channel settings in comments", RadioSettingValueString(0,4,self.COMMENT_HACK and "Yes" or "No"))
1060
        roinfo.append(rs)
1061

    
1062

    
1063
        return top
1064

    
1065
    # Store details about a high-level memory to the memory map
1066
    # This is called when a user edits a memory in the UI
1067
    def set_memory(self, mem):
1068
        number=mem.number-1
1069

    
1070

    
1071
        # Get a low-level memory object mapped to the image
1072
        _mem = self._memobj.channel[number]
1073
        #empty memory
1074
        if mem.empty:
1075
            _mem.set_raw("\xFF" * 16)
1076
            if number<200:
1077
                _mem2 = self._memobj.channelname[number]
1078
                _mem2.set_raw("\xFF" * 16)
1079
            return mem
1080

    
1081
        # mode
1082
        if mem.mode == "AM":
1083
            _mem.flags1 = _mem.flags1 | FLAGS1_ISAM
1084
            _mem.flags2 = _mem.flags2 & ~FLAGS2_BANDWIDTH
1085
        else:
1086
            _mem.flags1 = _mem.flags1 & ~FLAGS1_ISAM
1087
            if mem.mode == "NFM":
1088
                _mem.flags2 = _mem.flags2 | FLAGS2_BANDWIDTH
1089
            else:
1090
                _mem.flags2 = _mem.flags2 & ~FLAGS2_BANDWIDTH
1091

    
1092
        # frequency/offset
1093
        _mem.freq = mem.freq/10
1094
        _mem.offset = mem.offset/10
1095

    
1096
        if mem.duplex == "off":
1097
            _mem.offset=0
1098
        elif mem.duplex == '-':
1099
            _mem.flags1=_mem.flags1|FLAGS1_OFFSET
1100
        elif mem.duplex == '+':
1101
            _mem.flags1=_mem.flags1&~FLAGS1_OFFSET
1102

    
1103
        #channels >200 are the 14 VFO chanells and don't have names
1104
        if number<200:
1105
            _mem2 = self._memobj.channelname[number]
1106
            tag=mem.name.ljust(16)[:16]
1107
            _mem2.name = tag  # Store the alpha tag
1108

    
1109
        # tone data
1110
        self._set_tone(mem, _mem)
1111

    
1112
        # step
1113
        _mem.step=STEPS.index(mem.tuning_step)<<1
1114

    
1115
        # tx power
1116
        if str(mem.power) == str(UVK5_POWER_LEVELS[2]):
1117
            _mem.flags2 = (_mem.flags2 & ~FLAGS2_POWER_MASK ) | FLAGS2_POWER_HIGH
1118
        elif str(mem.power) == str(UVK5_POWER_LEVELS[1]):
1119
            _mem.flags2 = (_mem.flags2 & ~FLAGS2_POWER_MASK ) | FLAGS2_POWER_MEDIUM
1120
        else:
1121
            _mem.flags2 = (_mem.flags2 & ~FLAGS2_POWER_MASK ) 
1122

    
1123

    
1124
        for setting in mem.extra:
1125
            sname=setting.get_name()
1126
            svalue=setting.value.get_value()
1127

    
1128
            if sname == "bclo":
1129
                if svalue:
1130
                    _mem.flags2 = _mem.flags2 | FLAGS2_BCLO
1131
                else:
1132
                    _mem.flags2 = _mem.flags2 & ~FLAGS2_BCLO
1133

    
1134
            if sname == "pttid":
1135
                _mem.dtmf_flags = (_mem.dtmf_flags & ~FLAGS_DTMF_PTTID_MASK) | (PTTID_LIST.index(svalue)<<1)
1136

    
1137
            if sname == "frev":
1138
                if svalue:
1139
                    _mem.flags2 = _mem.flags2 | FLAGS2_REVERSE;
1140
                else:
1141
                    _mem.flags2 = _mem.flags2 & ~FLAGS2_REVERSE;
1142

    
1143
            if sname == "dtmfdecode":
1144
                if svalue:
1145
                    _mem.dtmf_flags = _mem.dtmf_flags | FLAGS_DTMF_DECODE;
1146
                else:
1147
                    _mem.dtmf_flags = _mem.dtmf_flags & ~FLAGS_DTMF_DECODE;
1148

    
1149
            if sname == "scrambler":
1150
                _mem.scrambler = (_mem.scrambler & 0xf0) | SCRAMBLER_LIST.index(svalue)
1151

    
1152

    
1153

    
1154

    
1155
        return mem
1156

    
(5-5/47)