Project

General

Profile

Bug #10616 » ft2800.py

963ddeec - Dan Smith, 06/04/2023 10:31 AM

 
1
# Copyright 2011 Dan Smith <dsmith@danplanet.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
import time
17
import logging
18

    
19
from chirp import util, memmap, chirp_common, bitwise, directory, errors
20
from chirp.drivers.yaesu_clone import YaesuCloneModeRadio
21

    
22
LOG = logging.getLogger(__name__)
23

    
24
CHUNK_SIZE = 16
25

    
26

    
27
def _send(s, data):
28
    for i in range(0, len(data), CHUNK_SIZE):
29
        chunk = data[i:i+CHUNK_SIZE]
30
        s.write(chunk)
31
        echo = s.read(len(chunk))
32
        if chunk != echo:
33
            raise Exception("Failed to read echo chunk")
34

    
35
IDBLOCK = b"\x0c\x01\x41\x33\x35\x02\x00\xb8"
36
TRAILER = b"\x0c\x02\x41\x33\x35\x00\x00\xb7"
37
ACK = b"\x0C\x06\x00"
38

    
39

    
40
def _download(radio):
41
    data = ""
42
    for _i in range(0, 10):
43
        data = radio.pipe.read(8)
44
        if data == IDBLOCK:
45
            break
46

    
47
    LOG.debug("Header:\n%s" % util.hexprint(data))
48

    
49
    if len(data) != 8:
50
        raise Exception("Failed to read header")
51

    
52
    _send(radio.pipe, ACK)
53

    
54
    data = ""
55

    
56
    while len(data) < radio._block_sizes[1]:
57
        time.sleep(0.1)
58
        chunk = radio.pipe.read(38)
59
        LOG.debug("Got: %i:\n%s" % (len(chunk), util.hexprint(chunk)))
60
        if len(chunk) == 8:
61
            LOG.debug("END?")
62
        elif len(chunk) != 38:
63
            LOG.debug("Should fail?")
64
            break
65
            # raise Exception("Failed to get full data block")
66
        else:
67
            cs = 0
68
            for byte in chunk[:-1]:
69
                cs += ord(byte)
70
            if ord(chunk[-1]) != (cs & 0xFF):
71
                raise Exception("Block failed checksum!")
72

    
73
            data += chunk[5:-1]
74

    
75
        _send(radio.pipe, ACK)
76
        if radio.status_fn:
77
            status = chirp_common.Status()
78
            status.max = radio._block_sizes[1]
79
            status.cur = len(data)
80
            status.msg = "Cloning from radio"
81
            radio.status_fn(status)
82

    
83
    LOG.debug("Total: %i" % len(data))
84

    
85
    return memmap.MemoryMapBytes(data)
86

    
87

    
88
def _upload(radio):
89
    for _i in range(0, 10):
90
        data = radio.pipe.read(256)
91
        if not data:
92
            break
93
        LOG.debug("What is this garbage?\n%s" % util.hexprint(data))
94

    
95
    _send(radio.pipe, IDBLOCK)
96
    time.sleep(1)
97
    ack = radio.pipe.read(300)
98
    LOG.debug("Ack was (%i):\n%s" % (len(ack), util.hexprint(ack)))
99
    if ack != ACK:
100
        raise Exception("Radio did not ack ID")
101

    
102
    block = 0
