# Add per-country band plans (3GPP NR/LTE, Wi‑Fi+DFS, ISM), acronym crosswalk,
# automatic site→spectrum validation policy suite, and satellite beam GeoJSON overlays.
# Update gates/panels and docker compose with a static server to host GeoJSON.
# Create a new zip deliverable.
#
# Result zip: /mnt/data/ucls-dns-packs-spectrum-suite.zip
import os, json, yaml, zipfile, datetime, textwrap, re
base = "/mnt/data/ucls-dns-packs"
assert os.path.isdir(base), "Base repo not found."
def write(path, content):
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "w", encoding="utf-8") as f:
f.write(content.strip() + "\n")
def dump_yaml(path, obj):
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "w", encoding="utf-8") as f:
yaml.safe_dump(obj, f, sort_keys=False)
now_iso = datetime.datetime.utcnow().replace(microsecond=0).isoformat() + "Z"
# 1) Spectrum data: bands per country (seed US/EU, extensible) ----------------
spec_dir = os.path.join(base, "spectrum", "bands")
os.makedirs(spec_dir, exist_ok=True)
US = {
"region": "US",
"regulator": "FCC",
"mobile": {
"NR": [
{"band":"n2", "duplex":"FDD", "downlink_mhz":[1850,1910], "uplink_mhz":[1930,1990]},
{"band":"n5", "duplex":"FDD", "downlink_mhz":[824,849], "uplink_mhz":[869,894]},
{"band":"n12","duplex":"FDD", "downlink_mhz":[699,716], "uplink_mhz":[729,746]},
{"band":"n41","duplex":"TDD", "tdd_mhz":[2496,2690]},
{"band":"n66","duplex":"FDD", "downlink_mhz":[2110,2200], "uplink_mhz":[1710,1780]},
{"band":"n77","duplex":"TDD", "tdd_mhz":[3450,3980]}, # US slice of n77
{"band":"n258","duplex":"TDD", "tdd_mhz":[24250,27500]}
],
"LTE": [
{"band":"B2", "duplex":"FDD", "downlink_mhz":[1850,1910], "uplink_mhz":[1930,1990]},
{"band":"B4", "duplex":"FDD", "downlink_mhz":[1710,1755], "uplink_mhz":[2110,2155]},
{"band":"B5", "duplex":"FDD", "downlink_mhz":[824,849], "uplink_mhz":[869,894]},
{"band":"B12","duplex":"FDD", "downlink_mhz":[699,716], "uplink_mhz":[729,746]},
{"band":"B13","duplex":"FDD", "downlink_mhz":[746,756], "uplink_mhz":[777,787]},
{"band":"B66","duplex":"FDD", "downlink_mhz":[2110,2200], "uplink_mhz":[1710,1780]}
]
},
"wifi": {
"2g4": {
"channels":[{"ch":i,"center_mhz":2412+(i-1)*5} for i in range(1,12)],
"dfs": False
},
"5g": {
"channels":[
{"ch":36,"center_mhz":5180,"dfs":False}, {"ch":40,"center_mhz":5200,"dfs":False},
{"ch":44,"center_mhz":5220,"dfs":False}, {"ch":48,"center_mhz":5240,"dfs":False},
{"ch":52,"center_mhz":5260,"dfs":True}, {"ch":56,"center_mhz":5280,"dfs":True},
{"ch":60,"center_mhz":5300,"dfs":True}, {"ch":64,"center_mhz":5320,"dfs":True},
{"ch":100,"center_mhz":5500,"dfs":True}, {"ch":104,"center_mhz":5520,"dfs":True},
{"ch":108,"center_mhz":5540,"dfs":True}, {"ch":112,"center_mhz":5560,"dfs":True},
{"ch":116,"center_mhz":5580,"dfs":True}, {"ch":120,"center_mhz":5600,"dfs":True},
{"ch":124,"center_mhz":5620,"dfs":True}, {"ch":128,"center_mhz":5640,"dfs":True},
{"ch":132,"center_mhz":5660,"dfs":True}, {"ch":136,"center_mhz":5680,"dfs":True},
{"ch":140,"center_mhz":5700,"dfs":True}, {"ch":144,"center_mhz":5720,"dfs":True},
{"ch":149,"center_mhz":5745,"dfs":False},{"ch":153,"center_mhz":5765,"dfs":False},
{"ch":157,"center_mhz":5785,"dfs":False},{"ch":161,"center_mhz":5805,"dfs":False},
{"ch":165,"center_mhz":5825,"dfs":False}
]
},
"6g": {
"desc":"U-NII-5..8; AFC may apply",
"subbands":[
{"name":"U-NII-5","mhz":[5925,6425]},
{"name":"U-NII-6","mhz":[6425,6875]},
{"name":"U-NII-7","mhz":[6525,6875]},
{"name":"U-NII-8","mhz":[6875,7125]}
]
}
},
"ism": [
{"name":"902-928 MHz ISM","mhz":[902,928]},
{"name":"2.4 GHz ISM","mhz":[2400,2483.5]},
{"name":"5.8 GHz ISM","mhz":[5725,5850]}
]
}
EU = {
"region": "EU",
"regulator": "ETSI/CEPT",
"mobile": {
"NR": [
{"band":"n1", "duplex":"FDD", "downlink_mhz":[2110,2170], "uplink_mhz":[1920,1980]},
{"band":"n3", "duplex":"FDD", "downlink_mhz":[1805,1880], "uplink_mhz":[1710,1785]},
{"band":"n7", "duplex":"FDD", "downlink_mhz":[2620,2690], "uplink_mhz":[2500,2570]},
{"band":"n20","duplex":"FDD", "downlink_mhz":[791,821], "uplink_mhz":[832,862]},
{"band":"n28","duplex":"FDD", "downlink_mhz":[758,803], "uplink_mhz":[703,748]},
{"band":"n78","duplex":"TDD", "tdd_mhz":[3300,3800]}
],
"LTE": [
{"band":"B1","duplex":"FDD", "downlink_mhz":[2110,2170], "uplink_mhz":[1920,1980]},
{"band":"B3","duplex":"FDD", "downlink_mhz":[1805,1880], "uplink_mhz":[1710,1785]},
{"band":"B7","duplex":"FDD", "downlink_mhz":[2620,2690], "uplink_mhz":[2500,2570]},
{"band":"B20","duplex":"FDD","downlink_mhz":[791,821], "uplink_mhz":[832,862]},
{"band":"B28","duplex":"FDD","downlink_mhz":[758,803], "uplink_mhz":[703,748]}
]
},
"wifi": {
"2g4": {
"channels":[{"ch":i,"center_mhz":2412+(i-1)*5} for i in range(1,14)],
"dfs": False
},
"5g": {
"channels":[
{"ch":36,"center_mhz":5180,"dfs":False},{"ch":40,"center_mhz":5200,"dfs":False},
{"ch":44,"center_mhz":5220,"dfs":False},{"ch":48,"center_mhz":5240,"dfs":False},
{"ch":52,"center_mhz":5260,"dfs":True}, {"ch":56,"center_mhz":5280,"dfs":True},
{"ch":60,"center_mhz":5300,"dfs":True}, {"ch":64,"center_mhz":5320,"dfs":True},
{"ch":100,"center_mhz":5500,"dfs":True},{"ch":104,"center_mhz":5520,"dfs":True},
{"ch":108,"center_mhz":5540,"dfs":True},{"ch":112,"center_mhz":5560,"dfs":True},
{"ch":116,"center_mhz":5580,"dfs":True},{"ch":120,"center_mhz":5600,"dfs":True},
{"ch":124,"center_mhz":5620,"dfs":True},{"ch":128,"center_mhz":5640,"dfs":True},
{"ch":132,"center_mhz":5660,"dfs":True},{"ch":136,"center_mhz":5680,"dfs":True},
{"ch":140,"center_mhz":5700,"dfs":True}
]
},
"6g": {
"desc":"countries in EU approving 6 GHz vary; use regulator data",
"subbands":[{"name":"5925-6425","mhz":[5925,6425]}]
}
},
"ism": [
{"name":"433 MHz SRD","mhz":[433.05,434.79]},
{"name":"863-870 MHz SRD","mhz":[863,870]},
{"name":"2.4 GHz ISM","mhz":[2400,2483.5]}
]
}
dump_yaml(os.path.join(spec_dir, "US.yaml"), US)
dump_yaml(os.path.join(spec_dir, "EU.yaml"), EU)
# 1b) Acronym crosswalk ------------------------------------------------------
crosswalk = {
"3GPP": {
"NR": {
"n41":"2.5 GHz TDD (2496–2690 MHz)",
"n77":"3.3–4.2 GHz TDD (regional slices vary)",
"n78":"3.3–3.8 GHz TDD",
"n258":"24.25–27.5 GHz mmWave"
},
"LTE": {
"B12":"700 MHz Lower A/B",
"B13":"700 MHz Upper C",
"B66":"AWS-3 extension of B4"
}
},
"WIFI": {
"UNII-1":[5150,5350],
"UNII-2A":[5250,5350],
"UNII-2C/EXT":[5470,5725],
"UNII-3":[5725,5850],
"DFS": "Radar detection required on UNII-2A/2C in many regions"
},
"ISM": {
"US915":[902,928],
"EU868":[863,870],
"EU433":[433.05,434.79],
"2G4":[2400,2483.5]
}
}
dump_yaml(os.path.join(base, "spectrum", "crosswalk", "acronyms.yaml"), crosswalk)
# 2) Policy Suite + gates update --------------------------------------------
packs_dir = os.path.join(base, "packs")
# New Spectrum-Policy 1.1.0 referencing the band files
spectrum_110 = {
"packId":"Spectrum-Policy",
"policyVersion":"1.1.0",
"requires":["RF-Emissions-Policy@1.0.0"],
"appliesTo":[{"targetKind":"RFLink"}],
"config":{
"bandDataPath":"spectrum/bands",
"acronymsPath":"spectrum/crosswalk/acronyms.yaml",
"regionSelect":"CLDR" # choose US/EU etc by locale
},
"enforces":{
"bandMembership":[
"RFLink(freqFromMHz..freqToMHz) MUST fall within any allowed band for {region} given tech (NR/LTE/WIFI/BLE/ISM)"
],
"dfsEnforcement":[
"If WIFI channel marked dfs=true, require radar CAC events or controller attestation"
]
},
"metrics":[
{"key":"band_membership_violation_rate","target":"==0"},
{"key":"dfs_missing_attest_rate","target":"==0"}
]
}
dump_yaml(os.path.join(packs_dir, "Spectrum-Policy-1.1.0.yaml"), spectrum_110)
# Meta suite
suite = {
"packId":"Spectrum-Policy-Suite",
"policyVersion":"1.0.0",
"requires":[
"Broadcast-Site-Policy@1.0.0",
"RF-Emissions-Policy@1.0.0",
"Spectrum-Policy@1.1.0"
],
"appliesTo":[{"targetKind":"RFLink"},{"targetKind":"BroadcastSite"}]
}
dump_yaml(os.path.join(packs_dir, "Spectrum-Policy-Suite-1.0.0.yaml"), suite)
# Update gate rf-link to use 1.1.0
gates_dir = os.path.join(base, "gates")
rf_gate_path = os.path.join(gates_dir, "gate-rf-link.json")
rf_gate = json.load(open(rf_gate_path, "r", encoding="utf-8"))
rf_gate["packs"] = ["RF-Emissions-Policy@1.0.0","Spectrum-Policy@1.1.0"]
write(rf_gate_path, json.dumps(rf_gate, indent=2))
# Binding
bindings_dir = os.path.join(base, "bindings")
write(os.path.join(bindings_dir, "bind-spectrum-suite.json"), json.dumps({
"@type":"PolicyBinding",
"bindingId":"bind:spectrum-suite",
"selector":{"kind":["RFLink","BroadcastSite"]},
"packs":[ "Spectrum-Policy-Suite@1.0.0" ],
"precedence":8
}, indent=2))
# 3) GeoJSON satellite beam footprints + static server -----------------------
geo_dir = os.path.join(base, "dashboards", "geo")
os.makedirs(geo_dir, exist_ok=True)
beams = {
"type":"FeatureCollection",
"features":[
{"type":"Feature","properties":{"satellite":"Starlink-EX","beam":"BEAM-101","color":"#33a02c"},
"geometry":{"type":"Polygon","coordinates":[[
[-124.5,48.5],[-114,48.5],[-114,42],[-124.5,42],[-124.5,48.5]
]]}},
{"type":"Feature","properties":{"satellite":"Intelsat-EX","beam":"IS-29E-Spot-7","color":"#1f78b4"},
"geometry":{"type":"Polygon","coordinates":[[
[-10,53], [2,53], [2,45], [-10,45], [-10,53]
]]}}
]
}
write(os.path.join(geo_dir, "sat_beams.geojson"), json.dumps(beams))
# 4) Docker Compose — add static-geo service ---------------------------------
compose_path = os.path.join(base, "adapters", "docker-compose.yml")
compose_txt = open(compose_path, "r", encoding="utf-8").read()
if "static-geo:" not in compose_txt:
block = """
static-geo:
image: nginx:alpine
volumes:
- ../dashboards/geo:/usr/share/nginx/html/geo:ro
ports:
- "8089:80"
"""
# insert before networks:
compose_txt = compose_txt.replace("\nnetworks:\n default: {}", block + "\nnetworks:\n default: {}")
write(compose_path, compose_txt)
# 5) Grafana panels — Geomap overlays & spectrum compliance ------------------
dash_path = os.path.join(base, "dashboards", "ucls-observability.json")
dash = json.load(open(dash_path, "r", encoding="utf-8"))
next_id = max(p["id"] for p in dash["panels"]) + 1 if dash.get("panels") else 200
# Geomap: Broadcast Sites from ClickHouse
dash["panels"].append({
"type":"geomap",
"title":"Broadcast Sites (Geo)",
"id": next_id,
"datasource":{"type":"vertamedia-clickhouse-datasource","uid":"CLICKHOUSE_DS"},
"targets":[{"refId":"A","query":"SELECT site_id, siteCode, lat AS latitude, lon AS longitude FROM sites_ch ORDER BY ts DESC LIMIT 1000"}],
"gridPos":{"h":12,"w":24,"x":0,"y":78},
"options":{"view":{"id":"world"}},
}); next_id += 1
# Geomap: Satellite Beams (GeoJSON layer via static-geo)
dash["panels"].append({
"type":"geomap",
"title":"Satellite Beam Footprints",
"id": next_id,
"datasource":{"type":"grafana","uid":"grafana"},
"gridPos":{"h":12,"w":24,"x":0,"y":90},
"options":{
"view":{"id":"world"},
"layers":[
{"type":"geojson","name":"Sat Beams","config":{"style":{"color":"#ff8800","fillOpacity":0.15}},
"url":"http://static-geo:8089/geo/sat_beams.geojson"}
]
}
}); next_id += 1
# Spectrum compliance quick table
dash["panels"].append({
"type":"table",
"title":"Spectrum Compliance",
"id": next_id,
"datasource":{"type":"vertamedia-clickhouse-datasource","uid":"CLICKHOUSE_DS"},
"targets":[{"refId":"A","query":"SELECT ts, link_id, endpoint_id, site_id, tech, band, freqFromMHz, freqToMHz, out_of_band, license_valid FROM rf_links_ch ORDER BY ts DESC LIMIT 500"}],
"gridPos":{"h":10,"w":24,"x":0,"y":102}
})
write(dash_path, json.dumps(dash, indent=2))
# 6) README addendum ---------------------------------------------------------
readme = os.path.join(base, "README.md")
with open(readme, "a", encoding="utf-8") as f:
f.write(textwrap.dedent(f"""
## Spectrum Policy Suite (per‑country 3GPP + Wi‑Fi/DFS + ISM) — with satellite overlays
- **Data:** `spectrum/bands/*.yaml` (seeded `US.yaml`, `EU.yaml`) + `spectrum/crosswalk/acronyms.yaml`.
- **Policy:** `Spectrum-Policy@1.1.0` (band membership + DFS), meta `Spectrum-Policy-Suite@1.0.0`.
- **Gates:** `gate:rf-link` updated to `Spectrum-Policy@1.1.0`; binding `bind-spectrum-suite.json`.
- **Geo:** `dashboards/geo/sat_beams.geojson`; served by `static-geo` (nginx on :8089).
- **Grafana:** New Geomap panels for Broadcast Sites (ClickHouse) and Satellite Beams (GeoJSON).
- **Flow:** Locale (CLDR) → region bands → RFLink validation; DFS channels require CAC/controller attest.
- **Note:** Band tables are seeds; replace with authoritative regulator exports as needed.
Run the static server:
```bash
cd adapters && docker compose up -d static-geo
# Open Grafana panel 'Satellite Beam Footprints' (points at http://static-geo:8089/geo/sat_beams.geojson)