Project

General

Profile

Bug #11459 » puxing.py

Dan Smith, 08/23/2024 12:50 PM

 
# Copyright 2011 Dan Smith <dsmith@danplanet.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

"""Puxing radios management module"""

import time
import logging

from chirp import util, chirp_common, bitwise, errors, directory
from chirp.drivers.wouxun import wipe_memory, do_download, do_upload

LOG = logging.getLogger(__name__)


def _puxing_prep(radio):
radio.pipe.write(b"\x02PROGRA")
ack = radio.pipe.read(1)
if ack != b"\x06":
raise Exception("Radio did not ACK first command")

radio.pipe.write(b"M\x02")
ident = radio.pipe.read(8)
if len(ident) != 8:
LOG.debug(util.hexprint(ident))
raise Exception("Radio did not send identification")

radio.pipe.write(b"\x06")
if radio.pipe.read(1) != b"\x06":
raise Exception("Radio did not ACK ident")


def puxing_prep(radio):
"""Do the Puxing PX-777 identification dance"""
ex = None
for _i in range(0, 10):
try:
return _puxing_prep(radio)
except Exception as e:
time.sleep(1)
ex = e

raise ex


def puxing_download(radio):
"""Talk to a Puxing PX-777 and do a download"""
try:
puxing_prep(radio)
return do_download(radio, 0x0000, 0x0C60, 0x0008)
except errors.RadioError:
raise
except Exception as e:
raise errors.RadioError("Failed to communicate with radio: %s" % e)


def puxing_upload(radio):
"""Talk to a Puxing PX-777 and do an upload"""
try:
puxing_prep(radio)
return do_upload(radio, 0x0000, 0x0C40, 0x0008)
except errors.RadioError:
raise
except Exception as e:
raise errors.RadioError("Failed to communicate with radio: %s" % e)


POWER_LEVELS = [chirp_common.PowerLevel("High", watts=5.00),
chirp_common.PowerLevel("Low", watts=1.00)]

PUXING_CHARSET = list("0123456789") + \
[chr(x + ord("A")) for x in range(0, 26)] + \
list("- ")

PUXING_MEM_FORMAT = """
#seekto 0x0000;
struct {
lbcd rx_freq[4];
lbcd tx_freq[4];
lbcd rx_tone[2];
lbcd tx_tone[2];
u8 _3_unknown_1;
u8 _2_unknown_1:2,
power_high:1,
iswide:1,
skip:1,
bclo:2,
_2_unknown_2:1;
u8 _4_unknown1:7,
pttid:1;
u8 unknown;
} memory[128];

#seekto 0x080A;
struct {
u8 limits;
u8 model;
} model[1];

#seekto 0x0850;
struct {
u8 name[6];
u8 pad[2];
} names[128];
"""

# Limits
# 67- 72: 0xEE
# 136-174: 0xEF
# 240-260: 0xF0
# 350-390: 0xF1
# 400-430: 0xF2
# 430-450: 0xF3
# 450-470: 0xF4
# 470-490: 0xF5
# 400-470: 0xF6
# 460-520: 0xF7

PUXING_MODELS = {
328: 0x38,
338: 0x39,
777: 0x3A,
}

PUXING_777_BANDS = [
(67000000, 72000000),
(136000000, 174000000),
(240000000, 260000000),
(350000000, 390000000),
(400000000, 430000000),
(430000000, 450000000),
(450000000, 470000000),
(470000000, 490000000),
(400000000, 470000000),
(460000000, 520000000),
]


@directory.register
class Puxing777Radio(chirp_common.CloneModeRadio):
"""Puxing PX-777"""
VENDOR = "Puxing"
MODEL = "PX-777"

def sync_in(self):
self._mmap = puxing_download(self)
self.process_mmap()

def sync_out(self):
puxing_upload(self)

def get_features(self):
rf = chirp_common.RadioFeatures()
rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"]
rf.valid_modes = ["FM", "NFM"]
rf.valid_power_levels = POWER_LEVELS
rf.valid_characters = ''.join(set(PUXING_CHARSET))
rf.valid_name_length = 6
rf.valid_tuning_steps = [2.5, 5.0, 6.25, 10.0, 12.5, 15.0, 20.0,
25.0, 30.0, 50.0, 100.0]
rf.has_ctone = False
rf.has_tuning_step = False
rf.has_bank = False
rf.memory_bounds = (1, 128)

