Project

General

Profile

Bug #10414 » alinco.py

Dan Smith, 03/06/2023 11:59 PM

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

    
17
from chirp import chirp_common, bitwise, memmap, errors, directory, util
18
from chirp.settings import RadioSettingGroup, RadioSetting
19
from chirp.settings import RadioSettingValueBoolean, RadioSettings
20

    
21
from textwrap import dedent
22

    
23
import time
24
import logging
25
import codecs
26

    
27
LOG = logging.getLogger(__name__)
28

    
29

    
30
DRX35_MEM_FORMAT = """
31
#seekto 0x0120;
32
u8 used_flags[25];
33

    
34
#seekto 0x0200;
35
struct {
36
  u8 new_used:1,
37
     unknown1:1,
38
     isnarrow:1,
39
     isdigital:1,
40
     ishigh:1,
41
     unknown2:3;
42
  u8 unknown3:6,
43
     duplex:2;
44
  u8 unknown4:4,
45
     tmode:4;
46
  u8 unknown5:4,
47
     step:4;
48
  bbcd freq[4];
49
  u8 unknown6[1];
50
  bbcd offset[3];
51
  u8 rtone;
52
  u8 ctone;
53
  u8 dtcs_tx;
54
  u8 dtcs_rx;
55
  u8 name[7];
56
  u8 unknown8[2];
57
  u8 unknown9:6,
58
     power:2;
59
  u8 unknownA[6];
60
} memory[100];
61

    
62
#seekto 0x0130;
63
u8 skips[25];
64
"""
65

    
66
# 0000 0111
67
# 0000 0010
68

    
69
# Response length is:
70
# 1. \r\n
71
# 2. Four-digit address, followed by a colon
72
# 3. 16 bytes in hex (32 characters)
73
# 4. \r\n
74
RLENGTH = 2 + 5 + 32 + 2
75

    
76
STEPS = [5.0, 10.0, 12.5, 15.0, 20.0, 25.0, 30.0]
77

    
78

    
79
class AlincoStyleRadio(chirp_common.CloneModeRadio):
80
    """Base class for all known Alinco radios"""
81
    _memsize = 0
82
    _model = b"NONE"
83
    NEEDS_COMPAT_SERIAL = False
84

    
85
    def _send(self, data):
86
        LOG.debug("PC->R: (%2i)\n%s" % (len(data), util.hexprint(data)))
87
        self.pipe.write(data)
88
        self.pipe.read(len(data))
89

    
90
    def _read(self, length):
91
        data = self.pipe.read(length)
92
        LOG.debug("R->PC: (%2i)\n%s" % (len(data), util.hexprint(data)))
93
        return data
94

    
95
    def _download_chunk(self, addr):
96
        if addr % 16:
97
            raise Exception("Addr 0x%04x not on 16-byte boundary" % addr)
98

    
99
        cmd = b"AL~F%04XR\r\n" % addr
100
        self._send(cmd)
101

    
102
        resp = self._read(RLENGTH).strip()
103
        if len(resp) == 0:
104
            raise errors.RadioError("No response from radio")
105
        if b":" not in resp:
106
            raise errors.RadioError("Unexpected response from radio")
107
        addr, _data = resp.split(b":", 1)
108
        data = codecs.decode(_data, 'hex')
109

    
110
        if len(data) != 16:
111
            LOG.debug("Response was:")
112
            LOG.debug("|%s|")
113
            LOG.debug("Which I converted to:")
114
            LOG.debug(util.hexprint(data))
115
            raise Exception("Radio returned less than 16 bytes")
116

    
117
        return data
118

    
119
    def _download(self, limit):
120
        self._identify()
121

    
122
        data = b""
123
        for addr in range(0, limit, 16):
124
            time.sleep(0.1)
125
            data += self._download_chunk(addr)
126

    
127
            if self.status_fn:
128
                status = chirp_common.Status()
129
                status.cur = addr + 16
130
                status.max = self._memsize
131
                status.msg = "Downloading from radio"
132
                self.status_fn(status)
133

    
134
        self._send(b"AL~E\r\n")
