Project

General

Profile

New Model #10334 » iradio_uv_5118plus-v0.1.py

Jim Unroe, 03/03/2023 08:42 PM

 
1
# Copyright 2023 Jim Unroe <rock.unroe@gmail.com>
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 time
17
import os
18
import struct
19
import re
20
import logging
21

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

    
29
LOG = logging.getLogger(__name__)
30

    
31
MEM_FORMAT = """
32
struct memory {
33
  ul32 rxfreq;      // RX Frequency          00-03
34
  ul16 rx_tone;     // PL/DPL Decode         04-05
35
  ul32 txfreq;      // TX Frequency          06-09
36
  ul16 tx_tone;     // PL/DPL Encode         0a-0b
37
  ul24 mutecode;    // Mute Code             0c-0e
38
  u8 unknown_0:2,   //                       0f
39
     mutetype:2,    // Mute Type
40
     unknown_1:4;   //
41
  u8 isnarrow:1,    // Bandwidth             00
42
     lowpower:1,    // Power
43
     scan:1,        // Scan Add
44
     bcl:2,         // Busy Lock
45
     unknown_2:1,   //
46
     unknown_3:1,   //
47
     unknown_4:1;   //
48
  u8 unknown_5;     //                       01
49
  u8 unused_0:4,    //                       02
50
     scno:4;        // SC No.
51
  u8 unknown_6[3];  //                       03-05
52
  char name[10];    //                       06-0f
53
};
54

    
55
#seekto 0x1000;
56
struct memory channels[999];
57

    
58
#seekto 0x0000;
59
struct {
60
  char startuplabel[32];  // Startup Label         0000-001f
61
  char personalid[16];    // Personal ID           0020-002f
62
  u8 displaylogo:1,       // Display Startup Logo  0030
63
     displayvoltage:1,    // Display Voltage
64
     displaylabel:1,      // Display Startup Label
65
     tailtone:1,          // Tail Tone
66
     startupringtone:1,   // Startup Ringtone
67
     voiceprompt:1,       // Voice Prompt
68
     keybeep:1,           // Key Beep
69
     unknown_0:1;
70
  u8 txpriority:1,        // TX Priority           0031
71
     rogerbeep:2,         // Roger Beep
72
     savemode:1,          // Save Mode
73
     frequencystep:4;     // Frequency Step
74
  u8 squelch:4,           // Squelch               0032
75
     talkaround:2,        // Talkaround
76
     noaaalarm:1,         // NOAA Alarm
77
     dualdisplay:1;       // Dual Display
78
  u8 displaytimer;        // Display Timer         0033
79
  u8 locktimer;           // Lock Timer            0034
80
  u8 timeouttimer;        // Timeout Timer         0035
81
  u8 voxlevel:4,          // VOX Level             0036
82
     voxdelay:4;          // Delay
83
  ul16 tonefrequency;     // Tone Frequency        0037-0038
84
  ul16 fmfrequency;       // FM Frequency          0039-003a
85
  u8 fmstandby:1,         // FM Standby            003b
86
     dualstandby:1,       // Dual Standby
87
     standbyarea:1,       // Standby Area
88
     scandirection:1,     // Scan Direction
89
     unknown_2:2,
90
     workmode:1,          // Work Mode
91
     unknown_3:1;
92
  ul16 areaach;           // Area A CH             003c-003d
93
  ul16 areabch;           // Area B CH             003e-003f
94
  u8 unused_0:4,          //                       0040
95
     key1long:4;          // Key 1 Long
96
  u8 unused_1:4,          //                       0041
97
     key1short:4;         // Key 1 Short
98
  u8 unused_2:4,          //                       0042
99
     key2long:4;          // Key 2 Long
100
  u8 unused_3:4,          //                       0043
101
     key2short:4;         // Key 2 Short
102
  u8 unknown_4:4,         //                       0044
103
     vox:1,               // VOX
104
     unknown_5:3;
105
  u8 xposition;           // X position (0-159)    0045
106
  u8 yposition;           // Y position (0-110)    0046
107
  ul16 bordercolor;       // Border  Color         0047-0048
108
  u8 unknown_6[9];        // 0x00                  0049-0051
109
  u8 unknown_7[2];        // 0xFF                  0052-0053
110
  u8 range174_240;        // 174-240 MHz           0054
111
  u8 range240_320;        // 240-320 MHz           0055
112
  u8 range320_400;        // 320-400 MHz           0056
113
  u8 range480_560;        // 480-560 MHz           0057
114
  u8 unused_4[7];         // 0xFF                  0058-005e
115
  u8 unknown_8;           // 0x00                  005f
116
  u8 unused_5[12];        // 0xFF                  0060-006b
117
  u8 unknown_9[4];        // 0x00                  006c-006f
118
  ul16 quickch2;          // Quick CH 2            0070-0071
119
  ul16 quickch1;          // Quick CH 1            0072-0073
120
  ul16 quickch4;          // Quick CH 4            0074-0075
121
  ul16 quickch3;          // Quick CH 3            0076-0077
122
} settings;
123

    
124
#seekto 0x8D20;
125
struct {
126
  u8 senddelay;           // Send Delay            8d20
127
  u8 sendinterval;        // Send Interval         8d21
128
  u8 unused_0:6,          //                       8d22
129
     sendmode:2;          // Send Mode
130
  u8 unused_2:4,          //                       8d23
131
     sendselect:4;        // Send Select
132
  u8 unused_3:7,          //                       8d24
133
     recvdisplay:1;       // Recv Display
134
  u8 encodegain;          // Encode Gain           8d25
135
  u8 decodeth;            // Decode TH             8d26
136
} dtmf;
137

    
138
#seekto 0x8D30;
139
struct {
140
  char code[14];          // DTMF code
141
  u8 unused_ff;
142
  u8 code_len;            // DTMF code length
143
} dtmfcode[16];
144

    
145
#seekto 0x8E30;
146
struct {
147
  char kill[14];          // Remotely Kill         8e30-8e3d
148
  u8 unknown_0;           //                       8e3e
149
  u8 kill_len;            // Remotely Kill Length  83ef
150
  char stun[14];          // Remotely Stun         8e40-834d
151
  u8 unknown_1;           //                       8e4e
152
  u8 stun_len;            // Remotely Stun Length  8e4f
153
  char wakeup[14];        // Wake Up               8e50-8e5d
154
  u8 unknown_2;           //                       8e5e
155
  u8 wakeup_len;          // Wake Up Length        8e5f
156
} dtmf2;
157

    
158
"""
159

    
160
CMD_ACK = b"\x06"
161

    
162
DTCS_CODES = tuple(sorted(chirp_common.DTCS_CODES + (645,)))
163

    
164
_STEP_LIST = [0.25, 1.25, 2.5, 5., 6.25, 10., 12.5, 25., 50., 100., 500.,
165
              1000., 5000.]