103
    while block < (radio.get_memsize() // 32):
104
        data = b"\x0C\x03\x00\x00" + bytes([block])
105
        data += radio.get_mmap()[block*32:(block+1)*32]
106
        cs = 0
107
        for byte in data:
108
            cs += byte
109
        data += bytes([cs & 0xFF])
110

    
111
        LOG.debug("Writing block %i:\n%s" % (block, util.hexprint(data)))
112

    
113
        _send(radio.pipe, data)
114
        time.sleep(0.1)
115
        ack = radio.pipe.read(3)
116
        if ack != ACK:
117
            raise Exception("Radio did not ack block %i" % block)
118

    
119
        if radio.status_fn:
120
            status = chirp_common.Status()
121
            status.max = radio._block_sizes[1]
122
            status.cur = block * 32
123
            status.msg = "Cloning to radio"
124
            radio.status_fn(status)
125
        block += 1
126

    
127
    _send(radio.pipe, TRAILER)
128

    
129
MEM_FORMAT = """
130
struct {
131
  bbcd freq[4];
132
  u8 unknown1[4];
133
  bbcd offset[2];
134
  u8 unknown2[2];
135
  u8 pskip:1,
136
     skip:1,
137
     unknown3:1,
138
     isnarrow:1,
139
     power:2,
140
     duplex:2;
141
  u8 unknown4:6,
142
     tmode:2;
143
  u8 tone;
144
  u8 dtcs;
145
} memory[200];
146

    
147
#seekto 0x0E00;
148
struct {
149
  char name[6];
150
} names[200];
151
"""
152

    
153
MODES = ["FM", "NFM"]
154
TMODES = ["", "Tone", "TSQL", "DTCS"]
155
DUPLEX = ["", "-", "+", ""]
156
POWER_LEVELS = [chirp_common.PowerLevel("Hi", watts=65),
157
                chirp_common.PowerLevel("Mid", watts=25),
158
                chirp_common.PowerLevel("Low2", watts=10),
159
                chirp_common.PowerLevel("Low1", watts=5),
160
                ]
161
CHARSET = chirp_common.CHARSET_UPPER_NUMERIC + "()+-=*/???|_"
162

    
163

    
164
@directory.register
165
class FT2800Radio(YaesuCloneModeRadio):
166
    """Yaesu FT-2800"""
167
    VENDOR = "Yaesu"
168
    MODEL = "FT-2800M"
169

    
170
    _block_sizes = [8, 7680]
171
    _memsize = 7680
172

    
173
    def get_features(self):
174
        rf = chirp_common.RadioFeatures()
175

    
176
        rf.memory_bounds = (0, 199)
177

    
178
        rf.has_ctone = False
179
        rf.has_tuning_step = False
180
        rf.has_dtcs_polarity = False
181
        rf.has_bank = False
182

    
183
        rf.valid_tuning_steps = [5.0, 10.0, 12.5, 15.0,
184
                                 20.0, 25.0, 50.0, 100.0]
185
        rf.valid_modes = MODES
186
        rf.valid_tmodes = TMODES
187
        rf.valid_bands = [(137000000, 174000000)]
188
        rf.valid_power_levels = POWER_LEVELS
189
        rf.valid_duplexes = DUPLEX
190
        rf.valid_skips = ["", "S", "P"]
191
        rf.valid_name_length = 6
192
        rf.valid_characters = CHARSET
193

    
194
        return rf
195

    
196
    def sync_in(self):
197
        self.pipe.parity = "E"
198
        start = time.time()
199
        try:
200
            self._mmap = _download(self)
201
        except errors.RadioError:
202
            raise
203
        except Exception as e:
204
            LOG.exception('Failed download')
205
            raise errors.RadioError("Failed to communicate with radio: %s" % e)
206
        LOG.info("Downloaded in %.2f sec" % (time.time() - start))
207
        self.process_mmap()
208

    
209
    def sync_out(self):
210
        self.pipe.timeout = 1
211
        self.pipe.parity = "E"
212
        start = time.time()
213
        try:
214
            _upload(self)
215
        except errors.RadioError:
216
            raise
217
        except Exception as e:
218
            LOG.exception('Failed upload')
219
            raise errors.RadioError("Failed to communicate with radio: %s" % e)
220
        LOG.info("Uploaded in %.2f sec" % (time.time() - start))
221

    
222
    def process_mmap(self):
223
        self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
224

    
225
    def get_raw_memory(self, number):
226
        return repr(self._memobj.memory[number])
227

    
228
    def get_memory(self, number):
229
        _mem = self._memobj.memory[number]
230
        _nam = self._memobj.names[number]
231
        mem = chirp_common.Memory()
232

    
233
        mem.number = number
234

    
235
        if _mem.get_raw()[0] == "\xFF":
236
            mem.empty = True
237
            return mem
238

    
239
        mem.freq = int(_mem.freq) * 10
240
        mem.offset = int(_mem.offset) * 100000
241
        mem.duplex = DUPLEX[_mem.duplex]
242
        mem.tmode = TMODES[_mem.tmode]
243
        mem.rtone = chirp_common.TONES[_mem.tone]
244
        mem.dtcs = chirp_common.DTCS_CODES[_mem.dtcs]
245
        mem.name = str(_nam.name).rstrip()
246
        mem.mode = _mem.isnarrow and "NFM" or "FM"
247
        mem.skip = _mem.pskip and "P" or _mem.skip and "S" or ""
248
        mem.power = POWER_LEVELS[_mem.power]
249

    
250
        return mem
251

    
252
    def set_memory(self, mem):
253
        _mem = self._memobj.memory[mem.number]
254
        _nam = self._memobj.names[mem.number]
255

    
256
        if mem.empty:
257
            _mem.set_raw("\xFF" * (_mem.size() // 8))
258
            return
259

    
260
        if _mem.get_raw()[0] == "\xFF":
261
            # Empty -> Non-empty, so initialize
262
            _mem.set_raw("\x00" * (_mem.size() // 8))
263

    
264
        _mem.freq = mem.freq / 10
265
        _mem.offset = mem.offset / 100000
266
        _mem.duplex = DUPLEX.index(mem.duplex)
267
        _mem.tmode = TMODES.index(mem.tmode)
268
        _mem.tone = chirp_common.TONES.index(mem.rtone)
269
        _mem.dtcs = chirp_common.DTCS_CODES.index(mem.dtcs)
270
        _mem.isnarrow = MODES.index(mem.mode)
271
        _mem.pskip = mem.skip == "P"
272
        _mem.skip = mem.skip == "S"
273
        if mem.power:
274
            _mem.power = POWER_LEVELS.index(mem.power)
275
        else:
276
            _mem.power = 0
277

    
278
        _nam.name = mem.name.ljust(6)[:6]
279

    
280
    @classmethod
281
    def match_model(cls, filedata, filename):
282
        return len(filedata) == cls._memsize
(14-14/41)