Project

General

Profile

Bug #11300 » alinco_dr735t.py

Fourth, potentially dangerous DR-735T driver - Jacob Calvert, 04/22/2024 06:04 AM

 
1
# Copyright 2024 Jacob Calvert <jcalvert@jacobncalvert.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 3 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
from chirp import chirp_common, bitwise, errors, memmap, directory
17
from chirp.settings import RadioSettingGroup, RadioSetting
18
from chirp.settings import RadioSettingValueBoolean, RadioSettingValueList
19
from chirp.drivers.alinco import ALINCO_TONES, CHARSET
20

    
21
import logging
22
import codecs
23

    
24
MEM_FORMAT = """
25
struct {
26
  u8 used;
27
  u8 skip;
28
  u8 favorite;
29
  u8 unknown3;
30
  ul32 frequency;
31
  ul32 shift;
32
  u8 shift_direction;
33
  u8 subtone_selection;
34
  u8 rx_tone_index;
35
  u8 tx_tone_index;
36
  u8 dcs_index;
37
  u8 unknown17;
38
  u8 power_index;
39
  u8 busy_channel_lockout;
40
  u8 mode;
41
  u8 heterodyne_mode;
42
  u8 unknown22;
43
  u8 bell;
44
  u8 name[6];
45
  u8 dcs_off;
46
  u8 unknown31;
47
  u8 standby_screen_color;
48
  u8 rx_screen_color;
49
  u8 tx_screen_color;
50
  u8 unknown35_to_63[29];
51
} memory[1000];
52
"""
53

    
54

    
55
LOG = logging.getLogger(__name__)
56

    
57

    
58
@directory.register
59
class AlincoDR735T(chirp_common.CloneModeRadio):
60
    """Base class for DR735T radio"""
61

    
62
    """Alinco DR735T"""
63
    VENDOR = "Alinco"
64
    MODEL = "DR735T"
65
    BAUD_RATE = 38400
66
    NEEDS_COMPAT_SERIAL = False
67

    
68
    TONE_MODE_MAP = {
69
        0x00: "",
70
        0x01: "Tone",
71
        0x03: "TSQL",
72
        0x0C: "DTCS"
73
    }
74

    
75
    SHIFT_DIR_MAP = ["", "-", "+"]
76

    
77
    POWER_MAP = [
78
        chirp_common.PowerLevel("High", watts=50.0),
79
        chirp_common.PowerLevel("Mid", watts=25.00),
80
        chirp_common.PowerLevel("Low", watts=5.00),
81
    ]
82
    MODE_MAP = {
83
        0x00: "FM",
84
        0x01: "NFM",
85
        0x02: "AM",
86
        0x03: "NAM",
87
        0x80: "Auto"
88
    }
89

    
90
    HET_MODE_MAP = ["Normal", "Reverse"]
91

    
92
    SCREEN_COLOR_MAP = [f"Color {n+1}" for n in range(16)]
93

    
94
    _freq_ranges = [
95
        (108000000, 136000000),
96
        (136000000, 174000000),
97
        (400000000, 480000000)
98
    ]
99
    _no_channels = 1000
100

    
101
    _model = b"DR735TN"
102

    
103
    def get_features(self):
104
        rf = chirp_common.RadioFeatures()
105
        # convert to list to deal with dict_values unsubscriptable
106
        rf.valid_tmodes = list(self.TONE_MODE_MAP.values())
107
        rf.valid_modes = list(self.MODE_MAP.values())
108
        rf.valid_skips = ["", "S"]
109
        rf.valid_bands = self._freq_ranges
110
        rf.memory_bounds = (0, self._no_channels-1)
111
        rf.has_ctone = True
112
        rf.has_bank = False
113
        rf.has_dtcs_polarity = False
114
        rf.has_tuning_step = False
115

    
116
        rf.can_delete = False
117
        rf.valid_name_length = 6
118
        rf.valid_characters = chirp_common.CHARSET_UPPER_NUMERIC
119
        rf.valid_power_levels = self.POWER_MAP
120
        rf.valid_dtcs_codes = chirp_common.DTCS_CODES
121

    
122
        return rf
123

    
124
    def _identify(self) -> bool:
125
        command = b"AL~WHO\r\n"
126
        self.pipe.write(command)
127
        self.pipe.read(len(command))
128
        # expect DR735TN\r\n
129
        radio_id = self.pipe.read(9).strip()
130
        return radio_id in (b"DR735TN", b"DR735TE")