if not hasattr(self, "_memobj") or self._memobj is None:
rf.valid_bands = [PUXING_777_BANDS[1]]
elif self._memobj.model.model == PUXING_MODELS[777]:
limit_idx = self._memobj.model.limits - 0xEE
try:
rf.valid_bands = [PUXING_777_BANDS[limit_idx]]
except IndexError:
LOG.error("Invalid band index %i (0x%02x)" %
(limit_idx, self._memobj.model.limits))
rf.valid_bands = [PUXING_777_BANDS[1]]
elif self._memobj.model.model == PUXING_MODELS[328]:
# There are PX-777 that says to be model 328 ...
# for them we only know this freq limits till now
if self._memobj.model.limits in (0xEE, 0xEF):
rf.valid_bands = [PUXING_777_BANDS[1]]
else:
raise Exception("Unsupported band limits 0x%02x for PX-777" %
(self._memobj.model.limits) + " submodel 328"
" - PLEASE REPORT THIS ERROR TO DEVELOPERS!!")

return rf

def process_mmap(self):
self._memobj = bitwise.parse(PUXING_MEM_FORMAT, self._mmap)

def get_raw_memory(self, number):
return repr(self._memobj.memory[number - 1]) + "\r\n" + \
repr(self._memobj.names[number - 1])

@classmethod
def match_model(cls, filedata, filename):
# There are PX-777 that says to be model 328 ...
return (len(filedata) == 3168 and
(util.byte_to_int(filedata[0x080B]) == PUXING_MODELS[777] or
(util.byte_to_int(filedata[0x080B]) == PUXING_MODELS[328] and
util.byte_to_int(filedata[0x080A]) == 0xEE)))

def get_memory(self, number):
_mem = self._memobj.memory[number - 1]
_nam = self._memobj.names[number - 1]

def _is_empty():
for i in range(0, 4):
if _mem.rx_freq[i].get_raw(asbytes=False) != "\xFF":
return False
return True

def _is_no_tone(field):
return field.get_raw(asbytes=False) in ["\x00\x00", "\xFF\xFF"]

def _get_dtcs(value):
# Upper nibble 0x80 -> DCS, 0xC0 -> Inv. DCS
if value > 12000:
return "R", value - 12000
elif value > 8000:
return "N", value - 8000
else:
raise Exception("Unable to convert DCS value")

def _do_dtcs(mem, txfield, rxfield):
if int(txfield) < 8000 or int(rxfield) < 8000:
raise Exception("Split tone not supported")

if txfield[0].get_raw(asbytes=False) == "\xFF":
tp, tx = "N", None
else:
tp, tx = _get_dtcs(int(txfield))

if rxfield[0].get_raw(asbytes=False) == "\xFF":
rp, rx = "N", None
else:
rp, rx = _get_dtcs(int(rxfield))

if not rx:
rx = tx
if not tx:
tx = rx

if tx != rx:
raise Exception("Different RX and TX DCS codes not supported")

mem.dtcs = tx
mem.dtcs_polarity = "%s%s" % (tp, rp)

mem = chirp_common.Memory()
mem.number = number

if _is_empty():
mem.empty = True
return mem

mem.freq = int(_mem.rx_freq) * 10
mem.offset = (int(_mem.tx_freq) * 10) - mem.freq
if mem.offset < 0:
mem.duplex = "-"
elif mem.offset:
mem.duplex = "+"
mem.offset = abs(mem.offset)
if not _mem.skip:
mem.skip = "S"
if not _mem.iswide:
mem.mode = "NFM"

if _is_no_tone(_mem.tx_tone):
pass # No tone
elif int(_mem.tx_tone) > 8000 or \
(not _is_no_tone(_mem.rx_tone) and int(_mem.rx_tone) > 8000):
mem.tmode = "DTCS"
_do_dtcs(mem, _mem.tx_tone, _mem.rx_tone)
else:
mem.rtone = int(_mem.tx_tone) / 10.0
mem.tmode = _is_no_tone(_mem.rx_tone) and "Tone" or "TSQL"

mem.power = POWER_LEVELS[not _mem.power_high]