166

    
167
LIST_AB = ["A", "B"]
168
LIST_BCL = ["Off", "Carrier", "CTC/DCS"]
169
LIST_CHREPORT = ["CH Number", "CH Name"]
170
LIST_DELAY = ["%s ms" % x for x in range(0, 2100, 100)]
171
LIST_DIRECTION = ["Up", "Down"]
172
LIST_FREQSTEP = ["0.25K", "1.25K", "2.5K", "5K", "6.25K", "10K", "12.5K",
173
                 "20K", "25K", "50K", "100K", "500K", "1M", "5M"]
174
LIST_INTERVAL = ["%s ms" % x for x in range(30, 210, 10)]
175
LIST_MUTETYPE = ["Off", "-", "23b", "24b"]
176
LIST_ROGER = ["Off", "Roger 1", "Roger 2", "Send ID"]
177
LIST_SENDM = ["Off", "TX Start", "TX End", "Start and End"]
178
LIST_SENDS = ["DTMF %s" % x for x in range(1, 17)]
179
LIST_SKEY = ["None", "Monitor", "Frequency Detect", "Talkaround",
180
             "Quick CH", "Local Alarm", "Remote Alarm", "Weather CH",
181
             "Send Tone", "Roger Beep"]
182
LIST_REPEATER = ["Off", "Talkaround", "Frequency Reversal"]
183
LIST_TIMER = ["Off", "5 seconds", "10 seconds"] + [
184
              "%s seconds" % x for x in range(15, 615, 15)]
