Project

General

Profile

New Model #7599 » ftlx011.py

Pavel Milanes, 07/30/2021 07:41 PM

 
1
# Copyright 2019 Pavel Milanes, CO7WT <pavelmc@gmail.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 2 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 struct
17
import os
18
import logging
19
import time
20

    
21
from time import sleep
22
from chirp import chirp_common, directory, memmap, errors, util, bitwise
23
from chirp.settings import RadioSettingGroup, RadioSetting, \
24
    RadioSettingValueBoolean, RadioSettingValueList, \
25
    RadioSettingValueString, RadioSettingValueInteger, \
26
    RadioSettingValueFloat, RadioSettings, InvalidValueError
27
from textwrap import dedent
28

    
29
LOG = logging.getLogger(__name__)
30

    
31
### SAMPLE MEM DUMP as sent from the radios
32

    
33
# FTL-1011
34
#0x000000  52 f0 16 90 04 08 38 c0  00 00 00 01 00 00 00 ff  |R.....8.........|
35
#0x000010  20 f1 00 20 00 00 00 20  04 47 25 04 47 25 00 00  | .. ... .G%.G%..|
36

    
37
# FTL-2011
38
#0x000000: 50 90 21 40 04 80 fc 40  00 00 00 01 00 00 00 ff  |P.!@...@........|
39
#0x000010: 20 f1 00 0b 00 00 00 0b  14 51 70 14 45 70 00 00  |.........Qp.Ep..|
40

    
41

    
42
MEM_FORMAT = """
43
#seekto 0x000;
44
u8 rid;                 // Radio Identification
45
u8 scan_time:4,         // Scan timer per channel: 0-15 (5-80msec in 5msec steps)
46
   unknownA:4;
47
bbcd if[2];             // Radio internal IF, depending on model (16.90, 21.40, 45.10, 47.90)
48
u8 chcount;             // how many channels are programmed
49
u8 scan_resume:1,           // Scan sesume: 0 = 0.5 seconds, 1 = Carrier
50
   priority_during_scan:1,  // Priority during scan: 0 = enabled, 1 = disabled
51
   priority_speed:1,        // Priority speed: 0 = slow, 1 = fast
52
   monitor:1,               // Monitor: 0 = enabled, 1 = disabled
53
   off_hook:1,              // Off hook: 0 = enabled, 1 = disabled
54
   home_channel:1,          // Home Channel: 0 = Scan Start ch, 1 = Priority 1ch
55
   talk_back:1,             // Talk Back: 0 = enabled, 1 = disabled
56
   tx_carrier_delay:1;      // TX carrier delay: 1 = enabled, 0 = disabled
57
u8 tot:4,                   // Time out timer: 16 values (0.0-7.5 in 0.5s step)
58
   tot_resume:2,            // Time out timer resume: 3, 2, 1, 0 => 0s, 6s, 20s, 60s 
59
   unknownB:2;
60
u8 a_key:4,                 // A key function: resume: 0-3: Talkaround, High/Low, Call, Accessory
61
   unknownB:4;
62

    
63
#seekto 0x010;
64
struct {
65
  u8 notx:1,            // 0 = Tx posible,  1 = Tx disabled
66
     empty:1,           // 0 = channel enabled, 1 = channed empty
67
     tot:1,             // 0 = tot disabled, 1 = tot enabled
68
     power:1,           // 0 = high, 1 = low
69
     bclo_cw:1,         // 0 = disabled, 1 = Busy Channel Lock out by carrier
70
     bclo_tone:1,       // 0 = disabled, 1 = Busy Channel Lock out by tone (set rx tone)
71
     skip:1,            // 0 = scan enabled, 1 = skip on scanning
72
     unknownA0:1;
73
  u8 chname;
74
  u8 rx_tone[2];      // empty value is \x00\x0B / disabled is \x00\x00
75
  u8 unknown4;
76
  u8 unknown5;
77
  u8 tx_tone[2];      // empty value is \x00\x0B / disabled is \x00\x00
78
  bbcd rx_freq[3];      // RX freq
79
  bbcd tx_freq[3];      // TX freq
80
  u8 unknownA[2];
81
} memory[24];
82

    
83
#seekto 0x0190;
84
char filename[11];
85

    
86
#seekto 0x19C;
87
u8 checksum;
88
"""
89

    
90
MEM_SIZE = 0x019C
91
POWER_LEVELS = [chirp_common.PowerLevel("High", watts=50),
92
                chirp_common.PowerLevel("Low", watts=5)]