for i in _nam.name:
if i == 0xFF:
break
mem.name += PUXING_CHARSET[i]
mem.name = mem.name.rstrip()

return mem

def set_memory(self, mem):
_mem = self._memobj.memory[mem.number - 1]
_nam = self._memobj.names[mem.number - 1]

if mem.empty:
wipe_memory(_mem, "\xFF")
return

_mem.rx_freq = mem.freq / 10
if mem.duplex == "+":
_mem.tx_freq = (mem.freq / 10) + (mem.offset / 10)
elif mem.duplex == "-":
_mem.tx_freq = (mem.freq / 10) - (mem.offset / 10)
else:
_mem.tx_freq = (mem.freq / 10)
_mem.skip = mem.skip != "S"
_mem.iswide = mem.mode != "NFM"

_mem.rx_tone[0].set_raw("\xFF")
_mem.rx_tone[1].set_raw("\xFF")
_mem.tx_tone[0].set_raw("\xFF")
_mem.tx_tone[1].set_raw("\xFF")

if mem.tmode == "DTCS":
_mem.tx_tone = int("%x" % int("%i" % (mem.dtcs), 16))
_mem.rx_tone = int("%x" % int("%i" % (mem.dtcs), 16))

# Argh. Set the high order two bits to signal DCS or Inv. DCS
txm = mem.dtcs_polarity[0] == "N" and 0x80 or 0xC0
rxm = mem.dtcs_polarity[1] == "N" and 0x80 or 0xC0
_mem.tx_tone[1].set_raw(
chr(ord(_mem.tx_tone[1].get_raw(asbytes=False)) | txm))
_mem.rx_tone[1].set_raw(
chr(ord(_mem.rx_tone[1].get_raw(asbytes=False)) | rxm))

elif mem.tmode:
_mem.tx_tone = int(mem.rtone * 10)
if mem.tmode == "TSQL":
_mem.rx_tone = int(_mem.tx_tone)

if mem.power:
_mem.power_high = not POWER_LEVELS.index(mem.power)
else:
_mem.power_high = True

# Default to disabling the busy channel lockout
# 00 == Close
# 01 == Carrier
# 10 == QT/DQT
_mem.bclo = 0

_nam.name = [0xFF] * 6
for i in range(0, len(mem.name)):
try:
_nam.name[i] = PUXING_CHARSET.index(mem.name[i])
except IndexError:
raise Exception("Character `%s' not supported")


def puxing_2r_prep(radio):
"""Do the Puxing 2R identification dance"""
radio.pipe.timeout = 0.2
radio.pipe.write(b"PROGRAM\x02")
ack = radio.pipe.read(1)
if ack != b"\x06":
raise Exception("Radio is not responding")

radio.pipe.write(ack)
ident = radio.pipe.read(16)
LOG.info("Radio ident: %s (%i)" % (repr(ident), len(ident)))


def puxing_2r_download(radio):
"""Talk to a Puxing 2R and do a download"""
try:
puxing_2r_prep(radio)
return do_download(radio, 0x0000, 0x0FE0, 0x0010)
except errors.RadioError:
raise
except Exception as e:
raise errors.RadioError("Failed to communicate with radio: %s" % e)


def puxing_2r_upload(radio):
"""Talk to a Puxing 2R and do an upload"""
try:
puxing_2r_prep(radio)
return do_upload(radio, 0x0000, 0x0FE0, 0x0010)
except errors.RadioError:
raise
except Exception as e:
raise errors.RadioError("Failed to communicate with radio: %s" % e)


PUXING_2R_MEM_FORMAT = """
#seekto 0x0010;
struct {
lbcd freq[4];
lbcd offset[4];
u8 rx_tone;
u8 tx_tone;
u8 duplex:2,
txdtcsinv:1,
rxdtcsinv:1,
simplex:1,
unknown2:1,
iswide:1,
ishigh:1;
u8 name[5];
} memory[128];
"""

PX2R_DUPLEX = ["", "+", "-", ""]
PX2R_POWER_LEVELS = [chirp_common.PowerLevel("Low", watts=1.0),
chirp_common.PowerLevel("High", watts=2.0)]
PX2R_CHARSET = "0123456789- ABCDEFGHIJKLMNOPQRSTUVWXYZ +"