185
LIST_TXPRI = ["Edit", "Busy"]
186
LIST_WORKMODE = ["Frequency", "Channel"]
187

    
188
TXALLOW_CHOICES = ["RX Only", "TX/RX"]
189
TXALLOW_VALUES = [0xFF, 0x00]
190

    
191
VALID_CHARS = chirp_common.CHARSET_ALPHANUMERIC + \
192
    "`{|}!\"#$%&'()*+,-./:;<=>?@[]^_"
193
DTMF_CHARS = list("0123456789ABCD*#")
194

    
195

    
196
def _checksum(data):
197
    cs = 0
198
    for byte in data:
199
        cs += byte
200
    return cs % 256
201

    
202

    
203
def _enter_programming_mode(radio):
204
    serial = radio.pipe
205

    
206
    # lengthen the timeout here as these radios are reseting due to timeout
207
    radio.pipe.timeout = 0.75
208

    
209
    exito = False
210
    for i in range(0, 5):
211
        serial.write(radio.magic)
212
        ack = serial.read(1)
213

    
214
        try:
215
            if ack == CMD_ACK:
216
                exito = True
217
                break
218
        except:
219
            LOG.debug("Attempt #%s, failed, trying again" % i)
220
            pass
221

    
222
    # return timeout to default value
223
    radio.pipe.timeout = 0.25
224

    
225
    # check if we had EXITO
226
    if exito is False:
227
        msg = "The radio did not accept program mode after five tries.\n"
228
        msg += "Check you interface cable and power cycle your radio."
229
        raise errors.RadioError(msg)
230

    
231

    
232
def _exit_programming_mode(radio):
233
    serial = radio.pipe
234
    try:
235
        serial.write(b"58" + b"\x05\xEE\x60")
236
    except:
237
        raise errors.RadioError("Radio refused to exit programming mode")
238

    
239

    
240
def _read_block(radio, block_addr, block_size):
241
    serial = radio.pipe
242

    
243
    cmd = struct.pack(">BH", ord(b'R'), block_addr + radio.READ_OFFSET)
244

    
245
    ccs = bytes([_checksum(cmd)])
246

    
247
    expectedresponse = b"R" + cmd[1:]
248

    
249
    cmd = cmd + ccs
250

    
251
    LOG.debug("Reading block %04x..." % block_addr)
252

    
253
    try:
254
        serial.write(cmd)
255
        response = serial.read(3 + block_size + 1)
256

    
257
        cs = _checksum(response[:-1])
258

    
259
        if response[:3] != expectedresponse:
260
            raise Exception("Error reading block %04x." % block_addr)
261

    
262
        chunk = response[3:]
263

    
264
        if chunk[-1] != cs:
265
            raise Exception("Block failed checksum!")
266

    
267
        block_data = chunk[:-1]
268
    except:
269
        raise errors.RadioError("Failed to read block at %04x" % block_addr)
270

    
271
    return block_data
272

    
273

    
274
def _write_block(radio, block_addr, block_size):
275
    serial = radio.pipe
276

    
277
    # map the upload address to the mmap start and end addresses
278
    start_addr = block_addr * block_size
279
    end_addr = start_addr + block_size
280

    
281
    data = radio.get_mmap()[start_addr:end_addr]
282

    
283
    cmd = struct.pack(">BH", ord(b'I'), block_addr)
284

    
285
    cs = bytes([_checksum(cmd + data)])
286
    data += cs
287

    
288
    LOG.debug("Writing Data:")
289
    LOG.debug(util.hexprint(cmd + data))
290

    
291
    try:
292
        serial.write(cmd + data)
293
        if serial.read(1) != CMD_ACK:
294
            raise Exception("No ACK")
