|
# Copyright 2012 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 2 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/>.
|
|
|
|
import struct
|
|
import os
|
|
import logging
|
|
import unittest
|
|
|
|
from chirp import chirp_common, directory, memmap, errors, util
|
|
from chirp import bitwise
|
|
from textwrap import dedent
|
|
from chirp.settings import RadioSettingGroup, RadioSetting, \
|
|
RadioSettingValueBoolean, RadioSettingValueList, \
|
|
RadioSettingValueString, RadioSettings
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
MEM_FORMAT = """
|
|
#seekto 0x0010;
|
|
struct {
|
|
lbcd rx_freq[4];
|
|
lbcd tx_freq[4];
|
|
lbcd rx_tone[2];
|
|
lbcd tx_tone[2];
|
|
u8 unknown:4,
|
|
scramble:1,
|
|
unknown1:1,
|
|
unknown2:1,
|
|
busy_lock:1;
|
|
u8 unknown3[3];
|
|
} memory[99];
|
|
|
|
#seekto 0x0640;
|
|
struct {
|
|
lbcd vrx_freq[4];
|
|
lbcd vtx_freq[4];
|
|
lbcd vrx_tone[2];
|
|
lbcd vtx_tone[2];
|
|
u8 shift_plus:1,
|
|
shift_minus:1,
|
|
unknown11:2,
|
|
scramble:1,
|
|
unknown12:1,
|
|
unknown13:1,
|
|
busy_lock:1;
|
|
u8 unknown14[3];
|
|
} vfo;
|
|
|
|
#seekto 0x07C0;
|
|
struct {
|
|
u8 unknown22:5,
|
|
bw1:1,
|
|
bs1:1,
|
|
warning1:1;
|
|
u8 sql[1];
|
|
u8 monitorval;
|
|
u8 tot[1];
|
|
u8 unknown23[4];
|
|
u8 unknown24[8];
|
|
char model[8];
|
|
u8 unknown26[8];
|
|
u8 step;
|
|
u8 unknown27:2,
|
|
power:1,
|
|
lamp:1,
|
|
lamp_auto:1,
|
|
key:1,
|
|
monitor:1,
|
|
bw:1;
|
|
u8 unknown28:3,
|
|
warning:1,
|
|
bs:1,
|
|
unknown29:1,
|
|
wmem:1,
|
|
wvfo:1;
|
|
u8 active_ch;
|
|
u8 unknown30[4];
|
|
u8 unknown31[4];
|
|
bbcd vfo_shift[4];
|
|
} settings;
|
|
"""
|
|
|
|
######################
|
|
# Radio settigns NOTES
|
|
######################
|
|
#
|
|
# sql: 0..9
|
|
# tot: is val * 10; 0 = OFF (01 = 10; x00..x09 )
|
|
#
|
|
# power, lamp, lamp_auto. wmem, wvfo, moni: 1 on / 0 off;
|
|
# warning, bs : 1 on / 0 off;
|
|
#
|
|
# key => auto = 0, manu = 1
|
|
#
|
|
# model is reversed [...7013P] = P3107...
|
|
# active_ch is CH - 1 (CH15 = x0e)
|
|
# monitorval: moni=0 > monitorval = 0 ; moni=1 > monitorval = x30
|
|
# warning must equal warning1
|
|
# bw: 0 = nar, 1 = Wide & must equal bw1
|
|
# battery save (bs) must equal bs1
|
|
|
|
# VFO
|
|
# shift_minus 0 = null; 1 = "-" + update the TX freq. in VFO
|
|
# shift_pos 0 = null; 1 = "+" + update the TX freq. in VFO
|
|
#
|
|
# Simplex
|
|
# vfo_shift: no care
|
|
# shift_minus = 0
|
|
# shift_pos = 0
|
|
#
|
|
# - shift
|
|
# vfo_shift: set + tx vfo updated
|
|
# shift_minus = 1
|
|
# shift_pos = 0
|
|
#
|
|
# + shift
|
|
# vfo_shift: set + tx vfo updated
|
|
# shift_minus = 0
|
|
# shift_pos = 1
|
|
|
|
MEM_SIZE=0x0800
|
|
CMD_ACK = "\x06"
|
|
BLOCK_SIZE = 0x08
|
|
POWER_LEVELS = ["Low", "High"]
|
|
LIST_SQL = ["Off", "1", "2", "3", "4", "5", "6", "7", "8", "9"]
|
|
LIST_TOT = ["Off", "10", "20", "30", "40", "50", "60", "70", "80", "90"]
|
|
BS = ["Off", "On"]
|
|
LAMP = BS + ["Auto"]
|
|
STEPF = ["5", "10", "6.25", "12.5", "25"]
|
|
ACTIVE_CH = ["%s" % x for x in range(1, 100)]
|
|
KEY_LOCK =["Automatic", "Manual"]
|
|
BW = ["Narrow", "Wide"]
|
|
W_MODE = ["VFO", "Memory"]
|
|
VSHIFT = ["Off", "-", "+"]
|
|
|
|
# Settings are disabled by now, it's a work in progress
|
|
|
|
def raw_recv(radio, amount):
|
|
"""Raw read from the radio device"""
|
|
data = ""
|
|
try:
|
|
data = radio.pipe.read(amount)
|
|
except:
|
|
raise errors.RadioError("Error reading data from radio")
|
|
|
|
return data
|
|
|
|
def raw_send(radio, data):
|
|
"""Raw send to the radio device"""
|
|
try:
|
|
data = radio.pipe.write(data)
|
|
except:
|
|
raise errors.RadioError("Error sending data to radio")
|
|
|
|
def make_frame(cmd, addr, length = BLOCK_SIZE):
|
|
"""Pack the info in the format it likes"""
|
|
return struct.pack(">BHB", ord(cmd), addr, length)
|
|
|
|
def send(radio, frame, data = ""):
|
|
"""Generic send data to the radio"""
|
|
raw_send(radio, frame)
|
|
|
|
if data != "":
|
|
raw_send(radio, data)
|
|
ack = raw_recv(radio, 1)
|
|
if ack != CMD_ACK:
|
|
raise errors.RadioError("Radio didn't ack the last block of data")
|
|
|
|
def recv(radio):
|
|
"""Generic receive data from the radio, return just data"""
|
|
# you must get it all 12 at once (4 header + 8 data)
|
|
rxdata = raw_recv(radio, 12)
|
|
if (len(rxdata) != 12):
|
|
raise errors.RadioError(
|
|
"Radio sent %i bytes, we expected 12" % (len(rxdata)))
|
|
else:
|
|
data = rxdata[4:]
|
|
|
|
# send ACK
|
|
send(radio, CMD_ACK)
|
|
ack = raw_recv(radio, 1)
|
|
if ack != CMD_ACK:
|
|
raise errors.RadioError(
|
|
"Radio did not send ack to the last command")
|
|
|
|
return data
|
|
|
|
def do_program(radio):
|
|
"""Feidaxin program mode and identification dance"""
|
|
def do_magic():
|
|
"""Try to get the radio in program mode, this is standard
|
|
procedure of the factory software (FDX-288) from factory,
|
|
we try 8 times to get the correct response, if not work
|
|
try again a few times"""
|
|
# UI information
|
|
status = chirp_common.Status()
|
|
status.cur = 0
|
|
status.max = 8
|
|
status.msg = "Linking to radio, please wait."
|
|
radio.status_fn(status)
|
|
# every byte of this magic chain must be send separatedly
|
|
magic = "\x02PROGRA"
|
|
|
|
# start the fun, finger crossed please...
|
|
for a in range(0, 8):
|
|
# UI update
|
|
status.cur = a
|
|
radio.status_fn(status)
|
|
for i in range(0, len(magic)):
|
|
# this is needed due to timming
|
|
send(radio, magic[i])
|
|
|
|
# Now you get a x06 of ACK
|
|
ack = raw_recv(radio, 1)
|
|
if ack == CMD_ACK:
|
|
return True
|
|
|
|
return False
|
|
|
|
# try to get the radio in program mode
|
|
ack = do_magic()
|
|
if not ack:
|
|
raise errors.RadioError(
|
|
"Radio did not accept program mode, try again.")
|
|
|
|
# now we request identification
|
|
send(radio, "M")
|
|
send(radio, "\x02")
|
|
ident = raw_recv(radio, 8)
|
|
|
|
# WARNING !!!!
|
|
#
|
|
# radio identify it self in different modes:
|
|
# - hardware reset: "\xFF" * 8
|
|
# - FDX-288 soft from clean template: "P3107" + "\x00" * 3
|
|
# - Mine, in original image has: "P2107" + "\x00" * 3
|
|
#
|
|
# I tested writing "FD-268A " to the ident zone and the radio
|
|
# and FDX-288 software works ok, seems they don't care about it.
|
|
#
|
|
# I need to compare others radio's imgs to determine what to do.
|
|
#
|
|
# Seems that ident has a link with the soft used to program it.
|
|
# It's like FDX-288 soft does not make any try to correctly
|
|
# ident the radio using the ident mechanism.
|
|
#
|
|
# So we ignore ident by now, user must care about it.
|
|
|
|
# final ACK
|
|
send(radio, CMD_ACK)
|
|
ack = raw_recv(radio, 1)
|
|
if ack != CMD_ACK:
|
|
raise errors.RadioError("Radio refused to enter programming mode")
|
|
|
|
def do_download(radio):
|
|
""" The download function """
|
|
do_program(radio)
|
|
# UI progress
|
|
status = chirp_common.Status()
|
|
status.cur = 0
|
|
status.max = MEM_SIZE
|
|
status.msg = "Cloning from radio..."
|
|
radio.status_fn(status)
|
|
data = ""
|
|
for addr in range(0x0000, MEM_SIZE, BLOCK_SIZE):
|
|
send(radio, make_frame("R", addr))
|
|
d = recv(radio)
|
|
data += d
|
|
# UI Update
|
|
status.cur = addr
|
|
radio.status_fn(status)
|
|
|
|
return memmap.MemoryMap(data)
|
|
|
|
def do_upload(radio):
|
|
"""The upload function"""
|
|
do_program(radio)
|
|
# UI progress
|
|
status = chirp_common.Status()
|
|
status.cur = 0
|
|
status.max = MEM_SIZE
|
|
status.msg = "Cloning to radio..."
|
|
radio.status_fn(status)
|
|
|
|
for addr in range(0x0000, MEM_SIZE, BLOCK_SIZE):
|
|
send(radio, make_frame("W",addr), radio.get_mmap()[addr:addr+BLOCK_SIZE])
|
|
# UI Update
|
|
status.cur = addr
|
|
radio.status_fn(status)
|
|
|
|
class FeidaxinFD268xRadio(chirp_common.CloneModeRadio):
|
|
"""Feidaxin FD-268x Radio"""
|
|
VENDOR = "Feidaxin"
|
|
MODEL = "FD-268x"
|
|
BAUD_RATE = 9600
|
|
_memsize = MEM_SIZE
|
|
_upper = 99
|
|
|
|
@classmethod
|
|
def get_prompts(cls):
|
|
rp = chirp_common.RadioPrompts()
|
|
rp.experimental = \
|
|
('The program mode of this radio has his tricks,'
|
|
'so this driver is completely experimental.')
|
|
rp.pre_download = _(dedent("""\
|
|
This radio has a tricky way of enter into program mode,
|
|
even the original software has a few tries to get inside.
|
|
|
|
I will try 8 times (most the time ~3 will doit) and this
|
|
can take a few seconds, if don't work, try again a few times.
|
|
|
|
If you can get into it, please check the radio and cable. """))
|
|
rp.pre_upload = _(dedent("""\
|
|
This radio has a tricky way of enter into program mode,
|
|
even the original software has a few tries to get inside.
|
|
|
|
I will try 8 times (most the time ~3 will doit) and this
|
|
can take a few seconds, if don't work, try again a few times.
|
|
|
|
If you can get into it, please check the radio and cable. """))
|
|
return rp
|
|
|
|
def get_features(self):
|
|
"""Return information about this radio's features"""
|
|
rf = chirp_common.RadioFeatures()
|
|
# this feature is READ ONLY by now.
|
|
rf.has_settings = False # not finished yet
|
|
rf.has_bank = False
|
|
rf.has_tuning_step = False
|
|
rf.has_name = False
|
|
rf.has_offset = True
|
|
rf.has_mode = False
|
|
rf.has_dtcs = True
|
|
rf.has_rx_dtcs = True
|
|
rf.has_dtcs_polarity = True
|
|
rf.has_ctone = True
|
|
rf.has_cross = True
|
|
rf.valid_tmodes = ['', 'Tone', 'TSQL', 'DTCS', 'Cross']
|
|
rf.valid_cross_modes = chirp_common.CROSS_MODES
|
|
#rf.valid_power_levels = POWER_LEVELS
|
|
rf.valid_bands = [self._range]
|
|
rf.memory_bounds = (1, self._upper)
|
|
return rf
|
|
|
|
def sync_in(self):
|
|
"""Do a download of the radio eeprom"""
|
|
self._mmap = do_download(self)
|
|
self.process_mmap()
|
|
|
|
def sync_out(self):
|
|
"""Do an upload to the radio eeprom"""
|
|
do_upload(self)
|
|
|
|
def process_mmap(self):
|
|
"""Process the memory objet, mainly used on file load"""
|
|
self._memobj = bitwise.parse(MEM_FORMAT, self._mmap)
|
|
|
|
def get_raw_memory(self, number):
|
|
"""Return a raw representation of the memory object, which
|
|
is very helpful for development"""
|
|
return repr(self._memobj.memory[number])
|
|
|
|
def _decode_tone(self, val):
|
|
"""Parse the tone data to decode from mem, it returns:
|
|
Mode (''|DTCS|Tone), Value (None|###), Polarity (None,N,R)"""
|
|
val = int(val)
|
|
if val == 16665:
|
|
return '', None, None
|
|
elif val >= 12000:
|
|
a = val - 12000
|
|
return 'DTCS', a, 'R'
|
|
elif val >= 8000:
|
|
a = val - 8000
|
|
return 'DTCS', a, 'N'
|
|
else:
|
|
a = val / 10.0
|
|
return 'Tone', a, None
|
|
|
|
def _encode_tone(self, memval, mode, value, pol):
|
|
"""Parse the tone data to encode from UI to mem"""
|
|
if mode == '':
|
|
memval[0].set_raw(0xFF)
|
|
memval[1].set_raw(0xFF)
|
|
elif mode == 'Tone':
|
|
memval.set_value(int(value * 10))
|
|
elif mode == 'DTCS':
|
|
if pol == 'N':
|
|
memval.set_value(int(value) + 8000)
|
|
else:
|
|
cent = (value / 100) + 192
|
|
memval.set_value(int(value))
|
|
memval[1].set_raw(cent)
|
|
else:
|
|
raise Exception("Internal error: invalid mode `%s'" % mode)
|
|
|
|
|
|
def get_memory(self, number):
|
|
"""Extract a high-level memory object from the low-level memory map
|
|
This is called to populate a memory in the UI"""
|
|
# Get a low-level memory object mapped to the image
|
|
_mem = self._memobj.memory[number -1 ]
|
|
# Create a high-level memory object to return to the UI
|
|
mem = chirp_common.Memory()
|
|
# number
|
|
mem.number = number
|
|
|
|
# empty
|
|
if _mem.get_raw()[0] == "\xFF":
|
|
mem.freq = 0
|
|
mem.empty = True
|
|
return mem
|
|
|
|
# freq + offset + duplex
|
|
mem.freq = int(_mem.rx_freq) *10
|
|
offset = (int(_mem.tx_freq) * 10) - mem.freq
|
|
if offset < 0:
|
|
mem.offset = abs(offset)
|
|
mem.duplex = "-"
|
|
elif offset > 0:
|
|
mem.offset = offset
|
|
mem.duplex = "+"
|
|
else:
|
|
mem.offset = 0
|
|
|
|
# tone data
|
|
rxtone = txtone = None
|
|
txtone = self._decode_tone(_mem.tx_tone)
|
|
rxtone = self._decode_tone(_mem.rx_tone)
|
|
chirp_common.split_tone_decode(mem, txtone, rxtone)
|
|
|
|
# Extra setting group
|
|
mem.extra = RadioSettingGroup("all", "All Settings")
|
|
busy = RadioSetting("Busy", "Busy Channel Lockout",
|
|
RadioSettingValueBoolean(bool(_mem.busy_lock)))
|
|
mem.extra.append(busy)
|
|
scramble = RadioSetting("Scramble", "Scrambler Option",
|
|
RadioSettingValueBoolean(bool(_mem.scramble)))
|
|
mem.extra.append(scramble)
|
|
|
|
# return mem
|
|
return mem
|
|
|
|
def set_memory(self, mem):
|
|
"""Store details about a high-level memory to the memory map
|
|
This is called when a user edits a memory in the UI"""
|
|
# Get a low-level memory object mapped to the image
|
|
_mem = self._memobj.memory[mem.number - 1]
|
|
|
|
# Empty memory
|
|
if mem.empty:
|
|
_mem.set_raw("\xFF" * 16)
|
|
return
|
|
|
|
# freq rx
|
|
_mem.rx_freq = mem.freq / 10
|
|
|
|
# freq tx
|
|
if mem.duplex == "+":
|
|
_mem.tx_freq = (mem.freq + mem.offset) / 10
|
|
elif mem.duplex == "-":
|
|
_mem.tx_freq = (mem.freq - mem.offset) / 10
|
|
else:
|
|
_mem.tx_freq = mem.freq / 10
|
|
|
|
# tone data
|
|
((txmode, txtone, txpol), (rxmode, rxtone, rxpol)) = \
|
|
chirp_common.split_tone_encode(mem)
|
|
self._encode_tone(_mem.tx_tone, txmode, txtone, txpol)
|
|
self._encode_tone(_mem.rx_tone, rxmode, rxtone, rxpol)
|
|
|
|
|
|
# extra settings
|
|
for setting in mem.extra:
|
|
setattr(_mem, setting.get_name(), setting.value)
|
|
|
|
return mem
|
|
|
|
def get_settings(self):
|
|
"""Translate the bit in the mem_struct into settings in the UI"""
|
|
self._mem = self._memobj
|
|
basic = RadioSettingGroup("basic", "Basic")
|
|
work = RadioSettingGroup("work", "Work Mode Settings")
|
|
top = RadioSettings(basic, work)
|
|
|
|
# Basic
|
|
sql = RadioSetting("sql", "Squelch Level",
|
|
RadioSettingValueList(LIST_SQL,
|
|
LIST_SQL[_mem.settings.sql]))
|
|
basic.append(sql)
|
|
|
|
tot = RadioSetting("tot", "Time out timer",
|
|
RadioSettingValueList(LIST_TOT,
|
|
LIST_TOT[_mem.settings.tot]))
|
|
basic.append(tot)
|
|
|
|
power = RadioSetting("power", "Power",
|
|
RadioSettingValueList(POWER_LEVELS,
|
|
POWER_LEVELS[_mem.settings.power]))
|
|
basic.append(power)
|
|
|
|
key_lock = RadioSetting("key_lock", "Keyboard Lock",
|
|
RadioSettingValueList(KEY_LOCK,
|
|
KEY_LOCK[_mem.settings.key]))
|
|
basic.append(key_lock)
|
|
|
|
bw = RadioSetting("bw", "Bandwidth",
|
|
RadioSettingValueList(BW,
|
|
BW[_mem.settings.bw]))
|
|
basic.append(bw)
|
|
|
|
lamp = RadioSetting("lamp", "LCD Lamp",
|
|
RadioSettingValueBoolean(
|
|
self._mem.settings.lamp))
|
|
basic.append(lamp)
|
|
|
|
lamp_auto = RadioSetting("lamp_auto", "LCD Lamp Automatic",
|
|
RadioSettingValueBoolean(
|
|
self._mem.settings.lamp_auto))
|
|
basic.append(lamp_auto)
|
|
|
|
bs = RadioSetting("bs", "Battery Save",
|
|
RadioSettingValueBoolean(
|
|
self._mem.settings.bs))
|
|
basic.append(bs)
|
|
|
|
warning = RadioSetting("warning", "Warning Alerts",
|
|
RadioSettingValueBoolean(
|
|
self._mem.settings.warning))
|
|
basic.append(warning)
|
|
|
|
monitor = RadioSetting("monitor", "Monitor key",
|
|
RadioSettingValueBoolean(
|
|
self._mem.settings.monitor))
|
|
basic.append(monitor)
|
|
|
|
# Work mode settings
|
|
wmset = RadioSetting("wmset", "VFO/MR Mode",
|
|
RadioSettingValueList(W_MODE,
|
|
W_MODE[_mem.settings.wmem]))
|
|
work.append(wmset)
|
|
|
|
active_ch = RadioSetting("active_ch", "Default Channel",
|
|
RadioSettingValueList(ACTIVE_CH,
|
|
ACTIVE_CH[_mem.settings.active_ch]))
|
|
work.append(active_ch)
|
|
|
|
vf_freq = RadioSetting("vfo_freq", "VFO frequency",
|
|
RadioSettingValueString(0, 7,
|
|
str(int(_mem.vfo.vrx_freq) / 100000.0000)))
|
|
work.append(vf_freq)
|
|
|
|
sset = 0
|
|
if _mem.vfo.shift_minus == 1:
|
|
sset = 1
|
|
elif _mem.vfo.shift_plus == 1:
|
|
sset = 2
|
|
|
|
shift = RadioSetting("shift", "VFO Shift",
|
|
RadioSettingValueList(VSHIFT,
|
|
VSHIFT[sset]))
|
|
work.append(shift)
|
|
|
|
offset = RadioSetting("offset", "VFO Offset",
|
|
RadioSettingValueString(0, 7,
|
|
str(int(_mem.settings.vfo_shift) / 100000.0000)))
|
|
work.append(offset)
|
|
|
|
step = RadioSetting("step", "VFO step",
|
|
RadioSettingValueList(STEPF,
|
|
STEPF[_mem.settings.step]))
|
|
work.append(step)
|
|
|
|
return top
|
|
|
|
def set_settings(self, settings):
|
|
"""Translate the settings in the UI into bit in the mem_struct"""
|
|
# work in progress, wait for it
|
|
pass
|
|
|
|
@directory.register
|
|
class FeidaxinFD268ARadio(FeidaxinFD268xRadio):
|
|
"""Feidaxin FD-268A Radio"""
|
|
MODEL = "FD-268A"
|
|
_range = (136000000, 174000000)
|
|
|
|
# FD-268B UHF: This radio can have the same layout...
|
|
# I need a img to confirm and validate, see
|
|
# http://chirp.danplanet.com/issues/2169
|
|
#
|
|
#@directory.register
|
|
#class FeidaxinFD268BRadio(FeidaxinFD268xRadio):
|
|
# """Feidaxin FD-268B Radio"""
|
|
# MODEL = "FD-268B"
|
|
# _range = (400000000, 470000000)
|