Project

General

Profile

New Model #6129 » Quansheng UV-R50 driver.py

Driver written by Uriel Corfa, 6 Oct 2020 - Gary P, 08/28/2023 08:52 PM

 
1
# Copyright 2020 Uriel Corfa <uriel@corfa.fr>
2
#
3
# This program is free software: you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation, either version 2 of the License, or
6
# (at your option) any later version.
7
#
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
# GNU General Public License for more details.
12
#
13
# You should have received a copy of the GNU General Public License
14
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
15

    
16
import struct
17
import time
18
import os
19
import logging
20

    
21
from chirp import chirp_common, errors, util, directory, memmap
22
from chirp.settings import RadioSetting, RadioSettingGroup, \
23
    RadioSettingValueInteger, RadioSettingValueList, \
24
    RadioSettingValueBoolean, RadioSettingValueString, \
25
    RadioSettings
26
from chirp import bitwise
27
from textwrap import dedent
28

    
29
LOG = logging.getLogger(__name__)
30

    
31
MEM_FORMAT = """
32
struct channel {
33
  lbcd rxfreq[4];
34
  lbcd txfreq[4];
35
  ul16 rxtone;
36
  ul16 txtone;
37
  u8 unknown1:3,
38
     busylock:1,
39
     scanadd:1,
40
     unknown2:3;
41
  u8 unknown3:2,
42
     power:1,
43
     width:1,
44
     unknown4:4;
45
  u8 signalcode;
46
  u8 unknown5;
47
};
48

    
49
#seekto 0x0000;
50
struct channel memory[128];
51

    
52
#seekto 0x0f60;
53
struct channel vfo1;
54
#seekto 0x0f70;
55
struct channel vfo2;
56

    
57
#seekto 0x1000;
58
struct {
59
  u8 name[8];
60
  u8 unknown[8];
61
} names[128];
62

    
63
#seekto 0x0E20;
64
struct {
65
  u8 mdfa;           // 0x0E20
66
  u8 step;           // 0x0E21
67
  u8 squelch;        // 0x0E22
68
  u8 powersave;      // 0x0E23
69
  u8 vox;            // 0x0E24
70
  u8 unknownE25;     // 0x0E25
71
  u8 tot;            // 0x0E26
72
  u8 unknownE27;     // 0x0E27
73
  u8 dualwatch;      // 0x0E28
74
  u8 unknownE29[4];  // 0x0E29 .. 2C
75
  u8 beep;           // 0x0E2D
76
  u8 lang;           // 0x0E2E
77
  u8 unknownE2F;     // 0x0E2F
78

    
79
  u8 dtmfst;         // 0x0E30
80
  u8 unknownE31;     // 0x0E31
81
  u8 chan1;          // 0x0E32
82
  u8 chan2;          // 0x0E33
83
  u8 pttlt;          // 0x0E34
84
  u8 pttid;          // 0x0E35
85
  u8 unknownE36;     // 0x0E36
86
  u8 mdfb;           // 0x0E37
87
  u8 scanresmode;    // 0x0E38
88
  u8 autolk;         // 0x0E39
89
  u8 unknnownE3A[3]; // 0x0E3A .. 3C
90
  u8 stbycolor;      // 0x0E3D
91
  u8 rxcolor;        // 0x0E3E
92
  u8 txcolor;        // 0x0E3F
93

    
94
  u8 alarm;          // 0x0E40
95
  u8 unknownE41;     // 0x0E41
96
  u8 tdrab;          // 0x0E42
97
  u8 ste;            // 0x0E43
98
  u8 rpste;          // 0x0E44
99
  u8 rptrl;          // 0x0E45
100
  u8 ponmsg;         // 0x0E46
101
  u8 roger;          // 0x0E47
102
  u8 unknownE48[4];  // 0x0E48 .. 4B
103
  u8 vfomr;          // 0x0E4C
104
  u8 locked;         // 0x0E4D
105
  u8 abr;            // 0x0E4E
106
  u8 unknownE4F;     // 0x0E4F
107

    
108
  u8 unknownE50[10]; // 0x0E50 .. 59
109
  u8 aniid[5];       // 0x0E5A .. 5E
110
  u8 unknownE5F;     // 0x0E5F  // This is probably supposed to remain
111
                                // at 0xff, as a string terminator for
112
                                // the ANI ID
113
} settings;
114

    
115
#seekto 0x0B00;
116
struct {
117
  u8 dtmf[5];
118
  u8 unused[11];
119
} dtmf[16];
120
"""
121

    
122
MAGIC = b"\x4b\x6b\x4e\x48\x53\x47\x30\x4e\x02"
123
VERSION = b"\x06\x4a\x35\x36\x30\x32\x43\xf8"
124

    
125
SPECIALS = {
126
    "VFO1": ("vfo1", -2),
127
    "VFO2": ("vfo2", -1),
128
}
129
NUM_CHANNELS = 128
130

    
131
POWER_LEVELS = [chirp_common.PowerLevel("Low",  watts=1.00),
132
                chirp_common.PowerLevel("High", watts=4.00)]
