Project

General

Profile

Bug #10907 » ft8100.py

409f0cf2 - Dan Smith, 10/21/2023 09:05 PM

 
1
# Copyright 2010 Eric Allen <eric@hackerengineer.net>
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 logging
17
import time
18

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

    
22
LOG = logging.getLogger(__name__)
23

    
24
TONES = chirp_common.OLD_TONES
25
TMODES = ["", "Tone"]
26
MODES = ['FM', 'AM']
27
STEPS = [5.0, 10.0, 12.5, 15.0, 20.0, 25.0, 50.0]
28
DUPLEX = ["", "-", "+", "split"]
29
# "M" for masked memories, which are invisible until un-masked
30
SKIPS = ["", "S", "M"]
31
POWER_LEVELS_VHF = [chirp_common.PowerLevel("Low", watts=5),
32
                    chirp_common.PowerLevel("Mid", watts=20),
33
                    chirp_common.PowerLevel("High", watts=50)]
34
POWER_LEVELS_UHF = [chirp_common.PowerLevel("Low", watts=5),
35
                    chirp_common.PowerLevel("Mid", watts=20),
36
                    chirp_common.PowerLevel("High", watts=35)]
37
SPECIALS = {'1L': -1,
38
            '1U': -2,
39
            '2L': -3,
40
            '2U': -4,
41
            'Home': -5}
42

    
43
MEM_FORMAT = """
44
#seekto 0x{skips:X};
45
u8 skips[13];
46

    
47
#seekto 0x{enables:X};
48
u8 enables[13];
49

    
50
struct mem_struct {{
51
    u8 unknown4:2,
52
       baud9600:1,
53
       am:1,
54
       unknown4b:4;
55
    u8 power:2,
56
       duplex:2,
57
       unknown1b:4;
58
    u8 unknown2:1,
59
       tone_enable:1,
60
       tone:6;
61
    bbcd freq[3];
62
    bbcd offset[3];
63
}};
64

    
65
#seekto 0x{memories:X};
66
struct mem_struct memory[99];
67
"""
68

    
69

    
70
@directory.register
71
class FT8100Radio(yaesu_clone.YaesuCloneModeRadio):
72
    """Implementation for Yaesu FT-8100"""
73
    MODEL = "FT-8100"
74

    
75
    _memstart = 0
76
    _memsize = 2968
77
    _block_lengths = [10, 32, 114, 101, 101, 97, 128, 128, 128, 128, 128, 9, 9,
78
                      9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,
79
                      9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,
80
                      9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,
81
                      9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,
82
                      9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,
83
                      9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,
84
                      9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,
85
                      9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,
86
                      9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,
87
                      9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,
88
                      9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 1]
89

    
90
    @classmethod
91
    def match_model(cls, data, path):
92
        if (len(data) == cls._memsize and
93
                data[1:10] == b'\x01\x01\x07\x08\x02\x01\x01\x00\x01'):
94
            return True
95

    
96
        return False
97

    
98
    def get_features(self):
99
        rf = chirp_common.RadioFeatures()
100
        rf.memory_bounds = (1, 99)
101
        rf.has_ctone = False
102
        rf.has_dtcs = False
103
        rf.has_dtcs_polarity = False
104
        rf.has_bank = False
105
        rf.has_name = False
106

    
107
        rf.valid_tones = list(TONES)
108
        rf.valid_modes = list(MODES)
109
        rf.valid_tmodes = list(TMODES)
110
        rf.valid_duplexes = list(DUPLEX)
111
        rf.valid_power_levels = POWER_LEVELS_VHF
112
        rf.has_sub_devices = self.VARIANT == ''
113

    
114
        rf.valid_tuning_steps = list(STEPS)
115
        # This is not implemented properly, so don't expose it
116
        rf.valid_tuning_steps.remove(12.5)
117

    
118
        # This driver doesn't properly support the upper bound of 1300MHz
119
        # so limit us to 999MHz
120
        if self.VARIANT == 'VHF':
121
            rf.valid_bands = [(110000000, 280000000)]
122
        else:
123
            rf.valid_bands = [(280000000, 550000000),
124
                              (750000000, 999000000)]
125

    
126
        # This is not actually implemented, so don't expose it
127
        # rf.valid_skips = SKIPS
128
        rf.valid_skips = []
129

    
130
        # TODO
131
        # rf.valid_special_chans = SPECIALS.keys()
132
        # TODO
133
        # rf.has_tuning_step = False
134

    
135
        rf.can_odd_split = True
136

    
137
        return rf
138

    
139
    def sync_in(self):
140
        super(FT8100Radio, self).sync_in()
141
        self.pipe.write(bytes([yaesu_clone.CMD_ACK]))
142
        self.pipe.read(1)
143

    
144
    def sync_out(self):
145
        self.update_checksums()
146
        return _clone_out(self)
147

    
148
    def process_mmap(self):
149
        if not self._memstart:
150
            return
151

    
152
        mem_format = MEM_FORMAT.format(memories=self._memstart,
153
                                       skips=self._skipstart,
154
                                       enables=self._enablestart)
155

    
156
        self._memobj = bitwise.parse(mem_format, self._mmap)
157

    
158
    def get_sub_devices(self):