135
        self._read(20)
136

    
137
        return memmap.MemoryMap(data)
138

    
139
    def _identify(self):
140
        for _i in range(0, 3):
141
            self._send(b"%s\r\n" % self._model)
142
            resp = self._read(6)
143
            if resp.strip() == b"OK":
144
                return True
145
            time.sleep(1)
146

    
147
        return False
148

    
149
    def _upload_chunk(self, addr):
150
        if addr % 16:
151
            raise Exception("Addr 0x%04x not on 16-byte boundary" % addr)
152

    
153
        _data = self._mmap[addr:addr+16]
154
        data = codecs.encode(_data, 'hex').upper()
155

    
156
        cmd = b"AL~F%04XW%s\r\n" % (addr, data)
157
        self._send(cmd)
158

    
159
    def _upload(self, limit):
160
        if not self._identify():
161
            raise Exception("I can't talk to this model")
162

    
163
        for addr in range(0x100, limit, 16):
164
            time.sleep(0.1)
165
            self._upload_chunk(addr)
166

    
167
            if self.status_fn:
168
                status = chirp_common.Status()
169
                status.cur = addr + 16
170
                status.max = self._memsize
171
                status.msg = "Uploading to radio"
172
                self.status_fn(status)
173

    
174
        self._send(b"AL~E\r\n")
175
        self._read(20)
176

    
177
    def process_mmap(self):
178
        self._memobj = bitwise.parse(DRX35_MEM_FORMAT, self._mmap)
179

    
180
    def sync_in(self):
181
        try:
182
            self._mmap = self._download(self._memsize)
183
        except errors.RadioError:
184
            raise
185
        except Exception as e:
186
            raise errors.RadioError("Failed to communicate with radio: %s" % e)
187
        self.process_mmap()
188

    
189
    def sync_out(self):
190
        try:
191
            self._upload(self._memsize)
192
        except errors.RadioError:
193
            raise
194
        except Exception as e:
195
            raise errors.RadioError("Failed to communicate with radio: %s" % e)
196

    
197
    def get_raw_memory(self, number):
198
        return repr(self._memobj.memory[number])
199

    
200

    
201
DUPLEX = ["", "-", "+"]
202
TMODES = ["", "Tone", "", "TSQL"] + [""] * 12
203
TMODES[12] = "DTCS"
204
DCS_CODES = {
205
    "Alinco": chirp_common.DTCS_CODES,
206
    "Jetstream": (17,) + chirp_common.DTCS_CODES,
207
}
208

    
209
CHARSET = (["\x00"] * 0x30) + \
210
    [chr(x + ord("0")) for x in range(0, 10)] + \
211
    [chr(x + ord("A")) for x in range(0, 26)] + [" "] + \
212
    list("\x00" * 128)
213

    
214

    
215
def _get_name(_mem):
216
    name = ""
217
    for i in _mem.name:
218
        if i in [0x00, 0xFF]:
219
            break
220
        name += CHARSET[i]
221
    return name
222

    
223

    
224
def _set_name(mem, _mem):
225
    name = [0x00] * 7
226
    j = 0
227
    for i in range(0, 7):
228
        try:
229
            name[j] = CHARSET.index(mem.name[i])
230
            j += 1
231
        except IndexError:
232
            pass
233
        except ValueError:
234
            pass
235
    return name
236

    
237
ALINCO_TONES = list(chirp_common.TONES)
238
ALINCO_TONES.remove(159.8)
239
ALINCO_TONES.remove(165.5)
240
ALINCO_TONES.remove(171.3)
241
ALINCO_TONES.remove(177.3)
242
ALINCO_TONES.remove(183.5)
243
ALINCO_TONES.remove(189.9)
244
ALINCO_TONES.remove(196.6)
245
ALINCO_TONES.remove(199.5)
246
ALINCO_TONES.remove(206.5)
247
ALINCO_TONES.remove(229.1)
248
ALINCO_TONES.remove(254.1)
249

    
250

    
251
class DRx35Radio(AlincoStyleRadio):
252
    """Base class for the DR-x35 radios"""