133
MODES = ["NFM", "FM"]
134
STEPS = [2.5, 5.0, 6.25, 10.0, 12.5, 25.0]
135
LANGUAGES = ["Off", "Chinese", "English"]
136
SCANRES_MODES = ["Skip after 5s",
137
                 "Skip when carrier-wave drops",
138
                 "Stop when found"]
139
COLORS = ["Off", "Green", "Purple", "Sky blue"]
140
DTMFST_MODES = ["Off", "DT-ST", "ANI", "DT-ST + ANI"]
141
PTTID_MODES = ["Off", "BOT", "EOT", "Both"]
142
DISPLAY_MODES = ["Frequency", "Channel no", "Name"]
143
ALARM_MODES = ["Tone", "Code", "Site"]
144
TDRAB_MODES = ["Off", "A only (upper row)", "B only (lower row)"]
145
VFOMR_MODES = ["VFO", "Channels"]
146
TOT_TIMES = [str(x) for x in range(15, 601, 15)]
147

    
148
DTMF_CHARS = "1234567890*#ABCD"
149

    
150

    
151
def _send(radio, msg):
152
    LOG.debug("Sending msg:\n%s" % util.hexprint(msg))
153
    try:
154
        radio.pipe.write(msg)
155
    except Exception, e:
156
        raise errors.RadioError("Serial write error: %s", e)
157

    
158

    
159
def _read(radio, size):
160
    try:
161
        answer = radio.pipe.read(size)
162
    except Exception, e:
163
        raise errors.RadioError("Serial read error: %s", e)
164
    LOG.debug("Recv:\n%s" % util.hexprint(answer))
165
    return answer
166

    
167

    
168
def ident(radio):
169
    radio.pipe.timeout = 1
170

    
171
    _send(radio, MAGIC)
172
    ack = _read(radio, 1)
173
    time.sleep(0.1)
174
    if ack != b"\x06":
175
        raise errors.RadioError("Radio did not respond to ident")
176
    _send(radio, b"\x02")
177
    version = _read(radio, 8)
178
    if version != VERSION:
179
        raise errors.RadioError("Unknown version: %s", version)
180

    
181

    
182
def _read_block(radio, start, size):
183
    _send(radio, b"\x06")
184
    ack = _read(radio, 1)
185
    if ack != b"\x06":
186
        raise errors.RadioError("Radio not ready to read")
187

    
188
    msg = struct.pack(">BHB", ord("R"), start, size)
189
    _send(radio, msg)
190

    
191
    header = _read(radio, 4)
192
    op, rstart, rsize = struct.unpack(">BHB", header)
193
    if op != ord('W'):
194
        raise errors.RadioError(
195
            "Wrong reply: expected 87 (W), got %s (%s)" % (op, chr(op)))
196
    if rstart != start:
197
        raise errors.RadioError(
198
            "Radio read started at %s, expected %s" % (rstart, start))
199
    data = _read(radio, rsize)
200
    if len(data) != size:
201
        raise errors.RadioError("Radio refused to send block 0x%04x" % start)
202
    return data
203

    
204

    
205
def _write_block(radio, start, block):
206
    size = len(block)
207
    msg = struct.pack(">BHB", ord("W"), start, size)
208
    _send(radio, msg)
209
    _send(radio, block)
210
    ack = _read(radio, 1)
211
    if ack != b"\x06":
212
        raise errors.RadioError("Radio failed to write")
213

    
214

    
215
def do_download(radio):
216
    """This is your download function"""
217
    # Check we're talking to the real deal
218
    ident(radio)
219

    
220
    status = chirp_common.Status()
