Project

General

Profile

Bug #11300 » alinco_dr735t.py

Dan Smith, 04/12/2024 06:44 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
        LOG.debug('Model string is %s' % util.hexprint(radio_id))
131
        return radio_id == b"DR735TN"
132

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

    
137
        channel_data = b""
138

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

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

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

    
158
        return memmap.MemoryMapBytes(channel_data)
159

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

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

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

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

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

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

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

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

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

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

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

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

    
234
        self._get_extra(_mem, mem)
235

    
236
        if _mem.used == 0:
237
            mem.empty = True
238
        return mem
239

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

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

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

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

    
297
        self._set_extra(_mem, mem)
298

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

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

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

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

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

    
339
        mem.extra.append(het_mode)
340
        mem.extra.append(bcl)
341
        mem.extra.append(stby_screen)
342
        mem.extra.append(rx_screen)
343
        mem.extra.append(tx_screen)
344

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

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

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

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

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