253
    _range = [(118000000, 155000000)]
254
    _power_levels = []
255
    _valid_tones = ALINCO_TONES
256

    
257
    def get_features(self):
258
        rf = chirp_common.RadioFeatures()
259
        rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"]
260
        rf.valid_modes = ["FM", "NFM"]
261
        rf.valid_skips = ["", "S"]
262
        rf.valid_bands = self._range
263
        rf.memory_bounds = (0, 99)
264
        rf.has_ctone = True
265
        rf.has_bank = False
266
        rf.has_dtcs_polarity = False
267
        rf.can_delete = False
268
        rf.valid_tuning_steps = STEPS
269
        rf.valid_name_length = 7
270
        rf.valid_power_levels = self._power_levels
271
        rf.valid_dtcs_codes = DCS_CODES[self.VENDOR]
272
        return rf
273

    
274
    def _get_used(self, number):
275
        _usd = self._memobj.used_flags[number / 8]
276
        bit = (0x80 >> (number % 8))
277
        return _usd & bit
278

    
279
    def _set_used(self, number, is_used):
280
        _usd = self._memobj.used_flags[number / 8]
281
        bit = (0x80 >> (number % 8))
282
        if is_used:
283
            _usd |= bit
284
        else:
285
            _usd &= ~bit
286

    
287
    def _get_power(self, _mem):
288
        if self._power_levels:
289
            return self._power_levels[_mem.ishigh]
290
        return None
291

    
292
    def _set_power(self, _mem, mem):
293
        if self._power_levels:
294
            _mem.ishigh = mem.power is None or \
295
                mem.power == self._power_levels[1]
296

    
297
    def _get_extra(self, _mem, mem):
298
        mem.extra = RadioSettingGroup("extra", "Extra")
299
        dig = RadioSetting("isdigital", "Digital",
300
                           RadioSettingValueBoolean(bool(_mem.isdigital)))
301
        dig.set_doc("Digital/Packet mode enabled")
302
        mem.extra.append(dig)
303

    
304
    def _set_extra(self, _mem, mem):
305
        for setting in mem.extra:
306
            setattr(_mem, setting.get_name(), setting.value)
307

    
308
    def get_memory(self, number):
309
        _mem = self._memobj.memory[number]
310
        _skp = self._memobj.skips[number / 8]
311
        _usd = self._memobj.used_flags[number / 8]
312
        bit = (0x80 >> (number % 8))
313

    
314
        mem = chirp_common.Memory()
315
        mem.number = number
316
        if not self._get_used(number) and self.MODEL != "JT220M":
317
            mem.empty = True
318
            return mem
319
        elif self.MODEL == 'JT220M':
320
            mem.immutable = ['empty']
321

    
322
        mem.freq = int(_mem.freq) * 100
323
        mem.rtone = self._valid_tones[_mem.rtone]
324
        mem.ctone = self._valid_tones[_mem.ctone]
325
        mem.duplex = DUPLEX[_mem.duplex]
326
        mem.offset = int(_mem.offset) * 100
327
        mem.tmode = TMODES[_mem.tmode]
328
        mem.dtcs = DCS_CODES[self.VENDOR][_mem.dtcs_tx]
329
        mem.tuning_step = STEPS[_mem.step]
330

    
331
        if _mem.isnarrow:
332
            mem.mode = "NFM"
333

    
334
        mem.power = self._get_power(_mem)
335

    
336
        if _skp & bit:
337
            mem.skip = "S"
338

    
339
        mem.name = _get_name(_mem).rstrip()
340

    
341
        self._get_extra(_mem, mem)
342

    
343
        return mem
344

    
345
    def set_memory(self, mem):
346
        _mem = self._memobj.memory[mem.number]
347
        _skp = self._memobj.skips[mem.number / 8]
348
        _usd = self._memobj.used_flags[mem.number / 8]
349
        bit = (0x80 >> (mem.number % 8))
350

    
351
        if self._get_used(mem.number) and not mem.empty:
352
            # Initialize the memory
353
            _mem.set_raw("\x00" * 32)