221
    status.msg = "Cloning from UV-R50"
222
    status.max = 0x2000
223

    
224
    mm = memmap.MemoryMap(b"\x00" * 0x2000)
225
    for i in range(0x0000, 0x1fb0, 0x40):
226
        status.cur = i
227
        radio.status_fn(status)
228
        data = _read_block(radio, i, 0x40)
229
        mm.set(i, data)
230

    
231
    return mm
232

    
233

    
234
def do_upload(radio):
235
    """This is your upload function"""
236
    ident(radio)
237

    
238
    status = chirp_common.Status()
239
    status.msg = "Cloning to UV-R50"
240
    status.max = 0x2000
241

    
242
    # Unclear why, but the official client reads this before writing
243
    # and the radio won't let us write before we read this block.
244
    _read_block(radio, 0x0fd0, 0x40)
245

    
246
    _send(radio, b"\x06")
247
    ack = _read(radio, 1)
248
    if ack != b"\x06":
249
        raise errors.RadioError("Radio not ready to write")
250

    
251
    mm = radio.get_mmap()
252
    for i in range(0x0000, 0x1fe0, 0x10):
253
        status.cur = i
254
        radio.status_fn(status)
255
        block = mm[i:i+0x10]
256
        _write_block(radio, i, block)
257

    
258
# Get the serial port connection
259
    serial = radio.pipe
260

    
261
    # Our fake radio is just a simple upload of 1000 bytes
262
    # to the serial port. Do that one byte at a time, reading
263
    # from our memory map
264
    for i in range(0, 1000):
265
        serial.write(radio.get_mmap()[i])
266

    
267

    
268
@directory.register
269
class QuanshengUVR50Radio(chirp_common.CloneModeRadio):
270
    """Quansheng UV-R50 radio.
271

    
272
    This is probably V1 of this radio, and documentation about other
273
    models is sparse on the internet. As far as I can tell, UV-R50-2
274
    and UV-R50-CX are just different cases for the same internal
275
    hardware, but I only have one model and I'm not sure which one.
276

    
277
    """
278
    VENDOR = "Quansheng"
279
    MODEL = "UV-R50"
280
    BAUD_RATE = 9600
281
    NEEDS_COMPAT_SERIAL = False
282

    
283
    _chars = dict(enumerate(range(ord('0'), ord('9') + 1) +
284
                            range(ord('A'), ord('Z') + 1)))
285

    
286
    def _ascii2quansheng(self, asc, maxlen):
287
        """Converts ASCII to an encoding the R50 understands."""
288
        revchars = {c: int(pos) for (pos, c) in self._chars.items()}
289
        asc = asc.ljust(maxlen).upper()
290
        qs = []
291
        for i in range(maxlen):
292
            qs.append(revchars.get(ord(asc[i]), 0xff))
293
        return qs
294

    
295
    def _quansheng2ascii(self, qs, maxlen):
296
        """Converts the R50's text encoding scheme to ASCII."""
297
        asc = ''
298
        for i in range(maxlen):
299
            c = int(qs[i])
300
            if c not in self._chars:
301
                return asc
302
            asc += chr(self._chars[c])
303
        return asc
304

    
305
    def _dtmf2ascii(self, dtmf):
306
        asci = ""
307
        for i in range(5):
308
            c = dtmf[i]
309
            if c - 1 > len(DTMF_CHARS):
310
                return asci
311
            asci += DTMF_CHARS[c - 1]
312
        return asci
313

    
314
    def _ascii2dtmf(self, asci):
315
        dtmf = []
316
        for x in asci.ljust(5, "?"):
317
            try:
318
                dtmf.append(DTMF_CHARS.index(x) + 1)
319
            except ValueError:
320
                dtmf.append(0xff)
321
        return dtmf
322

    
323
    def get_settings(self):
324
        _set = self._memobj.settings
325
        _dtmf = self._memobj.dtmf
326
        grp = RadioSettingGroup("uvr50", "UV-R50")
327
        grp.append(RadioSetting("vfomr", "Operation mode", RadioSettingValueList(VFOMR_MODES, VFOMR_MODES[_set.vfomr])))
328
        grp.append(RadioSetting("tot", "Time-out timer", RadioSettingValueList(TOT_TIMES, TOT_TIMES[_set.tot])))
329
        grp.append(RadioSetting("squelch", "Squelch level", RadioSettingValueInteger(0, 9, _set.squelch)))