295
    except:
296
        raise errors.RadioError("Failed to send block "
297
                                "to radio at %04x" % block_addr)
298

    
299

    
300
def do_download(radio):
301
    LOG.debug("download")
302
    _enter_programming_mode(radio)
303

    
304
    data = b""
305

    
306
    status = chirp_common.Status()
307
    status.msg = "Cloning from radio"
308

    
309
    status.cur = 0
310
    status.max = radio.END_ADDR
311

    
312
    for addr in range(radio.START_ADDR, radio.END_ADDR, 1):
313
        status.cur = addr
314
        radio.status_fn(status)
315

    
316
        block = _read_block(radio, addr, radio.BLOCK_SIZE)
317
        data += block
318

    
319
        LOG.debug("Address: %04x" % addr)
320
        LOG.debug(util.hexprint(block))
321

    
322
    _exit_programming_mode(radio)
323

    
324
    return memmap.MemoryMapBytes(data)
325

    
326

    
327
def _split(rf, f1, f2):
328
    """Returns False if the two freqs are in the same band (no split)
329
    or True otherwise"""
330

    
331
    # determine if the two freqs are in the same band
332
    for low, high in rf.valid_bands:
333
        if f1 >= low and f1 <= high and \
334
                f2 >= low and f2 <= high:
335
            # if the two freqs are on the same Band this is not a split
336
            return False
337

    
338
    # if you get here is because the freq pairs are split
339
    return True
340

    
341

    
342
class IradioUV5118plus(chirp_common.CloneModeRadio):
343
    """IRADIO UV5118plus"""
344
    VENDOR = "Iradio"
345
    MODEL = "UV-5118plus"
346
    NAME_LENGTH = 10
347
    BAUD_RATE = 115200
348
    NEEDS_COMPAT_SERIAL = False
349

    
350
    BLOCK_SIZE = 0x80
351
    magic = b"58" + b"\x05\x10\x82"
352

    
353
    airband = [108000000, 136000000]
354

    
355
    VALID_BANDS = [(108000000, 136000000),  # RX only (Air Band)
356
                   (136000000, 174000000),  # TX/RX (VHF)
357
                   (174000000, 240000000),  # TX/RX
358
                   (240000000, 320000000),  # TX/RX
359
                   (320000000, 400000000),  # TX/RX
360
                   (400000000, 480000000),  # TX/RX (UHF)
361
                   (480000000, 560000000)]  # TX/RX
362

    
363
    POWER_LEVELS = [chirp_common.PowerLevel("High", watts=2.00),
364
                    chirp_common.PowerLevel("Low", watts=0.50)]
365

    
366
    # Radio's write address starts at 0x0000
367
    # Radio's write address ends at 0x0140
368
    START_ADDR = 0
369
    END_ADDR = 0x0140
370
    # Radio's read address starts at 0x7820
371
    # Radio's read address ends at 0x795F
372
    READ_OFFSET = 0x7820
373

    
374
    _ranges = [
375
               (0x0000, 0x0140),
376
              ]
377
    _memsize = 0xA000  # 0x0140 * 0x80
378

    
379
    _upper = 999
380

    
381
    def get_features(self):
382
        rf = chirp_common.RadioFeatures()
383
        rf.has_settings = False
384
        rf.has_bank = False
385
        rf.has_ctone = True
386
        rf.has_cross = True
387
        rf.has_rx_dtcs = True
388
        rf.has_tuning_step = False
389
        rf.can_odd_split = True
390
        rf.has_name = True
391
        rf.valid_name_length = self.NAME_LENGTH
392
        rf.valid_characters = chirp_common.CHARSET_ASCII
393
        rf.valid_skips = ["", "S"]
394
        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS", "Cross"]
395
        rf.valid_cross_modes = ["Tone->Tone", "Tone->DTCS", "DTCS->Tone",
396
                                "->Tone", "->DTCS", "DTCS->", "DTCS->DTCS"]
397
        rf.valid_power_levels = self.POWER_LEVELS
398
        rf.valid_duplexes = ["", "-", "+", "split"]