354

    
355
        self._set_used(mem.number, not mem.empty)
356
        if mem.empty:
357
            return
358

    
359
        _mem.freq = mem.freq / 100
360

    
361
        try:
362
            _tone = mem.rtone
363
            _mem.rtone = self._valid_tones.index(mem.rtone)
364
            _tone = mem.ctone
365
            _mem.ctone = self._valid_tones.index(mem.ctone)
366
        except ValueError:
367
            raise errors.UnsupportedToneError("This radio does not support " +
368
                                              "tone %.1fHz" % _tone)
369

    
370
        _mem.duplex = DUPLEX.index(mem.duplex)
371
        _mem.offset = mem.offset / 100
372
        _mem.tmode = TMODES.index(mem.tmode)
373
        _mem.dtcs_tx = DCS_CODES[self.VENDOR].index(mem.dtcs)
374
        _mem.dtcs_rx = DCS_CODES[self.VENDOR].index(mem.dtcs)
375
        _mem.step = STEPS.index(mem.tuning_step)
376

    
377
        _mem.isnarrow = mem.mode == "NFM"
378
        self._set_power(_mem, mem)
379

    
380
        if mem.skip:
381
            _skp |= bit
382
        else:
383
            _skp &= ~bit
384

    
385
        _mem.name = _set_name(mem, _mem)
386

    
387
        self._set_extra(_mem, mem)
388

    
389

    
390
@directory.register
391
class DR03Radio(DRx35Radio):
392
    """Alinco DR03"""
393
    VENDOR = "Alinco"
394
    MODEL = "DR03T"
395

    
396
    _model = b"DR135"
397
    _memsize = 4096
398
    _range = [(28000000, 29695000)]
399

    
400
    @classmethod
401
    def match_model(cls, filedata, filename):
402
        return len(filedata) == cls._memsize and \
403
                filedata[0x64:0x66] == b'\x00\x28'
404

    
405

    
406
@directory.register
407
class DR06Radio(DRx35Radio):
408
    """Alinco DR06"""
409
    VENDOR = "Alinco"
410
    MODEL = "DR06T"
411

    
412
    _model = b"DR435"
413
    _memsize = 4096
414
    _range = [(50000000, 53995000)]
415

    
416
    @classmethod
417
    def match_model(cls, filedata, filename):
418
        return len(filedata) == cls._memsize and \
419
                filedata[0x64:0x66] == b'\x00\x50'
420

    
421

    
422
@directory.register
423
class DR135Radio(DRx35Radio):
424
    """Alinco DR135"""
425
    VENDOR = "Alinco"
426
    MODEL = "DR135T"
427

    
428
    _model = b"DR135"
429
    _memsize = 4096
430
    _range = [(118000000, 173000000)]
431

    
432
    @classmethod
433
    def match_model(cls, filedata, filename):
434
        return len(filedata) == cls._memsize and \
435
                filedata[0x64:0x66] == b'\x01\x44'
436

    
437

    
438
@directory.register
439
class DR235Radio(DRx35Radio):
440
    """Alinco DR235"""
441
    VENDOR = "Alinco"
442
    MODEL = "DR235T"
443

    
444
    _model = b"DR235"
445
    _memsize = 4096
446
    _range = [(216000000, 280000000)]
447

    
448
    @classmethod
449
    def match_model(cls, filedata, filename):
450
        return len(filedata) == cls._memsize and \
451
                filedata[0x64:0x66] == b'\x02\x22'
452

    
453

    
454
@directory.register
455
class DR435Radio(DRx35Radio):
456
    """Alinco DR435"""
457
    VENDOR = "Alinco"
458
    MODEL = "DR435T"
459

    
460
    _model = b"DR435"
461
    _memsize = 4096
462
    _range = [(350000000, 511000000)]
463

    
464
    @classmethod
465
    def match_model(cls, filedata, filename):
466
        return len(filedata) == cls._memsize and \
467
                filedata[0x64:0x66] == b'\x04\x00'
468

    
469

    
470
@directory.register
471
class DJ596Radio(DRx35Radio):
472
    """Alinco DJ596"""
