PMR446 GeoConfig: Technical Protocol Specification
This document serves as the formal technical specification, mathematical description, reference algorithmic pseudocode, and compliance verification standard for PMR446 GeoConfig.
1. PMR446 Radio Model & Configuration Space
Compliant implementations MUST operate strictly within the legacy/common PMR446 configuration space: - 8 PMR446 frequency channels (12.5 kHz spacing). - 38 standard CTCSS tones. - No DCS, digital modes, or proprietary signaling.
The complete configuration space \(\mathcal{S}\) is: $\(\mathcal{S} = C \times T \quad \text{where} \quad |\mathcal{S}| = 8 \times 38 = 304\)$
A configuration is represented as: $\(s = (\text{channel}, \text{ctcss-tone})\)$
1.1 Frequency Channels (\(C\))
Let \(C\) be the set of standard PMR446 channels: $\(C = \{1, 2, 3, 4, 5, 6, 7, 8\}\)$
The physical carrier frequencies corresponding to each channel number are defined as: - CH1: 446.00625 MHz - CH2: 446.01875 MHz - CH3: 446.03125 MHz - CH4: 446.04375 MHz - CH5: 446.05625 MHz - CH6: 446.06875 MHz - CH7: 446.08125 MHz - CH8: 446.09375 MHz
1.2 CTCSS Tone Set (\(T\))
The CTCSS set \(T\) MUST be represented as a stable, ordered constant containing the following 38 standard sub-audible tone squelch frequencies in Hertz:
[
67.0, 71.9, 74.4, 77.0, 79.7, 82.5, 85.4, 88.5, 91.5, 94.8,
97.4, 100.0, 103.5, 107.2, 110.9, 114.8, 118.8, 123.0, 127.3, 131.8,
136.5, 141.3, 146.2, 151.4, 156.7, 162.2, 167.9, 173.8, 179.9, 186.2,
192.8, 203.5, 210.7, 218.1, 225.7, 233.6, 241.8, 250.3
]
1.3 Stable Configuration Encoding
Every configuration is bijectively mapped to a stable integer ID in the range \(0 \dots 303\): $\(\text{config-id}(c, t_i) = (c - 1) \times 38 + i\)$
The decoding inverse function is defined as: $\(\text{config-from-id}(\text{id}) = \left( \lfloor \text{id} / 38 \rfloor + 1, ~ T_{\text{id} \bmod 38} \right)\)$
This mapping MUST remain stable and immutable across all version implementations.
2. Geographic Input and Spatial Modeling
2.1 Coordinate System
The standard geographic datum is WGS-84. Coordinates are represented as: - Latitude: \([-90.0, 90.0]\) - Longitude: \([-180.0, 180.0]\)
2.2 Maidenhead Locator Interface
When receiving a Maidenhead Locator, the system MUST decode it to its exact coordinate center-point before discretization. - Fields (character pair 1): Subdivides the globe into \(18 \times 18\) cells of \(20^\circ \text{ longitude} \times 10^\circ \text{ latitude}\). - Squares (digit pair 2): Subdivides each field into \(10 \times 10\) cells of \(2^\circ \text{ longitude} \times 1^\circ \text{ latitude}\). - Subsquares (character pair 3): Subdivides each square into \(24 \times 24\) cells of \(5'\text{ longitude} \times 2.5'\text{ latitude}\).
2.3 Geohash Discretization
Geohashing is used to discretize continuous geographic space. Precision \(P\) (character length) is treated as an algorithm parameter and MUST NOT be hard-coded. - Default precision for nominal calculations is \(P=5\) (\(\approx 4.9\text{ km} \times 4.9\text{ km}\) at the equator).
2.4 Adaptive Horizontal Clustering (Latitude Shrinking Compensation)
To ensure scale-invariance and guard against the narrowing of Geohash cells at high latitudes, adjacent cells on the same latitude band are mathematically clustered into a single Logical Cell for coloring and configuration purposes.
At latitude \(\phi\), the horizontal merging factor \(M(\phi)\) is defined as: $\(M(\phi) = \max\left(1, ~ \text{round}\left( \frac{1}{\cos(\phi)} \right)\right)\)$
Let \(X\) be the integer horizontal index of a Geohash cell. The logical horizontal index \(X_{\text{logical}}\) is computed as: $\(X_{\text{logical}} = M(\phi) \times \left\lfloor \frac{X}{M(\phi)} \right\rfloor + \left\lfloor \frac{M(\phi)}{2} \right\rfloor\)$
This maps all physical cells within the cluster to a single canonical representative, ensuring the logical width of the cell remains approximately \(5\text{ km}\) at any latitude, keeping the number of neighbors bounded and eliminating color collisions near the poles.
2.5 Distance Metric
To compute physical distance \(d\) between coordinates, implementations MUST use the Haversine formula: $\(a = \sin^2\left(\frac{\phi_2 - \phi_1}{2}\right) + \cos(\phi_1)\cos(\phi_2)\sin^2\left(\frac{\lambda_2 - \lambda_1}{2}\right)\)$ $\(c = 2 \arcsin(\sqrt{a})\)$ $\(d = R_{\text{earth}} \times c\)$ where \(\phi_1, \phi_2\) are latitudes, \(\lambda_1, \lambda_2\) are longitudes, and the Earth radius is \(R_{\text{earth}} = 6371.0088\text{ km}\). Do not use latitude/longitude Euclidean projections.
3. Geographic Graph and Compatibility Invariants
3.1 Geographic Graph Connectivity
The discrete geographic space is modeled as an undirected graph \(G = (V, E)\) where: - \(V \subset \mathcal{L}_P\): A set of Logical Cells at precision \(P\) (where each logical cell \(u\) represents the horizontal cluster of \(M(\phi)\) physical Geohash cells mapped to a single canonical representative). - \(E\): Edges between Logical Cells whose closest physical boundary distance is within the nominal radio radius: $\(E = \{ (u, v) \in V \times V \mid \text{dist}_{\text{boundary}}(u, v) \le R_{\text{radio}} \}\)$ - Default radius parameter: \(R_{\text{radio}} = 10.0\text{ km}\) (fully configurable).
3.2 The Core Compatibility Invariant (Symmetric Standby-Calling)
For any two geographically adjacent cells connected in the graph, the following must hold: $\((u, v) \in E \implies \text{Primary}(v) \in \mathcal{C}(u) \quad \text{and} \quad \text{Primary}(u) \in \mathcal{C}(v)\)$ This guarantees that operators in adjacent cells can always contact each other by tuning to the other's designated primary calling channel.
3.3 Rule 1: No Transitive Propagation
To avoid transitive channel locking across long ranges, adjacent Logical Cells are forbidden from having the same primary configuration: $\(\text{Primary}(u) \ne \text{Primary}(v) \quad \text{where } (u, v) \in E\)$
4. Reference Algorithms
4.1 Geohash Encoding (EncodeGeohash)
Input: latitude, longitude, precision
Output: geohash_string
Let BASE32 = "0123456789bcdefghjkmnpqrstuvwxyz"
Let lat_min = -90.0, lat_max = 90.0
Let lon_min = -180.0, lon_max = 180.0
Initialize geohash = []
Let total_bits = precision * 5
Initialize current_val = 0
For bit_idx from 0 to total_bits - 1:
If bit_idx is even:
Let mid = (lon_min + lon_max) / 2
If longitude >= mid:
current_val = (current_val << 1) | 1
lon_min = mid
Else:
current_val = (current_val << 1) | 0
lon_max = mid
Else:
Let mid = (lat_min + lat_max) / 2
If latitude >= mid:
current_val = (current_val << 1) | 1
lat_min = mid
Else:
current_val = (current_val << 1) | 0
lat_max = mid
If (bit_idx + 1) mod 5 == 0:
Append BASE32[current_val] to geohash
current_val = 0
Return concatenate(geohash)
4.2 Primary Configuration Tessellation (GetPrimaryByTessellation)
Compliant devices MUST map Geohashes to their primary configuration using this local, graph-free, modular shift pattern. This guarantees adjacent cells receive different primary configuration IDs.
Input: geohash_str
Output: configuration_id (integer 0 to 303)
Let lat, lon, lat_err, lon_err = DecodeGeohash(geohash_str)
Let lat_height = 2.0 * lat_err
Let lon_width = 2.0 * lon_err
Let Y = Round((lat + 90.0) / lat_height)
Let X = Round((lon + 180.0) / lon_width)
# Compensate for longitude shrinking at high latitudes
Let M = Max(1, Round(1.0 / Cos(Radians(lat))))
Let X_logical = M * Floor(X / M) + Floor(M / 2)
Let color_id = (Y * 17 + X_logical) mod 304
If color_id < 0:
color_id = color_id + 304
Return color_id
4.3 Get Logical Geohash (GetLogicalGeohash)
Compliant devices MUST map any physical Geohash string to its corresponding canonical Logical Cell Geohash representative to ensure scale-invariance and bounded profiles globally.
Input: geohash_str
Output: logical_geohash_str
Let lat, lon, lat_err, lon_err = DecodeGeohash(geohash_str)
Let lon_width = 2.0 * lon_err
Let M = Max(1, Round(1.0 / Cos(Radians(lat))))
Let X = Floor((lon + 180.0) / lon_width)
Let X_logical = M * Floor(X / M) + Floor(M / 2)
Let lon_logical = -180.0 + (X_logical + 0.5) * lon_width
Return EncodeGeohash(lat, lon_logical, Length(geohash_str))
4.4 Local Profile Expansion (ResolveLocalProfile)
Computes the complete local profile set \(\mathcal{C}(u)\) of size \(K\) without building a global regional graph.
Input: lat, lon, precision, radius_km, K
Output: profile_dictionary { geohash, primary, configurations }
Let raw_cell_geohash = EncodeGeohash(lat, lon, precision)
Let cell_geohash = GetLogicalGeohash(raw_cell_geohash)
Let primary_id = GetPrimaryByTessellation(cell_geohash)
If K <= 1:
Return { geohash: cell_geohash, primary: primary_id, configurations: [primary_id] }
Let lat_step = radius_km / 111.1
Let cos_lat = cos(radians(lat))
Let lon_step = lat_step / max(0.01, cos_lat)
Let cell_lat, cell_lon, lat_err, lon_err = DecodeGeohash(cell_geohash)
Let step_lat_deg = 2.0 * lat_err
Let step_lon_deg = 2.0 * lon_err
Let lat_bound = ceiling(lat_step / step_lat_deg) + 1
Let lon_bound = ceiling(lon_step / step_lon_deg) + 1
Initialize logical_neighbors_set = EmptySet()
For i from -lat_bound to lat_bound:
For j from -lon_bound to lon_bound:
Let test_lat = lat + i * step_lat_deg
Let test_lon = lon + j * step_lon_deg
Let test_lat = Max(-90.0, Min(90.0, test_lat))
Let test_lon = Max(-180.0, Min(180.0, test_lon))
Let gh = EncodeGeohash(test_lat, test_lon, precision)
Let lgh = GetLogicalGeohash(gh)
If lgh != cell_geohash:
logical_neighbors_set.add(lgh)
Initialize neighbors = EmptySet()
For each lgh in logical_neighbors_set:
Let n_lat, n_lon, n_lat_err, n_lon_err = DecodeGeohash(lgh)
Let M = Max(1, Round(1.0 / Cos(Radians(n_lat))))
Let n_lat_min = n_lat - n_lat_err
Let n_lat_max = n_lat + n_lat_err
Let n_lon_width_merged = (2.0 * n_lon_err) * M
Let n_lon_min = n_lon - (n_lon_width_merged / 2.0)
Let n_lon_max = n_lon + (n_lon_width_merged / 2.0)
# Closest point on the merged logical cell boundaries to clicked lat/lon
Let closest_lat = Max(n_lat_min, Min(n_lat_max, lat))
Let closest_lon = Max(n_lon_min, Min(n_lon_max, lon))
Let dist = HaversineDistance(lat, lon, closest_lat, closest_lon)
If dist <= radius_km:
neighbors.add( (dist, lgh) )
# Sort neighbors ascending by distance, and alphabetically by geohash for stable ties
Sort neighbors
Initialize allocated_configs = [primary_id]
Initialize seen_configs = {primary_id}
For each (dist, lgh) in neighbors:
If length(allocated_configs) >= K:
Break
Let neigh_primary = GetPrimaryByTessellation(lgh)
If neigh_primary not in seen_configs:
Add neigh_primary to allocated_configs
Add neigh_primary to seen_configs
Return {
geohash: cell_geohash,
primary: primary_id,
configurations: allocated_configs
}
4.4 Alphabetical Greedy Graph Coloring (AllocatePrimaryColoring)
Used by simulators to construct and optimize primary assignments across a global bounded region.
Input: nodes (set of geohashes), adjacency (map of node to neighbor set)
Output: primary_assignments (map of node to config_id)
Let sorted_nodes = SortAlphabetically(nodes)
Initialize primary_assignments = EmptyMap()
For each node in sorted_nodes:
Let neighbor_colors = EmptySet()
For each neighbor in adjacency[node]:
If neighbor in primary_assignments:
neighbor_colors.add(primary_assignments[neighbor])
Let assigned_color = -1
For color_id from 0 to 303:
If color_id not in neighbor_colors:
assigned_color = color_id
Break
If assigned_color == -1:
assigned_color = GetPrimaryByTessellation(node)
primary_assignments[node] = assigned_color
Return primary_assignments
5. Simulation and Metric Guidelines
5.1 The Italy Bounding Box (IT-BBOX-01)
Simulators MUST support the standard geographical test benchmark IT-BBOX-01:
- Min Latitude: \(35.5^\circ \text{ N}\)
- Max Latitude: \(47.1^\circ \text{ N}\)
- Min Longitude: \(6.6^\circ \text{ E}\)
- Max Longitude: \(18.5^\circ \text{ E}\)
5.2 Metrics Framework
Every simulation run MUST calculate: - Cell Count (\(N\)): Total unique Geohash vertices. - Edge Count (\(M\)): Total unique connected edges (\(\le R_{\text{radio}}\)). - Maximum Degree (\(\Delta(G)\)): Maximum neighbor count. - Average Degree (\(d_{\text{avg}}(G)\)): \(2M/N\). - Uncovered Edges (\(U_K\)): The number of connected edges with empty set intersection: $\(U_K = \Big| \Big[ (u, v) \in E \mid \mathcal{C}(u) \cap \mathcal{C}(v) = \emptyset \Big] \Big|\)$ - Satisfied Link % (\(P_K\)): \((1 - U_K / M) \times 100\%\). - Minimum Successful K (\(K_{\text{successful}}\)): \(\min \{ K \in \mathbb{N} \mid U_K = 0 \}\).
5.3 Performance Optimizations
For large-scale simulations, O(N²) geographic graph comparisons are prohibited. Simulators SHOULD use a 2D Spatial Grid Bucketing index of step size \(D_{\phi} = R_{\text{radio}} / 111.1\) and \(D_{\lambda} = D_{\phi} / \cos(\phi_{\max})\) to ensure \(O(N)\) average lookup times.
5.4 JSON Export Schema
To ensure scientific reproducibility, simulation results exported to JSON MUST validate perfectly against the standard JSON schema:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "PMR446 GeoConfig-SimulationResult",
"type": "object",
"required": [
"algorithm",
"version",
"geohash_precision",
"radio_radius_km",
"configuration_count",
"primary_strategy",
"graph_metrics",
"minimum_successful_k",
"k_steps"
],
"properties": {
"algorithm": { "type": "string", "const": "PMR446 GeoConfig" },
"version": { "type": "string" },
"geohash_precision": { "type": "integer", "minimum": 1 },
"radio_radius_km": { "type": "number", "minimum": 0.1 },
"configuration_count": { "type": "integer", "const": 304 },
"primary_strategy": { "type": "string", "enum": ["coloring", "tessellation"] },
"graph_metrics": {
"type": "object",
"required": [
"number_of_cells",
"number_of_edges",
"max_node_degree",
"avg_node_degree",
"primary_configurations_used"
],
"properties": {
"number_of_cells": { "type": "integer" },
"number_of_edges": { "type": "integer" },
"max_node_degree": { "type": "integer" },
"avg_node_degree": { "type": "number" },
"primary_configurations_used": { "type": "integer" }
}
},
"minimum_successful_k": { "type": ["integer", "null"] },
"k_steps": {
"type": "array",
"items": {
"type": "object",
"required": [
"k",
"uncovered_edges",
"percentage_satisfied",
"max_config_set_size",
"avg_config_set_size"
],
"properties": {
"k": { "type": "integer" },
"uncovered_edges": { "type": "integer" },
"percentage_satisfied": { "type": "number" },
"max_config_set_size": { "type": "integer" },
"avg_config_set_size": { "type": "number" }
}
}
}
}
}
6. Verification & Property-Based Testing
Automated testing suites MUST verify the following invariants:
1. Bijective Mapping: config_from_id(config_id(c, t)) matches (c, t) exactly for all 304 positions.
2. Determinism: Identical parameter arrays (inputs, precision, radius) yield byte-identical primary and configuration set assignments.
3. Standby-Calling Coverage: For every edge where \(d(A, B) \le R_{\text{radio}}\), check that \(\mathcal{C}(A) \cap \mathcal{C}(B) \ne \emptyset\) for \(K \ge K_{\text{successful}}\).
4. Local/Global Convergence: Profile lists generated locally via ResolveLocalProfile must be mathematically equivalent to profile lists queried from the global geographical graph.