330
        grp.append(RadioSetting("vox", "VOX", RadioSettingValueInteger(0, 9, _set.vox)))
331
        grp.append(RadioSetting("lang", "Language", RadioSettingValueList(LANGUAGES, LANGUAGES[_set.lang])))
332
        grp.append(RadioSetting("scanresmode", "Scan Resume Mode", RadioSettingValueList(SCANRES_MODES, SCANRES_MODES[_set.scanresmode])))
333
        grp.append(RadioSetting("roger", "Roger sound", RadioSettingValueBoolean(_set.roger)))
334
        grp.append(RadioSetting("stbycolor", "Standby back-light color", RadioSettingValueList(COLORS, COLORS[_set.stbycolor])))
335
        grp.append(RadioSetting("rxcolor", "Receiving back-light color", RadioSettingValueList(COLORS, COLORS[_set.rxcolor])))
336
        grp.append(RadioSetting("txcolor", "Transmitting back-light color", RadioSettingValueList(COLORS, COLORS[_set.txcolor])))
337
        grp.append(RadioSetting("step", "Step", RadioSettingValueList([str(x) for x in STEPS], str(STEPS[_set.step]))))
338
        grp.append(RadioSetting("powersave", "Power saving", RadioSettingValueInteger(0, 4, _set.powersave)))
339
        grp.append(RadioSetting("abr", "Auto backlight duration (s)", RadioSettingValueInteger(0, 5, _set.abr)))
340
        grp.append(RadioSetting("dualwatch", "Dual watch", RadioSettingValueBoolean(_set.dualwatch)))
341
        grp.append(RadioSetting("tdrab", "Transmit row (TDR AB)", RadioSettingValueList(TDRAB_MODES, TDRAB_MODES[_set.tdrab])))
342
        grp.append(RadioSetting("beep", "Key press beep", RadioSettingValueBoolean(_set.beep)))
343

    
344
        def aniid_apply(setting, obj, encode):
345
            value = str(setting.value)
346
            obj.aniid = encode(value)
347
        rs = RadioSetting("aniid", "ANI ID", RadioSettingValueString(1, 5, self._quansheng2ascii(_set.aniid, 5), charset="0123456789ABCDEF "))
348
        rs.set_apply_callback(aniid_apply, _set, lambda x: self._ascii2quansheng(x, 5))
349
        grp.append(rs)
350

    
351
        grp.append(RadioSetting("dtmfst", "DTMFST", RadioSettingValueList(DTMFST_MODES, DTMFST_MODES[_set.dtmfst])))
352
        grp.append(RadioSetting("pttid", "Send PTT ID", RadioSettingValueList(PTTID_MODES, PTTID_MODES[_set.pttid])))
353
        grp.append(RadioSetting("pttlt", "PTT ID prolong time (ms)", RadioSettingValueInteger(0, 30, _set.pttlt)))
354
        grp.append(RadioSetting("mdfa", "Display mode channel A", RadioSettingValueList(DISPLAY_MODES, DISPLAY_MODES[_set.mdfa])))
355
        grp.append(RadioSetting("mdfb", "Display mode channel B", RadioSettingValueList(DISPLAY_MODES, DISPLAY_MODES[_set.mdfb])))
356
        grp.append(RadioSetting("autolk", "Auto-lock UI", RadioSettingValueBoolean(_set.autolk)))
357
        grp.append(RadioSetting("locked", "Locked UI", RadioSettingValueBoolean(_set.locked)))
358
        grp.append(RadioSetting("alarm", "Alarm mode", RadioSettingValueList(ALARM_MODES, ALARM_MODES[_set.alarm])))
359
        grp.append(RadioSetting("ste", "Tail tone elimination", RadioSettingValueBoolean(_set.ste)))
360
        grp.append(RadioSetting("rpste", "Repeater tail tone elimination (ms)", RadioSettingValueInteger(0, 10, _set.rpste)))
361
        grp.append(RadioSetting("rptrl", "Repeater tail tone elimination time (ms)", RadioSettingValueInteger(0, 10, _set.rptrl)))
362
        grp.append(RadioSetting("ponmsg", "Display logo at startup", RadioSettingValueBoolean(_set.ponmsg)))
363

    
364
        dtmf = RadioSettingGroup("dtmf", "DTMF")