399
        rf.valid_modes = ["FM", "NFM"]  # 25 KHz, 12.5 KHz.
400
        rf.valid_dtcs_codes = DTCS_CODES
401
        rf.memory_bounds = (1, self._upper)
402
        rf.valid_tuning_steps = _STEP_LIST
403
        rf.valid_bands = self.VALID_BANDS
404

    
405
        return rf
406

    
407
    def process_mmap(self):
408
        self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
409

    
410
    def sync_in(self):
411
        """Download from radio"""
412
        try:
413
            data = do_download(self)
414
        except errors.RadioError:
415
            # Pass through any real errors we raise
416
            raise
417
        except:
418
            # If anything unexpected happens, make sure we raise
419
            # a RadioError and log the problem
420
            LOG.exception('Unexpected error during download')
421
            raise errors.RadioError('Unexpected error communicating '
422
                                    'with the radio')
423
        self._mmap = data
424
        self.process_mmap()
425

    
426
    def sync_out(self):
427
        """Upload to radio"""
428
        try:
429
            do_upload(self)
430
        except:
431
            # If anything unexpected happens, make sure we raise
432
            # a RadioError and log the problem
433
            LOG.exception('Uploading is not impleted')
434
            raise errors.RadioError('Uploading is not impleted')
435

    
436
    def get_raw_memory(self, number):
437
        return repr(self._memobj.memory[number - 1])
438

    
439
    @staticmethod
440
    def _decode_tone(toneval):
441
        # DCS examples:
442
        # D023N - 1013 - 0001 0000 0001 0011
443
        #                   ^-DCS
444
        # D023I - 2013 - 0010 0000 0001 0100
445
        #                  ^--DCS inverted
446
        # D754I - 21EC - 0010 0001 1110 1100
447
        #    code in octal-------^^^^^^^^^^^
448

    
449
        if toneval == 0x3000:
450
            return '', None, None
451
        elif toneval & 0x1000:
452
            # DTCS N
453
            code = int('%o' % (toneval & 0x1FF))
454
            return 'DTCS', code, 'N'
455
        elif toneval & 0x2000:
456
            # DTCS R
457
            code = int('%o' % (toneval & 0x1FF))
458
            return 'DTCS', code, 'R'
459
        else:
460
            return 'Tone', toneval / 10.0, None
461

    
462
    @staticmethod
463
    def _encode_tone(mode, val, pol):
464
        if not mode:
465
            return 0x3000
466
        elif mode == 'Tone':
467
            return int(val * 10)
468
        elif mode == 'DTCS':
469
            code = int('%i' % val, 8)
470
            if pol == 'N':
471
                code |= 0x1800
472
            if pol == 'R':
473
                code |= 0x2800
474
            return code
475
        else:
476
            raise errors.RadioError('Unsupported tone mode %r' % mode)
477

    
478
    def get_memory(self, number):
479
        mem = chirp_common.Memory()
480
        _mem = self._memobj.channels[number - 1]
481
        mem.number = number
482

    
483
        mem.freq = int(_mem.rxfreq) * 10
484

    
485
        # We'll consider any blank (i.e. 0MHz frequency) to be empty
486
        if mem.freq == 0:
487
            mem.empty = True
488
            return mem
489

    
490
        if _mem.rxfreq.get_raw() == "\xFF\xFF\xFF\xFF":
491
            mem.freq = 0
492
            mem.empty = True
493
            return mem
494

    
495
        #if _mem.get_raw() == ("\xFF" * 16):
496
        #    LOG.debug("Initializing empty memory")
497
        #    _mem.set_raw("\xFF" * 4 + "\x00\x30" + "\xFF" * 4 + "\x00\x30" +
498
        #                 "\x00" * 4)
499

    
500
        # Freq and offset
501
        mem.freq = int(_mem.rxfreq) * 10
502
        # TX freq set
503
        offset = (int(_mem.txfreq) * 10) - mem.freq
504
        if offset != 0:
505
            if _split(self.get_features(), mem.freq, int(
506
                      _mem.txfreq) * 10):
