[chirp_devel] [PATCH 0 of 3] [RFC] Generic Settings API
Hi all,
Here is my proposed generic settings API implementation. The first patch adds chirp/settings.py and a method for a driver to declare a set of settings and constraints. The second patch implements most of the settings for the UV-5R. The third implements a very minimal UI.
This is very much not complete, but it works and I want to get it out there for comments. I envision the UI tolerating multiple levels of setting groups, which it would display to the user as labeled tabular sets (or something). Could also be a settings dialog with tabs or a tree.
The "string" setting type hasn't been tested since the UV-5R doesn't have any string settings. If this looks good, I think I'll move on to adding support for editing some of the APRS settings in the VX-8R, which will really push the limit.
Anyway, comments on this? Would this apply suitably to other radios?
Thanks!
--Dan
# HG changeset patch # User Dan Smith dsmith@danplanet.com # Date 1333739555 25200 # Node ID 21a443c222d1cfdb043b07f858a7b58d5fcc26d6 # Parent 0f5732297a9aabd5dc2e7cb603e0e8f6def94195 FIXME: radio settings Bug #00
diff -r 0f5732297a9a -r 21a443c222d1 chirp/chirp_common.py --- a/chirp/chirp_common.py Fri Apr 06 10:39:28 2012 -0700 +++ b/chirp/chirp_common.py Fri Apr 06 12:12:35 2012 -0700 @@ -778,6 +778,12 @@ def get_features(self): return RadioFeatures()
+ def get_settings(self): + return None + + def set_settings(self, settings): + raise Exception("Not implemented") + def _get_name_raw(*args): cls = args[-1] return "%s %s" % (cls.VENDOR, cls.MODEL) diff -r 0f5732297a9a -r 21a443c222d1 chirp/settings.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/chirp/settings.py Fri Apr 06 12:12:35 2012 -0700 @@ -0,0 +1,176 @@ +# 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/. + +from chirp import chirp_common + +class InvalidValueError(Exception): + pass + +class InternalError(Exception): + pass + +class RadioSettingValue: + def __init__(self): + self.set_value(None) + + def set_value(self, value): + raise Exception("Unimplemented") + + def get_value(self): + return self._current + + def __trunc__(self): + return int(self.get_value()) + + def __str__(self): + return str(self.get_value()) + +class RadioSettingValueInteger(RadioSettingValue): + def __init__(self, min, max, current, step=1): + self._min = min + self._max = max + self._step = step + self.set_value(current) + + def set_value(self, value): + try: + value = int(value) + except: + raise InvalidValueError("An integer is required") + if value > self._max or value < self._min: + raise InvalidValueError("Value %i not in range %i-%i" % (value, + self._min, + self._max)) + self._current = value + + def get_min(self): + return self._min + + def get_max(self): + return self._max + + def get_step(self): + return self._step + +class RadioSettingValueBoolean(RadioSettingValue): + def __init__(self, current): + self.set_value(current) + + def set_value(self, value): + self._current = bool(value) + + def __str__(self): + return str(bool(self.get_value())) + +class RadioSettingValueList(RadioSettingValue): + def __init__(self, options, current): + self._options = options + self.set_value(current) + + def set_value(self, value): + if not value in self._options: + raise InvalidValueError("%s is not valid for this setting" % value) + self._current = value + + def get_options(self): + return self._options + + def __trunc__(self): + return self._options.index(self._current) + +class RadioSettingValueString(RadioSettingValue): + def __init__(self, minlength, maxlength, current, + autopad=True): + self._minlength = minlength + self._maxlength = maxlength + self._charset = chirp_common.CHARSET_ASCII + self._autopad = autopad + self._autostrip = autostrip + self.set_value(current) + + def set_charset(self, charset): + self._charset = charset + + def set_value(self, value): + if len(value) < self._minlength or len(value) > self._maxlength: + raise InvalidValueError("Value must be between %i and %i chars" % (\ + self._minlength, self._maxlength)) + if self._autopad: + value = value.ljust(self._maxlength) + for char in value: + if char not in self._charset: + raise InvalidValueError("Value contains invalid " + + "character `%s'" % char) + self._current = value + + def __str__(self): + return self._current.rstrip() + +class RadioSettingGroup: + def __init__(self, name, shortname): + self._name = name # Setting identifier + self._shortname = shortname # Short human-readable name/description + self.__doc__ = name # Longer explanation/documentation + self._elements = {} + self._element_order = [] + + def get_name(self): + return self._name + + def get_shortname(self): + return self._shortname + + def add_element(self, element): + self._elements[element.get_name()] = element + self._element_order.append(element.get_name()) + + def get_element(self, name): + return self._elements[name] + + def get_elements(self): + return [self._elements[name] for name in self._element_order] + + def get_element_names(self): + return self._elemetn_order + + def set_doc(self, doc): + self.__doc__ = doc + + def __str__(self): + s = "{Settings Group %s:\n" % self._name + for element in self._elements.values(): + s += "%s: %s\n" % (element.get_name(), element.get_value()) + s += "}" + return s + +class RadioSetting(RadioSettingGroup): + def __init__(self, name, value): + self._name = name + self._value = value + + if not isinstance(value, RadioSettingValue): + raise InternalError("Incorrect type") + + def set_value(self, value): + self._value.set_value(value) + + def get_value(self): + return self._value + + def __str__(self): + return str(self._value) + + def __repr__(self): + return "[RadioSetting %s:%s]" % (self._name, self._value)
# HG changeset patch # User Dan Smith dsmith@danplanet.com # Date 1333739557 25200 # Node ID 0dd1b82210090092fd77336db3f65f406c4c5b0e # Parent 21a443c222d1cfdb043b07f858a7b58d5fcc26d6 FIXME #93
diff -r 21a443c222d1 -r 0dd1b8221009 chirp/settings.py --- a/chirp/settings.py Fri Apr 06 12:12:35 2012 -0700 +++ b/chirp/settings.py Fri Apr 06 12:12:37 2012 -0700 @@ -156,8 +156,8 @@ return s
class RadioSetting(RadioSettingGroup): - def __init__(self, name, value): - self._name = name + def __init__(self, name, shortname, value): + RadioSettingGroup.__init__(self, name, shortname) self._value = value
if not isinstance(value, RadioSettingValue): diff -r 21a443c222d1 -r 0dd1b8221009 chirp/uv5r.py --- a/chirp/uv5r.py Fri Apr 06 12:12:35 2012 -0700 +++ b/chirp/uv5r.py Fri Apr 06 12:12:37 2012 -0700 @@ -15,8 +15,9 @@
import struct
-from chirp import chirp_common, errors, util, directory, memmap +from chirp import chirp_common, errors, util, directory, memmap, settings from chirp import bitwise +from chirp.settings import *
mem_format = """ #seekto 0x0008; @@ -35,6 +36,44 @@ unknown5:2; } memory[128];
+#seekto 0x0E28; +struct { + u8 squelch; + u8 step; + u8 unknown1; + u8 save; + u8 vox; + u8 unknown2; + u8 abr; + u8 tdr; + u8 beep; + u8 timeout; + u8 unknown3[4]; + u8 voice; + u8 unknown4; + u8 dtmfst; + u8 unknown5; + u8 screv; + u8 pttid; + u8 pttlt; + u8 mdfa; + u8 mdfb; + u8 bcl; + u8 autolk; + u8 sftd; + u8 unknown6[3]; + u8 wtled; + u8 rxled; + u8 txled; + u8 almod; + u8 tdrab; + u8 ste; + u8 rpste; + u8 rptrl; + u8 ponmsg; + u8 roger; +} settings[2]; + #seekto 0x1000; struct { u8 unknown1[8]; @@ -43,6 +82,24 @@ } names[128]; """
+STEPS = [2.5, 5.0, 6.25, 10.0, 12.5, 25.0] +step_list = [str(x) for x in STEPS] +timeout_list = ["%s sec" % x for x in range(15, 615, 15)] +resume_list = ["TO", "CO", "SE"] +mode_list = ["Channel", "Name", "Frequency"] +color_list = ["Off", "Blue", "Orange", "Purple"] + +SETTING_LISTS = { + "step" : step_list, + "timeout" : timeout_list, + "screv" : resume_list, + "mdfa" : mode_list, + "mdfb" : mode_list, + "wtled" : color_list, + "rxled" : color_list, + "txled" : color_list, +} + def do_status(radio, block): s = chirp_common.Status() s.msg = "Cloning" @@ -152,6 +209,7 @@
def process_mmap(self): self._memobj = bitwise.parse(mem_format, self._mmap) + print self.get_settings()
def sync_in(self): try: @@ -318,3 +376,96 @@ _mem.scan = mem.skip != "S" _mem.wide = mem.mode == "FM" _mem.lowpower = mem.power == UV5R_POWER_LEVELS[0] + + def get_settings(self): + _settings = self._memobj.settings[0] + group = RadioSettingGroup("top", "All Settings") + + s = RadioSetting("squelch", "Carrier Squelch Level", + RadioSettingValueInteger(0, 9, _settings.squelch)) + group.add_element(s) + + s = RadioSetting("step", "Tuning Step", + RadioSettingValueList(step_list, + step_list[_settings.step])) + group.add_element(s) + + s = RadioSetting("save", "Battery Saver", + RadioSettingValueInteger(0, 4, _settings.save)) + group.add_element(s) + + s = RadioSetting("vox", "VOX Sensitivity", + RadioSettingValueInteger(0, 10, _settings.vox)) + group.add_element(s) + + s = RadioSetting("abr", "Backlight Timeout", + RadioSettingValueInteger(0, 5, _settings.abr)) + group.add_element(s) + + s = RadioSetting("tdr", "Dual Watch", + RadioSettingValueBoolean(_settings.tdr)) + group.add_element(s) + + s = RadioSetting("beep", "Beep", + RadioSettingValueBoolean(_settings.beep)) + group.add_element(s) + + s = RadioSetting("timeout", "Timeout Timer", + RadioSettingValueList(timeout_list, + timeout_list[_settings.tdr])) + group.add_element(s) + + s = RadioSetting("voice", "Voice", + RadioSettingValueBoolean(_settings.voice)) + group.add_element(s) + + s = RadioSetting("screv", "Scan Resume", + RadioSettingValueList(resume_list, + resume_list[_settings.screv])) + group.add_element(s) + + s = RadioSetting("mdfa", "Display Mode (A)", + RadioSettingValueList(mode_list, + mode_list[_settings.mdfa])) + group.add_element(s) + + s = RadioSetting("mdfb", "Display Mode (B)", + RadioSettingValueList(mode_list, + mode_list[_settings.mdfb])) + group.add_element(s) + + s = RadioSetting("bcl", "Busy Channel Lockout", + RadioSettingValueBoolean(_settings.bcl)) + group.add_element(s) + + s = RadioSetting("autolk", "Automatic Key Lock", + RadioSettingValueBoolean(_settings.autolk)) + group.add_element(s) + + s = RadioSetting("wtled", "Standby LED Color", + RadioSettingValueList(color_list, + color_list[_settings.wtled])) + group.add_element(s) + + s = RadioSetting("rxled", "RX LED Color", + RadioSettingValueList(color_list, + color_list[_settings.rxled])) + group.add_element(s) + + s = RadioSetting("txled", "TX LED Color", + RadioSettingValueList(color_list, + color_list[_settings.txled])) + group.add_element(s) + + return group + + def set_settings(self, settings): + _settings = self._memobj.settings[0] + for element in settings.get_elements(): + value = element.get_value().get_value() + if element.get_name() in SETTING_LISTS.keys(): + value = SETTING_LISTS[element.get_name()].index(value) + try: + setattr(_settings, element.get_name(), value) + except Exception, e: + raise Exception("%s: %s" % (element.get_name(), e))
# HG changeset patch # User Dan Smith dsmith@danplanet.com # Date 1333739557 25200 # Node ID 8fc20ff7fa28b3e563185fb18396c6d15977cbec # Parent 0dd1b82210090092fd77336db3f65f406c4c5b0e Settings UI #00
diff -r 0dd1b8221009 -r 8fc20ff7fa28 chirpui/editorset.py --- a/chirpui/editorset.py Fri Apr 06 12:12:37 2012 -0700 +++ b/chirpui/editorset.py Fri Apr 06 12:12:37 2012 -0700 @@ -19,7 +19,7 @@
from chirp import chirp_common, directory, generic_csv, xml from chirpui import memedit, dstaredit, bankedit, common, importdialog -from chirpui import inputdialog, reporting +from chirpui import inputdialog, reporting, settingsedit
class EditorSet(gtk.VBox): __gsignals__ = { @@ -64,6 +64,7 @@ "dstar" : None, "bank_names" : None, "bank_members" : None, + "settings" : None, }
if isinstance(self.radio, chirp_common.IcomDstarSupport): @@ -106,6 +107,11 @@ self.editors["bank_members"].root.show() self.editors["bank_members"].connect("changed", self.banks_changed)
+ self.editors["settings"] = settingsedit.SettingsEditor(self.rthread) + lab = gtk.Label(_("Settings")) + self.tabs.append_page(self.editors["settings"].root, lab) + self.editors["settings"].root.show() + self.pack_start(self.tabs) self.tabs.show()
diff -r 0dd1b8221009 -r 8fc20ff7fa28 chirpui/settingsedit.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/chirpui/settingsedit.py Fri Apr 06 12:12:37 2012 -0700 @@ -0,0 +1,131 @@ +# 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 gtk +import gobject + +from chirp import chirp_common, settings +from chirpui import common, miscwidgets + +class RadioSettingProxy(settings.RadioSetting): + def __init__(self, setting, editor): + self._setting = setting + self._editor = editor + +class SettingsEditor(common.Editor): + def __init__(self, rthread): + common.Editor.__init__(self) + self._rthread = rthread + + self.root = gtk.Table(20, 3) + self.root.set_col_spacings(10) + self._index = 0 + + self._top_setting_group = None + + job = common.RadioJob(self._build_ui, "get_settings") + job.set_desc("Getting radio settings") + self._rthread.submit(job) + + def _save_settings(self): + if self._top_setting_group is None: + return + + job = common.RadioJob(None, "set_settings", self._top_setting_group) + job.set_desc("Setting radio settings") + self._rthread.submit(job) + + def _load_setting(self, element, widget): + value = element.get_value() + if isinstance(value, settings.RadioSettingValueInteger): + adj = widget.get_adjustment() + adj.configure(value.get_value(), + value.get_min(), value.get_max(), + value.get_step(), 1, 0) + elif isinstance(value, settings.RadioSettingValueBoolean): + widget.set_active(value.get_value()) + elif isinstance(value, settings.RadioSettingValueList): + model = widget.get_model() + model.clear() + for option in value.get_options(): + widget.append_text(option) + widget.set_active(value.get_options().index(value.get_value())) + elif isinstance(value, settings.RadioSettingValueString): + widget.set_text(value.get_value()) + else: + print "Unsupported widget type %s for %s" % (value.__class__, + element.get_name()) + + def _save_setting(self, widget, element): + value = element.get_value() + if isinstance(value, settings.RadioSettingValueInteger): + value.set_value(widget.get_adjustment().get_value()) + elif isinstance(value, settings.RadioSettingValueBoolean): + value.set_value(widget.get_active()) + elif isinstance(value, settings.RadioSettingValueList): + value.set_value(widget.get_active_text()) + elif isinstance(value, settings.RadioSettingValueString): + value.set_value(widget.get_text()) + else: + print "Unsupported widget type %s for %s" % (value.__class__, + element.get_name()) + + self._save_settings() + + def _build_ui_real(self, group): + if not isinstance(group, settings.RadioSettingGroup): + print "Toplevel is not a group" + return + + self._top_setting_group = group + + def pack(widget, pos): + self.root.attach(widget, pos, pos+1, self._index, self._index+1, + xoptions=gtk.FILL, yoptions=0) + + for element in group.get_elements(): + label = gtk.Label(element.get_shortname()) + label.set_alignment(1.0, 0.5) + label.show() + pack(label, 0) + + value = element.get_value() + if isinstance(value, settings.RadioSettingValueInteger): + widget = gtk.SpinButton() + widget.connect("value-changed", + self._save_setting, element) + elif isinstance(value, settings.RadioSettingValueBoolean): + widget = gtk.CheckButton(_("Enabled")) + widget.connect("toggled", + self._save_setting, element) + elif isinstance(value, settings.RadioSettingValueList): + widget = miscwidgets.make_choice([], editable=False) + widget.connect("changed", + self._save_setting, element) + elif isinstance(value, settings.RadioSettingValueString): + widget = gtk.Entry() + widget.connect("changed", + self._save_setting, element) + else: + print "Unsupported widget type: %s" % value.__class__ + + self._load_setting(element, widget) + + widget.show() + pack(widget, 1) + self._index += 1 + + def _build_ui(self, group): + gobject.idle_add(self._build_ui_real, group)
participants (1)
-
Dan Smith