93
DTCS_CODES = chirp_common.DTCS_CODES
94
SKIP_VALUES = ["", "S"]
95
LIST_BCL = ["OFF", "Carrier", "Tone"]
96
# make a copy of the tones, is not funny to work with this directly
97
TONES = list(chirp_common.TONES)
98
# this old radios has not the full tone ranges in CST
99
invalid_tones = (
100
    69.3,
101
    159.8,
102
    165.5,
103
    171.3,
104
    177.3,
105
    183.5,
106
    189.9,
107
    196.6,
108
    199.5,
109
    206.5,
110
    229.1,
111
    245.1)
112

    
113
# remove invalid tones
114
for tone in invalid_tones:
115
    try:
116
        TONES.remove(tone)
117
    except:
118
        pass
119

    
120

    
121
def _set_serial(radio):
122
    """Set the serial protocol settings"""
123
    radio.pipe.timeout = 10
124
    radio.pipe.parity = "N"
125
    radio.pipe.baudrate = 9600
126

    
127

    
128
def _checksum(data):
129
    """the radio block checksum algorithm"""
130
    cs = 0
131
    for byte in data:
132
            cs += ord(byte)
133

    
134
    return cs % 256
135

    
136

    
137
def _update_cs(radio):
138
    """Update the checksum on the mmap"""
139
    payload = str(radio.get_mmap())[:-1]
140
    cs = _checksum(payload)
141
    radio._mmap[MEM_SIZE - 1] = cs
142

    
143

    
144
def _do_download(radio):
145
    """ The download function """
146
    # Get the whole 413 bytes (0x019D) bytes one at a time with plenty of time
147
    # to get to the user's pace
148

    
149
    # set serial discipline
150
    _set_serial(radio)
151

    
152
    # UI progress
153
    status = chirp_common.Status()
154
    status.cur = 0
155
    status.max = MEM_SIZE
156
    status.msg = " Press A to clone. "
157
    radio.status_fn(status)
158

    
159
    data = ""
160
    for i in range(0, MEM_SIZE):
161
        a = radio.pipe.read(1)
162
        if len(a) == 0:
163
            # error, no received data
164
            if len(data) != 0:
165
                # received some data, not the complete stream
166
                msg = "Just %02i bytes of the %02i received, try again." % \
167
                    (len(data), MEM_SIZE)
168
            else:
169
                # timeout, please retry
170
                msg = "No data received, try again."
171

    
172
            raise errors.RadioError(msg)
173

    
174
        data += a
175
        # UI Update
176
        status.cur = len(data)
177
        radio.status_fn(status)
178

    
179
    if len(data) != MEM_SIZE:
180
        msg = "Incomplete data, we need %02i but got %02i bytes." % \
181
            (MEM_SIZE, len(data))
182
        raise errors.RadioError(msg)
183

    
184
    if ord(data[-1]) != _checksum(data[:-1]):
185
        msg = "Bad checksum, please try again."
186
        raise errors.RadioError(msg)
187

    
188
    return data
189

    
190

    
191
def _do_upload(radio):
192
    """The upload function"""
193
    # set serial discipline
194
    _set_serial(radio)
195

    
196
    # UI progress
197
    status = chirp_common.Status()
198

    
199
    # 10 seconds timeout
200
    status.cur = 0
201
    status.max = 100
202
    status.msg = " Quick, press MON on the radio to start. "
203
    radio.status_fn(status)
204

    
205
    for byte in range(0,100):
206
        status.cur = byte
207
        radio.status_fn(status)
208
        time.sleep(0.1)
209

    
210

    
211
    # real upload if user don't cancel the timeout
212
    status.cur = 0
213
    status.max = MEM_SIZE
214
    status.msg = " Cloning to radio... "
215
    radio.status_fn(status)
216

    
217
    # send data
218
    data = str(radio.get_mmap())