507
                mem.duplex = "split"
508
                mem.offset = int(_mem.txfreq) * 10
509
            elif offset < 0:
510
                mem.offset = abs(offset)
511
                mem.duplex = "-"
512
            elif offset > 0:
513
                mem.offset = offset
514
                mem.duplex = "+"
515
        else:
516
            mem.offset = 0
517

    
518
        mem.name = str(_mem.name).rstrip('\xFF ')
519

    
520
        mem.mode = _mem.isnarrow and "NFM" or "FM"
521

    
522
        chirp_common.split_tone_decode(mem,
523
                                       self._decode_tone(_mem.tx_tone),
524
                                       self._decode_tone(_mem.rx_tone))
525

    
526
        mem.power = self.POWER_LEVELS[_mem.lowpower]
527

    
528
        if not _mem.scan:
529
            mem.skip = "S"
530

    
531
        mem.extra = RadioSettingGroup("Extra", "extra")
532

    
533
        rs = RadioSettingValueList(LIST_BCL, LIST_BCL[_mem.bcl])
534
        rset = RadioSetting("bcl", "Busy Channel Lockout", rs)
535
        mem.extra.append(rset)
536

    
537
        rs = RadioSettingValueList(LIST_MUTETYPE, LIST_MUTETYPE[_mem.mutetype])
538
        rset = RadioSetting("mutetype", "Mute Type", rs)
539
        mem.extra.append(rset)
540

    
541
        rs = RadioSettingValueInteger(0, 16777215, _mem.mutecode)
542
        rset = RadioSetting("mutecode", "Mute Code", rs)
543
        mem.extra.append(rset)
544

    
545
        rs = RadioSettingValueInteger(0, 8, _mem.scno)
546
        rset = RadioSetting("scno", "SC No.", rs)
547
        mem.extra.append(rset)
548

    
549
        return mem
550

    
551
    def set_memory(self, mem):
552
        LOG.debug("Setting %i(%s)" % (mem.number, mem.extd_number))
553
        _mem = self._memobj.channels[mem.number - 1]
554

    
555
        # if empty memmory
556
        if mem.empty:
557
            _mem.set_raw("\xFF" * 22 + "\20" * 10)
558
            return
559

    
560
        _mem.set_raw("\xFF" * 4 + "\x00\x30" + "\xFF" * 4 + "\x00\x30" +
561
                     "\x00" * 10 + "\x20" * 10)
562

    
563
        _mem.rxfreq = mem.freq / 10
564

    
565
        if mem.duplex == "split":
566
            _mem.txfreq = mem.offset / 10
567
        elif mem.duplex == "+":
568
            _mem.txfreq = (mem.freq + mem.offset) / 10
569
        elif mem.duplex == "-":
570
            _mem.txfreq = (mem.freq - mem.offset) / 10
571
        else:
572
            _mem.txfreq = mem.freq / 10
573

    
574
        _mem.name = mem.name.rstrip(' ').ljust(10, '\xFF')
575

    
576
        _mem.scan = mem.skip != "S"
577
        _mem.isnarrow = mem.mode == "NFM"
578

    
579
        dtcs_pol = ["N", "N"]
580

    
581
        txtone, rxtone = chirp_common.split_tone_encode(mem)
582
        _mem.tx_tone = self._encode_tone(*txtone)
583
        _mem.rx_tone = self._encode_tone(*rxtone)
584

    
585
        _mem.lowpower = mem.power == self.POWER_LEVELS[1]
586

    
587
        for setting in mem.extra:
588
            setattr(_mem, setting.get_name(), setting.value)
589

    
590
    @classmethod
591
    def match_model(cls, filedata, filename):
592
        # This radio has always been post-metadata, so never do
593
        # old-school detection
594
        return False
595

    
596

    
597
@directory.register
598
class RuyageUV58PlusRadio(IradioUV5118plus):
599
    """Ruyage UV58Plus"""
600
    VENDOR = "Ruyage"
601
    MODEL = "UV58Plus"
(3-3/3)