@directory.register
class Puxing2RRadio(chirp_common.CloneModeRadio):
"""Puxing PX-2R"""
VENDOR = "Puxing"
MODEL = "PX-2R"
NEEDS_COMPAT_SERIAL = True
_memsize = 0x0FE0

def get_features(self):
rf = chirp_common.RadioFeatures()
rf.valid_tmodes = ["", "Tone", "TSQL", "DTCS"]
rf.valid_modes = ["FM", "NFM"]
rf.valid_power_levels = PX2R_POWER_LEVELS
rf.valid_bands = [(400000000, 500000000)]
rf.valid_characters = PX2R_CHARSET
rf.valid_name_length = 5
rf.valid_duplexes = ["", "+", "-"]
rf.valid_skips = []
rf.has_ctone = False
rf.has_tuning_step = False
rf.has_bank = False
rf.memory_bounds = (1, 128)
rf.can_odd_split = False
return rf

@classmethod
def match_model(cls, filedata, filename):
return (len(filedata) == cls._memsize) and \
filedata[-16:] not in ("IcomCloneFormat3",
b'IcomCloneFormat3')

def sync_in(self):
self._mmap = puxing_2r_download(self)
self.process_mmap()

def sync_out(self):
puxing_2r_upload(self)

def process_mmap(self):
self._memobj = bitwise.parse(PUXING_2R_MEM_FORMAT, self._mmap)

def get_memory(self, number):
_mem = self._memobj.memory[number-1]

mem = chirp_common.Memory()
mem.number = number
if _mem.get_raw(asbytes=False)[0:4] == "\xff\xff\xff\xff":
mem.empty = True
return mem

mem.freq = int(_mem.freq) * 10
mem.offset = int(_mem.offset) * 10
mem.mode = _mem.iswide and "FM" or "NFM"
mem.duplex = PX2R_DUPLEX[_mem.duplex]
mem.power = PX2R_POWER_LEVELS[_mem.ishigh]

if _mem.tx_tone >= 0x33:
mem.dtcs = chirp_common.DTCS_CODES[_mem.tx_tone - 0x33]
mem.tmode = "DTCS"
mem.dtcs_polarity = \
(_mem.txdtcsinv and "R" or "N") + \
(_mem.rxdtcsinv and "R" or "N")
elif _mem.tx_tone:
mem.rtone = chirp_common.TONES[_mem.tx_tone - 1]
mem.tmode = _mem.rx_tone and "TSQL" or "Tone"

count = 0
for i in _mem.name:
if i == 0xFF:
break
try:
mem.name += PX2R_CHARSET[i]
except Exception:
LOG.error("Unknown name char %i: 0x%02x (mem %i)" %
(count, i, number))
mem.name += " "
count += 1
mem.name = mem.name.rstrip()

return mem

def set_memory(self, mem):
_mem = self._memobj.memory[mem.number-1]

if mem.empty:
_mem.set_raw("\xff" * 16)
return

_mem.freq = mem.freq / 10
_mem.offset = mem.offset / 10
_mem.iswide = mem.mode == "FM"
_mem.duplex = PX2R_DUPLEX.index(mem.duplex)
_mem.ishigh = mem.power == PX2R_POWER_LEVELS[1]

if mem.tmode == "DTCS":
_mem.tx_tone = chirp_common.DTCS_CODES.index(mem.dtcs) + 0x33
_mem.rx_tone = chirp_common.DTCS_CODES.index(mem.dtcs) + 0x33
_mem.txdtcsinv = mem.dtcs_polarity[0] == "R"
_mem.rxdtcsinv = mem.dtcs_polarity[1] == "R"
elif mem.tmode in ["Tone", "TSQL"]:
_mem.tx_tone = chirp_common.TONES.index(mem.rtone) + 1
_mem.rx_tone = mem.tmode == "TSQL" and int(_mem.tx_tone) or 0
else:
_mem.tx_tone = 0
_mem.rx_tone = 0

for i in range(0, 5):
try:
_mem.name[i] = PX2R_CHARSET.index(mem.name[i])
except IndexError:
_mem.name[i] = 0xFF

def get_raw_memory(self, number):
return repr(self._memobj.memory[number-1])
(4-4/7)