131

    
132
    def do_download(self):
133
        if not self._identify():
134
            raise errors.RadioError("Unsupported radio model.")
135

    
136
        channel_data = b""
137

    
138
        for channel_no in range(0, self._no_channels):
139

    
140
            command = f"AL~EEPEL{channel_no<<6 :04X}R\r\n".encode()
141
            self.pipe.write(command)
142
            self.pipe.read(len(command))
143
            channel_spec = self.pipe.read(128)  # 64 bytes, as hex
144
            self.pipe.read(2)  # \r\n
145
            channel_spec = codecs.decode(channel_spec, "hex")
146
            if len(channel_spec) != 64:
147
                exit(1)
148
            channel_data += channel_spec
149

    
150
            if self.status_fn:
151
                status = chirp_common.Status()
152
                status.cur = channel_no
153
                status.max = self._no_channels
154
                status.msg = f"Downloading channel {channel_no} from radio"
155
                self.status_fn(status)
156

    
157
        return memmap.MemoryMapBytes(channel_data)
158

    
159
    def do_upload(self):
160
        if not self._identify():
161
            raise errors.RadioError("Unsupported radio model.")
162

    
163
        command = b"AL~DR735J\r\n"
164
        self.pipe.write(command)
165
        self.pipe.read(len(command))
166
        resp = self.pipe.read(4)
167
        if resp != b"OK\r\n":
168
            errors.RadioError("Could not go into download mode.")
169

    
170
        for channel_no in range(0, self._no_channels):
171
            write_data = self.get_mmap()[channel_no*64:(channel_no+1)*64]
172
            write_data = codecs.encode(write_data, 'hex').upper()
173
            command = f"AL~EEPEL{channel_no<<6 :04X}W".encode(
174
            ) + write_data + b"\r\n"
175
            LOG.debug(f"COMM: {command}")
176
            self.pipe.write(command)
177
            back = self.pipe.read(len(command))
178
            LOG.debug(f"BACK: {back}")
179
            resp = self.pipe.read(4)
180
            LOG.debug(f"RESP: {resp}")
181
            if resp != b"OK\r\n":
182
                raise errors.RadioError("failed to write to channel")
183

    
184
            if self.status_fn:
185
                status = chirp_common.Status()
186
                status.cur = channel_no
187
                status.max = self._no_channels
188
                status.msg = f"Uploading channel {channel_no} to radio"
189
                self.status_fn(status)
190

    
191
        command = b"AL~RESET\r\n"
192
        self.pipe.write(command)
193
        self.pipe.read(len(command))  # command + OK\r\n
194
        self.pipe.read(4)
195

    
196
    def sync_in(self):
197
        try:
198
            self._mmap = self.do_download()
199
        except Exception as exc:
200
            raise errors.RadioError(f"Failed to download from radio: {exc}")
201
        self.process_mmap()
202

    
203
    def sync_out(self):
204
        try:
205
            self.do_upload()
206
        except Exception as exc:
207
            raise errors.RadioError(f"Failed to download from radio: {exc}")
208

    
209
    def process_mmap(self):
210
        self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
211

    
212
    def get_raw_memory(self, number):
213
        return repr(self._memobj.memory[number])
214

    
215
    def get_memory(self, number):
216

    
217
        _mem = self._memobj.memory[number]
218
        mem = chirp_common.Memory()
219
        mem.number = number                 # Set the memory number
220
        if _mem.used != 0x55:
221
            mem.empty = True
222
            mem.freq = 400000000
223
            mem.name = ""
224

    
225
            mem.tmode = self.TONE_MODE_MAP[0]
226
            mem.duplex = self.SHIFT_DIR_MAP[0]
227
            mem.offset = 0
228

    
229
            mem.rtone = ALINCO_TONES[0]
230
            mem.ctone = ALINCO_TONES[0]
231
            mem.dtcs = chirp_common.DTCS_CODES[0]
232
            mem.power = self.POWER_MAP[0]
233
            mem.skip = ''
234
            mem.mode = self.MODE_MAP[0]
235
            self._get_extra_default(mem)
236

    
237
            return mem
238
        else:
239
            mem.empty = False
240
            mem.freq = int(_mem.frequency)
241
            mem.name = "".join([CHARSET[_mem.name[i]]
242
                               for i in range(6)]).strip()
243

    
244
            mem.tmode = self.TONE_MODE_MAP[int(_mem.subtone_selection)]
245
            mem.duplex = self.SHIFT_DIR_MAP[_mem.shift_direction]