473
    VENDOR = "Alinco"
474
    MODEL = "DJ596"
475

    
476
    _model = b"DJ596"
477
    _memsize = 4096
478
    _range = [(136000000, 174000000), (400000000, 511000000)]
479
    _power_levels = [chirp_common.PowerLevel("Low", watts=1.00),
480
                     chirp_common.PowerLevel("High", watts=5.00)]
481

    
482
    @classmethod
483
    def match_model(cls, filedata, filename):
484
        return len(filedata) == cls._memsize and \
485
                filedata[0x64:0x66] == b'\x45\x01'
486

    
487

    
488
@directory.register
489
class JT220MRadio(DRx35Radio):
490
    """Jetstream JT220"""
491
    VENDOR = "Jetstream"
492
    MODEL = "JT220M"
493

    
494
    _model = b"DR136"
495
    _memsize = 8192
496
    _range = [(216000000, 280000000)]
497

    
498
    @classmethod
499
    def match_model(cls, filedata, filename):
500
        return len(filedata) == cls._memsize and \
501
            filedata[0x60:0x64] == b'2009'
502

    
503

    
504
@directory.register
505
class DJ175Radio(DRx35Radio):
506
    """Alinco DJ175"""
507
    VENDOR = "Alinco"
508
    MODEL = "DJ175"
509

    
510
    _model = b"DJ175"
511
    _memsize = 6896
512
    _range = [(136000000, 174000000), (400000000, 511000000)]
513
    _power_levels = [
514
        chirp_common.PowerLevel("Low", watts=0.50),
515
        chirp_common.PowerLevel("Mid", watts=2.00),
516
        chirp_common.PowerLevel("High", watts=5.00),
517
        ]
518

    
519
    @classmethod
520
    def match_model(cls, filedata, filename):
521
        return len(filedata) == cls._memsize
522

    
523
    def _get_used(self, number):
524
        return self._memobj.memory[number].new_used
525

    
526
    def _set_used(self, number, is_used):
527
        self._memobj.memory[number].new_used = is_used
528

    
529
    def _get_power(self, _mem):
530
        return self._power_levels[_mem.power]
531

    
532
    def _set_power(self, _mem, mem):
533
        if mem.power in self._power_levels:
534
            _mem.power = self._power_levels.index(mem.power)
535

    
536
    def _download_chunk(self, addr):
537
        if addr % 16:
538
            raise Exception("Addr 0x%04x not on 16-byte boundary" % addr)
539

    
540
        cmd = b"AL~F%04XR\r\n" % addr
541
        self._send(cmd)
542

    
543
        _data = self._read(34).strip()
544
        if len(_data) == 0:
545
            raise errors.RadioError("No response from radio")
546

    
547
        data = codecs.decode(_data, 'hex')
548

    
549
        if len(data) != 16:
550
            LOG.debug("Response was:")
551
            LOG.debug("|%r|" % _data)
552
            LOG.debug("Which I converted to:")
553
            LOG.debug(util.hexprint(data))
554
            raise Exception("Radio returned less than 16 bytes")
555

    
556
        return data
557

    
558

    
559
DJG7_MEM_FORMAT = """
560
#seekto 0x200;
561
ul16 bank[50];
562
ul16 special_bank[7];
563
#seekto 0x1200;
564
struct {
565
    u8   empty;
566
    ul32 freq;
567
    u8   mode;
568
    u8   step;
569
    ul32 offset;
570
    u8   duplex;
571
    u8   squelch_type;
572
    u8   tx_tone;
573
    u8   rx_tone;
574
    u8   dcs;
575
    ul24 unknown1;
576
    u8   skip;
577
    ul32 unknown2;
578
    ul32 unknown3;
579
    ul32 unknown4;
580
    char name[32];
581
} memory[1000];
582
"""
583

    
584

    
585
class AlincoDJG7(AlincoStyleRadio):
586
    """Alinco DJ-G7EG"""
587
    VENDOR = "Alinco"
588
    MODEL = "DJ-G7EG"
589
    BAUD_RATE = 57600
