[chirp_devel] [PATCH 2 of 2] Add support for Sainsonic AP510 APRS tracker. #2095
Tom Hayward
Sat Dec 6 16:02:53 PST 2014
# HG changeset patch
# User Tom Hayward <tom at tomh.us>
# Date 1417910452 28800
# Sat Dec 06 16:00:52 2014 -0800
# Node ID a61a5fc7c5e89df8a50d8c228c66ad71beaf4035
# Parent 9d504f2619b345da0735dbbb34cf9f8d02c3c17b
Add support for Sainsonic AP510 APRS tracker. #2095
The Sainsonic AP510 is an integrated single-channel VHF radio and APRS tracker.
This patch adds support for configuring all of the parameters of the AP510 (one
channel and lots of settings).
diff -r 9d504f2619b3 -r a61a5fc7c5e8 chirp/ap510.py
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/chirp/ap510.py Sat Dec 06 16:00:52 2014 -0800
@@ -0,0 +1,522 @@
+# Copyright 2014 Tom Hayward <tom at tomh.us>
+#
+# 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/>.
+
+import struct
+from chirp import chirp_common, directory, errors, util
+from chirp.settings import RadioSetting, RadioSettingGroup, \
+ RadioSettingValueInteger, RadioSettingValueList, \
+ RadioSettingValueBoolean, RadioSettingValueString, \
+ RadioSettingValueFloat, RadioSettingValue, InvalidValueError
+
+
+def encode_base100(v):
+ return (v / 100 << 8) + (v % 100)
+
+
+def decode_base100(u16):
+ return 100 * (u16 >> 8 & 0xff) + (u16 & 0xff)
+
+
+def encode_smartbeacon(d):
+ return struct.pack(
+ ">7H",
+ encode_base100(d['lowspeed']),
+ encode_base100(d['slowrate']),
+ encode_base100(d['highspeed']),
+ encode_base100(d['fastrate']),
+ encode_base100(d['turnslope']),
+ encode_base100(d['turnangle']),
+ encode_base100(d['turntime']),
+ )
+
+
+def decode_smartbeacon(smartbeacon):
+ return dict(zip((
+ 'lowspeed',
+ 'slowrate',
+ 'highspeed',
+ 'fastrate',
+ 'turnslope',
+ 'turnangle',
+ 'turntime',
+ ), map(decode_base100, struct.unpack(">7H", smartbeacon))))
+
+
+def drain(pipe):
+ """Chew up any data waiting on @pipe"""
+ for x in xrange(3):
+ buf = pipe.read(4096)
+ if not buf:
+ return
+ raise errors.RadioError('Your pipes are clogged.')
+
+
+def enter_setup(pipe):
+ """Put AP510 in configuration mode."""
+ for x in xrange(30):
+ pipe.write("@SETUP")
+ s = pipe.read(64)
+ if s and "SETUP" in s:
+ return True
+ raise errors.RadioError('Radio did not respond.')
+
+
+def download(radio):
+ status = chirp_common.Status()
+ drain(radio.pipe)
+
+ status.msg = " Power on AP510 now, waiting "
+ radio.status_fn(status)
+ enter_setup(radio.pipe)
+
+ status.cur = 1
+ status.max = 5
+ status.msg = "Downloading"
+ radio.status_fn(status)
+ radio.pipe.write("@DISP")
+ buf = radio.pipe.read(1024)
+
+ status.cur = 5
+ status.max = 5
+ radio.status_fn(status)
+
+ print "%04i P<R: %s" % (
+ len(buf), util.hexprint(buf).replace("\n", "\n "))
+ return buf
+
+
+def upload(radio):
+ status = chirp_common.Status()
+ drain(radio.pipe)
+
+ status.msg = " Power on AP510 now, waiting "
+ radio.status_fn(status)
+ enter_setup(radio.pipe)
+
+ status.msg = "Uploading"
+ status.cur = 1
+ status.max = len(radio._mmap._memobj.items())
+ for k, v in radio._mmap._memobj.items():
+ if k == '00':
+ continue
+ if k in ('09', '10', '15'):
+ radio.pipe.write("@" + k + v + "\x00\r\n")
+ else:
+ radio.pipe.write("@" + k + v)
+ # Piece of crap acks every command except 15 with OK.
+ if radio.pipe.read(2) != "OK" and k != '15':
+ raise errors.RadioError("Radio did not acknowledge upload: %s" % k)
+ status.cur += 1
+ radio.status_fn(status)
+
+
+def strbool(s):
+ return s == '1'
+
+
+def boolstr(b):
+ return b and '1' or '0'
+
+
+class AP510Memory(object):
+ """Parses and generates AP510 key/value format
+
+ The AP510 sends it's configuration as a set of keys and values. There
+ is one key/value pair per line. Line separators are \r\n. Keys are
+ deliminated from values with the = symbol.
+
+ Sample:
+ 00=AVRT5 20140829
+ 01=KD7LXL7
+ 02=3
+ 03=1
+ """
+
+ ATTR_MAP = {
+ 'version': '00',
+ 'callsign': '01',
+ 'pttdelay': '02',
+ 'output': '03',
+ 'mice': '04',
+ 'path': '05',
+ 'symbol': '06',
+ 'beacon': '07',
+ 'rate': '08',
+ 'status': '09',
+ 'comment': '10',
+ 'digipeat': '12',
+ 'autooff': '13',
+ 'chinamapfix': '14',
+ 'virtualgps': '15',
+ 'freq': '16',
+ 'beep': '17',
+ 'smartbeacon': '18',
+ 'highaltitude': '19',
+ 'busywait': '20',
+ }
+
+ def __init__(self, data):
+ self._data = data
+ self.process_data()
+
+ def get_packed(self):
+ self._data = "\r\n"
+ for v in sorted(self.ATTR_MAP.values()):
+ self._data += "%s=%s\r\n" % (v, self._memobj[v])
+ return self._data
+
+ def process_data(self):
+ data = []
+ for line in self._data.split('\r\n'):
+ if '=' in line:
+ data.append(line.split('=', 1))
+ self._memobj = dict(data)
+ print self.version
+
+ def __getattr__(self, name):
+ return self._memobj[self.ATTR_MAP[name]]
+
+ def __setattr__(self, name, value):
+ if name.startswith('_'):
+ super(AP510Memory, self).__setattr__(name, value)
+ return
+ self._memobj[self.ATTR_MAP[name]] = value
+
+
+PTT_DELAY = ['60 ms', '120 ms', '180 ms', '300 ms', '480 ms',
+ '600 ms', '1000 ms']
+OUTPUT = ['KISS', 'Waypoint out', 'UI out']
+PATH = [
+ '(None)',
+ 'WIDE1-1',
+ 'WIDE1-1,WIDE2-1',
+ 'WIDE1-1,WIDE2-2',
+ 'TEMP1-1',
+ 'TEMP1-1,WIDE 2-1',
+ 'WIDE2-1',
+]
+TABLE = "/\#&0>AW^_acnsuvz"
+SYMBOL = "".join(map(chr, range(ord("!"), ord("~")+1)))
+BEACON = ['manual', 'auto', 'auto + manual', 'smart', 'smart + manual']
+ALIAS = ['WIDE1-N', 'WIDE2-N', 'WIDE1-N + WIDE2-N']
+CHARSET = "".join(map(chr, range(0, 256)))
+
+RP_IMMUTABLE = ["number", "skip", "bank", "extd_number", "name", "rtone",
+ "ctone", "dtcs", "tmode", "dtcs_polarity", "skip", "duplex",
+ "offset", "mode", "tuning_step", "bank_index"]
+
+
+ at directory.register
+class AP510Radio(chirp_common.CloneModeRadio):
+ """Sainsonic AP510"""
+ BAUD_RATE = 9600
+ VENDOR = "Sainsonic"
+ MODEL = "AP510"
+
+ _model = "AVRT5"
+ mem_upper_limit = 0
+
+ def get_features(self):
+ rf = chirp_common.RadioFeatures()
+ rf.has_settings = True
+ rf.valid_modes = ["FM"]
+ rf.valid_tmodes = [""]
+ rf.valid_characters = ""
+ rf.valid_duplexes = [""]
+ rf.valid_name_length = 0
+ rf.valid_skips = []
+ rf.valid_tuning_steps = []
+ rf.has_bank = False
+ rf.has_ctone = False
+ rf.has_dtcs = False
+ rf.has_dtcs_polarity = False
+ rf.has_mode = False
+ rf.has_name = False
+ rf.has_offset = False
+ rf.has_tuning_step = False
+ rf.valid_bands = [(136000000, 174000000)]
+ rf.memory_bounds = (0, 0)
+ return rf
+
+ def sync_in(self):
+ # _mmap isn't a Chirp MemoryMap, but since AP510Memory implements
+ # get_packed(), the standard Chirp save feature works.
+ try:
+ self._mmap = AP510Memory(download(self))
+ except errors.RadioError:
+ raise
+ except Exception, e:
+ raise errors.RadioError("Failed to communicate with radio: %s" % e)
+
+ def process_mmap(self):
+ self._mmap.process_data()
+
+ def sync_out(self):
+ try:
+ upload(self)
+ except errors.RadioError:
+ raise
+ except Exception, e:
+ raise errors.RadioError("Failed to communicate with radio: %s" % e)
+
+ def load_mmap(self, filename):
+ """Load the radio's memory map from @filename"""
+ mapfile = file(filename, "rb")
+ self._mmap = AP510Memory(mapfile.read())
+ mapfile.close()
+
+ def get_raw_memory(self, number):
+ return self._mmap.get_packed()
+
+ def get_memory(self, number):
+ if number != 0:
+ raise errors.InvalidMemoryLocation("AP510 has only one slot")
+
+ mem = chirp_common.Memory()
+ mem.number = 0
+ mem.freq = float(self._mmap.freq) * 1000000
+ mem.name = "TX/RX"
+ mem.mode = "FM"
+ mem.offset = 0.0
+ mem.immutable = RP_IMMUTABLE
+
+ return mem
+
+ def set_memory(self, mem):
+ if mem.number != 0:
+ raise errors.InvalidMemoryLocation("AP510 has only one slot")
+
+ self._mmap.freq = "%8.4f" % (mem.freq / 1000000.0)
+
+ def get_settings(self):
+ china = RadioSettingGroup("china", "China Map Fix")
+ smartbeacon = RadioSettingGroup("smartbeacon", "Smartbeacon")
+
+ aprs = RadioSettingGroup("aprs", "APRS", china, smartbeacon)
+ digipeat = RadioSettingGroup("digipeat", "Digipeat")
+ system = RadioSettingGroup("system", "System")
+ settings = RadioSettingGroup("all", "Settings", aprs, digipeat, system)
+
+ # The RadioSetting syntax is really verbose, iterate it.
+ fields = [
+ ("callsign", "Callsign",
+ RadioSettingValueString(0, 6, self._mmap.callsign[:6])),
+ ("ssid", "SSID", RadioSettingValueInteger(
+ 0, 15, ord(self._mmap.callsign[6]) - 0x30)),
+ ("pttdelay", "PTT Delay",
+ RadioSettingValueList(
+ PTT_DELAY, PTT_DELAY[int(self._mmap.pttdelay) - 1])),
+ ("output", "Output",
+ RadioSettingValueList(
+ OUTPUT, OUTPUT[int(self._mmap.output) - 1])),
+ ("mice", "Mic-E",
+ RadioSettingValueBoolean(strbool(self._mmap.mice))),
+ ("path", "Path",
+ RadioSettingValueList(PATH, PATH[int(self._mmap.path)])),
+ ("table", "Table or Overlay",
+ RadioSettingValueList(TABLE, self._mmap.symbol[1])),
+ ("symbol", "Symbol",
+ RadioSettingValueList(SYMBOL, self._mmap.symbol[0])),
+ ("beacon", "Beacon Mode",
+ RadioSettingValueList(
+ BEACON, BEACON[int(self._mmap.beacon) - 1])),
+ ("rate", "Beacon Rate (seconds)",
+ RadioSettingValueInteger(10, 9999, self._mmap.rate)),
+ ("comment", "Comment", RadioSettingValueString(
+ 0, 34, self._mmap.comment, autopad=False, charset=CHARSET)),
+ ("status", "Status", RadioSettingValueString(
+ 0, 34, self._mmap.status, autopad=False, charset=CHARSET)),
+ ]
+ for field in fields:
+ aprs.append(RadioSetting(*field))
+
+ fields = [
+ ("chinamapfix", "China map fix",
+ RadioSettingValueBoolean(strbool(self._mmap.chinamapfix[0]))),
+ ("chinalat", "Lat",
+ RadioSettingValueInteger(
+ -45, 45, ord(self._mmap.chinamapfix[2]) - 80)),
+ ("chinalon", "Lon",
+ RadioSettingValueInteger(
+ -45, 45, ord(self._mmap.chinamapfix[1]) - 80)),
+ ]
+ for field in fields:
+ china.append(RadioSetting(*field))
+
+ fields = [
+ ("digipeat", "Digipeat",
+ RadioSettingValueBoolean(strbool(self._mmap.digipeat[0]))),
+ ("alias", "Digipeat Alias",
+ RadioSettingValueList(
+ ALIAS, ALIAS[int(self._mmap.digipeat[1]) - 1])),
+ ("virtualgps", "Static Position",
+ RadioSettingValueBoolean(strbool(self._mmap.virtualgps[0]))),
+ ("btext", "Static Position BTEXT", RadioSettingValueString(
+ 0, 27, self._mmap.virtualgps[1:], autopad=False,
+ charset=CHARSET)),
+ ]
+ for field in fields:
+ digipeat.append(RadioSetting(*field))
+
+ sb = decode_smartbeacon(self._mmap.smartbeacon)
+ fields = [
+ ("lowspeed", "Low Speed"),
+ ("highspeed", "High Speed"),
+ ("slowrate", "Slow Rate (seconds)"),
+ ("fastrate", "Fast Rate (seconds)"),
+ ("turnslope", "Turn Slope"),
+ ("turnangle", "Turn Angle"),
+ ("turntime", "Turn Time (seconds)"),
+ ]
+ for field in fields:
+ smartbeacon.append(RadioSetting(
+ field[0], field[1],
+ RadioSettingValueInteger(0, 9999, sb[field[0]])
+ ))
+
+ fields = [
+ ("version", "Version (read-only)",
+ RadioSettingValueString(0, 14, self._mmap.version)),
+ ("autooff", "Auto off (after 90 minutes)",
+ RadioSettingValueBoolean(strbool(self._mmap.autooff))),
+ ("beep", "Beep on transmit",
+ RadioSettingValueBoolean(strbool(self._mmap.beep))),
+ ("highaltitude", "High Altitude",
+ RadioSettingValueBoolean(strbool(self._mmap.highaltitude))),
+ ("busywait", "Wait for clear channel before transmit",
+ RadioSettingValueBoolean(strbool(self._mmap.busywait))),
+ ]
+ for field in fields:
+ system.append(RadioSetting(*field))
+
+ return settings
+
+ def set_settings(self, settings):
+ for setting in settings:
+ if not isinstance(setting, RadioSetting):
+ self.set_settings(setting)
+ continue
+ if not setting.changed():
+ continue
+ try:
+ name = setting.get_name()
+ if name == "callsign":
+ self.set_callsign(callsign=setting.value)
+ elif name == "ssid":
+ self.set_callsign(ssid=int(setting.value))
+ elif name == "pttdelay":
+ self._mmap.pttdelay = PTT_DELAY.index(
+ str(setting.value)) + 1
+ elif name == "output":
+ self._mmap.output = OUTPUT.index(str(setting.value)) + 1
+ elif name in ('mice', 'autooff', 'beep', 'highaltitude',
+ 'busywait'):
+ setattr(self._mmap, name, boolstr(setting.value))
+ elif name == "path":
+ self._mmap.path = PATH.index(str(setting.value)) + 1
+ elif name == "table":
+ self.set_symbol(table=setting.value)
+ elif name == "symbol":
+ self.set_symbol(symbol=setting.value)
+ elif name == "beacon":
+ self._mmap.beacon = BEACON.index(str(setting.value)) + 1
+ elif name == "rate":
+ self._mmap.rate = "%04d" % setting.value
+ elif name == "comment":
+ self._mmap.comment = str(setting.value)
+ elif name == "status":
+ self._mmap.status = str(setting.value)
+ elif name == "chinamapfix":
+ self.set_chinamapfix(enable=setting.value)
+ elif name == "chinalat":
+ self.set_chinamapfix(lat=int(setting.value))
+ elif name == "chinalon":
+ self.set_chinamapfix(lon=int(setting.value))
+ elif name == "digipeat":
+ self.set_digipeat(enable=setting.value)
+ elif name == "alias":
+ self.set_digipeat(
+ alias=str(ALIAS.index(str(setting.value)) + 1))
+ elif name == "virtualgps":
+ self.set_virtualgps(enable=setting.value)
+ elif name == "btext":
+ self.set_virtualgps(btext=str(setting.value))
+ elif name == "lowspeed":
+ self.set_smartbeacon(lowspeed=int(setting.value))
+ elif name == "highspeed":
+ self.set_smartbeacon(highspeed=int(setting.value))
+ elif name == "slowrate":
+ self.set_smartbeacon(slowrate=int(setting.value))
+ elif name == "fastrate":
+ self.set_smartbeacon(fastrate=int(setting.value))
+ elif name == "turnslope":
+ self.set_smartbeacon(turnslope=int(setting.value))
+ elif name == "turnangle":
+ self.set_smartbeacon(turnangle=int(setting.value))
+ elif name == "turntime":
+ self.set_smartbeacon(turntime=int(setting.value))
+ except Exception, e:
+ print setting.get_name()
+ raise
+
+ def set_callsign(self, callsign=None, ssid=None):
+ if callsign is None:
+ callsign = self._mmap.callsign[:6]
+ if ssid is None:
+ ssid = ord(self._mmap.callsign[6]) - 0x30
+ self._mmap.callsign = str(callsign) + chr(ssid + 0x30)
+
+ def set_symbol(self, table=None, symbol=None):
+ if table is None:
+ table = self._mmap.symbol[1]
+ if symbol is None:
+ symbol = self._mmap.symbol[0]
+ self._mmap.symbol = str(symbol) + str(table)
+
+ def set_chinamapfix(self, enable=None, lat=None, lon=None):
+ if enable is None:
+ enable = strbool(self._mmap.chinamapfix[0])
+ if lat is None:
+ lat = ord(self._mmap.chinamapfix[2]) - 80
+ if lon is None:
+ lon = ord(self._mmap.chinamapfix[1]) - 80
+ self._mmapchinamapfix = boolstr(enable) + chr(lon + 80) + chr(lat + 80)
+
+ def set_digipeat(self, enable=None, alias=None):
+ if enable is None:
+ enable = strbool(self._mmap.digipeat[0])
+ if alias is None:
+ alias = self._mmap.digipeat[1]
+ self._mmap.digipeat = boolstr(enable) + alias
+
+ def set_virtualgps(self, enable=None, btext=None):
+ if enable is None:
+ enable = strbool(self._mmap.virtualgps[0])
+ if btext is None:
+ btext = self._mmap.virtualgps[1:]
+ self._mmap.virtualgps = boolstr(enable) + btext
+
+ def set_smartbeacon(self, **kwargs):
+ sb = decode_smartbeacon(self._mmap.smartbeacon)
+ sb.update(kwargs)
+ if sb['lowspeed'] > sb['highspeed']:
+ raise InvalidValueError("Low speed must be less than high speed")
+ if sb['slowrate'] < sb['fastrate']:
+ raise InvalidValueError("Slow rate must be greater than fast rate")
+ self._mmap.smartbeacon = encode_smartbeacon(sb)
+
+ @classmethod
+ def match_model(cls, filedata, filename):
+ return filedata.startswith('\r\n00=' + cls._model)
More information about the chirp_devel
mailing list