246
            mem.offset = _mem.shift
247

    
248
            mem.rtone = ALINCO_TONES[_mem.rx_tone_index]
249
            mem.ctone = ALINCO_TONES[_mem.tx_tone_index]
250
            mem.dtcs = chirp_common.DTCS_CODES[_mem.dcs_index]
251
            mem.power = self.POWER_MAP[_mem.power_index]
252
            mem.skip = 'S' if bool(_mem.skip) else ''
253
            mem.mode = self.MODE_MAP[int(_mem.mode)]
254

    
255
            self._get_extra(_mem, mem)
256

    
257
            return mem
258

    
259
    def set_memory(self, mem):
260
        # Get a low-level memory object mapped to the image
261
        _mem = self._memobj.memory[mem.number]
262

    
263
        def find_key_in(d: dict, target_val):
264
            for k, v in d.items():
265
                if v == target_val:
266
                    return k
267

    
268
        if not mem.empty:
269
            mapped_name = [CHARSET.index(' ').to_bytes(1, 'little')]*6
270
            for (i, c) in enumerate(mem.name.ljust(6)[:6].upper().strip()):
271
                if c not in chirp_common.CHARSET_UPPER_NUMERIC:
272
                    c = " "  # just make it a space
273
                mapped_name[i] = CHARSET.index(c).to_bytes(1, 'little')
274
            _mem.frequency = int(mem.freq)
275
            _mem.name = b''.join(mapped_name)
276
            _mem.mode = find_key_in(self.MODE_MAP, mem.mode)
277
            _mem.subtone_selection = find_key_in(self.TONE_MODE_MAP, mem.tmode)
278
            _mem.shift = mem.offset
279
            _mem.used = 0x00 if mem.empty else 0x55
280
            _mem.power_index = self.POWER_MAP.index(
281
                mem.power) if mem.power in self.POWER_MAP else 0
282
            _mem.skip = 0x01 if mem.skip == "S" else 0x00
283
            try:
284
                _mem.rx_tone_index = ALINCO_TONES.index(
285
                    mem.rtone)
286
            except ValueError:
287
                raise errors.UnsupportedToneError("This radio does "
288
                                                  "not support "
289
                                                  "tone %.1fHz" % mem.rtone)
290
            try:
291

    
292
                _mem.tx_tone_index = ALINCO_TONES.index(
293
                    mem.ctone)
294
            except ValueError:
295
                raise errors.UnsupportedToneError("This radio does "
296
                                                  "not support "
297
                                                  "tone %.1fHz" % mem.ctone)
298
            _mem.dcs_index = chirp_common.DTCS_CODES.index(
299
                mem.dtcs if mem.dtcs else chirp_common.DTCS_CODES[0])
300
            _mem.shift_direction = self.SHIFT_DIR_MAP.index(
301
                mem.duplex if mem.duplex else self.SHIFT_DIR_MAP[0])
302
        else:
303
            _mem.frequency = 0
304
            _mem.name = b"\x00"*6
305
            _mem.mode = find_key_in(self.MODE_MAP, "Auto")
306
            _mem.subtone_selection = find_key_in(self.TONE_MODE_MAP, "")
307
            _mem.shift = 0
308
            _mem.used = 0x00 if mem.empty else 0x55
309
            _mem.power_index = 0
310
            _mem.skip = 0x01 if mem.skip == "S" else 0x00
311
            _mem.rx_tone_index = 0
312
            _mem.tx_tone_index = 0
313
            _mem.dcs_index = 0
314
            _mem.shift_direction = self.SHIFT_DIR_MAP.index("")
315

    
316
        self._set_extra(_mem, mem)
317

    
318
    def _get_extra_default(self, mem):
319
        mem.extra = RadioSettingGroup("extra", "Extra")
320
        het_mode = RadioSetting("heterodyne_mode", "Heterodyne Mode",
321
                                RadioSettingValueList(
322
                                    self.HET_MODE_MAP,
323
                                    current=self.HET_MODE_MAP[0]
324
                                ))
325
        het_mode.set_doc("Heterodyne Mode")
326

    
327
        bcl = RadioSetting("bcl", "BCL",
328
                           RadioSettingValueBoolean(
329
                               False
330
                           ))
331
        bcl.set_doc("Busy Channel Lockout")
332

    
333
        stby_screen = RadioSetting("stby_screen", "Standby Screen Color",
334
                                   RadioSettingValueList(
335
                                       self.SCREEN_COLOR_MAP,
336
                                       current=self.SCREEN_COLOR_MAP[0]
337
                                   ))