590

    
591
    # Those are different from the other Alinco radios.
592
    STEPS = [5.0, 6.25, 8.33, 10.0, 12.5, 15.0, 20.0, 25.0, 30.0, 50.0,
593
             100.0, 125.0, 150.0, 200.0, 500.0, 1000.0]
594
    DUPLEX = ["", "+", "-"]
595
    MODES = ["NFM", "FM", "AM", "WFM"]
596
    TMODES = ["", "??1", "Tone", "TSQL", "TSQL-R", "DTCS"]
597

    
598
    # This is a bit of a hack to avoid overwriting _identify()
599
    _memsize = 0x1a7c0
600
    _range = [(500000, 1300000000)]
601

    
602
    @classmethod
603
    def get_prompts(cls):
604
        rp = chirp_common.RadioPrompts()
605
        rp.pre_download = _(dedent("""\
606
            1. Ensure your firmware version is 4_10 or higher
607
            2. Turn radio off
608
            3. Connect your interface cable
609
            4. Turn radio on
610
            5. Press and release PTT 3 times while holding MONI key
611
            6. Supported baud rates: 57600 (default) and 19200
612
               (rotate dial while holding MONI to change)
613
            7. Click OK
614
            """))
615
        rp.pre_upload = _(dedent("""\
616
            1. Ensure your firmware version is 4_10 or higher
617
            2. Turn radio off
618
            3. Connect your interface cable
619
            4. Turn radio on
620
            5. Press and release PTT 3 times while holding MONI key
621
            6. Supported baud rates: 57600 (default) and 19200
622
               (rotate dial while holding MONI to change)
623
            7. Click OK
624
            """))
625
        return rp
626

    
627
    def get_features(self):
628
        rf = chirp_common.RadioFeatures()
629
        rf.has_dtcs_polarity = False
630
        rf.has_bank = False
631
        rf.has_settings = False
632

    
633
        rf.valid_modes = self.MODES
634
        rf.valid_tmodes = ["", "Tone", "TSQL", "Cross", "TSQL-R", "DTCS"]
635
        rf.valid_tuning_steps = self.STEPS
636
        rf.valid_bands = self._range
637
        rf.valid_skips = ["", "S"]
638
        rf.valid_characters = chirp_common.CHARSET_ASCII
639
        rf.valid_name_length = 16
640
        rf.memory_bounds = (0, 999)
641

    
642
        return rf
643

    
644
    def _download_chunk(self, addr):
645
        if addr % 0x40:
646
            raise Exception("Addr 0x%04x not on 64-byte boundary" % addr)
647

    
648
        cmd = b"AL~F%05XR\r" % addr
649
        self._send(cmd)
650

    
651
        # Response: "\r\n[ ... data ... ]\r\n
652
        # data is encoded in hex, hence we read two chars per byte
653
        _data = self._read(2+2*64+2).strip()
654
        if len(_data) == 0:
655
            raise errors.RadioError("No response from radio")
656

    
657
        data = codecs.decode(_data, "hex")
658

    
659
        if len(data) != 64:
660
            LOG.debug("Response was:")
661
            LOG.debug("|%s|" % _data)
662
            LOG.debug("Which I converted to:")
663
            LOG.debug(util.hexprint(data))
664
            raise Exception("Chunk from radio has wrong size")
665

    
666
        return data
667

    
668
    def _detect_baudrate_and_identify(self):
669
        if self._identify():
670
            return True
671
        else:
672
            # Apparently Alinco support suggests to try again at a lower baud
673
            # rate if their cable fails with the default rate. See #4355.
674
            LOG.info("Could not talk to radio. Trying again at 19200 baud")
675
            self.pipe.baudrate = 19200
676
            return self._identify()
677

    
678
    def _download(self, limit):
679
        self._detect_baudrate_and_identify()
680

    
681
        data = b"\x00"*0x200
682

    
683
        for addr in range(0x200, limit, 0x40):
684
            data += self._download_chunk(addr)
685
            # Other Alinco drivers delay here, but doesn't seem to be necessary
686
            # for this model.