219

    
220
    # this radio has a trick, the EEPROM is an ancient SPI one, so it needs
221
    # some time to write, so we send every byte and then allow
222
    # a 0.01 seg to complete the write from the MCU to the SPI EEPROM
223
    c = 0
224
    for byte in data:
225
        radio.pipe.write(byte)
226
        time.sleep(0.01)
227

    
228
        # UI Update
229
        status.cur = c
230
        radio.status_fn(status)
231

    
232
        # counter
233
        c = c + 1
234

    
235

    
236
def _model_match(cls, data):
237
    """Use a experimental guess to determine if the radio you just
238
    downloaded or the img you opened is for this model"""
239

    
240
    # It's hard to tell when this radio is really this radio.
241
    # I use the first byte, that appears to be the ID and the IF settings
242

    
243
    LOG.debug("Drivers's ID string:")
244
    LOG.debug(cls.finger)
245
    LOG.debug("Radio's ID string:")
246
    LOG.debug(util.hexprint(data[0:4]))
247

    
248
    radiod = [data[0], data[2:4]]
249
    if cls.finger == radiod:
250
        return True
251
    else:
252
        return False
253

    
254

    
255
def bcd_to_int(data):
256
    """Convert an array of bcdDataElement like \x12
257
    into an int like 12"""
258
    value = 0
259
    a = (data & 0xF0) >> 4
260
    b = data & 0x0F
261
    value = (a * 10) + b
262
    return value
263

    
264

    
265
def int_to_bcd(data):
266
    """Convert a int like 94 to 0x94"""
267
    data, lsb = divmod(data, 10)
268
    data, msb = divmod(data, 10)
269
    res = (msb << 4) + lsb
270
    return res
271

    
272

    
273
class ftlx011(chirp_common.CloneModeRadio, chirp_common.ExperimentalRadio):
274
    """Vertex FTL1011/2011/7011 4/8/12/24 channels"""
275
    VENDOR = "Vertex Standard"
276
    _memsize = MEM_SIZE
277
    _upper = 0
278
    _range = []
279
    finger = [] # two elements rid & IF
280

    
281
    @classmethod
282
    def get_prompts(cls):
283
        rp = chirp_common.RadioPrompts()
284
        rp.experimental = \
285
            ('This is a experimental driver, use it on your own risk.\n'
286
             '\n'
287
             'This driver is just for the 4/12/24 channels variants of '
288
             'these radios, 99 channel variants are not supported yet.\n'
289
             '\n'
290
             'The 99 channel versions appears to use another mem layout.\n'
291
             )
292
        rp.pre_download = _(dedent("""\
293
            Please follow this steps carefully:
294

    
295
            1 - Turn on your radio
296
            2 - Connect the interface cable to your radio.
297
            3 - Click the button on this window to start download
298
                (Radio will beep and led will flash)
299
            4 - Then press the "A" button in your radio to start cloning.
300
                (At the end radio will beep)
301
            """))
302
        rp.pre_upload = _(dedent("""\
303
            Please follow this steps carefully:
304

    
305
            1 - Turn on your radio
306
            2 - Connect the interface cable to your radio
307
            3 - Click the button on this window to start download
308
                (you may see another dialog, click ok)
309
            4 - Radio will beep and led will flash
310
            5 - You will get a 10 seconds timeout to press "MON" before
311
                data upload start
312
            6 - If all goes right radio will beep at end.
313

    
314
            After cloning remove the cable and power cycle your radio to
315
            get into normal mode.
316
            """))
317
        return rp
318

    
319
    def get_features(self):
320
        """Return information about this radio's features"""
321
        rf = chirp_common.RadioFeatures()
322
        rf.has_settings = False
323
        rf.has_bank = False
324
        rf.has_tuning_step = False
325
        rf.has_name = False
326
        rf.has_offset = True
327
        rf.has_mode = True
328
        rf.has_dtcs = True
329
        rf.has_rx_dtcs = True
330
        rf.has_dtcs_polarity = False
331
        rf.has_ctone = True
332
        rf.has_cross = True
333
        rf.valid_duplexes = ["", "-", "+", "off"]
334
        rf.valid_tmodes = ['', 'Tone', 'TSQL', 'DTCS', 'Cross']