159
        return [FT8100RadioVHF(self._mmap), FT8100RadioUHF(self._mmap)]
160

    
161
    def get_memory(self, number):
162
        bit, byte = self._bit_byte(number)
163

    
164
        _mem = self._memobj.memory[number - 1]
165

    
166
        mem = chirp_common.Memory()
167
        mem.number = number
168

    
169
        mem.freq = int(_mem.freq) * 1000
170
        if _mem.tone >= len(TONES) or _mem.duplex >= len(DUPLEX):
171
            mem.empty = True
172
            return mem
173
        else:
174
            mem.rtone = TONES[_mem.tone]
175
        mem.tmode = TMODES[_mem.tone_enable]
176
        mem.mode = MODES[_mem.am]
177
        mem.duplex = DUPLEX[_mem.duplex]
178
        mem.offset = int(_mem.offset) * 1000
179

    
180
        if mem.freq // 100 == 4:
181
            mem.power = POWER_LEVELS_UHF[_mem.power]
182
        else:
183
            mem.power = POWER_LEVELS_VHF[_mem.power]
184

    
185
        # M01 can't be disabled
186
        if not self._memobj.enables[byte] & bit and number != 1:
187
            mem.empty = True
188
        elif number == 1:
189
            mem.immutable = ['empty']
190

    
191
        return mem
192

    
193
    def get_raw_memory(self, number):
194
        return repr(self._memobj.memory[number - 1])
195

    
196
    def set_memory(self, mem):
197
        bit, byte = self._bit_byte(mem.number)
198

    
199
        _mem = self._memobj.memory[mem.number - 1]
200

    
201
        _mem.freq = mem.freq // 1000
202
        _mem.tone = TONES.index(mem.rtone)
203
        _mem.tone_enable = TMODES.index(mem.tmode)
204
        _mem.am = MODES.index(mem.mode)
205
        _mem.duplex = DUPLEX.index(mem.duplex)
206
        _mem.offset = mem.offset // 1000
207

    
208
        if mem.power:
209
            _mem.power = POWER_LEVELS_VHF.index(mem.power)
210
        else:
211
            _mem.power = 0
212

    
213
        if mem.empty:
214
            self._memobj.enables[byte] &= ~bit
215
        else:
216
            self._memobj.enables[byte] |= bit
217

    
218
        # TODO expose these options
219
        _mem.baud9600 = 0
220

    
221
        # These need to be cleared, otherwise strange things happen
222
        _mem.unknown4 = 0
223
        _mem.unknown4b = 0
224
        _mem.unknown1b = 0
225
        _mem.unknown2 = 0
226

    
227
    def _checksums(self):
228
        return [yaesu_clone.YaesuChecksum(0x0000, 0x0B96)]
229

    
230
    # I didn't believe this myself, but it seems that there's a bit for
231
    # enabling VHF M01, but no bit for UHF01, and the enables are shifted down,
232
    # so that the first bit is for M02
233
    def _bit_byte(self, number):
234
        if self.VARIANT == 'VHF':
235
            bit = 1 << ((number - 1) % 8)
236
            byte = (number - 1) // 8
237
        else:
238
            bit = 1 << ((number - 2) % 8)
239
            byte = (number - 2) // 8
240

    
241
        return bit, byte
242

    
243

    
244
class FT8100RadioVHF(FT8100Radio):
245
    """Yaesu FT-8100 VHF subdevice"""
246
    VARIANT = "VHF"
247
    _memstart = 0x447
248
    _skipstart = 0x02D
249
    _enablestart = 0x04D
250

    
251

    
252
class FT8100RadioUHF(FT8100Radio):
253
    """Yaesu FT-8100 UHF subdevice"""
254
    VARIANT = "UHF"
255
    _memstart = 0x7E6
256
    _skipstart = 0x03A
257
    _enablestart = 0x05A
258

    
259

    
260
def _clone_out(radio):
261
    try:
262
        return __clone_out(radio)
263
    except Exception as e:
264
        raise errors.RadioError("Failed to communicate with the radio: %s" % e)
265

    
266

    
267
def __clone_out(radio):
268
    pipe = radio.pipe
269
    block_lengths = radio._block_lengths
270
    total_written = 0
271

    
272
    def _status():
273
        status = chirp_common.Status()
274
        status.msg = "Cloning to radio"
275
        status.max = sum(block_lengths)
276
        status.cur = total_written
277
        radio.status_fn(status)
278

    
279
    start = time.time()
280

    
281
    pos = 0
282
    for block in radio._block_lengths:
283
        LOG.debug("\nSending %i-%i" % (pos, pos + block))
284
        out = radio.get_mmap()[pos:pos + block]
285

    
286
        # need to chew byte-by-byte here or else we lose the ACK...not sure why
287
        for b in out:
288
            pipe.write(bytes([b]))
289
            pipe.read(1)  # chew the echo
290

    
291
        ack = pipe.read(1)
292

    
293
        if ack[0] != yaesu_clone.CMD_ACK:
294
            raise Exception("block not ack'ed: %s" % repr(ack))
295

    
296
        total_written += len(out)
297
        _status()
298

    
299
        pos += block
300

    
301
    LOG.debug("Clone completed in %i seconds" % (time.time() - start))
302

    
303
    return True
(3-3/4)