changeset:   1928:8855bef8dc82
tag:         add_bandplans
tag:         qbase
tag:         qtip
tag:         tip
user:        Sean Burford <sburford@google.com>
date:        Wed Mar 27 12:42:49 2013 +1100
summary:     Add support for band plans (Bug 681)

diff -r 443ea98c0840 -r 8855bef8dc82 chirp/bandplan.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/chirp/bandplan.py	Wed Mar 27 12:42:49 2013 +1100
@@ -0,0 +1,152 @@
+# Copyright 2013 Sean Burford <sburford@google.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/>.
+
+from chirp import chirp_common
+
+
+class BandTree(object):
+    def __init__(self, band):
+        self.band = band
+        self.branches = []
+        self.limits = None
+        if band:
+            self.limits = band.limits
+
+    def search(self, request):
+        # The top level node doesn't have a specific band.
+        if self.band is not None:
+            if not self.band.contains(request):
+                return
+
+            # Inherit attributes as we walk towards the most specific match.
+            request.mode = self.band.mode or request.mode
+            request.step_khz = self.band.step_khz or request.step_khz
+            request.offset = self.band.offset or request.offset
+            request.duplex = self.band.duplex or request.duplex
+            request.tones = self.band.tones or request.tones
+            if self.band.name:
+                request.name = '/'.join((request.name or '', self.band.name))
+ 
+        matches = []
+        matches_rx = []
+        for branch in self.branches:
+            result = branch.search(request)
+            if result:
+                # Prioritise repeater outputs.  For example the North American
+                # 440-445MHz +5MHz range overlaps 447-450MHZ -5MHz and it's
+                # more appropriate to return -5MHz if the user enters 448MHz.
+                if result.band and result.band.duplex == "rpt RX":
+                    matches_rx.append(result)
+                else:
+                    matches.append(result)
+
+        if matches:
+            return matches[0]
+        elif matches_rx:
+            return matches_rx[0]
+        else:
+            return self
+
+    def add(self, new_bands):
+        for new_band in new_bands:
+            if self.band:
+                if (new_band.limits[0] == self.band.limits[0] and
+                    new_band.limits[1] == self.band.limits[1]):
+                    print "Discarding %s as a duplicate of %s" % (
+                        new_band, self.band)
+                    continue
+
+            # We use a new Band instance as search() modifies the instance.
+            best_node = self.search(Band(new_band.limits, new_band.name))
+
+            if best_node is self:
+                new_node = BandTree(new_band)
+
+                # Move existing sub bands that belong under the new band.
+                relocate_branches = []
+                for branch in self.branches:
+                    if new_band.contains(branch):
+                        relocate_branches.append(branch)
+                for band in relocate_branches:
+                    self.branches.remove(branch)
+                    new_node.add((branch.band,))
+
+                self.branches.append(new_node)
+            elif best_node:
+                best_node.add((new_band,))
+            else:
+                raise ValueError("%s does not contain %s" % (self, new_band))
+        return self
+
+
+class Band(object):
+    def __init__(self, limits, name, mode=None, step_khz=None,
+                 input_offset=None, output_offset=None, tones=None):
+        try:
+            # This applies semantic and chirp limitations to settings.
+            # memedit applies radio limitations when settings are used.
+            assert limits[0] <= limits[1], "Lower freq > upper freq"
+            if mode is not None:
+                assert mode in chirp_common.MODES, "Mode %s not one of %s" % (
+                    mode, chirp_common.MODES)
+            if step_khz is not None:
+                assert step_khz in chirp_common.TUNING_STEPS, (
+                    "step_khz %s not one of %s" %
+                    (step_khz, chirp_common.TUNING_STEPS))
+            if tones:
+                for tone in tones:
+                    assert tone in chirp_common.TONES, (
+                        "tone %s not one of %s" % (tone, chirp_common.TONES))
+        except AssertionError, e:
+            raise ValueError("%s %s: %s" % (name, limits, e))
+
+        self.name = name
+        self.mode = mode
+        self.step_khz = step_khz
+        self.tones = tones
+        self.limits = limits
+        self.offset = None
+        self.duplex = "simplex"
+        if input_offset is not None:
+            self.offset = input_offset
+            self.duplex = "rpt TX"
+        elif output_offset is not None:
+            self.offset = output_offset
+            self.duplex = "rpt RX"
+
+    def contains(self, other):
+        return (other.limits[0] >= self.limits[0] and
+                other.limits[1] <= self.limits[1])
+
+    def inverse(self):
+        """Create an RX/TX shadow of this band using the offset."""
+        limits = (self.limits[0] + self.offset, self.limits[1] + self.offset)
+        offset = -1 * self.offset
+        if self.duplex == "rpt RX":
+            return Band(limits, self.name, self.mode, self.step_khz,
+                        input_offset=offset, tones=self.tones)
+        return Band(limits, self.name, self.mode, self.step_khz,
+                    output_offset=offset, tones=self.tones)
+
+    def __repr__(self):
+        desc = '%s%s%s%s' % (
+            self.mode and 'mode: %s ' % (self.mode,) or '',
+            self.step_khz and 'step_khz: %s ' % (self.step_khz,) or '',
+            self.offset and 'offset: %s ' % (self.offset,) or '',
+            self.tones and 'tones: %s ' % (self.tones,) or '')
+        
+        return "%s-%s %s %s %s" % (
+            self.limits[0], self.limits[1], self.name, self.duplex, desc)
+
diff -r 443ea98c0840 -r 8855bef8dc82 chirp/bandplan_au.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/chirp/bandplan_au.py	Wed Mar 27 12:42:49 2013 +1100
@@ -0,0 +1,88 @@
+# Copyright 2013 Sean Burford <sburford@google.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/>.
+
+
+from chirp import bandplan
+
+
+SHORTNAME = "australia"
+
+DESC = {
+  "name": "Australian Amateur Band Plan",
+  "updated": "April 2010",
+  "url": "http://www.wia.org.au/members/bandplans/data/documents/Australian%20Band%20Plans%20100404.pdf",
+}
+
+BANDS = (
+  bandplan.Band((28000000, 29700000), "10 Meter Band"),
+  bandplan.Band((29520000, 29680000), "FM Simplex and Repeaters",
+                mode="FM", step_khz=20),
+  bandplan.Band((29620000, 29680000), "FM Repeaters", input_offset=-100000),
+  bandplan.Band((50000000, 54000000), "6 Meter Band"),
+  bandplan.Band((52525000, 53975000), "FM Simplex and Repeaters",
+                mode="FM", step_khz=25),
+  bandplan.Band((53550000, 53975000), "FM Repeaters", input_offset=-1000000),
+  bandplan.Band((144000000, 148000000), "2 Meter Band",
+                tones=(91.5, 123.0, 141.3, 146.2, 85.4)),
+  bandplan.Band((144400000, 144600000), "Beacons", step_khz=1),
+  bandplan.Band((146025000, 147975000), "FM Simplex and Repeaters",
+                mode="FM", step_khz=25),
+  bandplan.Band((146625000, 147000000), "FM Repeaters Group A",
+                input_offset=-600000),
+  bandplan.Band((147025000, 147375000), "FM Repeaters Group B",
+                input_offset=600000),
+  bandplan.Band((420000000, 450000000), "70cm Band",
+                tones=(91.5, 123.0, 141.3, 146.2, 85.4)),
+  bandplan.Band((432400000, 432600000), "Beacons", step_khz=1),
+  bandplan.Band((438025000, 439975000), "FM Simplex and Repeaters",
+                mode="FM", step_khz=25),
+  bandplan.Band((438025000, 438725000), "FM Repeaters Group A",
+                input_offset=-5000000),
+  bandplan.Band((439275000, 439975000), "FM Repeaters Group B",
+                input_offset=-5000000),
+  bandplan.Band((1240000000, 1300000000), "23cm Band"),
+  bandplan.Band((1273025000, 1273975000), "FM Repeaters",
+                mode="FM", step_khz=25, input_offset=20000000),
+  bandplan.Band((1296400000, 1296600000), "Beacons", step_khz=1),
+  bandplan.Band((1297025000, 1300400000), "General FM Simplex Data",
+                mode="FM", step_khz=25),
+  bandplan.Band((2300000000, 2450000000), "13cm Band"),
+  bandplan.Band((2403400000, 2403600000), "Beacons", step_khz=1),
+  bandplan.Band((2425000000, 2428000000), "FM Simplex",
+                mode="FM", step_khz=25),
+  bandplan.Band((2428025000, 2429000000), "FM Duplex (Voice)",
+                mode="FM", step_khz=25, input_offset=20000000),
+  bandplan.Band((2429000000, 2429975000), "FM Duplex (Data)",
+                mode="FM", step_khz=100, input_offset=20000000),
+  bandplan.Band((3300000000, 3600000000), "9cm Band"),
+  bandplan.Band((3320000000, 3340000000), "WB Channel 2: Voice/Data",
+                step_khz=100),
+  bandplan.Band((3400400000, 3400600000), "Beacons", step_khz=1),
+  bandplan.Band((3402000000, 3403000000), "FM Simplex (Voice)",
+                mode="FM", step_khz=100),
+  bandplan.Band((3403000000, 3405000000), "FM Simplex (Data)",
+                mode="FM", step_khz=100),
+  bandplan.Band((5650000000, 5850000000), "6cm Band"),
+  bandplan.Band((5760400000, 5760600000), "Beacons", step_khz=1),
+  bandplan.Band((5700000000, 5720000000), "WB Channel 2: Data",
+                step_khz=100, input_offset=70000000),
+  bandplan.Band((5720000000, 5740000000), "WB Channel 3: Voice",
+                step_khz=100, input_offset=70000000),
+  bandplan.Band((5762000000, 5763000000), "FM Simplex (Voice)",
+                mode="FM", step_khz=100),
+  bandplan.Band((5763000000, 5765000000), "FM Simplex (Data)",
+                mode="FM", step_khz=100),
+)
+
diff -r 443ea98c0840 -r 8855bef8dc82 chirp/bandplan_iaru_r1.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/chirp/bandplan_iaru_r1.py	Wed Mar 27 12:42:49 2013 +1100
@@ -0,0 +1,23 @@
+# Copyright 2013 Sean Burford <sburford@google.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/>.
+
+SHORTNAME = "iaru_r1"
+
+DESC = {
+  "name": "IARU Region 1",
+}
+
+BANDS = ()
+
diff -r 443ea98c0840 -r 8855bef8dc82 chirp/bandplan_iaru_r2.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/chirp/bandplan_iaru_r2.py	Wed Mar 27 12:42:49 2013 +1100
@@ -0,0 +1,145 @@
+# Copyright 2013 Sean Burford <sburford@google.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/>.
+
+from chirp import bandplan
+
+SHORTNAME = "iaru_r2"
+
+DESC = {
+  "name": "IARU Region 2",
+  "updated": "October 8, 2010",
+  "url": "http://www.iaru.org/uploads/1/3/0/7/13073366/r2_band_plan.pdf"
+}
+
+# Bands are broken up like this so that other plans can import bits.
+
+BANDS_160M = (
+  bandplan.Band((1800000, 2000000), "160 Meter Band"),
+  bandplan.Band((1800000, 1810000), "Digimodes"),
+  bandplan.Band((1810000, 1830000), "CW", mode="CW"),
+  bandplan.Band((1830000, 1840000), "CW, priority for DX", mode="CW"),
+  bandplan.Band((1840000, 1850000), "SSB, priority for DX", mode="LSB"),
+  bandplan.Band((1850000, 1999000), "All modes", mode="LSB"),
+  bandplan.Band((1999000, 2000000), "Beacons", mode="CW"),
+)
+
+BANDS_80M = (
+  bandplan.Band((3500000, 4000000), "80 Meter Band"),
+  bandplan.Band((3500000, 3510000), "CW, priority for DX", mode="CW"),
+  bandplan.Band((3510000, 3560000), "CW, contest preferred", mode="CW"),
+  bandplan.Band((3560000, 3580000), "CW", mode="CW"),
+  bandplan.Band((3580000, 3590000), "All narrow band modes, digimodes"),
+  bandplan.Band((3590000, 3600000), "All modes"),
+  bandplan.Band((3600000, 3650000), "All modes, SSB contest preferred",
+                mode="LSB"),
+  bandplan.Band((3650000, 3700000), "All modes", mode="LSB"),
+  bandplan.Band((3700000, 3775000), "All modes, SSB contest preferred",
+                mode="LSB"),
+  bandplan.Band((3775000, 3800000), "All modes, SSB DX preferred", mode="LSB"),
+  bandplan.Band((3800000, 4000000), "All modes"),
+)
+
+BANDS_40M = (
+  bandplan.Band((7000000, 7300000), "40 Meter Band"),
+  bandplan.Band((7000000, 7025000), "CW, priority for DX", mode="CW"),
+  bandplan.Band((7025000, 7035000), "CW", mode="CW"),
+  bandplan.Band((7035000, 7038000), "All narrow band modes, digimodes"),
+  bandplan.Band((7038000, 7040000), "All narrow band modes, digimodes"),
+  bandplan.Band((7040000, 7043000), "All modes, digimodes"),
+  bandplan.Band((7043000, 7300000), "All modes"),
+)
+
+BANDS_30M = (
+  bandplan.Band((10100000, 10150000), "30 Meter Band"),
+  bandplan.Band((10100000, 10130000), "CW", mode="CW"),
+  bandplan.Band((10130000, 10140000), "All narrow band digimodes"),
+  bandplan.Band((10140000, 10150000), "All modes, digimodes, no phone"),
+)
+
+BANDS_20M = (
+  bandplan.Band((14000000, 14350000), "20 Meter Band"),
+  bandplan.Band((14000000, 14025000), "CW, priority for DX", mode="CW"),
+  bandplan.Band((14025000, 14060000), "CW, contest preferred", mode="CW"),
+  bandplan.Band((14060000, 14070000), "CW", mode="CW"),
+  bandplan.Band((14070000, 14089000), "All narrow band modes, digimodes"),
+  bandplan.Band((14089000, 14099000), "All modes, digimodes"),
+  bandplan.Band((14099000, 14101000), "IBP, exclusively for beacons",
+                mode="CW"),
+  bandplan.Band((14101000, 14112000), "All modes, digimodes"),
+  bandplan.Band((14112000, 14285000), "All modes, SSB contest preferred",
+                mode="USB"),
+  bandplan.Band((14285000, 14300000), "All modes", mode="AM"),
+  bandplan.Band((14300000, 14350000), "All modes"),
+)
+
+BANDS_17M = (
+  bandplan.Band((18068000, 18168000), "17 Meter Band"),
+  bandplan.Band((18068000, 18095000), "CW", mode="CW"),
+  bandplan.Band((18095000, 18105000), "All narrow band modes, digimodes"),
+  bandplan.Band((18105000, 18109000), "All narrow band modes, digimodes"),
+  bandplan.Band((18109000, 18111000), "IBP, exclusively for beacons",
+                mode="CW"),
+  bandplan.Band((18111000, 18120000), "All modes, digimodes"),
+  bandplan.Band((18120000, 18168000), "All modes"),
+)
+
+BANDS_15M = (
+  bandplan.Band((21000000, 21450000), "15 Meter Band"),
+  bandplan.Band((21000000, 21070000), "CW", mode="CW"),
+  bandplan.Band((21070000, 21090000), "All narrow band modes, digimodes"),
+  bandplan.Band((21090000, 21110000), "All narrow band modes, digimodes"),
+  bandplan.Band((21110000, 21120000), "All modes (exc SSB), digimodes"),
+  bandplan.Band((21120000, 21149000), "All narrow band modes"),
+  bandplan.Band((21149000, 21151000), "IBP, exclusively for beacons",
+                mode="CW"),
+  bandplan.Band((21151000, 21450000), "All modes", mode="USB"),
+)
+
+BANDS_12M = (
+  bandplan.Band((24890000, 24990000), "12 Meter Band"),
+  bandplan.Band((24890000, 29150000), "CW", mode="CW"),
+  bandplan.Band((24915000, 24925000), "All narrow band modes, digimodes"),
+  bandplan.Band((24925000, 24929000), "All narrow band modes, digimodes"),
+  bandplan.Band((24929000, 24931000), "IBP, exclusively for beacons",
+                mode="CW"),
+  bandplan.Band((24931000, 24940000), "All modes, digimodes"),
+  bandplan.Band((24940000, 24990000), "All modes", mode="USB"),
+)
+
+BANDS_10M = (
+  bandplan.Band((28000000, 29520000), "10 Meter Band"),
+  bandplan.Band((28000000, 28070000), "CW", mode="CW"),
+  bandplan.Band((28070000, 28120000), "All narrow band modes, digimodes"),
+  bandplan.Band((28120000, 28150000), "All narrow band modes, digimodes"),
+  bandplan.Band((28150000, 28190000), "All narrow band modes, digimodes"),
+  bandplan.Band((28190000, 28199000), "Regional time shared beacons",
+                mode="CW"),
+  bandplan.Band((28199000, 28201000), "IBP, exclusively for beacons",
+                mode="CW"),
+  bandplan.Band((28201000, 28225000), "Continuous duty beacons",
+                mode="CW"),
+  bandplan.Band((28225000, 28300000), "All modes, beacons"),
+  bandplan.Band((28300000, 28320000), "All modes, digimodes"),
+  bandplan.Band((28320000, 29000000), "All modes"),
+  bandplan.Band((29000000, 29200000), "All modes, AM preferred", mode="AM"),
+  bandplan.Band((29200000, 29300000), "All modes including FM, digimodes"),
+  bandplan.Band((29300000, 29510000), "Satellite downlink"),
+  bandplan.Band((29510000, 29520000), "Guard band, no transmission allowed"),
+  bandplan.Band((29520000, 29700000), "FM", step_khz=10, mode="NFM"),
+  bandplan.Band((29620000, 29690000), "FM Repeaters", input_offset=-100000),
+)
+
+BANDS = BANDS_160M + BANDS_80M + BANDS_40M + BANDS_30M + BANDS_20M
+BANDS = BANDS + BANDS_17M + BANDS_15M + BANDS_12M + BANDS_10M
diff -r 443ea98c0840 -r 8855bef8dc82 chirp/bandplan_iaru_r3.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/chirp/bandplan_iaru_r3.py	Wed Mar 27 12:42:49 2013 +1100
@@ -0,0 +1,25 @@
+# Copyright 2013 Sean Burford <sburford@google.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/>.
+
+SHORTNAME = "iaru_r3"
+
+DESC = {
+  "name": "IARU Region 3",
+  "updated": "16 October 2009",
+  "url": "http://www.iaru.org/uploads/1/3/0/7/13073366/r3_band_plan.pdf"
+}
+
+BANDS = ()
+
diff -r 443ea98c0840 -r 8855bef8dc82 chirp/bandplan_na.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/chirp/bandplan_na.py	Wed Mar 27 12:42:49 2013 +1100
@@ -0,0 +1,42 @@
+# Copyright 2013 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/>.
+
+from chirp import bandplan, bandplan_iaru_r2
+
+
+SHORTNAME = "north_america"
+
+DESC = {
+  "name": "North American Band Plan",
+}
+
+BANDS = bandplan_iaru_r2.BANDS + (
+  bandplan.Band((50000000, 54000000), "6 Meter Band"),
+  bandplan.Band((51620000, 51980000), "Repeaters A", input_offset=-500000),
+  bandplan.Band((52500000, 52980000), "Repeaters B", input_offset=-500000),
+  bandplan.Band((53500000, 53980000), "Repeaters C", input_offset=-500000),
+  bandplan.Band((144000000, 148000000), "2 Meter Band"),
+  bandplan.Band((145100000, 145500000), "Repeaters A", input_offset=-600000),
+  bandplan.Band((146600000, 147000000), "Repeaters B", input_offset=-600000),
+  bandplan.Band((147000000, 147400000), "Repeaters C", input_offset=600000),
+  bandplan.Band((219000000, 225000000), "220MHz Band"),
+  bandplan.Band((223850000, 224980000), "Repeaters", input_offset=-1600000),
+  bandplan.Band((420000000, 450000000), "70cm Band"),
+  bandplan.Band((440000000, 445000000), "Repeaters A", input_offset=5000000),
+  bandplan.Band((447000000, 450000000), "Repeaters B", input_offset=-5000000),
+  bandplan.Band((1240000000, 1300000000), "23cm Band"),
+  bandplan.Band((1282000000, 1288000000), "Repeaters", input_offset=-12000000),
+)
+
diff -r 443ea98c0840 -r 8855bef8dc82 chirp/chirp_common.py
--- a/chirp/chirp_common.py	Tue Mar 05 09:49:47 2013 -0800
+++ b/chirp/chirp_common.py	Wed Mar 27 12:42:49 2013 +1100
@@ -68,58 +68,6 @@
 MODES = ["WFM", "FM", "NFM", "AM", "NAM", "DV", "USB", "LSB", "CW", "RTTY",
          "DIG", "PKT", "NCW", "NCWR", "CWR", "P25", "Auto"]
 