335
        rf.valid_cross_modes = [
336
            "Tone->Tone",
337
            "DTCS->DTCS",
338
            "DTCS->",
339
            "->DTCS"]
340
        rf.valid_dtcs_codes = DTCS_CODES
341
        rf.valid_skips = SKIP_VALUES
342
        rf.valid_modes = ["FM"]
343
        rf.valid_power_levels = POWER_LEVELS
344
        #rf.valid_tuning_steps = [5.0]
345
        rf.valid_bands = [self._range]
346
        rf.memory_bounds = (1, self._upper)
347
        return rf
348

    
349
    def sync_in(self):
350
        """Do a download of the radio eeprom"""
351
        try:
352
            data = _do_download(self)
353
        except Exception, e:
354
            raise errors.RadioError("Failed to communicate with radio:\n %s" % e)
355

    
356
        # match model
357
        if _model_match(self, data) is False:
358
            raise errors.RadioError("Incorrect radio model")
359

    
360
        self._mmap = memmap.MemoryMap(data)
361
        self.process_mmap()
362

    
363
        # set the channel count from the radio eeprom
364
        self._upper = int(ord(data[4]))
365

    
366
    def sync_out(self):
367
        """Do an upload to the radio eeprom"""
368
        # update checksum
369
        _update_cs(self)
370

    
371
        # sanity check, match model
372
        data = str(self.get_mmap())
373
        if len(data) != MEM_SIZE:
374
            raise errors.RadioError("Wrong radio image? Size miss match.")
375

    
376
        if _model_match(self, data) is False:
377
            raise errors.RadioError("Wrong image? Fingerprint miss match")
378

    
379
        try:
380
            _do_upload(self)
381
        except Exception, e:
382
            msg = "Failed to communicate with radio:\n%s" % e
383
            raise errors.RadioError(msg)
384

    
385
    def process_mmap(self):
386
        """Process the memory object"""
387
        self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
388

    
389
    def get_raw_memory(self, number):
390
        """Return a raw representation of the memory object"""
391
        return repr(self._memobj.memory[number])
392

    
393
    def _decode_tone(self, mem, rx=True):
394
        """Parse the tone data to decode from mem tones are encodded like this
395
        CTCS: mapped [0x80...0xa5] = [67.0...250.3]
396
        DTCS: mixed  [0x88, 0x23] last is BCD and first is the 100 power - 88
397

    
398
        It return: ((''|DTCS|Tone), Value (None|###), None)"""
399
        mode = ""
400
        tone = None
401

    
402
        # get the tone depending of rx or tx
403
        if rx:
404
            t = mem.rx_tone
405
        else:
406
            t = mem.tx_tone
407
        
408
        tMSB = t[0]
409
        tLSB = t[1]
410

    
411
        # no tone at all
412
        if (tMSB == 0 and tLSB < 128):
413
            return ('', None, None)
414

    
415
        # extract the tone info
416
        if tMSB == 0x00:
417
            # CTCS
418
            mode = "Tone"
419
            try:
420
                tone = TONES[tLSB - 128]
421
            except IndexError:
422
                LOG.debug("Error decoding a CTCS tone")
423
                pass
424
        else:
425
            # DTCS
426
            mode = "DTCS"
427
            try:
428
                tone = ((tMSB - 0x88) * 100) + bcd_to_int(tLSB)
429
            except IndexError:
430
                LOG.debug("Error decoding a DTCS tone")
431
                pass
432

    
433
        return (mode, tone, None)
434

    
435
    def _encode_tone(self, mem, mode, value, pol, rx=True):
436
        """Parse the tone data to encode from UI to mem
437
        CTCS: mapped [0x80...0xa5] = [67.0...250.3]
438
        DTCS: mixed  [0x88, 0x23] last is BCD and first is the 100 power - 88
439
        """
440

    
441
        # array to pass
442
        tone = [0x00, 0x00]
443

    
444
        # which mod
445
        if mode == "DTCS":
446
            tone[0] = int(value / 100) + 0x88
447
            tone[1] = int_to_bcd(value % 100)
448
        
449
        if mode == "Tone":
450
            #CTCS
451
            tone[1] = TONES.index(value) + 128