365

    
366
        def dtmf_apply(setting, obj, encode):
367
            value = str(setting.value)
368
            obj.dtmf = encode(value)
369

    
370
        for i in range(0, 15):
371
            vs = RadioSettingValueString(0, 5,
372
                                         self._dtmf2ascii(_dtmf[i].dtmf),
373
                                         False)
374
            vs.set_charset(DTMF_CHARS)
375
            rs = RadioSetting("dtmf/%d" % i, "DTMF %d" % (i + 1), vs)
376
            rs.set_apply_callback(dtmf_apply, self._memobj.dtmf[i],
377
                                  self._ascii2dtmf)
378
            dtmf.append(rs)
379

    
380
        g = RadioSettings(grp, dtmf)
381
        return g
382

    
383
    def set_settings(self, settings):
384
        """Automatically apply the settings recursively."""
385
        _settings = self._memobj.settings
386
        for element in settings:
387
            if not isinstance(element, RadioSetting):
388
                self.set_settings(element)
389
                continue
390
            else:
391
                name = element.get_name()
392
                if "." in name:
393
                    bits = name.split(".")
394
                    obj = self._memobj
395
                    for bit in bits[:-1]:
396
                        if "/" in bit:
397
                            bit, index = bit.split("/", 1)
398
                            index = int(index)
399
                            obj = getattr(obj, bit)[index]
400
                        else:
401
                            obj = getattr(obj, bit)
402
                    setting = bits[-1]
403
                else:
404
                    obj = _settings
405
                    setting = element.get_name()
406

    
407
                if element.has_apply_callback():
408
                    element.run_apply_callback()
409
                elif element.value.get_mutable():
410
                    setattr(obj, setting, element.value)
411

    
412
    def get_features(self):
413
        rf = chirp_common.RadioFeatures()
414
        rf.has_bank = False
415
        rf.has_dtcs = True
416
        rf.has_ctone = True
417
        rf.has_rx_dtcs = True
418
        rf.has_offset = True
419
        rf.has_name = True
420
        rf.has_cross = True
421
        rf.has_settings = True
422
        rf.has_tuning_step = False
423
        rf.can_odd_split = True
424
        rf.can_delete = True
425

    
426
        rf.memory_bounds = (0, NUM_CHANNELS - 1)
427

    
428
        rf.valid_skips = [""]  # UV-R50 has no support for scan skips
429
        rf.valid_bands = [(136000000, 174000000),
430
                          (400000000, 520000000),
431
                          ]
432
        rf.valid_duplexes = ["", "-", "+", "off", "split"]
433
        rf.valid_characters = "".join([chr(x) for x in self._chars.values()])
434
        rf.valid_name_length = 7
435
        rf.valid_modes = MODES
436
        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"]
437
        rf.valid_cross_modes = ["Tone->Tone", "Tone->DTCS",
438
                                "DTCS->Tone", "DTCS->DTCS", "DTCS->",
439
                                "->Tone", "->DTCS"]
440
        rf.valid_power_levels = POWER_LEVELS
441
        rf.valid_tuning_steps = STEPS
442
        rf.valid_special_chans = SPECIALS.keys()
443
        return rf
444

    
445
    def process_mmap(self):
446
        self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
447

    
448
    def sync_in(self):
449
        self._mmap = do_download(self)
450
        self.process_mmap()
451

    
452
    def sync_out(self):
453
        do_upload(self)
454

    
455
    def get_raw_memory(self, number):
456
        return repr(self._memobj.memory[number])
457

    
458
    def get_memory(self, number):
459
        mem = chirp_common.Memory()
460

    
461
        if number in SPECIALS:
462
            name = number
463
            _mem = self._memobj.__getattr__(SPECIALS[number][0])
464
            mem.extd_number = number
465
            number = SPECIALS[number][1]
466
        else:
467
            _nam = self._memobj.names[number]
468
            name = self._quansheng2ascii(_nam.name, 8)
469
            _mem = self._memobj.memory[number]
470

    
471
        mem.number = number
472

    
473
        if _mem.rxfreq.get_raw() in ["\xff\xff\xff\xff", "\xa5\xa5\xa5\xa5"]:
474
            mem.empty = True
475
            return mem
476

    
477
        mem.name = name
478
        mem.power = POWER_LEVELS[_mem.power]
479
        mem.mode = MODES[_mem.width]