-STD_6M_OFFSETS = [
-    (51620000, 51980000, -500000),
-    (52500000, 52980000, -500000),
-    (53500000, 53980000, -500000),
-    ]
-
-STD_2M_OFFSETS = [
-    (145100000, 145500000, -600000),
-    (146000000, 146400000,  600000),
-    (146600000, 147000000, -600000),
-    (147000000, 147400000,  600000),
-    (147600000, 148000000, -600000),
-    ]
-
-STD_220_OFFSETS = [
-    (223850000, 224980000, -1600000),
-    ]
-
-STD_70CM_OFFSETS = [
-    (440000000, 445000000,  5000000),
-    (447000000, 450000000, -5000000),
-    ]
-
-STD_23CM_OFFSETS = [
-    (1282000000, 1288000000, -12000000),
-    ]
-
-# Standard offsets, indexed by band (wavelength in cm)
-STD_OFFSETS = {
-    600 : STD_6M_OFFSETS,
-    200 : STD_2M_OFFSETS,
-    125 : STD_220_OFFSETS,
-     70 : STD_70CM_OFFSETS,
-     23 : STD_23CM_OFFSETS,
-    }
-
-BAND_TO_MHZ = {
-    600 : (   50000000,   54000000 ),
-    200 : (  144000000,  148000000 ),
-    125 : (  219000000,  225000000 ),
-    70 :  (  420000000,  450000000 ),
-    23 :  ( 1240000000, 1300000000 ),
-}
-
-# NB: This only works for some bands, throws an Exception otherwise
-def freq_to_band(freq):
-    """Returns the band (in cm) for a given frequency"""
-    for band, (lo, hi) in BAND_TO_MHZ.items():
-        if int(freq) > lo and int(freq) < hi:
-            return band
-    raise Exception("No conversion for frequency %i" % freq)
-
 TONE_MODES = [
     "",
     "Tone",
diff -r 443ea98c0840 -r 8855bef8dc82 chirpui/bandplans.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/chirpui/bandplans.py	Wed Mar 27 12:42:49 2013 +1100
@@ -0,0 +1,56 @@
+# Copyright 2013 Sean Burford <sburford@google.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/>.
+
+from chirp import bandplan, bandplan_na, bandplan_au
+from chirp import bandplan_iaru_r1, bandplan_iaru_r2, bandplan_iaru_r3
+
+class BandPlans(object):
+    def __init__(self, config):
+        self._config = config
+        self.plans = {}
+
+        for plan in (bandplan_na, bandplan_au, bandplan_iaru_r1,
+                     bandplan_iaru_r2, bandplan_iaru_r3):
+            name = plan.DESC.get("name", plan.SHORTNAME)
+            self.plans[plan.SHORTNAME] = (name, plan)
+
+            # Add repeater inputs as distinct bands.
+            rpt_inputs = []
+            for band in plan.BANDS:
+                if band.duplex != "simplex":
+                    rpt_inputs.append(band.inverse())
+                
+            # Build the master band list for this bandplan.
+            try:
+                print "Building %s" % name
+                master = bandplan.BandTree(None)
+                master.add(list(plan.BANDS) + rpt_inputs)
+                plan.bands = master
+            except ValueError, e:
+                print "Failed to parse bandplan %s: %s" % (name, e)
+
+    def get_defaults_for_frequency(self, freq):
+        freq = int(freq)
+        defaults = bandplan.Band((freq, freq), repr(freq))
+
+        # This is limited to one band plan match for simplicity.
+        for shortname, plan_details in self.plans.items():
+            if self._config.get_bool(shortname, "bandplan"):
+                if plan_details[1].bands.search(defaults):
+                    print "Match in %s: %s" % (plan_details[0], defaults)
+                    break
+
+        return defaults
+
diff -r 443ea98c0840 -r 8855bef8dc82 chirpui/config.py
--- a/chirpui/config.py	Tue Mar 05 09:49:47 2013 -0800
+++ b/chirpui/config.py	Wed Mar 27 12:42:49 2013 +1100
@@ -54,6 +54,12 @@
     def is_defined(self, key, section):
         return self.__config.has_option(section, key)
 
+    def remove_option(self, section, key):
+        self.__config.remove_option(section, key)
+
+        if not self.__config.items(section):
+            self.__config.remove_section(section)
+
 class ChirpConfigProxy:
     def __init__(self, config, section="global"):
         self._config = config
@@ -98,6 +104,10 @@
     def is_defined(self, key, section=None):
         return self._config.is_defined(key, section or self._section)
 
+    def remove_option(self, key, section):
+        self._config.remove_option(section, key)
+
+
 _CONFIG = None
 def get(section="global"):
     global _CONFIG
diff -r 443ea98c0840 -r 8855bef8dc82 chirpui/mainapp.py
--- a/chirpui/mainapp.py	Tue Mar 05 09:49:47 2013 -0800
+++ b/chirpui/mainapp.py	Wed Mar 27 12:42:49 2013 +1100
@@ -40,6 +40,7 @@
 from chirp import CHIRP_VERSION, chirp_common, detect, errors
 from chirp import icf, ic9x_icf
 from chirpui import editorset, clone, miscwidgets, config, reporting, fips
+from chirpui import bandplans
 
 CONF = config.get()
 
@@ -1256,8 +1257,30 @@
         conf = config.get()
         conf.set_bool("no_report", not action.get_active())
 
-    def do_toggle_autorpt(self, action):
-        CONF.set_bool("autorpt", action.get_active(), "memedit")
+    def do_select_bandplan(self):
+        plans = ["None"]
+        for shortname, details in self.bandplans.plans.iteritems():
+            if CONF.get_bool(shortname, "bandplan"):
+                plans.insert(0, details[0])
+            else:
+                plans.append(details[0])
+
+        d = inputdialog.ChoiceDialog(plans, parent=self,
+                                     title="Choose Defaults")
+        d.label.set_text(_("Band plans define default channel settings for "
+                           "frequencies in a region.  Choose a band plan "
+                           "or None for completely manual channel "
+                           "settings."))
+        d.label.set_line_wrap(True)
+        r = d.run()
+
+        if r == gtk.RESPONSE_OK:
+            selection = d.choice.get_active_text()
+            for shortname, details in self.bandplans.plans.iteritems():
+                CONF.set_bool(shortname, selection == details[0], "bandplan")
+                if selection == details[0]:
+                    print "Selected band plan %s: %s" % (shortname, selection)
+        d.destroy()
 
     def do_toggle_no_smart_tmode(self, action):
         CONF.set_bool("no_smart_tmode", not action.get_active(), "memedit")
@@ -1351,8 +1374,8 @@
             self.do_clearq()
         elif action == "report":
             self.do_toggle_report(_action)
-        elif action == "autorpt":
-            self.do_toggle_autorpt(_action)
+        elif action == "channel_defaults":
+            self.do_select_bandplan()
         elif action == "no_smart_tmode":
             self.do_toggle_no_smart_tmode(_action)
         elif action == "developer":
@@ -1429,7 +1452,7 @@
       </menu>
       <menu action="stock" name="stock"/>
       <separator/>
-      <menuitem action="autorpt"/>
+      <menuitem action="channel_defaults"/>
       <separator/>
       <menuitem action="cancelq"/>
     </menu>
@@ -1485,6 +1508,7 @@
             ('export_chirp', None, _("CHIRP Native File"), None, None, self.mh),
             ('export_csv', None, _("CSV File"), None, None, self.mh),
             ('stock', None, _("Import from stock config"), None, None, self.mh),
+            ('channel_defaults', None, _("Channel defaults"), None, None, self.mh),
             ('cancelq', gtk.STOCK_STOP, None, "Escape", None, self.mh),
             ('help', None, _('Help'), None, None, self.mh),
             ('about', gtk.STOCK_ABOUT, None, None, None, self.mh),
@@ -1493,15 +1517,20 @@
         conf = config.get()
         re = not conf.get_bool("no_report");
         hu = conf.get_bool("hide_unused", "memedit")
-        ro = conf.get_bool("autorpt", "memedit")
         dv = conf.get_bool("developer", "state")
         st = not conf.get_bool("no_smart_tmode", "memedit")
 
+        # Migrate old "automatic repeater offset" setting to
+        # "North American Amateur Band Plan"
+        ro = conf.get("autorpt", "memedit")
+        if ro is not None:
+            conf.set_bool("north_america", "bandplan", ro)
+            conf.remove_option("autorpt", "memedit")
+
         toggles = [\
             ('report', None, _("Report statistics"), None, None, self.mh, re),
             ('hide_unused', None, _("Hide Unused Fields"), None, None, self.mh, hu),
             ('no_smart_tmode', None, _("Smart Tone Modes"), None, None, self.mh, st),
-            ('autorpt', None, _("Automatic Repeater Offset"), None, None, self.mh, ro),
             ('developer', None, _("Enable Developer Functions"), None, None, self.mh, dv),
             ]
 
@@ -1652,6 +1681,8 @@
 
         self._recent = []
 
+        self.bandplans = bandplans.BandPlans(CONF)
+
         self.menu_ag = None
         mbar = self.make_menubar()
 
@@ -1695,10 +1726,6 @@
             d.destroy()
         CONF.set_bool("warned_about_reporting", True)
 
-        if not CONF.is_defined("autorpt", "memedit"):
-            print "autorpt not set et"
-            CONF.set_bool("autorpt", True, "memedit")
-
         self.update_recent_files()
         self.update_stock_configs()
         self.setup_extra_hotkeys()
diff -r 443ea98c0840 -r 8855bef8dc82 chirpui/memedit.py
--- a/chirpui/memedit.py	Tue Mar 05 09:49:47 2013 -0800
+++ b/chirpui/memedit.py	Wed Mar 27 12:42:49 2013 +1100
@@ -32,7 +32,7 @@
 import pickle
 import os
 
-from chirpui import common, shiftdialog, miscwidgets, config, memdetail
+from chirpui import common, shiftdialog, miscwidgets, config, memdetail, bandplans
 from chirp import chirp_common, errors, directory, import_logic
 
 def handle_toggle(_, path, store, col):
@@ -122,10 +122,10 @@
         return chirp_common.parse_freq(new)
 
     def ed_freq(self, _foo, path, new, colnum):
-        iter = self.store.get_iter(path)
-        prev, = self.store.get(iter, colnum)
+        path_iter = self.store.get_iter(path)
+        was_filled, prev = self.store.get(path_iter, self.col("_filled"), colnum)
 
-        def set_offset(path, offset):
+        def set_offset(offset):
             if offset > 0:
                 dup = "+"
             elif offset == 0:
@@ -134,16 +134,34 @@
                 dup = "-"
                 offset *= -1
 
+            if not dup in self.choices[_("Duplex")]:
+                return
+
             if offset:
-                self.store.set(iter, self.col(_("Offset")), offset)
+                self.store.set(path_iter, self.col(_("Offset")), offset)
 
-            self.store.set(iter, self.col(_("Duplex")), dup)
+            self.store.set(path_iter, self.col(_("Duplex")), dup)
 
         def set_ts(ts):
-            self.store.set(iter, self.col(_("Tune Step")), ts)
+            if ts in self.choices[_("Tune Step")]:
+                self.store.set(path_iter, self.col(_("Tune Step")), ts)
+            else:
+                print "Tune step %s not supported by this radio" % ts
 
         def get_ts(path):
-            return self.store.get(iter, self.col(_("Tune Step")))[0]
+            return self.store.get(path_iter, self.col(_("Tune Step")))[0]
+
+        def set_mode(mode):
+            if mode in self.choices[_("Mode")]:
+                self.store.set(path_iter, self.col(_("Mode")), mode)
+            else:
+                print "Mode %s not supported by this radio (%s)" % (mode, self.choices[_("Mode")])
+
+        def set_tone(tone):
+            if tone in self.choices[_("Tone")]:
+                self.store.set(path_iter, self.col(_("Tone")), tone)
+            else:
+                print "Tone %s not supported by this radio" % tone
 
         try:
             new = chirp_common.parse_freq(new)
@@ -154,16 +172,16 @@
         if not self._features.has_nostep_tuning:
             set_ts(chirp_common.required_step(new))
 
-        if new and self._config.get_bool("autorpt") and new != prev:
-            try:
-                band = chirp_common.freq_to_band(new)
-                set_offset(path, 0)
-                for lo, hi, offset in chirp_common.STD_OFFSETS[band]:
-                    if new > lo and new < hi:
-                        set_offset(path, offset)
-                        break
-            except Exception, e:
-                pass
+        is_changed = new != prev if was_filled else True
+        if new and is_changed:
+            defaults = self.bandplans.get_defaults_for_frequency(new)
+            set_offset(defaults.offset or 0)
+            if defaults.step_khz:
+                set_ts(defaults.step_khz)
+            if defaults.mode:
+                set_mode(defaults.mode)
+            if defaults.tones:
+                set_tone(defaults.tones[0])
 
         return new
 
@@ -190,11 +208,8 @@
             # RX frequency as the default TX frequency
             self.store.set(iter, self.col("Offset"), freq)
         else:
-            band = int(freq / 100000000)
-            if chirp_common.STD_OFFSETS.has_key(band):
-                offset = chirp_common.STD_OFFSETS[band][0][2]
-            else:
-                offset = 0
+            defaults = self.bandplans.get_defaults_for_frequency(freq)
+            offset = defaults.offset or 0
             self.store.set(iter, self.col(_("Offset")), abs(offset))
 
         return new
@@ -1232,6 +1247,8 @@
 
         self._config = config.get("memedit")
 
+        self.bandplans = bandplans.BandPlans(config.get())
+
         self.allowed_bands = [144, 440]
         self.count = 100
         self.show_special = self._config.get_bool("show_special")