452

    
453
        # set it
454
        if rx:
455
            mem.rx_tone = tone
456
        else:
457
            mem.tx_tone = tone
458

    
459
    def get_memory(self, number):
460
        """Extract a memory object from the memory map"""
461
        # Get a low-level memory object mapped to the image
462
        _mem = self._memobj.memory[number - 1]
463
        # Create a high-level memory object to return to the UI
464
        mem = chirp_common.Memory()
465
        # number
466
        mem.number = number
467

    
468
        # empty
469
        if bool(_mem.empty) is True:
470
            mem.empty = True
471
            return mem
472

    
473
        # rx freq
474
        mem.freq = int(_mem.rx_freq) * 1000
475

    
476
        # power
477
        mem.power = POWER_LEVELS[int(_mem.power)]
478

    
479
        # checking if tx freq is disabled
480
        if bool(_mem.notx) is True:
481
            mem.duplex = "off"
482
            mem.offset = 0
483
        else:
484
            tx = int(_mem.tx_freq) * 1000
485
            if tx == mem.freq:
486
                mem.offset = 0
487
                mem.duplex = ""
488
            else:
489
                mem.duplex = mem.freq > tx and "-" or "+"
490
                mem.offset = abs(tx - mem.freq)
491

    
492
        # skip
493
        mem.skip = SKIP_VALUES[_mem.skip]
494

    
495
        # tone data
496
        rxtone = txtone = None
497
        rxtone = self._decode_tone(_mem)
498
        txtone = self._decode_tone(_mem, False)
499
        chirp_common.split_tone_decode(mem, txtone, rxtone)
500

    
501
        # this radio has a primitive mode to show the channel number on a 7-segment
502
        # two digit LCD, we will use channel number
503
        # we will use a trick to show the numbers < 10 wit a space not a zero in front
504
        chname = int_to_bcd(mem.number)
505
        if mem.number < 10:
506
            # convert to F# as BCD
507
            chname = mem.number + 240
508

    
509
        _mem.chname = chname
510

    
511
        # Extra
512
        mem.extra = RadioSettingGroup("extra", "Extra")
513

    
514
        # bcl preparations: ["OFF", "Carrier", "Tone"]
515
        bcls = 0
516
        if _mem.bclo_cw:
517
            bcls = 1
518
        if _mem.bclo_tone:
519
            bcls = 2
520

    
521
        bcl = RadioSetting("bclo", "Busy channel lockout",
522
                           RadioSettingValueList(LIST_BCL,
523
                                                 LIST_BCL[bcls]))
524
        mem.extra.append(bcl)
525

    
526
        # return mem
527
        return mem
528

    
529
    def set_memory(self, mem):
530
        """Store details about a high-level memory to the memory map
531
        This is called when a user edits a memory in the UI"""
532
        # Get a low-level memory object mapped to the image
533
        _mem = self._memobj.memory[mem.number - 1]
534

    
535
        # Empty memory
536
        if mem.empty:
537
            _mem.empty = True
538
            _mem.rx_freq = _mem.tx_freq = 0
539
            return
540

    
541
        # freq rx
542
        _mem.rx_freq = mem.freq / 1000
543

    
544
        # power, # default power level is high
545
        _mem.power = 0 if mem.power is None else POWER_LEVELS.index(mem.power)
546

    
547
        # freq tx
548
        if mem.duplex == "+":
549
            _mem.tx_freq = (mem.freq + mem.offset) / 1000
550
        elif mem.duplex == "-":
551
            _mem.tx_freq = (mem.freq - mem.offset) / 1000
552
        elif mem.duplex == "off":
553
            _mem.notx = 1
554
            _mem.tx_freq = _mem.rx_freq
555
        else:
556
            _mem.tx_freq = mem.freq / 1000
557

    
558
        # scan add property
559
        _mem.skip = SKIP_VALUES.index(mem.skip)
560

    
561
        # tone data
562
        ((txmode, txtone, txpol), (rxmode, rxtone, rxpol)) = \
563
            chirp_common.split_tone_encode(mem)
564

    
565
        # validate tone data from here
566
        if rxmode == "Tone" and rxtone in invalid_tones:
567
            msg = "The tone %shz is not valid for this radio" % rxtone
568
            raise errors.UnsupportedToneError(msg)
569

    
570
        if txmode == "Tone" and txtone in invalid_tones:
571
            msg = "The tone %shz is not valid for this radio" % txtone
572
            raise errors.UnsupportedToneError(msg)
573

    
574
        if rxmode == "DTCS" and rxtone not in DTCS_CODES:
575
            msg = "The digital tone %s is not valid for this radio" % rxtone
576
            raise errors.UnsupportedToneError(msg)
577

    
578
        if txmode == "DTCS" and txtone not in DTCS_CODES:
579
            msg = "The digital tone %s is not valid for this radio" % txtone
580
            raise errors.UnsupportedToneError(msg)
581

    
582
        self._encode_tone(_mem, rxmode, rxtone, rxpol)
583
        self._encode_tone(_mem, txmode, txtone, txpol, False)
584

    
585
        # this radio has a primitive mode to show the channel number on a 7-segment
586
        # two digit LCD, we will use channel number
587
        # we will use a trick to show the numbers < 10 wit a space not a zero in front
588
        chname = int_to_bcd(mem.number)
589
        if mem.number < 10:
590
            # convert to F# as BCD
591
            chname = mem.number + 240
592

    
593
        def _zero_settings():
594
            _mem.bclo_cw = 0
595
            _mem.bclo_tone = 0
596

    
597
        # extra settings
598
        if len(mem.extra) > 0:
599
            # there are setting, parse
600
            LOG.debug("Extra-Setting supplied. Setting them.")
601
            # Zero them all first so any not provided by model don't
602
            # stay set
603
            _zero_settings()
604
            for setting in mem.extra:
605
                if setting.get_name() == "bclo":
606
                    sw = LIST_BCL.index(str(setting.value))
607
                    if sw == 0:
608
                        # empty
609
                        _zero_settings()
610
                    if sw == 1:
611
                        # carrier
612
                        _mem.bclo_cw = 1
613
                    if sw == 2:
614
                        # tone
615
                        _mem.bclo_tone = 1
616
                        # activate the tone
617
                        _mem.rx_tone = [0x00, 0x80]
618

    
619
        else:
620
            # reset extra settings 
621
            _zero_settings()
622

    
623
        _mem.chname = chname
624

    
625
        return mem
626

    
627
    @classmethod
628
    def match_model(cls, filedata, filename):
629
        match_size = False
630
        match_model = False
631

    
632
        # testing the file data size
633
        if len(filedata) == cls._memsize:
634
            match_size = True
635
            print("Comp: %i file / %i memzise" % (len(filedata), cls._memsize) )
636

    
637
        # testing the firmware fingerprint, this experimental
638
        match_model = _model_match(cls, filedata)
639

    
640
        if match_size and match_model:
641
            return True
642
        else:
643
            return False
644

    
645

    
646
@directory.register
647
class ftl1011(ftlx011):
648
    """Vertex FTL-1011"""
649
    MODEL = "FTL-1011"
650
    _memsize = MEM_SIZE
651
    _upper = 4
652
    _range = [44000000, 56000000]
653
    finger = ["\x52", "\x16\x90"]
654

    
655

    
656
@directory.register
657
class ftl2011(ftlx011):
658
    """Vertex FTL-2011"""
659
    MODEL = "FTL-2011"
660
    _memsize = MEM_SIZE
661
    _upper = 24
662
    _range = [134000000, 174000000]
663
    finger = ["\x50", "\x21\x40"]
664

    
665

    
666
@directory.register
667
class ftl7011(ftlx011):
668
    """Vertex FTL-7011"""
669
    MODEL = "FTL-7011"
670
    _memsize = MEM_SIZE
671
    _upper = 24
672
    _range = [400000000, 512000000]
673
    finger = ["\x54", "\x47\x90"]
674

    
675

    
676
@directory.register
677
class ftl8011(ftlx011):
678
    """Vertex FTL-8011"""
679
    MODEL = "FTL-8011"
680
    _memsize = MEM_SIZE
681
    _upper = 24
682
    _range = [400000000, 512000000]
683
    finger = ["\x5c", "\x45\x10"]
(12-12/16)