480
        mem.freq = int(_mem.rxfreq) * 10
481
        offset = (int(_mem.txfreq) - int(_mem.rxfreq)) * 10
482

    
483
        if offset == 0:
484
            mem.duplex = "off"
485
            mem.offset = 0
486
        elif offset > 120000000:  # 120MHz
487
            mem.duplex = "split"
488
            mem.offset = _mem.txfreq * 10
489
        elif offset < 0:
490
            mem.duplex = "-"
491
            mem.offset = abs(offset)
492
        else:
493
            mem.duplex = "+"
494
            mem.offset = offset
495

    
496
        class ToneWord(object):
497
            def __init__(self, word):
498
                """Decodes the tx or rx "tone" word of a channel or freq.
499

    
500
                The encoding scheme varies depend on what is encoded:
501

    
502
                - The most significant nibble encodes what kind of
503
                  tone/code squelch is used (CTCSS or DTCS);
504

    
505
                - For CTCSS, the last 3 nibbles encode the tone as a
506
                  straight frequency;
507

    
508
                - For DTCS, the last 2 nibbles (lower byte) encode the
509
                  DTCS code, and the polarity is encoded in the most
510
                  significant bit of the word.
511

    
512
                If this channel is not set to tx/rx anything, the high
513
                byte is 0xff.
514
                """
515
                self.is_set = (word & 0xff00 != 0xff00) and word != 0x0000
516
                self.is_dtcs = word & 0x2000 and self.is_set
517
                if self.is_dtcs:
518
                    self.dtcs_pol = "R" if word & 0x8000 else "N"
519
                    self.dtcs_index = (word & 0x01ff)
520
                else:
521
                    self.dtcs_pol = "N"
522
                    self.dtcs_index = None
523
                self.is_ctcss = not self.is_dtcs and self.is_set
524
                self.tone = (word & 0x0fff) if self.is_ctcss else None
525
                if self.is_ctcss:
526
                    self.mode = "Tone"
527
                elif self.is_dtcs:
528
                    self.mode = "DTCS"
529
                else:
530
                    self.mode = ""
531

    
532
            def equals(self, o):
533
                if self.is_set != o.is_set:
534
                    return False
535
                if self.mode != o.mode:
536
                    return False
537
                if self.mode == "Tone" and self.tone != o.tone:
538
                    return False
539
                # For DTCS, we consider different polarities as
540
                # "equal", i.e. that's not a "Cross" mode.
541
                if self.mode == "DTCS" and self.dtcs_index != o.dtcs_index:
542
                    return False
543
                return True
544

    
545
        rx = ToneWord(_mem.rxtone)
546
        tx = ToneWord(_mem.txtone)
547

    
548
        # The logic here gets a bit complicated, because this radio
549
        # lets you do any kind of crossing and just stores "what do I
550
        # need to send" and "what do I expect to receive" for each
551
        # channel. Chirp likes symmetry more, e.g. TSQL is
552
        # "Tone->Tone" when the tones are equal. The wiki page at
553
        # https://chirp.danplanet.com/projects/chirp/wiki/DevelopersToneModes
554
        # has an overview of Chirp's tone modes.
555
        # The reverse logic in set_memory is maybe easier to follow.
556
        if tx.equals(rx):
557
            if tx.is_ctcss:
558
                mem.rtone = float(int(tx.tone)) / 10.0
559
                mem.ctone = float(int(tx.tone)) / 10.0
560
                mem.tmode = "TSQL"
561
            elif tx.is_dtcs:
562
                mem.dtcs = chirp_common.ALL_DTCS_CODES[tx.dtcs_index]
563
                mem.rx_dtcs = chirp_common.ALL_DTCS_CODES[tx.dtcs_index]
564
                mem.tmode = "DTCS"
565
            else:
566
                mem.tmode = ""
567
        elif tx.is_ctcss and not rx.is_set:
568
            mem.tmode = "Tone"
569
            mem.rtone = float(int(tx.tone)) / 10.0
570
        else:
571
            mem.tmode = "Cross"
572
            mem.cross_mode = "%s->%s" % (tx.mode, rx.mode)
573
            if rx.is_ctcss:
574
                mem.ctone = float(int(rx.tone)) / 10.0
575
            elif rx.is_dtcs:
576
                mem.rx_dtcs = chirp_common.ALL_DTCS_CODES[rx.dtcs_index]