687

    
688
            if self.status_fn:
689
                status = chirp_common.Status()
690
                status.cur = addr
691
                status.max = limit
692
                status.msg = "Downloading from radio"
693
                self.status_fn(status)
694
        return memmap.MemoryMapBytes(data)
695

    
696
    def _upload_chunk(self, addr):
697
        if addr % 0x40:
698
            raise Exception("Addr 0x%04x not on 64-byte boundary" % addr)
699

    
700
        _data = self._mmap[addr:addr+0x40]
701
        data = codecs.encode(_data, "hex").upper()
702

    
703
        cmd = b"AL~F%05XW%s\r" % (addr, data)
704
        self._send(cmd)
705

    
706
        resp = self._read(6)
707
        if resp.strip() != b"OK":
708
            raise Exception("Unexpected response from radio: %s" % resp)
709

    
710
    def _upload(self, limit):
711
        if not self._detect_baudrate_and_identify():
712
            raise Exception("I can't talk to this model")
713

    
714
        for addr in range(0x200, self._memsize, 0x40):
715
            self._upload_chunk(addr)
716
            # Other Alinco drivers delay here, but doesn't seem to be necessary
717
            # for this model.
718

    
719
            if self.status_fn:
720
                status = chirp_common.Status()
721
                status.cur = addr
722
                status.max = self._memsize
723
                status.msg = "Uploading to radio"
724
                self.status_fn(status)
725

    
726
    def _get_empty_flag(self, freq, mode):
727
        # Returns flag used to hide a channel from the main band. This occurs
728
        # when the mode is anything but NFM or FM (main band can only do those)
729
        # or when the frequency is outside of the range supported by the main
730
        # band.
731
        if mode not in ("NFM", "FM"):
732
            return 0x01
733
        if (freq >= 136000000 and freq < 174000000) or \
734
           (freq >= 400000000 and freq < 470000000) or \
735
           (freq >= 1240000000 and freq < 1300000000):
736
            return 0x02
737
        else:
738
            return 0x01
739

    
740
    def _check_channel_consistency(self, number):
741
        _mem = self._memobj.memory[number]
742
        if _mem.empty != 0x00:
743
            if _mem.unknown1 == 0xffffff:
744
                # Previous versions of this code have skipped the unknown
745
                # fields. They contain bytes of value if the channel is empty
746
                # and thus those bytes remain 0xff when the channel is put to
747
                # use. The radio is totally fine with this but the Alinco
748
                # programming software is not (see #5275). Here, we check for
749
                # this and report if it is encountered.
750
                LOG.warning("Channel %d is inconsistent: Found 0xff in "
751
                            "non-empty channel. Touch channel to fix."
752
                            % number)
753

    
754
            if _mem.empty != self._get_empty_flag(_mem.freq,
755
                                                  self.MODES[_mem.mode]):
756
                LOG.warning("Channel %d is inconsistent: Found out of band "
757
                            "frequency. Touch channel to fix." % number)
758

    
759
    def process_mmap(self):
760
        self._memobj = bitwise.parse(DJG7_MEM_FORMAT, self._mmap)
761
        # We check all channels for corruption (see bug #5275) but we don't fix
762
        # it automatically because it would be unpolite to modify something on
763
        # a read operation. A log message is emitted though for the user to
764
        # take actions.
765
        for number in range(len(self._memobj.memory)):
766
            self._check_channel_consistency(number)
767

    
768
    def get_memory(self, number):
769
        _mem = self._memobj.memory[number]
770
        mem = chirp_common.Memory()
771
        mem.number = number
772
        if _mem.empty == 0:
773
            mem.empty = True
774
        else:
775
            mem.freq = int(_mem.freq)
776
            mem.mode = self.MODES[_mem.mode]
777
            mem.tuning_step = self.STEPS[_mem.step]
778
            mem.offset = int(_mem.offset)
779
            mem.duplex = self.DUPLEX[_mem.duplex]
780
            if self.TMODES[_mem.squelch_type] == "TSQL" and \
781
                    _mem.tx_tone != _mem.rx_tone:
782
                mem.tmode = "Cross"
783
                mem.cross_mode = "Tone->Tone"
784
            else:
785
                mem.tmode = self.TMODES[_mem.squelch_type]
786
            mem.rtone = ALINCO_TONES[_mem.tx_tone-1]
787
            mem.ctone = ALINCO_TONES[_mem.rx_tone-1]
788
            mem.dtcs = DCS_CODES[self.VENDOR][_mem.dcs]
789
            if _mem.skip:
790
                mem.skip = "S"
791
            # FIXME find out what every other byte is used for. Japanese?
792
            mem.name = str(_mem.name.get_raw()[::2]).rstrip('\0')
793
        return mem
794

    
795
    def set_memory(self, mem):
796
        # Get a low-level memory object mapped to the image
797
        _mem = self._memobj.memory[mem.number]
798
        if mem.empty:
799
            _mem.set_raw("\xff" * (_mem.size() // 8))
800
            _mem.empty = 0x00
801
        else:
802
            _mem.empty = self._get_empty_flag(mem.freq, mem.mode)
803
            _mem.freq = mem.freq
804
            _mem.mode = self.MODES.index(mem.mode)
805
            _mem.step = self.STEPS.index(mem.tuning_step)
806
            _mem.offset = mem.offset
807
            _mem.duplex = self.DUPLEX.index(mem.duplex)
808
            if mem.tmode == "Cross":
809
                _mem.squelch_type = self.TMODES.index("TSQL")
810
                try:
811
                    _mem.tx_tone = ALINCO_TONES.index(mem.rtone)+1
812
                except ValueError:
813
                    raise errors.UnsupportedToneError(
814
                        "This radio does not support tone %.1fHz" % mem.rtone)
815
                try:
816
                    _mem.rx_tone = ALINCO_TONES.index(mem.ctone)+1
817
                except ValueError:
818
                    raise errors.UnsupportedToneError(
819
                        "This radio does not support tone %.1fHz" % mem.ctone)
820
            elif mem.tmode == "TSQL":
821
                _mem.squelch_type = self.TMODES.index("TSQL")
822
                # Note how the same TSQL tone is copied to both memory
823
                # locaations
824
                try:
825
                    _mem.tx_tone = ALINCO_TONES.index(mem.ctone)+1
826
                    _mem.rx_tone = ALINCO_TONES.index(mem.ctone)+1
827
                except ValueError:
828
                    raise errors.UnsupportedToneError(
829
                        "This radio does not support tone %.1fHz" % mem.ctone)
830
            else:
831
                _mem.squelch_type = self.TMODES.index(mem.tmode)
832
                try:
833
                    _mem.tx_tone = ALINCO_TONES.index(mem.rtone)+1
834
                except ValueError:
835
                    raise errors.UnsupportedToneError(
836
                        "This radio does not support tone %.1fHz" % mem.rtone)
837
                try:
838
                    _mem.rx_tone = ALINCO_TONES.index(mem.ctone)+1
839
                except ValueError:
840
                    raise errors.UnsupportedToneError(
841
                        "This radio does not support tone %.1fHz" % mem.ctone)
842
            _mem.dcs = DCS_CODES[self.VENDOR].index(mem.dtcs)
843
            _mem.skip = (mem.skip == "S")
844
            _mem.name = "\x00".join(mem.name.rstrip()).ljust(32, "\x00")
845
            _mem.unknown1 = 0x3e001c
846
            _mem.unknown2 = 0x0000000a
847
            _mem.unknown3 = 0x00000000
848
            _mem.unknown4 = 0x00000000
849

    
850

    
851
@directory.register
852
class AlincoDJG7EG(AlincoDJG7):
853
    """Alinco DJ-G7EG"""
854
    MODEL = "DJ-G7EG"
855
    _model = b"AL~DJ-G7EG"
856

    
857

    
858
@directory.register
859
class AlincoDJG7T(AlincoDJG7):
860
    """Alinco DJ-G7T"""
861
    MODEL = "DJ-G7T"
862
    _model = b"AL~DJ-G7T"
(2-2/6)