Project

General

Profile

Bug #11300 » alinco_dr735t.py

Potentially dangerous modified DR735 driver - Dan Smith, 04/20/2024 08:37 PM

 
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
from chirp import util
21

    
22
import logging
23
import codecs
24

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

    
55

    
56
LOG = logging.getLogger(__name__)
57

    
58

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

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

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

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

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

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

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

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

    
102
    _model = b"DR735TN"
103

    
104
    def get_features(self):
105
        rf = chirp_common.RadioFeatures()
106
        # convert to list to deal with dict_values unsubscriptable
107
        rf.valid_tmodes = list(self.TONE_MODE_MAP.values())
108
        rf.valid_modes = list(self.MODE_MAP.values())
109
        rf.valid_skips = ["", "S"]
110
        rf.valid_bands = self._freq_ranges
111
        rf.memory_bounds = (0, self._no_channels-1)
112
        rf.has_ctone = True
113
        rf.has_bank = False
114
        rf.has_dtcs_polarity = False
115
        rf.valid_tuning_steps = [5.0]
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
        if not radio_id:
131
            raise errors.RadioError("No response from radio")
132
        LOG.debug('Model string is %s' % util.hexprint(radio_id))
133
        return radio_id in (b"DR735TN", b"DR735TE")
134

    
135
    def do_download(self):
136
        if not self._identify():
137
            raise errors.RadioError("Unsupported radio model.")
138

    
139
        channel_data = b""
140

    
141
        for channel_no in range(0, self._no_channels):
142

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

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

    
160
        return memmap.MemoryMapBytes(channel_data)
161

    
162
    def do_upload(self):
163
        if not self._identify():
164
            raise errors.RadioError("Unsupported radio model.")
165

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

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

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

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

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

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

    
212
    def process_mmap(self):
213
        self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
214

    
215
    def get_raw_memory(self, number):
216
        return repr(self._memobj.memory[number])
217

    
218
    def get_memory(self, number):
219
        _mem = self._memobj.memory[number]
220
        mem = chirp_common.Memory()
221
        mem.number = number                 # Set the memory number
222
        mem.freq = int(_mem.frequency)
223
        mem.name = "".join([CHARSET[_mem.name[i]] for i in range(6)]).strip()
224

    
225
        mem.tmode = self.TONE_MODE_MAP[int(_mem.subtone_selection)]
226
        mem.duplex = self.SHIFT_DIR_MAP[_mem.shift_direction]
227
        mem.offset = _mem.shift
228

    
229
        mem.rtone = ALINCO_TONES[_mem.rx_tone_index]
230
        mem.ctone = ALINCO_TONES[_mem.tx_tone_index]
231
        mem.dtcs = chirp_common.DTCS_CODES[_mem.dcs_index]
232
        mem.power = self.POWER_MAP[_mem.power_index]
233
        mem.skip = 'S' if bool(_mem.skip) else ''
234
        mem.mode = self.MODE_MAP[int(_mem.mode)]
235

    
236
        self._get_extra(_mem, mem)
237

    
238
        if _mem.used == 0:
239
            mem.empty = True
240
        return mem
241

    
242
    def set_memory(self, mem):
243
        # Get a low-level memory object mapped to the image
244
        _mem = self._memobj.memory[mem.number]
245

    
246
        def find_key_in(d: dict, target_val):
247
            for k, v in d.items():
248
                if v == target_val:
249
                    return k
250

    
251
        if not mem.empty:
252
            mapped_name = [CHARSET.index(' ').to_bytes(1, 'little')]*6
253
            for (i, c) in enumerate(mem.name.ljust(6)[:6].upper().strip()):
254
                if c not in chirp_common.CHARSET_UPPER_NUMERIC:
255
                    c = " "  # just make it a space
256
                mapped_name[i] = CHARSET.index(c).to_bytes(1, 'little')
257
            _mem.frequency = int(mem.freq)
258
            _mem.name = b''.join(mapped_name)
259
            _mem.mode = find_key_in(self.MODE_MAP, mem.mode)
260
            _mem.subtone_selection = find_key_in(self.TONE_MODE_MAP, mem.tmode)
261
            _mem.shift = mem.offset
262
            _mem.used = 0x00 if mem.empty else 0x55
263
            _mem.power_index = self.POWER_MAP.index(
264
                mem.power) if mem.power in self.POWER_MAP else 0
265
            _mem.skip = 0x01 if mem.skip == "S" else 0x00
266
            try:
267
                _mem.rx_tone_index = ALINCO_TONES.index(
268
                    mem.rtone)
269
            except ValueError:
270
                raise errors.UnsupportedToneError("This radio does "
271
                                                  "not support "
272
                                                  "tone %.1fHz" % mem.rtone)
273
            try:
274

    
275
                _mem.tx_tone_index = ALINCO_TONES.index(
276
                    mem.ctone)
277
            except ValueError:
278
                raise errors.UnsupportedToneError("This radio does "
279
                                                  "not support "
280
                                                  "tone %.1fHz" % mem.ctone)
281
            _mem.dcs_index = chirp_common.DTCS_CODES.index(
282
                mem.dtcs if mem.dtcs else chirp_common.DTCS_CODES[0])
283
            _mem.shift_direction = self.SHIFT_DIR_MAP.index(
284
                mem.duplex if mem.duplex else self.SHIFT_DIR_MAP[0])
285
        else:
286
            _mem.frequency = 0
287
            _mem.name = b"\x00"*6
288
            _mem.mode = find_key_in(self.MODE_MAP, "Auto")
289
            _mem.subtone_selection = find_key_in(self.TONE_MODE_MAP, "")
290
            _mem.shift = 0
291
            _mem.used = 0x00 if mem.empty else 0x55
292
            _mem.power_index = 0
293
            _mem.skip = 0x01 if mem.skip == "S" else 0x00
294
            _mem.rx_tone_index = 0
295
            _mem.tx_tone_index = 0
296
            _mem.dcs_index = 0
297
            _mem.shift_direction = self.SHIFT_DIR_MAP.index("")
298

    
299
        self._set_extra(_mem, mem)
300

    
301
    def _get_extra(self, _mem, mem):
302
        mem.extra = RadioSettingGroup("extra", "Extra")
303
        het_mode = RadioSetting("heterodyne_mode", "Heterodyne Mode",
304
                                RadioSettingValueList(
305
                                    self.HET_MODE_MAP,
306
                                    current=self.HET_MODE_MAP[int(
307
                                        _mem.heterodyne_mode)]
308
                                ))
309
        het_mode.set_doc("Heterodyne Mode")
310

    
311
        bcl = RadioSetting("bcl", "BCL",
312
                           RadioSettingValueBoolean(
313
                               bool(_mem.busy_channel_lockout)
314
                           ))
315
        bcl.set_doc("Busy Channel Lockout")
316

    
317
        stby_screen = RadioSetting("stby_screen", "Standby Screen Color",
318
                                   RadioSettingValueList(
319
                                       self.SCREEN_COLOR_MAP,
320
                                       current=self.SCREEN_COLOR_MAP[int(
321
                                           _mem.standby_screen_color)]
322
                                   ))
323
        stby_screen.set_doc("Standby Screen Color")
324

    
325
        rx_screen = RadioSetting("rx_screen", "RX Screen Color",
326
                                 RadioSettingValueList(
327
                                     self.SCREEN_COLOR_MAP,
328
                                     current=self.SCREEN_COLOR_MAP[int(
329
                                         _mem.rx_screen_color)]
330
                                 ))
331
        rx_screen.set_doc("RX Screen Color")
332

    
333
        tx_screen = RadioSetting("tx_screen", "TX Screen Color",
334
                                 RadioSettingValueList(
335
                                     self.SCREEN_COLOR_MAP,
336
                                     current=self.SCREEN_COLOR_MAP[int(
337
                                         _mem.tx_screen_color)]
338
                                 ))
339
        tx_screen.set_doc("TX Screen Color")
340

    
341
        mem.extra.append(het_mode)
342
        mem.extra.append(bcl)
343
        mem.extra.append(stby_screen)
344
        mem.extra.append(rx_screen)
345
        mem.extra.append(tx_screen)
346

    
347
    def _set_extra(self, _mem, mem):
348
        for setting in mem.extra:
349
            if setting.get_name() == "heterodyne_mode":
350
                _mem.heterodyne_mode = \
351
                    self.HET_MODE_MAP.index(
352
                        setting.value) if \
353
                    setting.value else self.HET_MODE_MAP[0]
354

    
355
            if setting.get_name() == "bcl":
356
                _mem.busy_channel_lockout = \
357
                    0x01 if setting.value else 0x00
358

    
359
            if setting.get_name() == "stby_screen":
360
                _mem.standby_screen_color = \
361
                    self.SCREEN_COLOR_MAP.index(setting.value) if \
362
                    setting.value else self.SCREEN_COLOR_MAP[0]
363

    
364
            if setting.get_name() == "rx_screen":
365
                _mem.rx_screen_color = \
366
                    self.SCREEN_COLOR_MAP.index(setting.value) if \
367
                    setting.value else self.SCREEN_COLOR_MAP[0]
368

    
369
            if setting.get_name() == "tx_screen":
370
                _mem.tx_screen_color = \
371
                    self.SCREEN_COLOR_MAP.index(setting.value) if \
372
                    setting.value else self.SCREEN_COLOR_MAP[0]
(5-5/15)