577
            if tx.is_ctcss:
578
                mem.rtone = float(int(tx.tone)) / 10.0
579
            elif tx.is_dtcs:
580
                mem.dtcs = chirp_common.ALL_DTCS_CODES[tx.dtcs_index]
581

    
582
        mem.dtcs_polarity = "%s%s" % (tx.dtcs_pol, rx.dtcs_pol)
583

    
584
        mem.extra = RadioSettingGroup("Extra", "extra")
585
        mem.extra.append(RadioSetting("busylock", "Busy lock",
586
                                      RadioSettingValueBoolean(_mem.busylock)))
587
        mem.extra.append(RadioSetting("scanadd", "Scan add",
588
                                      RadioSettingValueBoolean(_mem.scanadd)))
589

    
590
        if mem.freq == 0:
591
            mem.empty = True
592

    
593
        return mem
594

    
595
    def set_memory(self, mem):
596
        # Get a low-level memory object mapped to the image
597
        if mem.number < 0:
598
            _mem = self._memobj.__getattr__(SPECIALS[mem.extd_number][0])
599
        else:
600
            _mem = self._memobj.memory[mem.number]
601
            _nam = self._memobj.names[mem.number]
602
            _nam.name = self._ascii2quansheng(mem.name, 8)
603

    
604
        if mem.empty:
605
            _mem.set_raw(b'\xa5' * 8 + b'\xff' * 8)
606
            return
607

    
608
        # Convert to low-level frequency representation
609
        _mem.rxfreq = mem.freq / 10
610
        if mem.duplex in [None, "", "off"]:
611
            _mem.txfreq = _mem.rxfreq
612
        elif mem.duplex == "+":
613
            _mem.txfreq = (mem.freq + mem.offset) / 10
614
        elif mem.duplex == "-":
615
            _mem.txfreq = (mem.freq - mem.offset) / 10
616
        else:  # "split", aka odd split
617
            _mem.txfreq = mem.offset / 10
618

    
619
        _mem.power = (mem.power != POWER_LEVELS[0])
620
        _mem.width = (mem.mode == "FM")
621

    
622
        def encodetone(mode, tone, dtcs, polarity):
623
            """This is the reverse of decodetone in get_memory."""
624
            if mode == "":
625
                return 0xffff
626
            elif mode == "Tone":
627
                return 0x0fff & int(tone * 10)
628
            elif mode == "DTCS":
629
                return (
630
                    (0x8000 if polarity == "R" else 0x0000) |
631
                    (0x2000) |
632
                    (0x0fff & (chirp_common.ALL_DTCS_CODES.index(dtcs)))
633
                )
634

    
635
        if mem.tmode == "Cross":
636
            txmode, rxmode = mem.cross_mode.split("->")
637
            _mem.txtone = encodetone(txmode, mem.rtone,
638
                                     mem.dtcs, mem.dtcs_polarity[0])
639
            _mem.rxtone = encodetone(rxmode, mem.ctone,
640
                                     mem.rx_dtcs, mem.dtcs_polarity[1])
641
        elif mem.tmode == "Tone":
642
            _mem.txtone = encodetone("Tone", mem.rtone,
643
                                     mem.dtcs, mem.dtcs_polarity[0])
644
            _mem.rxtone = 0xffff
645
        elif mem.tmode == "TSQL":
646
            _mem.txtone = encodetone("Tone", mem.ctone,
647
                                     mem.dtcs, mem.dtcs_polarity[0])
648
            _mem.rxtone = encodetone("Tone", mem.ctone,
649
                                     mem.dtcs, mem.dtcs_polarity[1])
650
        elif mem.tmode == "DTCS":
651
            _mem.txtone = encodetone("DTCS", mem.rtone,
652
                                     mem.dtcs, mem.dtcs_polarity[0])
653
            _mem.rxtone = encodetone("DTCS", mem.ctone,
654
                                     mem.dtcs, mem.dtcs_polarity[1])
655
        else:
656
            _mem.txtone = 0xffff
657
            _mem.rxtone = 0xffff
658

    
659
        # TODO: we may want to work around issue 4121: settings is
660
        # empty when editing in the "table" interface. See UV5R driver
661
        # for a working approach.
662
        for setting in mem.extra:
663
            setattr(_mem, setting.get_name(), setting.value)
664

    
665

    
(4-4/4)