338
        stby_screen.set_doc("Standby Screen Color")
339

    
340
        rx_screen = RadioSetting("rx_screen", "RX Screen Color",
341
                                 RadioSettingValueList(
342
                                     self.SCREEN_COLOR_MAP,
343
                                     current=self.SCREEN_COLOR_MAP[0]
344
                                 ))
345
        rx_screen.set_doc("RX Screen Color")
346

    
347
        tx_screen = RadioSetting("tx_screen", "TX Screen Color",
348
                                 RadioSettingValueList(
349
                                     self.SCREEN_COLOR_MAP,
350
                                     current=self.SCREEN_COLOR_MAP[0]
351
                                 ))
352

    
353
        tx_screen.set_doc("TX Screen Color")
354

    
355
        mem.extra.append(het_mode)
356
        mem.extra.append(bcl)
357
        mem.extra.append(stby_screen)
358
        mem.extra.append(rx_screen)
359
        mem.extra.append(tx_screen)
360

    
361
    def _get_extra(self, _mem, mem):
362
        mem.extra = RadioSettingGroup("extra", "Extra")
363
        het_mode = RadioSetting("heterodyne_mode", "Heterodyne Mode",
364
                                RadioSettingValueList(
365
                                    self.HET_MODE_MAP,
366
                                    current=self.HET_MODE_MAP[int(
367
                                        _mem.heterodyne_mode)]
368
                                ))
369
        het_mode.set_doc("Heterodyne Mode")
370

    
371
        bcl = RadioSetting("bcl", "BCL",
372
                           RadioSettingValueBoolean(
373
                               bool(_mem.busy_channel_lockout)
374
                           ))
375
        bcl.set_doc("Busy Channel Lockout")
376

    
377
        stby_screen = RadioSetting("stby_screen", "Standby Screen Color",
378
                                   RadioSettingValueList(
379
                                       self.SCREEN_COLOR_MAP,
380
                                       current=self.SCREEN_COLOR_MAP[int(
381
                                           _mem.standby_screen_color)]
382
                                   ))
383
        stby_screen.set_doc("Standby Screen Color")
384

    
385
        rx_screen = RadioSetting("rx_screen", "RX Screen Color",
386
                                 RadioSettingValueList(
387
                                     self.SCREEN_COLOR_MAP,
388
                                     current=self.SCREEN_COLOR_MAP[int(
389
                                         _mem.rx_screen_color)]
390
                                 ))
391
        rx_screen.set_doc("RX Screen Color")
392

    
393
        tx_screen = RadioSetting("tx_screen", "TX Screen Color",
394
                                 RadioSettingValueList(
395
                                     self.SCREEN_COLOR_MAP,
396
                                     current=self.SCREEN_COLOR_MAP[int(
397
                                         _mem.tx_screen_color)]
398
                                 ))
399
        tx_screen.set_doc("TX Screen Color")
400

    
401
        mem.extra.append(het_mode)
402
        mem.extra.append(bcl)
403
        mem.extra.append(stby_screen)
404
        mem.extra.append(rx_screen)
405
        mem.extra.append(tx_screen)
406

    
407
    def _set_extra(self, _mem, mem):
408
        for setting in mem.extra:
409
            if setting.get_name() == "heterodyne_mode":
410
                _mem.heterodyne_mode = \
411
                    self.HET_MODE_MAP.index(
412
                        setting.value) if \
413
                    setting.value else self.HET_MODE_MAP[0]
414

    
415
            if setting.get_name() == "bcl":
416
                _mem.busy_channel_lockout = \
417
                    0x01 if setting.value else 0x00
418

    
419
            if setting.get_name() == "stby_screen":
420
                _mem.standby_screen_color = \
421
                    self.SCREEN_COLOR_MAP.index(setting.value) if \
422
                    setting.value else self.SCREEN_COLOR_MAP[0]
423

    
424
            if setting.get_name() == "rx_screen":
425
                _mem.rx_screen_color = \
426
                    self.SCREEN_COLOR_MAP.index(setting.value) if \
427
                    setting.value else self.SCREEN_COLOR_MAP[0]
428

    
429
            if setting.get_name() == "tx_screen":
430
                _mem.tx_screen_color = \
431
                    self.SCREEN_COLOR_MAP.index(setting.value) if \
432
                    setting.value else self.SCREEN_COLOR_MAP[0]
(12-12/15)