diff --git a/README.md b/README.md
index aeeea67..0d78d5a 100644
--- a/README.md
+++ b/README.md
@@ -8,79 +8,134 @@ Frequency plans are defined for a band. Bands are specified by the LoRa Alliance
## File Format
-Frequency plan are defined in YAML files. Most settings in the frequency plan are optional. When not specifying optional settings, the band defaults are used.
+Frequency plan are defined in YAML files. Most settings in the frequency plan are optional. When not specifying optional settings, the band defaults are used. The definitions are split up into gateway and end-device plans:
+gateways:
```yml
-band-id: BAND_ID # ID of the band (needs to match band-id in the index)
+band-id: BAND_ID # ID of the band (needs to match band-id in the index)
sub-bands:
-- min-frequency: 868000000 # Minimum frequency (Hz, inclusive)
- max-frequency: 868600000 # Maximum frequency (Hz, inclusive)
- duty-cycle: 0.01 # Duty cycle for this sub-band (optional; default: 1)
- max-eirp: 16.15 # Maximum EIRP for this sub-band (optional; takes precedence over frequency plan's max-eirp)
-uplink-channels: # List of uplink channels (zero indexed)
-- frequency: 868100000 # Frequency (Hz)
- min-data-rate: 0 # Mininum data rate index
- max-data-rate: 5 # Maximum data rate index
- radio: 0 # Radio index (see below)
-downlink-channels: # List of downlink channels (zero indexed)
-- frequency: 868100000
- min-data-rate: 0
- max-data-rate: 5
+- min-frequency: 868000000 # Minimum frequency (Hz, inclusive)
+ max-frequency: 868600000 # Maximum frequency (Hz, inclusive)
+ duty-cycle: 0.01 # Duty cycle for this sub-band (optional; default: 1)
+ max-eirp: 16.15 # Maximum EIRP for this sub-band (optional; takes precedence over frequency plan's max-eirp)
+channels: # List of channels (zero indexed)
+- uplink-frequency: 868100000 # Uplink frequency (Hz)
+ downlink-frequency: 868100000 # Downlink frequency (Hz)
+ min-data-rate: SF7BW125 # Minimum data rate
+ max-data-rate: SF11BW125 # Maximum data rate
+ radio: 0 # Radio index (see below)
+lora-standard-channel: # LoRa standard channel (optional)
+ frequency: 863000000
+ data-rate: SF7BW125
+ radio: 0
+fsk-channel: # FSK channel (optional)
+ frequency: 868800000
+ data-rate: SF7BW125
radio: 0
-lora-standard-channel: # LoRa standard channel (optional)
+time-off-air: # Time-off-air (optional)
+ fraction: 0.1 # Minimum fraction of the emission time (optional)
+ duration: 1s # Minimum duration (optional)
+dwell-time: # Dwell time (optional)
+ uplinks: true # Enabled for uplink (optional)
+ downlinks: true # Enabled for downlink (optional)
+ duration: 1s # Duration (optional)
+listen-before-talk: # Listen-before-talk (optional)
+ rssi-offset: 0 # RSSI offset (dbm)
+ rssi-target: -80 # RSSI target (dbm)
+ scan-time: 128000 # Scan time (nanoseconds)
+radios: # Radio configuration (zero indexed, optional)
+- enable: true # Enable the radio
+ chip-type: SX1257 # Chip type
+ frequency: 867500000 # Frequency (Hz)
+ rssi-offset: -166 # RSSI offset (dbm)
+ tx: # Radio transmission configuration (optional)
+ min-frequency: 863000000 # Minimum frequency (Hz)
+ max-frequency: 867000000 # Maximum frequency (Hz)
+ notch-frequency: 129000 # Notch frequency 126000..250000 (Hz)
+clock-source: 0 # Gateway clock source
+max-eirp: 29.15 # Maximum EIRP (optional; used when sub-bands do not have max-eirp, takes precedence over band's default)
+```
+
+end-device:
+```yml
+band-id: BAND_ID # ID of the band (needs to match band-id in the index)
+sub-bands:
+- min-frequency: 868000000 # Minimum frequency (Hz, inclusive)
+ max-frequency: 868600000 # Maximum frequency (Hz, inclusive)
+ duty-cycle: 0.01 # Duty cycle for this sub-band (optional; default: 1)
+ max-eirp: 16.15 # Maximum EIRP for this sub-band (optional; takes precedence over frequency plan's max-eirp)
+channels: # List of channels (zero indexed)
+- uplink-frequency: 868100000 # Uplink frequency (Hz)
+ downlink-frequency: 868100000 # Downlink frequency (Hz)
+ min-data-rate: 0 # Minimum data rate index
+ max-data-rate: 5 # Maximum data rate index
+ default: false # Channel is defined by RP
+lora-standard-channel: # LoRa standard channel (optional)
frequency: 863000000
data-rate: 6
- radio: 0
-fsk-channel: # FSK channel (optional)
+fsk-channel: # FSK channel (optional)
frequency: 868800000
data-rate: 7
- radio: 0
-time-off-air: # Time-off-air (optional)
- fraction: 0.1 # Minimum fraction of the emission time (optional)
- duration: 1s # Minimum duration (optional)
-dwell-time: # Dwell time (optional)
- uplinks: true # Enabled for uplink (optional)
- downlinks: true # Enabled for downlink (optional)
- duration: 1s # Duration (optional)
-listen-before-talk: # Listen-before-talk (optional)
- rssi-offset: 0 # RSSI offset (dbm)
- rssi-target: -80 # RSSI target (dbm)
- scan-time: 128000 # Scan time (nanoseconds)
-radios: # Radio configuration (zero indexed, optional)
-- enable: true # Enable the radio
- chip-type: SX1257 # Chip type
- frequency: 867500000 # Frequency (Hz)
- rssi-offset: -166 # RSSI offset (dbm)
- tx: # Radio transmission configuration (optional)
- min-frequency: 863000000 # Minimum frequency (Hz)
- max-frequency: 867000000 # Maximum frequency (Hz)
- notch-frequency: 129000 # Notch frequency 126000..250000 (Hz)
-clock-source: 0 # Gateway clock source
-ping-slot: # Class B ping slot settings (optional)
+time-off-air: # Time-off-air (optional)
+ fraction: 0.1 # Minimum fraction of the emission time (optional)
+ duration: 1s # Minimum duration (optional)
+dwell-time: # Dwell time (optional)
+ uplinks: true # Enabled for uplink (optional)
+ downlinks: true # Enabled for downlink (optional)
+ duration: 1s # Duration (optional)
+listen-before-talk: # Listen-before-talk (optional)
+ rssi-offset: 0 # RSSI offset (dbm)
+ rssi-target: -80 # RSSI target (dbm)
+ scan-time: 128000 # Scan time (nanoseconds)
+ping-slot: # Class B ping slot settings (optional)
frequency: 869525000
min-data-rate: 0
max-data-rate: 5
- radio: 0
-ping-slot-default-data-rate: 3 # Default data rate index of class B ping slot (optional)
-rx2-channel: # Rx2 channel (optional)
+ping-slot-default-data-rate: 3 # Default data rate index of class B ping slot (optional)
+rx2-channel: # Rx2 channel (optional)
frequency: 869525000
min-data-rate: 0
max-data-rate: 5
- radio: 0
-rx2-default-data-rate: 0 # Default data rate index of Rx2 (optional)
-max-eirp: 29.15 # Maximum EIRP (optional; used when sub-bands do not have max-eirp, takes precedence over band's default)
+rx2-default-data-rate: 0 # Default data rate index of Rx2 (optional)
+max-eirp: 29.15 # Maximum EIRP (optional; used when sub-bands do not have max-eirp, takes precedence over band's default)
```
-An index of frequency plans is in `frequency-plans.yml`:
+An index of frequency plans is in `frequency-plans.yml`. This can take two forms depending on if it is a root frequency plan definition, or if it is a plan definition that modifies an existing plan.
+Base plan definition
+```yml
+end-device-descriptions:
+ - id: EU_863_870 # ID of the frequency plan
+ band-id: EU_863_870 # ID of the LoRaWAN band (needs to match band-id in the definition)
+ name: Region 863-870 MHz # Name of the frequency plan, ending with frequency ranges
+ description: Default frequency plan for Europe # Description of the frequency plan
+ base-frequency: 868 # Base frequency in MHz for hardware support (433, 470, 868, 915 or 2450)
+ country-codes: [] # List of 2-digit ISO country codes for countries where this plan can be used
+ file: EU_863_870.yml # Filename of the plan residing in the `end-device` folder
+ endorsed: true
+
+gateway-descriptions:
+ - id: EU_863_870 # ID of the frequency plan
+ band-id: EU_863_870 # ID of the LoRaWAN band (needs to match band-id in the definition)
+ name: Region 863-870 MHz # Name of the frequency plan, ending with frequency ranges
+ description: Default frequency plan for Europe # Description of the frequency plan
+ base-frequency: 868 # Base frequency in MHz for hardware support (433, 470, 868, 915 or 2450)
+ country-codes: [] # List of 2-digit ISO country codes for countries where this plan can be used
+ file: EU_863_870.yml # Filename of the plan residing in the `gateway` folder
+ endorsed: true
+```
+
+Inherited definition
```yml
- id: EU_863_870_TTN # ID of the frequency plan
band-id: EU_863_870 # ID of the LoRaWAN band (needs to match band-id in the definition)
base-id: EU_863_870 # ID that this frequency plan extends (refers to id of another frequency plan)
name: Region 863-870 MHz # Name of the frequency plan, ending with frequency ranges
- base-frequency: 868 # Base frequency in MHz for hardware support (433, 470, 868 or 915)
+ description: Default frequency plan for Europe # Description of the frequency plan
+ base-frequency: 868 # Base frequency in MHz for hardware support (433, 470, 868, 915 or 2450)
country-codes: [] # List of 2-digit ISO country codes for countries where this plan can be used
- file: EU_863_870.yml # File of the frqeuency plan definition
+ modifiers: [] # Filename of the modifier residing in the respective `end-device/modifiers` or `gateway/modifiers` folder.
+ endorsed: true
```
> Country codes are taken from the [LoRaWAN Regional Parameters 1.0.1 Specification](https://lora-alliance.org/sites/default/files/2020-06/rp_2-1.0.1.pdf)
@@ -91,6 +146,21 @@ Thank you for your interest in building this thing together with us. We're reall
The Things Stack uses the `github.com/TheThingsNetwork/lorawan-frequency-plans` as default source for fetching frequency plans. Therefore, contributing to this open source repository makes frequency plans automatically available to Stack deployments with default settings. You can contribute by submitting pull requests. Are you new to GitHub? That's great! [Read here about pull requests](https://help.github.com/articles/about-pull-requests/). Please also use the editor settings as defined in `.editorconfig`.
+There are json schemas available for all configuration files. These define files structure and make it easier to fill them in. To enable them in `VSCode` add the following to your `settings.json` file.
+```json
+{
+ "yaml.schemas": {
+ "schema.json": "frequency-plans.yml",
+ "end-device/schema.json": "end-device/*.yml",
+ "end-device/modifiers/schema.json": "end-device/modifiers/*.yml",
+ "gateway/schema.json": "gateway/*.yml",
+ "gateway/modifiers/schema.json": "gateway/modifiers/*.yml"
+ }
+}
+```
+
+Make sure to run `go run . -schema -docs` after changes or additions to the frequency plans. This ensures that the schemas and documentation stay up to date.
+
### Local Regulations
When submitting a new frequency plan or making changes to an existing frequency plan, please make sure that the band is allowed to be used in the concerning region and that settings respect regional regulations. When submitting a pull request for a new region, please upload or link to a document that describes the local regulations.
diff --git a/docs/frequency-plans.md b/docs/frequency-plans.md
new file mode 100644
index 0000000..38e6416
--- /dev/null
+++ b/docs/frequency-plans.md
@@ -0,0 +1,75 @@
+# LoRaWAN Frequency Plans for The Things Stack
+
+# End device frequency plans
+
+## [`EU_863_870`](../end-device/EU_863_870.yml): Europe 863-870 MHz
+
+>> Default frequency plan for Europe
+
+![EU_863_870](images/end-device/EU_863_870.svg)
+
+## `EU_863_870_TTN`: Europe 863-870 MHz
+Based on [EU_863_870](##EU_863_870) and modified by [rx2_default_data_rata_3.yml](../end-device/modifiers/rx2_default_data_rata_3.yml)
+
+>> TTN Community Network frequency plan for Europe, using SF9 for RX2
+
+![EU_863_870_TTN](images/end-device/EU_863_870_TTN.svg)
+
+## [`EU_863_870_ROAMING_DRAFT`](../end-device/EU_863_870_ROAMING_DRAFT.yml): Europe 863-870 MHz, 6 channels for roaming (Draft)
+
+>> European 6 channel plan used by major operators to support LoRaWAN Passive Roaming
+
+![EU_863_870_ROAMING_DRAFT](images/end-device/EU_863_870_ROAMING_DRAFT.svg)
+
+## [`EU_433`](../end-device/EU_433.yml): Europe 433 MHz (ITU region 1)
+
+>> Default frequency plan for worldwide 433MHz
+
+![EU_433](images/end-device/EU_433.svg)
+
+## [`US_902_928_FSB_1`](../end-device/US_902_928_FSB_1.yml): United States 902-928 MHz, FSB 1
+
+>> Default frequency plan for the United States and Canada, using sub-band 1
+
+![US_902_928_FSB_1](images/end-device/US_902_928_FSB_1.svg)
+
+## [`AS_920_923`](../end-device/AS_920_923.yml): Asia 920-923 MHz
+
+>> TTN Community Network frequency plan for Asian countries, using frequencies ≤ 923 MHz
+
+![AS_920_923](images/end-device/AS_920_923.svg)
+
+## `AS_920_923_LBT`: Asia 920-923 MHz with LBT
+Based on [AS_920_923](##AS_920_923) and modified by [lbt_80_over_128.yml](../end-device/modifiers/lbt_80_over_128.yml)
+
+>> TTN Community Network frequency plan for Asian countries, using frequencies ≤ 923 MHz with listen-before-talk
+
+![AS_920_923_LBT](images/end-device/AS_920_923_LBT.svg)
+
+# Gateway frequency plans
+
+## [`EU_863_870`](../gateway/EU_863_870.yml): Europe 863-870 MHz
+
+>> Default frequency plan for Europe
+
+![EU_863_870](images/gateway/EU_863_870.svg)
+
+## [`EU_433`](../gateway/EU_433.yml): Europe 433 MHz (ITU region 1)
+
+>> Default frequency plan for worldwide 433MHz
+
+![EU_433](images/gateway/EU_433.svg)
+
+## [`AS_920_923`](../gateway/AS_920_923.yml): Asia 920-923 MHz
+
+>> TTN Community Network frequency plan for Asian countries, using frequencies ≤ 923 MHz
+
+![AS_920_923](images/gateway/AS_920_923.svg)
+
+## `AS_920_923_LBT`: Asia 920-923 MHz with LBT
+Based on AS_920_923 and modified by [lbt_80_over_128.yml](../gateway/modifiers/lbt_80_over_128.yml)
+
+
+>> TTN Community Network frequency plan for Asian countries, using frequencies ≤ 923 MHz with listen-before-talk
+
+![AS_920_923_LBT](images/gateway/AS_920_923_LBT.svg)
diff --git a/docs/images/end-device/AS_920_923.svg b/docs/images/end-device/AS_920_923.svg
new file mode 100644
index 0000000..3153466
--- /dev/null
+++ b/docs/images/end-device/AS_920_923.svg
@@ -0,0 +1,271 @@
+
\ No newline at end of file
diff --git a/docs/images/end-device/AS_920_923_LBT.svg b/docs/images/end-device/AS_920_923_LBT.svg
new file mode 100644
index 0000000..9d13638
--- /dev/null
+++ b/docs/images/end-device/AS_920_923_LBT.svg
@@ -0,0 +1,271 @@
+
\ No newline at end of file
diff --git a/docs/images/end-device/EU_433.svg b/docs/images/end-device/EU_433.svg
new file mode 100644
index 0000000..b6c86e7
--- /dev/null
+++ b/docs/images/end-device/EU_433.svg
@@ -0,0 +1,258 @@
+
\ No newline at end of file
diff --git a/docs/images/end-device/EU_863_870.svg b/docs/images/end-device/EU_863_870.svg
new file mode 100644
index 0000000..1c36ed7
--- /dev/null
+++ b/docs/images/end-device/EU_863_870.svg
@@ -0,0 +1,271 @@
+
\ No newline at end of file
diff --git a/docs/images/end-device/EU_863_870_ROAMING_DRAFT.svg b/docs/images/end-device/EU_863_870_ROAMING_DRAFT.svg
new file mode 100644
index 0000000..1c4a6b6
--- /dev/null
+++ b/docs/images/end-device/EU_863_870_ROAMING_DRAFT.svg
@@ -0,0 +1,210 @@
+
\ No newline at end of file
diff --git a/docs/images/end-device/EU_863_870_TTN.svg b/docs/images/end-device/EU_863_870_TTN.svg
new file mode 100644
index 0000000..e3684ea
--- /dev/null
+++ b/docs/images/end-device/EU_863_870_TTN.svg
@@ -0,0 +1,271 @@
+
\ No newline at end of file
diff --git a/docs/images/end-device/US_902_928_FSB_1.svg b/docs/images/end-device/US_902_928_FSB_1.svg
new file mode 100644
index 0000000..f176b3a
--- /dev/null
+++ b/docs/images/end-device/US_902_928_FSB_1.svg
@@ -0,0 +1,170 @@
+
\ No newline at end of file
diff --git a/docs/images/gateway/AS_920_923.svg b/docs/images/gateway/AS_920_923.svg
new file mode 100644
index 0000000..e769514
--- /dev/null
+++ b/docs/images/gateway/AS_920_923.svg
@@ -0,0 +1,306 @@
+
\ No newline at end of file
diff --git a/docs/images/gateway/AS_920_923_LBT.svg b/docs/images/gateway/AS_920_923_LBT.svg
new file mode 100644
index 0000000..e4763d8
--- /dev/null
+++ b/docs/images/gateway/AS_920_923_LBT.svg
@@ -0,0 +1,306 @@
+
\ No newline at end of file
diff --git a/docs/images/gateway/EU_433.svg b/docs/images/gateway/EU_433.svg
new file mode 100644
index 0000000..e3c54ad
--- /dev/null
+++ b/docs/images/gateway/EU_433.svg
@@ -0,0 +1,297 @@
+
\ No newline at end of file
diff --git a/docs/images/gateway/EU_863_870.svg b/docs/images/gateway/EU_863_870.svg
new file mode 100644
index 0000000..f516e19
--- /dev/null
+++ b/docs/images/gateway/EU_863_870.svg
@@ -0,0 +1,304 @@
+
\ No newline at end of file
diff --git a/end-device/AS_920_923.yml b/end-device/AS_920_923.yml
new file mode 100644
index 0000000..bd3236d
--- /dev/null
+++ b/end-device/AS_920_923.yml
@@ -0,0 +1,43 @@
+band-id: AS_923
+sub-bands:
+- min-frequency: 922000000
+ max-frequency: 923400000
+channels:
+- uplink-frequency: 923200000
+ downlink-frequency: 923200000
+ min-data-rate: 0
+ max-data-rate: 5
+- uplink-frequency: 923400000
+ downlink-frequency: 923400000
+ min-data-rate: 0
+ max-data-rate: 5
+- uplink-frequency: 922200000
+ downlink-frequency: 922200000
+ min-data-rate: 0
+ max-data-rate: 5
+- uplink-frequency: 922400000
+ downlink-frequency: 922400000
+ min-data-rate: 0
+ max-data-rate: 5
+- uplink-frequency: 922600000
+ downlink-frequency: 922600000
+ min-data-rate: 0
+ max-data-rate: 5
+- uplink-frequency: 922800000
+ downlink-frequency: 922800000
+ min-data-rate: 0
+ max-data-rate: 5
+- uplink-frequency: 923000000
+ downlink-frequency: 923000000
+ min-data-rate: 0
+ max-data-rate: 5
+- uplink-frequency: 922000000
+ downlink-frequency: 922000000
+ min-data-rate: 0
+ max-data-rate: 5
+lora-standard-channel:
+ frequency: 922100000
+ data-rate: 6
+fsk-channel:
+ frequency: 921800000
+ data-rate: 7
diff --git a/end-device/EU_433.yml b/end-device/EU_433.yml
new file mode 100644
index 0000000..5b5aaa3
--- /dev/null
+++ b/end-device/EU_433.yml
@@ -0,0 +1,43 @@
+band-id: EU_433
+sub-bands:
+- min-frequency: 433050000
+ max-frequency: 434790000
+ duty-cycle: 0.1 # NOTE: ETSI EN300220 limit is 10%; LoRaWAN limit for end-devices is 1%
+ max-eirp: 12.15
+channels:
+- uplink-frequency: 433175000
+ downlink-frequency: 433175000
+ min-data-rate: 0
+ max-data-rate: 5
+- uplink-frequency: 433375000
+ downlink-frequency: 433375000
+ min-data-rate: 0
+ max-data-rate: 5
+- uplink-frequency: 433575000
+ downlink-frequency: 433575000
+ min-data-rate: 0
+ max-data-rate: 5
+- uplink-frequency: 433775000
+ downlink-frequency: 433775000
+ min-data-rate: 0
+ max-data-rate: 5
+- uplink-frequency: 433975000
+ downlink-frequency: 433975000
+ min-data-rate: 0
+ max-data-rate: 5
+- uplink-frequency: 434175000
+ downlink-frequency: 434175000
+ min-data-rate: 0
+ max-data-rate: 5
+- uplink-frequency: 434375000
+ downlink-frequency: 434375000
+ min-data-rate: 0
+ max-data-rate: 5
+- uplink-frequency: 434575000
+ downlink-frequency: 434575000
+ min-data-rate: 0
+ max-data-rate: 5
+lora-standard-channel:
+ frequency: 434075000
+ data-rate: 6
+
diff --git a/end-device/EU_863_870.yml b/end-device/EU_863_870.yml
new file mode 100644
index 0000000..a44d8b9
--- /dev/null
+++ b/end-device/EU_863_870.yml
@@ -0,0 +1,46 @@
+band-id: EU_863_870
+sub-bands:
+ - min-frequency: 867000000 # TODO: Verify this
+ max-frequency: 869000000
+channels:
+- uplink-frequency: 868100000
+ downlink-frequency: 868100000
+ min-data-rate: 0
+ max-data-rate: 5
+ default: true
+- uplink-frequency: 868300000
+ downlink-frequency: 868300000
+ min-data-rate: 0
+ max-data-rate: 5
+ default: true
+- uplink-frequency: 868500000
+ downlink-frequency: 868500000
+ min-data-rate: 0
+ max-data-rate: 5
+ default: true
+- uplink-frequency: 867100000
+ downlink-frequency: 867100000
+ min-data-rate: 0
+ max-data-rate: 5
+- uplink-frequency: 867300000
+ downlink-frequency: 867300000
+ min-data-rate: 0
+ max-data-rate: 5
+- uplink-frequency: 867500000
+ downlink-frequency: 867500000
+ min-data-rate: 0
+ max-data-rate: 5
+- uplink-frequency: 867700000
+ downlink-frequency: 867700000
+ min-data-rate: 0
+ max-data-rate: 5
+- uplink-frequency: 867900000
+ downlink-frequency: 867900000
+ min-data-rate: 0
+ max-data-rate: 5
+lora-standard-channel:
+ frequency: 868300000
+ data-rate: 6
+fsk-channel:
+ frequency: 868800000
+ data-rate: 7
diff --git a/end-device/EU_863_870_ROAMING_DRAFT.yml b/end-device/EU_863_870_ROAMING_DRAFT.yml
new file mode 100644
index 0000000..e097000
--- /dev/null
+++ b/end-device/EU_863_870_ROAMING_DRAFT.yml
@@ -0,0 +1,31 @@
+
+band-id: EU_863_870
+sub-bands:
+ - min-frequency: 867100000
+ max-frequency: 868500000
+channels:
+- uplink-frequency: 868100000
+ downlink-frequency: 868100000
+ min-data-rate: 0
+ max-data-rate: 5
+- uplink-frequency: 868300000
+ downlink-frequency: 868300000
+ min-data-rate: 0
+ max-data-rate: 5
+- uplink-frequency: 868500000
+ downlink-frequency: 868500000
+ min-data-rate: 0
+ max-data-rate: 5
+- uplink-frequency: 867100000
+ downlink-frequency: 867100000
+ min-data-rate: 0
+ max-data-rate: 5
+- uplink-frequency: 867300000
+ downlink-frequency: 867300000
+ min-data-rate: 0
+ max-data-rate: 5
+- uplink-frequency: 867900000
+ downlink-frequency: 867900000
+ min-data-rate: 0
+ max-data-rate: 5
+
diff --git a/end-device/US_902_928_FSB_1.yml b/end-device/US_902_928_FSB_1.yml
new file mode 100644
index 0000000..a2faff0
--- /dev/null
+++ b/end-device/US_902_928_FSB_1.yml
@@ -0,0 +1,36 @@
+band-id: US_902_928
+sub-bands:
+ - min-frequency: 902300000
+ max-frequency: 903700000
+channels:
+- uplink-frequency: 902300000
+ min-data-rate: 0
+ max-data-rate: 3
+- uplink-frequency: 902500000
+ min-data-rate: 0
+ max-data-rate: 3
+- uplink-frequency: 902700000
+ min-data-rate: 0
+ max-data-rate: 3
+- uplink-frequency: 902900000
+ min-data-rate: 0
+ max-data-rate: 3
+- uplink-frequency: 903100000
+ min-data-rate: 0
+ max-data-rate: 3
+- uplink-frequency: 903300000
+ min-data-rate: 0
+ max-data-rate: 3
+- uplink-frequency: 903500000
+ min-data-rate: 0
+ max-data-rate: 3
+- uplink-frequency: 903700000
+ min-data-rate: 0
+ max-data-rate: 3
+lora-standard-channel:
+ frequency: 903000000
+ data-rate: 12
+dwell-time:
+ uplinks: true
+ downlinks: false
+ duration: 400ms
diff --git a/disable_dwell_time.yml b/end-device/modifiers/disable_dwell_time.yml
similarity index 76%
rename from disable_dwell_time.yml
rename to end-device/modifiers/disable_dwell_time.yml
index 478b07e..4debef4 100644
--- a/disable_dwell_time.yml
+++ b/end-device/modifiers/disable_dwell_time.yml
@@ -1,3 +1,4 @@
dwell-time:
uplinks: false
downlinks: false
+ duration: 0ms
\ No newline at end of file
diff --git a/enable_dwell_time_400ms.yml b/end-device/modifiers/enable_dwell_time_400ms.yml
similarity index 71%
rename from enable_dwell_time_400ms.yml
rename to end-device/modifiers/enable_dwell_time_400ms.yml
index 53301f2..291fe42 100644
--- a/enable_dwell_time_400ms.yml
+++ b/end-device/modifiers/enable_dwell_time_400ms.yml
@@ -1,4 +1,4 @@
dwell-time:
uplinks: true
downlinks: true
- duration: 400ms
+ duration: 400ms
\ No newline at end of file
diff --git a/lbt_80_over_128.yml b/end-device/modifiers/lbt_80_over_128.yml
similarity index 74%
rename from lbt_80_over_128.yml
rename to end-device/modifiers/lbt_80_over_128.yml
index 5da831d..e1d43b8 100644
--- a/lbt_80_over_128.yml
+++ b/end-device/modifiers/lbt_80_over_128.yml
@@ -1,4 +1,4 @@
listen-before-talk:
rssi-offset: -4
rssi-target: -80
- scan-time: 128000
+ scan-time: 128000
\ No newline at end of file
diff --git a/EU_863_870_TTN.yml b/end-device/modifiers/rx2_default_data_rata_3.yml
similarity index 100%
rename from EU_863_870_TTN.yml
rename to end-device/modifiers/rx2_default_data_rata_3.yml
diff --git a/end-device/modifiers/schema.json b/end-device/modifiers/schema.json
new file mode 100644
index 0000000..8d39039
--- /dev/null
+++ b/end-device/modifiers/schema.json
@@ -0,0 +1,204 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "title": "Frequency Plan",
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "sub-bands": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "min-frequency": {
+ "type": "integer",
+ "description": "Minimum frequency [Hz] (inclusive)"
+ },
+ "max-frequency": {
+ "type": "integer",
+ "description": "Maximum frequency [Hz] (inclusive)"
+ },
+ "duty-cycle": {
+ "type": "number",
+ "description": "Duty cycle for this sub-band (optional; default: 1)"
+ },
+ "max-eirp": {
+ "type": "number",
+ "description": "Maximum EIRP for this sub-band (optional; takes precedence over frequency plan's max-eirp)"
+ }
+ },
+ "required": ["min-frequency", "max-frequency"]
+ }
+ },
+ "channels": {
+ "type": "array",
+ "description": "List of uplink channels (zero indexed)",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "uplink-frequency": {
+ "type": "integer",
+ "description": "Uplink frequency [Hz]"
+ },
+ "downlink-frequency": {
+ "type": "integer",
+ "description": "Downlink frequency [Hz] (optional)"
+ },
+ "min-data-rate": {
+ "type": "integer",
+ "description": "Minimum data rate index"
+ },
+ "max-data-rate": {
+ "type": "integer",
+ "description": "Maximum data rate index"
+ },
+ "default": {
+ "type": "boolean",
+ "description": "Channel is defined by RP"
+ }
+ },
+ "required": ["uplink-frequency", "min-data-rate", "max-data-rate"]
+ }
+ },
+ "lora-standard-channel": {
+ "type": "object",
+ "description": "LoRa standard channel (optional)",
+ "additionalProperties": false,
+ "properties": {
+ "frequency": {
+ "type": "integer",
+ "description": "Frequency [Hz]"
+ },
+ "data-rate": {
+ "type": "integer",
+ "description": "Data rate index"
+ }
+ },
+ "required": ["frequency", "data-rate"]
+ },
+ "fsk-channel": {
+ "type": "object",
+ "description": "FSK channel (optional)",
+ "additionalProperties": false,
+ "properties": {
+ "frequency": {
+ "type": "integer",
+ "description": "Frequency [Hz]"
+ },
+ "data-rate": {
+ "type": "integer",
+ "description": "Data rate index"
+ }
+ },
+ "required": ["frequency", "data-rate"]
+ },
+ "time-off-air": {
+ "type": "object",
+ "description": "Time-off-air (optional)",
+ "additionalProperties": false,
+ "properties": {
+ "fraction": {
+ "type": "number",
+ "description": "Minimum fraction of the emission time (optional)"
+ },
+ "duration": {
+ "type": "string",
+ "description": "Minimum duration (optional)"
+ }
+ }
+ },
+ "dwell-time": {
+ "type": "object",
+ "description": "Dwell time (optional)",
+ "additionalProperties": false,
+ "properties": {
+ "uplinks": {
+ "type": "boolean",
+ "description": "Enabled for uplink"
+ },
+ "downlinks": {
+ "type": "boolean",
+ "description": "Enabled for downlink"
+ },
+ "duration": {
+ "type": "string",
+ "description": "Duration"
+ }
+ },
+ "required": ["uplinks", "downlinks", "duration"]
+ },
+ "listen-before-talk": {
+ "type": "object",
+ "description": "Listen-before-talk (optional)",
+ "additionalProperties": false,
+ "properties": {
+ "rssi-offset": {
+ "type": "integer",
+ "description": "RSSI offset [dbm]"
+ },
+ "rssi-target": {
+ "type": "integer",
+ "description": "RSSI target [dbm]"
+ },
+ "scan-time": {
+ "type": "integer",
+ "description": "Scan time [nanoseconds]"
+ }
+ },
+ "required": ["rssi-offset", "rssi-target", "scan-time"]
+ },
+ "ping-slot": {
+ "type": "object",
+ "description": "Class B ping slot settings (optional)",
+ "additionalProperties": false,
+ "properties": {
+ "frequency": {
+ "type": "integer",
+ "description": "Frequency [Hz]"
+ },
+ "min-data-rate": {
+ "type": "integer",
+ "description": "Minimum data rate index"
+ },
+ "max-data-rate": {
+ "type": "integer",
+ "description": "Maximum data rate index"
+ }
+ },
+ "required": ["frequency", "min-data-rate", "max-data-rate"]
+ },
+ "ping-slot-default-data-rate": {
+ "type": "integer",
+ "description": "Default data rate index of class B ping slot (optional)"
+ },
+ "rx2-channel": {
+ "type": "object",
+ "description": "Rx2 channel (optional)",
+ "additionalProperties": false,
+ "properties": {
+ "frequency": {
+ "type": "integer",
+ "description": "Frequency [Hz]"
+ },
+ "min-data-rate": {
+ "type": "integer",
+ "description": "Minimum data rate index"
+ },
+ "max-data-rate": {
+ "type": "integer",
+ "description": "Maximum data rate index"
+ }
+ },
+ "required": ["frequency", "min-data-rate", "max-data-rate"]
+ },
+ "rx2-default-data-rate": {
+ "type": "integer",
+ "description": "Default data rate index of Rx2 (optional)"
+ },
+ "max-eirp": {
+ "type": "number",
+ "description": "Maximum EIRP (optional; used when sub-bands do not have max-eirp, takes precedence over band's default)"
+ }
+ }
+}
diff --git a/end-device/schema.json b/end-device/schema.json
new file mode 100644
index 0000000..9e493f7
--- /dev/null
+++ b/end-device/schema.json
@@ -0,0 +1,210 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "title": "Frequency Plan",
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "band-id": {
+ "type": "string",
+ "description": "ID of the band (needs to match band-id in the index)",
+ "enum": ["AS_923","AS_923_2","AS_923_3","AS_923_4","AU_915_928","CN_470_510","CN_470_510_20_A","CN_470_510_20_B","CN_470_510_26_A","CN_470_510_26_B","CN_779_787","EU_433","EU_863_870","IN_865_867","ISM_2400","KR_920_923","MA_869_870_DRAFT","RU_864_870","US_902_928"]
+ },
+ "sub-bands": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "min-frequency": {
+ "type": "integer",
+ "description": "Minimum frequency [Hz] (inclusive)"
+ },
+ "max-frequency": {
+ "type": "integer",
+ "description": "Maximum frequency [Hz] (inclusive)"
+ },
+ "duty-cycle": {
+ "type": "number",
+ "description": "Duty cycle for this sub-band (optional; default: 1)"
+ },
+ "max-eirp": {
+ "type": "number",
+ "description": "Maximum EIRP for this sub-band (optional; takes precedence over frequency plan's max-eirp)"
+ }
+ },
+ "required": ["min-frequency", "max-frequency"]
+ }
+ },
+ "channels": {
+ "type": "array",
+ "description": "List of uplink channels (zero indexed)",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "uplink-frequency": {
+ "type": "integer",
+ "description": "Uplink frequency [Hz]"
+ },
+ "downlink-frequency": {
+ "type": "integer",
+ "description": "Downlink frequency [Hz]"
+ },
+ "min-data-rate": {
+ "type": "integer",
+ "description": "Minimum data rate index"
+ },
+ "max-data-rate": {
+ "type": "integer",
+ "description": "Maximum data rate index"
+ },
+ "default": {
+ "type": "boolean",
+ "description": "Channel is defined by RP"
+ }
+ },
+ "required": ["uplink-frequency", "min-data-rate", "max-data-rate"]
+ }
+ },
+ "lora-standard-channel": {
+ "type": "object",
+ "description": "LoRa standard channel (optional)",
+ "additionalProperties": false,
+ "properties": {
+ "frequency": {
+ "type": "integer",
+ "description": "Frequency [Hz]"
+ },
+ "data-rate": {
+ "type": "integer",
+ "description": "Data rate index"
+ }
+ },
+ "required": ["frequency", "data-rate"]
+ },
+ "fsk-channel": {
+ "type": "object",
+ "description": "FSK channel (optional)",
+ "additionalProperties": false,
+ "properties": {
+ "frequency": {
+ "type": "integer",
+ "description": "Frequency [Hz]"
+ },
+ "data-rate": {
+ "type": "integer",
+ "description": "Data rate index"
+ }
+ },
+ "required": ["frequency", "data-rate"]
+ },
+ "time-off-air": {
+ "type": "object",
+ "description": "Time-off-air (optional)",
+ "additionalProperties": false,
+ "properties": {
+ "fraction": {
+ "type": "number",
+ "description": "Minimum fraction of the emission time (optional)"
+ },
+ "duration": {
+ "type": "string",
+ "description": "Minimum duration (optional)"
+ }
+ }
+ },
+ "dwell-time": {
+ "type": "object",
+ "description": "Dwell time (optional)",
+ "additionalProperties": false,
+ "properties": {
+ "uplinks": {
+ "type": "boolean",
+ "description": "Enabled for uplink"
+ },
+ "downlinks": {
+ "type": "boolean",
+ "description": "Enabled for downlink"
+ },
+ "duration": {
+ "type": "string",
+ "description": "Duration"
+ }
+ },
+ "required": ["uplinks", "downlinks", "duration"]
+ },
+ "listen-before-talk": {
+ "type": "object",
+ "description": "Listen-before-talk (optional)",
+ "additionalProperties": false,
+ "properties": {
+ "rssi-offset": {
+ "type": "integer",
+ "description": "RSSI offset [dbm]"
+ },
+ "rssi-target": {
+ "type": "integer",
+ "description": "RSSI target [dbm]"
+ },
+ "scan-time": {
+ "type": "integer",
+ "description": "Scan time [nanoseconds]"
+ }
+ },
+ "required": ["rssi-offset", "rssi-target", "scan-time"]
+ },
+ "ping-slot": {
+ "type": "object",
+ "description": "Class B ping slot settings (optional)",
+ "additionalProperties": false,
+ "properties": {
+ "frequency": {
+ "type": "integer",
+ "description": "Frequency [Hz]"
+ },
+ "min-data-rate": {
+ "type": "integer",
+ "description": "Minimum data rate index"
+ },
+ "max-data-rate": {
+ "type": "integer",
+ "description": "Maximum data rate index"
+ }
+ },
+ "required": ["frequency", "min-data-rate", "max-data-rate"]
+ },
+ "ping-slot-default-data-rate": {
+ "type": "integer",
+ "description": "Default data rate index of class B ping slot (optional)"
+ },
+ "rx2-channel": {
+ "type": "object",
+ "description": "Rx2 channel (optional)",
+ "additionalProperties": false,
+ "properties": {
+ "frequency": {
+ "type": "integer",
+ "description": "Frequency [Hz]"
+ },
+ "min-data-rate": {
+ "type": "integer",
+ "description": "Minimum data rate index"
+ },
+ "max-data-rate": {
+ "type": "integer",
+ "description": "Maximum data rate index"
+ }
+ },
+ "required": ["frequency", "min-data-rate", "max-data-rate"]
+ },
+ "rx2-default-data-rate": {
+ "type": "integer",
+ "description": "Default data rate index of Rx2 (optional)"
+ },
+ "max-eirp": {
+ "type": "number",
+ "description": "Maximum EIRP (optional; used when sub-bands do not have max-eirp, takes precedence over band's default)"
+ }
+ },
+ "required": ["band-id", "sub-bands", "channels"]
+}
diff --git a/frequency-plans.go b/frequency-plans.go
new file mode 100644
index 0000000..86b281d
--- /dev/null
+++ b/frequency-plans.go
@@ -0,0 +1,36 @@
+package main
+
+import (
+ "flag"
+ "log"
+
+ "github.com/TheThingsNetwork/lorawan-frequency-plans/internal/docs"
+ "github.com/TheThingsNetwork/lorawan-frequency-plans/internal/schema"
+ "github.com/TheThingsNetwork/lorawan-frequency-plans/internal/validate"
+)
+
+var (
+ generateDocs = flag.Bool("docs", false, "Generate docs for the frequency-plans.")
+ generateSchema = flag.Bool("schema", false, "Generate the `schema.json` file.")
+)
+
+func main() {
+ flag.Parse()
+
+ if err := validate.Validate(); err != nil {
+ log.Fatal(err)
+ }
+
+ if *generateDocs {
+ // TODO: update doc generation to support end-device and gateway folders + new structure
+ if err := docs.Generate("./frequency-plans.yml", "./docs"); err != nil {
+ log.Fatal(err)
+ }
+ }
+
+ if *generateSchema {
+ if err := schema.Generate(); err != nil {
+ log.Fatal(err)
+ }
+ }
+}
diff --git a/frequency-plans.yml b/frequency-plans.yml
index 255fadd..8aeb7d4 100644
--- a/frequency-plans.yml
+++ b/frequency-plans.yml
@@ -1,35 +1,40 @@
+end-device-descriptions:
- id: EU_863_870
band-id: EU_863_870
- name: Europe 863-870 MHz (SF12 for RX2)
+ name: Europe 863-870 MHz
description: Default frequency plan for Europe
base-frequency: 868
country-codes: [al, ad, ao, at, bh, be, ba, bw, bg, cg, hr, cy, cz, dk, ee, sz, fi, fr, gr, hu, is, ie, it, lv, ls, li, lt, lu, mg, mw, mt, mu, md, me, mz, na, nl, mk, ph, pl, pt, ro, ru, sa, rs, sc, sk, si, za, es, se, ch, tz, tr, ae, gb, va, zm, zw]
file: EU_863_870.yml
+ endorsed: false
- id: EU_863_870_TTN
band-id: EU_863_870
- name: Europe 863-870 MHz (SF9 for RX2 - recommended)
+ base-id: EU_863_870
+ name: Europe 863-870 MHz
description: TTN Community Network frequency plan for Europe, using SF9 for RX2
base-frequency: 868
- base-id: EU_863_870
country-codes: [al, ad, ao, at, bh, be, ba, bw, bg, cg, hr, cy, cz, dk, ee, sz, fi, fr, gr, hu, is, ie, it, lv, ls, li, lt, lu, mg, mw, mt, mu, md, me, mz, na, nl, mk, ph, pl, pt, ro, ru, sa, rs, sc, sk, si, za, es, se, ch, tz, tr, ae, gb, va, zm, zw]
- file: EU_863_870_TTN.yml
+ modifiers: [rx2_default_data_rata_3.yml]
+ endorsed: true
- id: EU_863_870_ROAMING_DRAFT
band-id: EU_863_870
name: Europe 863-870 MHz, 6 channels for roaming (Draft)
description: European 6 channel plan used by major operators to support LoRaWAN Passive Roaming
base-frequency: 868
- base-id: EU_863_870
country-codes: [al, ad, ao, at, bh, be, ba, bw, bg, cg, hr, cy, cz, dk, ee, sz, fi, fr, gr, hu, is, ie, it, lv, ls, li, lt, lu, mg, mw, mt, mu, md, me, mz, na, nl, mk, ph, pl, pt, ro, ru, sa, rs, sc, sk, si, za, es, se, ch, tz, tr, ae, gb, va, zm, zw]
file: EU_863_870_ROAMING_DRAFT.yml
+ endorsed: false
- id: EU_433
band-id: EU_433
name: Europe 433 MHz (ITU region 1)
description: Default frequency plan for worldwide 433MHz
base-frequency: 433
+ country-codes: [worldwide]
file: EU_433.yml
+ endorsed: true
- id: US_902_928_FSB_1
band-id: US_902_928
@@ -38,612 +43,61 @@
base-frequency: 915
country-codes: [ca, cr, ec, gy, mx, pa, pr, us]
file: US_902_928_FSB_1.yml
-
-- id: US_902_928_FSB_2
- band-id: US_902_928
- name: United States 902-928 MHz, FSB 2 (used by TTN)
- description: TTN Community Network frequency plan for the United States and Canada, using sub-band 2
- base-frequency: 915
- country-codes: [ca, cr, ec, gy, mx, pa, pr, us]
- file: US_902_928_FSB_2.yml
-
-- id: US_902_928_FSB_3
- band-id: US_902_928
- name: United States 902-928 MHz, FSB 3
- description: Default frequency plan for the United States and Canada, using sub-band 3
- base-frequency: 915
- country-codes: [ca, cr, ec, gy, mx, pa, pr, us]
- file: US_902_928_FSB_3.yml
-
-- id: US_902_928_FSB_4
- band-id: US_902_928
- name: United States 902-928 MHz, FSB 4
- description: Default frequency plan for the United States and Canada, using sub-band 4
- base-frequency: 915
- country-codes: [ca, cr, ec, gy, mx, pa, pr, us]
- file: US_902_928_FSB_4.yml
-
-- id: US_902_928_FSB_5
- band-id: US_902_928
- name: United States 902-928 MHz, FSB 5
- description: Default frequency plan for the United States and Canada, using sub-band 5
- base-frequency: 915
- country-codes: [ca, cr, ec, gy, mx, pa, pr, us]
- file: US_902_928_FSB_5.yml
-
-- id: US_902_928_FSB_6
- band-id: US_902_928
- name: United States 902-928 MHz, FSB 6
- description: Default frequency plan for the United States and Canada, using sub-band 6
- base-frequency: 915
- country-codes: [ca, cr, ec, gy, mx, pa, pr, us]
- file: US_902_928_FSB_6.yml
-
-- id: US_902_928_FSB_7
- band-id: US_902_928
- name: United States 902-928 MHz, FSB 7
- description: Default frequency plan for the United States and Canada, using sub-band 7
- base-frequency: 915
- country-codes: [ca, cr, ec, gy, mx, pa, pr, us]
- file: US_902_928_FSB_7.yml
-
-- id: US_902_928_FSB_8
- band-id: US_902_928
- name: United States 902-928 MHz, FSB 8
- description: Default frequency plan for the United States and Canada, using sub-band 8
- base-frequency: 915
- country-codes: [ca, cr, ec, gy, mx, pa, pr, us]
- file: US_902_928_FSB_8.yml
-
-- id: AU_915_928_FSB_1
- band-id: AU_915_928
- name: Australia 915-928 MHz, FSB 1
- description: Default frequency plan for Australia, using sub-band 1
- base-frequency: 915
- country-codes: [ar, au, bo, br, ca, cl, co, do, nz, py, pe, sr, uy]
- file: AU_915_928_FSB_1.yml
-
-- id: AU_915_928_FSB_2
- band-id: AU_915_928
- name: Australia 915-928 MHz, FSB 2 (used by TTN)
- description: TTN Community Network frequency plan for Australia, using sub-band 2
- base-frequency: 915
- country-codes: [ar, au, bo, br, ca, cl, co, do, nz, py, pe, sr, uy]
- file: AU_915_928_FSB_2.yml
-
-- id: AU_915_928_FSB_3
- band-id: AU_915_928
- name: Australia 915-928 MHz, FSB 3
- description: Default frequency plan for Australia, using sub-band 3
- base-frequency: 915
- country-codes: [ar, au, bo, br, ca, cl, co, do, nz, py, pe, sr, uy]
- file: AU_915_928_FSB_3.yml
-
-- id: AU_915_928_FSB_4
- band-id: AU_915_928
- name: Australia 915-928 MHz, FSB 4
- description: Default frequency plan for Australia, using sub-band 4
- base-frequency: 915
- country-codes: [ar, au, bo, br, ca, cl, co, do, nz, py, pe, sr, uy]
- file: AU_915_928_FSB_4.yml
-
-- id: AU_915_928_FSB_5
- band-id: AU_915_928
- name: Australia 915-928 MHz, FSB 5
- description: Default frequency plan for Australia, using sub-band 5
- base-frequency: 915
- country-codes: [ar, au, bo, br, ca, cl, co, do, nz, py, pe, sr, uy]
- file: AU_915_928_FSB_5.yml
-
-- id: AU_915_928_FSB_6
- band-id: AU_915_928
- name: Australia 915-928 MHz, FSB 6
- description: Frequency plan for Australia, using sub-band 6, which overlaps with Asia 923-925 MHz
- base-frequency: 915
- country-codes: [ar, au, bo, br, ca, cl, co, do, nz, py, pe, sr, uy]
- file: AU_915_928_FSB_6.yml
-
-- id: AU_915_928_FSB_7
- band-id: AU_915_928
- name: Australia 915-928 MHz, FSB 7
- description: Default frequency plan for Australia, using sub-band 7
- base-frequency: 915
- country-codes: [ar, au, bo, br, ca, cl, co, do, nz, py, pe, sr, uy]
- file: AU_915_928_FSB_7.yml
-
-- id: AU_915_928_FSB_8
- band-id: AU_915_928
- name: Australia 915-928 MHz, FSB 8
- description: Default frequency plan for Australia, using sub-band 8
- base-frequency: 915
- country-codes: [ar, au, bo, br, ca, cl, co, do, nz, py, pe, sr, uy]
- file: AU_915_928_FSB_8.yml
-
-- id: CN_470_510_FSB_1
- band-id: CN_470_510
- name: China 470-510 MHz, FSB 1
- description: Default frequency plan for China, using sub-band 1
- base-frequency: 470
- country-codes: [cn]
- file: CN_470_510_FSB_1.yml
-
-- id: CN_470_510_FSB_11
- band-id: CN_470_510
- name: China 470-510 MHz, FSB 11 (used by TTN)
- description: TTN Community Network frequency plan for China, using sub-band 11
- base-frequency: 470
- country-codes: [cn]
- file: CN_470_510_FSB_11.yml
+ endorsed: true
- id: AS_920_923
- band-id: AS_920_923
+ band-id: AS_923
name: Asia 920-923 MHz
description: TTN Community Network frequency plan for Asian countries, using frequencies ≤ 923 MHz
base-frequency: 915
country-codes: [my, sg]
file: AS_920_923.yml
+ endorsed: true
- id: AS_920_923_LBT
- band-id: AS_920_923
+ band-id: AS_923
+ base-id: AS_920_923
name: Asia 920-923 MHz with LBT
description: TTN Community Network frequency plan for Asian countries, using frequencies ≤ 923 MHz with listen-before-talk
base-frequency: 915
- base-id: AS_920_923
country-codes: [jp, my, sg]
- file: lbt_80_over_128.yml
-
-- id: AS_920_923_TTN_JP_1
- name: Japan 920-923 MHz with LBT (channels 31-38)
- description: Frequency plan for Japan, using continuous frequencies up to 923.4MHz with LBT.
- base-frequency: 915
- country-codes: [jp]
- file: AS_920_923_TTN_JP_1.yml
-
-- id: AS_920_923_TTN_JP_1_LAND_MOBILE
- name: Japan 920-923 MHz with LBT (channels 31-38), Max EIRP 27 dBm
- description: |
- Frequency plan for Japanese land mobile station, using continuous frequencies up to 923.4MHz with MAX EIRP 27 dBm and LBT.
- (note) A user who installs land mobile station in Japan must apply to Japanese Ministry of Internal Affairs and Communications.
- base-frequency: 915
- base-id: AS_920_923_TTN_JP_1
- country-codes: [jp]
- file: AS_920_923_TTN_JP_1_LAND_MOBILE.yml
-
-- id: AS_920_923_TTN_JP_2
- name: Japan 920-923 MHz with LBT (channels 24-27 and 35-38)
- description: Frequency plan for Japan, using top and bottom frequencies ≤ 923.4 MHz with LBT.
- base-frequency: 915
- country-codes: [jp]
- file: AS_920_923_TTN_JP_2.yml
-
-- id: AS_920_923_TTN_JP_3
- name: Japan 920-923 MHz with LBT (channels 24-31)
- description: Frequency plan for Japan (16 channels), using continuous frequencies from 920.6 MHz, expected to be used with AS_920_923_TTN_JP_1.
- base-frequency: 915
- country-codes: [jp]
- file: AS_920_923_TTN_JP_3.yml
-
-- id: AS_920_923_TTN_JP_3_LAND_MOBILE
- name: Japan 920-923 MHz with LBT (channels 24-31), Max EIRP 27 dBm
- description: |
- Frequency plan for Japanese land mobile station (16 channels), using continuous frequencies from 920.6 MHz with MAX EIRP 27 dBm and LBT, expected to be used with AS_920_923_TTN_JP_1.
- (note) A user who installs land mobile station in Japan must apply to Japanese Ministry of Internal Affairs and Communications.
- base-frequency: 915
- base-id: AS_920_923_TTN_JP_3
- country-codes: [jp]
- file: AS_920_923_TTN_JP_3_LAND_MOBILE.yml
-
-- id: AS_923
- band-id: AS_923
- name: Asia 915-928 MHz (AS923 Group 1) with only default channels
- description: Compatibility frequency plan for Asian countries with common channels in the 923.0-923.5 MHz sub-band
- base-frequency: 915
- file: AS_923.yml
-
-- id: AS_923_2
- band-id: AS_923_2
- name: Asia 920-923 MHz (AS923 Group 2) with only default channels
- description: Compatibility frequency plan for Asian countries with common channels in the 921.4-922.0 MHz sub-band
- base-frequency: 915
- file: AS_923_2.yml
+ modifiers: [lbt_80_over_128.yml]
+ endorsed: false
-- id: AS_923_3
- band-id: AS_923_3
- name: Asia 915-921 MHz (AS923 Group 3) with only default channels
- description: Compatibility frequency plan for Asian countries with common channels in the 916.5-917.0 MHz sub-band
- base-frequency: 915
- file: AS_923_3.yml
+gateway-descriptions:
+- id: EU_863_870
+ band-id: EU_863_870
+ name: Europe 863-870 MHz
+ description: Default frequency plan for Europe
+ base-frequency: 868
+ country-codes: [al, ad, ao, at, bh, be, ba, bw, bg, cg, hr, cy, cz, dk, ee, sz, fi, fr, gr, hu, is, ie, it, lv, ls, li, lt, lu, mg, mw, mt, mu, md, me, mz, na, nl, mk, ph, pl, pt, ro, ru, sa, rs, sc, sk, si, za, es, se, ch, tz, tr, ae, gb, va, zm, zw]
+ file: EU_863_870.yml
+ endorsed: false
-- id: AS_923_4
- band-id: AS_923_4
- name: Asia 917-920 MHz (AS923 Group 4) with only default channels
- description: Compatibility frequency plan for Asian countries with common channels in the 917.3-917.5 MHz sub-band
- base-frequency: 915
- file: AS_923_4.yml
+- id: EU_433
+ band-id: EU_433
+ name: Europe 433 MHz (ITU region 1)
+ description: Default frequency plan for worldwide 433MHz
+ base-frequency: 433
+ country-codes: [worldwide]
+ file: EU_433.yml
+ endorsed: true
-- id: AS_923_NDT
+- id: AS_920_923
band-id: AS_923
- name: Asia 915-928 MHz (AS923 Group 1) with only default channels and dwell time disabled
- description: Compatibility frequency plan for Asian countries with common channels in the 923.0-923.5 MHz sub-band and dwell time disabled
+ name: Asia 920-923 MHz
+ description: TTN Community Network frequency plan for Asian countries, using frequencies ≤ 923 MHz
base-frequency: 915
- base-id: AS_923
- file: disable_dwell_time.yml
+ country-codes: [my, sg]
+ file: AS_920_923.yml
+ endorsed: true
-- id: AS_923_DT
+- id: AS_920_923_LBT
band-id: AS_923
- name: Asia 915-928 MHz (AS923 Group 1) with only default channels and dwell time enabled
- description: Compatibility frequency plan for Asian countries with common channels in the 923.0-923.5 MHz sub-band and dwell time enabled
- base-frequency: 915
- base-id: AS_923
- file: enable_dwell_time_400ms.yml
-
-- id: AS_923_2_NDT
- band-id: AS_923_2
- name: Asia 920-923 MHz (AS923 Group 2) with only default channels and dwell time disabled
- description: Compatibility frequency plan for Asian countries with common channels in the 921.4-922.0 MHz sub-band and dwell time disabled
- base-frequency: 915
- base-id: AS_923_2
- file: disable_dwell_time.yml
-
-- id: AS_923_2_DT
- band-id: AS_923_2
- name: Asia 920-923 MHz (AS923 Group 2) with only default channels and dwell time enabled
- description: Compatibility frequency plan for Asian countries with common channels in the 921.4-922.0 MHz sub-band and dwell time enabled
- base-frequency: 915
- base-id: AS_923_2
- file: enable_dwell_time_400ms.yml
-
-- id: AS_923_3_NDT
- band-id: AS_923_3
- name: Asia 920-923 MHz (AS923 Group 3) with only default channels and dwell time disabled
- description: Compatibility frequency plan for Asian countries with common channels in the 921.4-922.0 MHz sub-band and dwell time disabled
- base-frequency: 915
- base-id: AS_923_3
- file: disable_dwell_time.yml
-
-- id: AS_923_3_DT
- band-id: AS_923_3
- name: Asia 920-923 MHz (AS923 Group 3) with only default channels and dwell time enabled
- description: Compatibility frequency plan for Asian countries with common channels in the 921.4-922.0 MHz sub-band and dwell time enabled
- base-frequency: 915
- base-id: AS_923_3
- file: enable_dwell_time_400ms.yml
-
-- id: AS_923_4_NDT
- band-id: AS_923_4
- name: Asia 920-923 MHz (AS923 Group 4) with only default channels and dwell time disabled
- description: Compatibility frequency plan for Asian countries with common channels in the 921.4-922.0 MHz sub-band and dwell time disabled
- base-frequency: 915
- base-id: AS_923_4
- file: disable_dwell_time.yml
-
-- id: AS_923_4_DT
- band-id: AS_923_4
- name: Asia 920-923 MHz (AS923 Group 4) with only default channels and dwell time enabled
- description: Compatibility frequency plan for Asian countries with common channels in the 921.4-922.0 MHz sub-band and dwell time enabled
- base-frequency: 915
- base-id: AS_923_4
- file: enable_dwell_time_400ms.yml
-
-- id: AS_923_925
- band-id: AS_923_925
- name: Asia 923-925 MHz
- description: TTN Community Network frequency plan for Asian countries, using frequencies ≥ 923 MHz
- base-frequency: 915
- country-codes: [bn, kh, hk, id, la, tw, th, vn]
- file: AS_923_925.yml
-
-- id: AS_923_925_LBT
- band-id: AS_923_925
- name: Asia 923-925 MHz with LBT
- description: TTN Community Network frequency plan for Asian countries, using frequencies ≥ 923 MHz with listen-before-talk
- base-frequency: 915
- base-id: AS_923_925
- country-codes: [bn, kh, hk, id, la, tw, th, vn]
- file: lbt_80_over_128.yml
-
-- id: AS_920_923_TTN_AU
- band-id: AS_923_925
- name: Asia 920-923 MHz (used by TTN Australia)
- description: TTN Community Network frequency plan for Asia 920-923 MHz in Australia
- base-frequency: 915
base-id: AS_920_923
- country-codes: [au]
- file: AS_920_923_TTN_AU.yml
-
-- id: AS_923_925_TTN_AU
- band-id: AS_923_925
- name: Asia 923-925 MHz (used by TTN Australia - secondary channels)
- description: TTN Community Network frequency plan for Asia 923-925 MHz in Australia. Secondary channels for 16 channel gateways.
- base-frequency: 915
- base-id: AS_923_925
- country-codes: [au]
- file: AS_923_925_TTN_AU.yml
-
-- id: KR_920_923_TTN
- band-id: KR_920_923
- name: South Korea 920-923 MHz
- description: TTN Community Network frequency plan for South Korea
+ name: Asia 920-923 MHz with LBT
+ description: TTN Community Network frequency plan for Asian countries, using frequencies ≤ 923 MHz with listen-before-talk
base-frequency: 915
- country-codes: [kr]
- file: KR_920_923_TTN.yml
-
-- id: MA_869_870_DRAFT
- band-id: MA_869_870_DRAFT
- name: Morocco 869-870 MHz
- description: Draft frequency plan for Morocco, with 4 channels
- base-frequency: 868
- country-codes: [ma]
- file: MA_869_870_DRAFT.yml
-
-- id: IN_865_867
- band-id: IN_865_867
- name: India 865-867 MHz
- description: Default frequency plan for India
- base-frequency: 868
- country-codes: [in]
- file: IN_865_867.yml
-
-- id: RU_864_870_TTN
- band-id: RU_864_870
- name: Russia 864-870 MHz
- description: TTN Community Network frequency plan for Russia
- base-frequency: 868
- country-codes: [ru]
- file: RU_864_870_TTN.yml
-
-- id: ISM_2400_3CH_DRAFT2
- band-id: ISM_2400
- name: LoRa 2.4 GHz with 3 channels (Draft 2)
- description: Global 3 channel plan for LoRa 2.4 GHz (Draft 2)
- base-frequency: 2450
- country-codes:
- [
- af,
- ax,
- al,
- dz,
- as,
- ad,
- ao,
- ai,
- aq,
- ag,
- ar,
- am,
- aw,
- au,
- at,
- az,
- bs,
- bh,
- bd,
- bb,
- by,
- be,
- bz,
- bj,
- bm,
- bt,
- bo,
- ba,
- bw,
- bv,
- br,
- io,
- bn,
- bg,
- bf,
- bi,
- kh,
- cm,
- ca,
- cv,
- ky,
- cf,
- td,
- cl,
- cn,
- cx,
- cc,
- co,
- km,
- cg,
- cd,
- ck,
- cr,
- ci,
- hr,
- cu,
- cy,
- cz,
- dk,
- dj,
- dm,
- do,
- ec,
- eg,
- sv,
- gq,
- er,
- ee,
- et,
- fk,
- fo,
- fj,
- fi,
- fr,
- gf,
- pf,
- tf,
- ga,
- gm,
- ge,
- de,
- gh,
- gi,
- gr,
- gl,
- gd,
- gp,
- gu,
- gt,
- gg,
- gn,
- gw,
- gy,
- ht,
- hm,
- va,
- hn,
- hk,
- hu,
- is,
- in,
- id,
- ir,
- iq,
- ie,
- im,
- il,
- it,
- jm,
- jp,
- je,
- jo,
- kz,
- ke,
- ki,
- kp,
- kr,
- kw,
- kg,
- la,
- lv,
- lb,
- ls,
- lr,
- ly,
- li,
- lt,
- lu,
- mo,
- mk,
- mg,
- mw,
- my,
- mv,
- ml,
- mt,
- mh,
- mq,
- mr,
- mu,
- yt,
- mx,
- fm,
- md,
- mc,
- mn,
- me,
- ms,
- ma,
- mz,
- mm,
- na,
- nr,
- np,
- nl,
- an,
- nc,
- nz,
- ni,
- ne,
- ng,
- nu,
- nf,
- mp,
- no,
- om,
- pk,
- pw,
- ps,
- pa,
- pg,
- py,
- pe,
- ph,
- pn,
- pl,
- pt,
- pr,
- qa,
- re,
- ro,
- ru,
- rw,
- bl,
- sh,
- kn,
- lc,
- mf,
- pm,
- vc,
- ws,
- sm,
- st,
- sa,
- sn,
- rs,
- sc,
- sl,
- sg,
- sk,
- si,
- sb,
- so,
- za,
- gs,
- es,
- lk,
- sd,
- sr,
- sj,
- sz,
- se,
- ch,
- sy,
- tw,
- tj,
- tz,
- th,
- tl,
- tg,
- tk,
- to,
- tt,
- tn,
- tr,
- tm,
- tc,
- tv,
- ug,
- ua,
- ae,
- gb,
- us,
- um,
- uy,
- uz,
- vu,
- ve,
- vn,
- vg,
- vi,
- wf,
- eh,
- ye,
- zm,
- zw,
- ]
- file: ISM_2400_3CH_DRAFT2.yml
+ country-codes: [jp, my, sg]
+ modifiers: [lbt_80_over_128.yml]
+ endorsed: false
diff --git a/gateway/AS_920_923.yml b/gateway/AS_920_923.yml
new file mode 100644
index 0000000..6a11d90
--- /dev/null
+++ b/gateway/AS_920_923.yml
@@ -0,0 +1,66 @@
+band-id: AS_923
+sub-bands:
+- min-frequency: 922000000
+ max-frequency: 923400000
+channels:
+- uplink-frequency: 923200000
+ downlink-frequency: 923200000
+ min-data-rate: SF7BW125
+ max-data-rate: SF12BW125
+ radio: 0
+- uplink-frequency: 923400000
+ downlink-frequency: 923400000
+ min-data-rate: SF7BW125
+ max-data-rate: SF12BW125
+ radio: 0
+- uplink-frequency: 922200000
+ downlink-frequency: 922200000
+ min-data-rate: SF7BW125
+ max-data-rate: SF12BW125
+ radio: 1
+- uplink-frequency: 922400000
+ downlink-frequency: 922400000
+ min-data-rate: SF7BW125
+ max-data-rate: SF12BW125
+ radio: 1
+- uplink-frequency: 922600000
+ downlink-frequency: 922600000
+ min-data-rate: SF7BW125
+ max-data-rate: SF12BW125
+ radio: 0
+- uplink-frequency: 922800000
+ downlink-frequency: 922800000
+ min-data-rate: SF7BW125
+ max-data-rate: SF12BW125
+ radio: 0
+- uplink-frequency: 923000000
+ downlink-frequency: 923000000
+ min-data-rate: SF7BW125
+ max-data-rate: SF12BW125
+ radio: 0
+- uplink-frequency: 922000000
+ downlink-frequency: 922000000
+ min-data-rate: SF7BW125
+ max-data-rate: SF12BW125
+ radio: 1
+lora-standard-channel:
+ frequency: 922100000
+ data-rate: SF7BW250
+ radio: 1
+fsk-channel:
+ frequency: 921800000
+ data-rate: FSK50
+ radio: 1
+radios:
+- enable: true
+ chip-type: SX1257
+ frequency: 923000000
+ rssi-offset: -166
+ tx:
+ min-frequency: 920000000
+ max-frequency: 923400000
+- enable: true
+ chip-type: SX1257
+ frequency: 922100000
+ rssi-offset: -166
+clock-source: 1
diff --git a/gateway/EU_433.yml b/gateway/EU_433.yml
new file mode 100644
index 0000000..9790f28
--- /dev/null
+++ b/gateway/EU_433.yml
@@ -0,0 +1,64 @@
+band-id: EU_433
+sub-bands:
+- min-frequency: 433050000
+ max-frequency: 434790000
+ duty-cycle: 0.1 # NOTE: ETSI EN300220 limit is 10%; LoRaWAN limit for end-devices is 1%
+ max-eirp: 12.15
+channels:
+- uplink-frequency: 433175000
+ downlink-frequency: 433175000
+ min-data-rate: SF7BW125
+ max-data-rate: SF12BW125
+ radio: 0
+- uplink-frequency: 433375000
+ downlink-frequency: 433375000
+ min-data-rate: SF7BW125
+ max-data-rate: SF12BW125
+ radio: 0
+- uplink-frequency: 433575000
+ downlink-frequency: 433575000
+ min-data-rate: SF7BW125
+ max-data-rate: SF12BW125
+ radio: 0
+- uplink-frequency: 433775000
+ downlink-frequency: 433775000
+ min-data-rate: SF7BW125
+ max-data-rate: SF12BW125
+ radio: 0
+- uplink-frequency: 433975000
+ downlink-frequency: 433975000
+ min-data-rate: SF7BW125
+ max-data-rate: SF12BW125
+ radio: 1
+- uplink-frequency: 434175000
+ downlink-frequency: 434175000
+ min-data-rate: SF7BW125
+ max-data-rate: SF12BW125
+ radio: 1
+- uplink-frequency: 434375000
+ downlink-frequency: 434375000
+ min-data-rate: SF7BW125
+ max-data-rate: SF12BW125
+ radio: 1
+- uplink-frequency: 434575000
+ downlink-frequency: 434575000
+ min-data-rate: SF7BW125
+ max-data-rate: SF12BW125
+ radio: 1
+lora-standard-channel:
+ frequency: 434075000
+ data-rate: SF7BW250
+ radio: 1
+radios:
+- enable: true
+ chip-type: SX1255
+ frequency: 433475000
+ rssi-offset: -176
+ tx:
+ min-frequency: 433050000
+ max-frequency: 434790000
+- enable: true
+ chip-type: SX1255
+ frequency: 434275000
+ rssi-offset: -176
+clock-source: 1
diff --git a/gateway/EU_863_870.yml b/gateway/EU_863_870.yml
new file mode 100644
index 0000000..1a81d97
--- /dev/null
+++ b/gateway/EU_863_870.yml
@@ -0,0 +1,74 @@
+band-id: EU_863_870
+sub-bands:
+ - min-frequency: 867000000 # TODO: Verify this
+ max-frequency: 869000000
+channels:
+- uplink-frequency: 868100000
+ downlink-frequency: 868100000
+ min-data-rate: SF12BW125
+ max-data-rate: SF7BW125
+ radio: 0
+ default: true
+- uplink-frequency: 868300000
+ downlink-frequency: 868300000
+ min-data-rate: SF12BW125
+ max-data-rate: SF7BW125
+ radio: 0
+ default: true
+- uplink-frequency: 868500000
+ downlink-frequency: 868500000
+ min-data-rate: SF12BW125
+ max-data-rate: SF7BW125
+ radio: 0
+ default: true
+- uplink-frequency: 867100000
+ downlink-frequency: 867100000
+ min-data-rate: SF12BW125
+ max-data-rate: SF7BW125
+ radio: 0
+ default: true
+- uplink-frequency: 867300000
+ downlink-frequency: 867300000
+ min-data-rate: SF12BW125
+ max-data-rate: SF7BW125
+ radio: 0
+ default: true
+- uplink-frequency: 867500000
+ downlink-frequency: 867500000
+ min-data-rate: SF12BW125
+ max-data-rate: SF7BW125
+ radio: 0
+ default: true
+- uplink-frequency: 867700000
+ downlink-frequency: 867700000
+ min-data-rate: SF12BW125
+ max-data-rate: SF7BW125
+ radio: 0
+ default: true
+- uplink-frequency: 867900000
+ downlink-frequency: 867900000
+ min-data-rate: SF12BW125
+ max-data-rate: SF7BW125
+ radio: 0
+ default: true
+lora-standard-channel:
+ frequency: 868300000
+ data-rate: SF7BW250
+ radio: 1
+fsk-channel:
+ frequency: 868800000
+ data-rate: FSK50 # TODO: verify
+ radio: 1
+radios:
+- enable: true
+ chip-type: SX1257
+ frequency: 867500000
+ rssi-offset: -166
+ tx:
+ min-frequency: 863000000
+ max-frequency: 870000000
+- enable: true
+ chip-type: SX1257
+ frequency: 868500000
+ rssi-offset: -166
+clock-source: 1
diff --git a/gateway/modifiers/disable_dwell_time.yml b/gateway/modifiers/disable_dwell_time.yml
new file mode 100644
index 0000000..4debef4
--- /dev/null
+++ b/gateway/modifiers/disable_dwell_time.yml
@@ -0,0 +1,4 @@
+dwell-time:
+ uplinks: false
+ downlinks: false
+ duration: 0ms
\ No newline at end of file
diff --git a/gateway/modifiers/enable_dwell_time_400ms.yml b/gateway/modifiers/enable_dwell_time_400ms.yml
new file mode 100644
index 0000000..291fe42
--- /dev/null
+++ b/gateway/modifiers/enable_dwell_time_400ms.yml
@@ -0,0 +1,4 @@
+dwell-time:
+ uplinks: true
+ downlinks: true
+ duration: 400ms
\ No newline at end of file
diff --git a/gateway/modifiers/lbt_80_over_128.yml b/gateway/modifiers/lbt_80_over_128.yml
new file mode 100644
index 0000000..e1d43b8
--- /dev/null
+++ b/gateway/modifiers/lbt_80_over_128.yml
@@ -0,0 +1,4 @@
+listen-before-talk:
+ rssi-offset: -4
+ rssi-target: -80
+ scan-time: 128000
\ No newline at end of file
diff --git a/gateway/modifiers/schema.json b/gateway/modifiers/schema.json
new file mode 100644
index 0000000..db386a6
--- /dev/null
+++ b/gateway/modifiers/schema.json
@@ -0,0 +1,225 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "title": "Frequency Plan",
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "sub-bands": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "min-frequency": {
+ "type": "integer",
+ "description": "Minimum frequency [Hz] (inclusive)"
+ },
+ "max-frequency": {
+ "type": "integer",
+ "description": "Maximum frequency [Hz] (inclusive)"
+ },
+ "duty-cycle": {
+ "type": "number",
+ "description": "Duty cycle for this sub-band (optional; default: 1)"
+ },
+ "max-eirp": {
+ "type": "number",
+ "description": "Maximum EIRP for this sub-band (optional; takes precedence over frequency plan's max-eirp)"
+ }
+ },
+ "required": ["min-frequency", "max-frequency"]
+ }
+ },
+ "channels": {
+ "type": "array",
+ "description": "List of uplink channels (zero indexed)",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "uplink-frequency": {
+ "type": "integer",
+ "description": "Uplink frequency [Hz]"
+ },
+ "downlink-frequency": {
+ "type": "integer",
+ "description": "Downlink frequency [Hz] (optional)"
+ },
+ "min-data-rate": {
+ "type": "string",
+ "description": "Minimum data rate index",
+ "enum": ["SF7BW125","SF7BW250","SF8BW125","SF8BW500","SF9BW125","SF9BW500","SF10BW125","SF10BW500","SF11BW125","SF11BW500","SF12BW125","SF12BW500"]
+ },
+ "max-data-rate": {
+ "type": "string",
+ "description": "Maximum data rate index",
+ "enum": ["SF7BW125","SF7BW250","SF8BW125","SF8BW500","SF9BW125","SF9BW500","SF10BW125","SF10BW500","SF11BW125","SF11BW500","SF12BW125","SF12BW500"]
+ },
+ "radio": {
+ "type": "integer",
+ "description": "Radio index"
+ },
+ "default": {
+ "type": "boolean",
+ "description": "Channel is defined by RP"
+ }
+ },
+ "required": ["uplink-frequency", "min-data-rate", "max-data-rate", "radio"]
+ }
+ },
+ "lora-standard-channel": {
+ "type": "object",
+ "description": "LoRa standard channel (optional)",
+ "additionalProperties": false,
+ "properties": {
+ "frequency": {
+ "type": "integer",
+ "description": "Frequency [Hz]"
+ },
+ "data-rate": {
+ "type": "string",
+ "description": "Data rate index",
+ "enum": ["SF7BW125","SF7BW250","SF8BW125","SF8BW500","SF9BW125","SF9BW500","SF10BW125","SF10BW500","SF11BW125","SF11BW500","SF12BW125","SF12BW500"]
+ },
+ "radio": {
+ "type": "integer",
+ "description": "Radio index"
+ }
+ },
+ "required": ["frequency", "data-rate", "radio"]
+ },
+ "fsk-channel": {
+ "type": "object",
+ "description": "FSK channel (optional)",
+ "additionalProperties": false,
+ "properties": {
+ "frequency": {
+ "type": "integer",
+ "description": "Frequency [Hz]"
+ },
+ "data-rate": {
+ "type": "string",
+ "description": "Data rate index",
+ "enum": ["FSK50"]
+ },
+ "radio": {
+ "type": "integer",
+ "description": "Radio index"
+ }
+ },
+ "required": ["frequency", "data-rate", "radio"]
+ },
+ "time-off-air": {
+ "type": "object",
+ "description": "Time-off-air (optional)",
+ "additionalProperties": false,
+ "properties": {
+ "fraction": {
+ "type": "number",
+ "description": "Minimum fraction of the emission time (optional)"
+ },
+ "duration": {
+ "type": "string",
+ "description": "Minimum duration (optional)"
+ }
+ }
+ },
+ "dwell-time": {
+ "type": "object",
+ "description": "Dwell time (optional)",
+ "additionalProperties": false,
+ "properties": {
+ "uplinks": {
+ "type": "boolean",
+ "description": "Enabled for uplink"
+ },
+ "downlinks": {
+ "type": "boolean",
+ "description": "Enabled for downlink"
+ },
+ "duration": {
+ "type": "string",
+ "description": "Duration"
+ }
+ },
+ "required": ["uplinks", "downlinks", "duration"]
+ },
+ "listen-before-talk": {
+ "type": "object",
+ "description": "Listen-before-talk (optional)",
+ "additionalProperties": false,
+ "properties": {
+ "rssi-offset": {
+ "type": "integer",
+ "description": "RSSI offset [dbm]"
+ },
+ "rssi-target": {
+ "type": "integer",
+ "description": "RSSI target [dbm]"
+ },
+ "scan-time": {
+ "type": "integer",
+ "description": "Scan time [nanoseconds]"
+ }
+ },
+ "required": ["rssi-offset", "rssi-target", "scan-time"]
+ },
+ "radios": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "enable": {
+ "type": "boolean",
+ "description": "Enable the radio"
+ },
+ "chip-type": {
+ "type": "string",
+ "description": "Chip type",
+ "enum": ["SX1255", "SX1257"]
+ },
+ "frequency": {
+ "type": "integer",
+ "description": "Center frequency [Hz]"
+ },
+ "rssi-offset": {
+ "type": "integer",
+ "description": "RSSI offset [dbm]"
+ },
+ "tx": {
+ "type": "object",
+ "description": "Radio transmission configuration (optional)",
+ "additionalProperties": false,
+ "properties": {
+ "min-frequency": {
+ "type": "integer",
+ "description": "Minimum frequency [Hz]"
+ },
+ "max-frequency": {
+ "type": "integer",
+ "description": "Maximum frequency [Hz]"
+ },
+ "notch-frequency": {
+ "type": "integer",
+ "description": "Notch frequency 126000..250000 [Hz] (optional)",
+ "minimum": 126000,
+ "maximum": 250000
+ }
+ },
+ "required": ["min-frequency", "max-frequency"]
+ }
+ },
+ "required": ["enable", "chip-type", "frequency", "rssi-offset"]
+ }
+ },
+ "clock-source": {
+ "type": "integer",
+ "description": "Gateway clock source"
+ },
+ "max-eirp": {
+ "type": "number",
+ "description": "Maximum EIRP (optional; used when sub-bands do not have max-eirp, takes precedence over band's default)"
+ }
+ }
+}
diff --git a/gateway/schema.json b/gateway/schema.json
new file mode 100644
index 0000000..c171887
--- /dev/null
+++ b/gateway/schema.json
@@ -0,0 +1,231 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "title": "Frequency Plan",
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "band-id": {
+ "type": "string",
+ "description": "ID of the band (needs to match band-id in the index)",
+ "enum": ["AS_923","AS_923_2","AS_923_3","AS_923_4","AU_915_928","CN_470_510","CN_470_510_20_A","CN_470_510_20_B","CN_470_510_26_A","CN_470_510_26_B","CN_779_787","EU_433","EU_863_870","IN_865_867","ISM_2400","KR_920_923","MA_869_870_DRAFT","RU_864_870","US_902_928"]
+ },
+ "sub-bands": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "min-frequency": {
+ "type": "integer",
+ "description": "Minimum frequency [Hz] (inclusive)"
+ },
+ "max-frequency": {
+ "type": "integer",
+ "description": "Maximum frequency [Hz] (inclusive)"
+ },
+ "duty-cycle": {
+ "type": "number",
+ "description": "Duty cycle for this sub-band (optional; default: 1)"
+ },
+ "max-eirp": {
+ "type": "number",
+ "description": "Maximum EIRP for this sub-band (optional; takes precedence over frequency plan's max-eirp)"
+ }
+ },
+ "required": ["min-frequency", "max-frequency"]
+ }
+ },
+ "channels": {
+ "type": "array",
+ "description": "List of uplink channels (zero indexed)",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "uplink-frequency": {
+ "type": "integer",
+ "description": "Uplink frequency [Hz]"
+ },
+ "downlink-frequency": {
+ "type": "integer",
+ "description": "Downlink frequency [Hz]"
+ },
+ "min-data-rate": {
+ "type": "string",
+ "description": "Minimum data rate index",
+ "enum": ["SF7BW125","SF7BW250","SF8BW125","SF8BW500","SF9BW125","SF9BW500","SF10BW125","SF10BW500","SF11BW125","SF11BW500","SF12BW125","SF12BW500"]
+ },
+ "max-data-rate": {
+ "type": "string",
+ "description": "Maximum data rate index",
+ "enum": ["SF7BW125","SF7BW250","SF8BW125","SF8BW500","SF9BW125","SF9BW500","SF10BW125","SF10BW500","SF11BW125","SF11BW500","SF12BW125","SF12BW500"]
+ },
+ "radio": {
+ "type": "integer",
+ "description": "Radio index"
+ },
+ "default": {
+ "type": "boolean",
+ "description": "Channel is defined by RP"
+ }
+ },
+ "required": ["uplink-frequency", "min-data-rate", "max-data-rate", "radio"]
+ }
+ },
+ "lora-standard-channel": {
+ "type": "object",
+ "description": "LoRa standard channel (optional)",
+ "additionalProperties": false,
+ "properties": {
+ "frequency": {
+ "type": "integer",
+ "description": "Frequency [Hz]"
+ },
+ "data-rate": {
+ "type": "string",
+ "description": "Data rate index",
+ "enum": ["SF7BW125","SF7BW250","SF8BW125","SF8BW500","SF9BW125","SF9BW500","SF10BW125","SF10BW500","SF11BW125","SF11BW500","SF12BW125","SF12BW500"]
+ },
+ "radio": {
+ "type": "integer",
+ "description": "Radio index"
+ }
+ },
+ "required": ["frequency", "data-rate", "radio"]
+ },
+ "fsk-channel": {
+ "type": "object",
+ "description": "FSK channel (optional)",
+ "additionalProperties": false,
+ "properties": {
+ "frequency": {
+ "type": "integer",
+ "description": "Frequency [Hz]"
+ },
+ "data-rate": {
+ "type": "string",
+ "description": "Data rate index",
+ "enum": ["FSK50"]
+ },
+ "radio": {
+ "type": "integer",
+ "description": "Radio index"
+ }
+ },
+ "required": ["frequency", "data-rate", "radio"]
+ },
+ "time-off-air": {
+ "type": "object",
+ "description": "Time-off-air (optional)",
+ "additionalProperties": false,
+ "properties": {
+ "fraction": {
+ "type": "number",
+ "description": "Minimum fraction of the emission time (optional)"
+ },
+ "duration": {
+ "type": "string",
+ "description": "Minimum duration (optional)"
+ }
+ }
+ },
+ "dwell-time": {
+ "type": "object",
+ "description": "Dwell time (optional)",
+ "additionalProperties": false,
+ "properties": {
+ "uplinks": {
+ "type": "boolean",
+ "description": "Enabled for uplink"
+ },
+ "downlinks": {
+ "type": "boolean",
+ "description": "Enabled for downlink"
+ },
+ "duration": {
+ "type": "string",
+ "description": "Duration"
+ }
+ },
+ "required": ["uplinks", "downlinks", "duration"]
+ },
+ "listen-before-talk": {
+ "type": "object",
+ "description": "Listen-before-talk (optional)",
+ "additionalProperties": false,
+ "properties": {
+ "rssi-offset": {
+ "type": "integer",
+ "description": "RSSI offset [dbm]"
+ },
+ "rssi-target": {
+ "type": "integer",
+ "description": "RSSI target [dbm]"
+ },
+ "scan-time": {
+ "type": "integer",
+ "description": "Scan time [nanoseconds]"
+ }
+ },
+ "required": ["rssi-offset", "rssi-target", "scan-time"]
+ },
+ "radios": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "enable": {
+ "type": "boolean",
+ "description": "Enable the radio"
+ },
+ "chip-type": {
+ "type": "string",
+ "description": "Chip type",
+ "enum": ["SX1255", "SX1257"]
+ },
+ "frequency": {
+ "type": "integer",
+ "description": "Center frequency [Hz]"
+ },
+ "rssi-offset": {
+ "type": "integer",
+ "description": "RSSI offset [dbm]"
+ },
+ "tx": {
+ "type": "object",
+ "description": "Radio transmission configuration (optional)",
+ "additionalProperties": false,
+ "properties": {
+ "min-frequency": {
+ "type": "integer",
+ "description": "Minimum frequency [Hz]"
+ },
+ "max-frequency": {
+ "type": "integer",
+ "description": "Maximum frequency [Hz]"
+ },
+ "notch-frequency": {
+ "type": "integer",
+ "description": "Notch frequency 126000..250000 [Hz] (optional)",
+ "minimum": 126000,
+ "maximum": 250000
+ }
+ },
+ "required": ["min-frequency", "max-frequency"]
+ }
+ },
+ "required": ["enable", "chip-type", "frequency", "rssi-offset"]
+ }
+ },
+ "clock-source": {
+ "type": "integer",
+ "description": "Gateway clock source"
+ },
+ "max-eirp": {
+ "type": "number",
+ "description": "Maximum EIRP (optional; used when sub-bands do not have max-eirp, takes precedence over band's default)"
+ }
+ },
+ "required": ["band-id", "sub-bands", "channels"]
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..06b5226
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,45 @@
+module github.com/TheThingsNetwork/lorawan-frequency-plans
+
+go 1.19
+
+// Use our fork of grpc-gateway.
+replace github.com/grpc-ecosystem/grpc-gateway => github.com/TheThingsIndustries/grpc-gateway v1.15.2-gogo
+
+// But the original grpc-gateway v2.
+replace github.com/grpc-ecosystem/grpc-gateway/v2 => github.com/grpc-ecosystem/grpc-gateway/v2 v2.10.3
+
+require (
+ github.com/wcharczuk/go-chart/v2 v2.1.0
+ go.thethings.network/lorawan-stack/v3 v3.22.2
+ gopkg.in/yaml.v2 v2.4.0
+)
+
+require (
+ github.com/TheThingsIndustries/protoc-gen-go-flags v1.0.0 // indirect
+ github.com/TheThingsIndustries/protoc-gen-go-json v1.4.0 // indirect
+ github.com/envoyproxy/protoc-gen-validate v0.6.3 // indirect
+ github.com/gogo/protobuf v1.3.2 // indirect
+ github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
+ github.com/golang/protobuf v1.5.2 // indirect
+ github.com/gotnospirit/makeplural v0.0.0-20180622080156-a5f48d94d976 // indirect
+ github.com/gotnospirit/messageformat v0.0.0-20190719172517-c1d0bdacdea2 // indirect
+ github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect
+ github.com/grpc-ecosystem/grpc-gateway/v2 v2.0.0-00010101000000-000000000000 // indirect
+ github.com/json-iterator/go v1.1.12 // indirect
+ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
+ github.com/modern-go/reflect2 v1.0.2 // indirect
+ github.com/pkg/errors v0.9.1 // indirect
+ github.com/satori/go.uuid v1.2.0 // indirect
+ github.com/spf13/pflag v1.0.5 // indirect
+ github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect
+ github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
+ golang.org/x/exp v0.0.0-20220706164943-b4a6d9510983 // indirect
+ golang.org/x/image v0.1.0 // indirect
+ golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect
+ golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64 // indirect
+ golang.org/x/text v0.4.0 // indirect
+ google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd // indirect
+ google.golang.org/grpc v1.46.2 // indirect
+ google.golang.org/protobuf v1.28.0 // indirect
+ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..d967461
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,321 @@
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.90.0 h1:MjvSkUq8RuAb+2JLDi5VQmmExRJPUQ3JLCWpRB6fmdw=
+cloud.google.com/go/storage v1.16.0 h1:1UwAux2OZP4310YXg5ohqBEpV16Y93uZG4+qOX7K2Kg=
+contrib.go.opencensus.io/exporter/prometheus v0.4.0 h1:0QfIkj9z/iVZgK31D9H9ohjjIDApI2GOPScCKwxedbs=
+github.com/Azure/azure-pipeline-go v0.2.3 h1:7U9HBg1JFK3jHl5qmo4CTZKFTVgMwdFHMVtCdfBE21U=
+github.com/Azure/azure-storage-blob-go v0.10.0 h1:evCwGreYo3XLeBV4vSxLbLiYb6e0SzsJiXQVRGsRXxs=
+github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs=
+github.com/Azure/go-autorest/autorest v0.11.24 h1:1fIGgHKqVm54KIPT+q8Zmd1QlVsmHqeUGso5qm2BqqE=
+github.com/Azure/go-autorest/autorest/adal v0.9.18 h1:kLnPsRjzZZUF3K5REu/Kc+qMQrvuza2bwSnNdhmzLfQ=
+github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw=
+github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+ZtXWSmf4Tg=
+github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/TheThingsIndustries/grpc-gateway v1.15.2-gogo h1:rWB4sbEKoL7xUC9ixUkJOBlPOeF0hcwzXHTISXZM7eA=
+github.com/TheThingsIndustries/grpc-gateway v1.15.2-gogo/go.mod h1:fU1VeKM8T+38FAMQNH0zO2BT6grnMyphff4CD9w1DTM=
+github.com/TheThingsIndustries/protoc-gen-go-flags v1.0.0 h1:zU7Sp5BES8h9Qi3dXeIhSpTGxrjtaqbzpdIm2L+G+FQ=
+github.com/TheThingsIndustries/protoc-gen-go-flags v1.0.0/go.mod h1:nlf5qiiSsW9OQfcSUUqzGv7v8OJg0MhI2rXvTiBf/fw=
+github.com/TheThingsIndustries/protoc-gen-go-json v1.4.0 h1:51GcLhX2se5RbQYb+++HTP+7qsrEIa5tdUb9+FPuFhs=
+github.com/TheThingsIndustries/protoc-gen-go-json v1.4.0/go.mod h1:7xYRiu4k/ioxM2/nqq2lyXLr4Lj4Nyx7AvYjr7rJSww=
+github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
+github.com/aws/aws-sdk-go v1.42.53 h1:56T04NWcmc0ZVYFbUc6HdewDQ9iHQFlmS6hj96dRjJs=
+github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
+github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=
+github.com/bluele/gcache v0.0.2 h1:WcbfdXICg7G/DGBh1PFfcirkWOQV+v077yF1pSy3DGw=
+github.com/census-instrumentation/opencensus-proto v0.2.1 h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk=
+github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
+github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
+github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4 h1:hzAQntlaYRkVSFEfj9OTWlVV1H155FMD8BTKktLv0QI=
+github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
+github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1 h1:zH8ljVhhq7yC0MIeUL/IviMtY8hx2mK8cN9wEYb8ggw=
+github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
+github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
+github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
+github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1 h1:xvqufLtNVwAhN8NMyWklVgxnWohi+wtMGQMhtxexlm0=
+github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=
+github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/envoyproxy/protoc-gen-validate v0.6.3 h1:HkntewfZJ9RofA/FX38zBCeIAqlLDFLbAI6eTpZqFJw=
+github.com/envoyproxy/protoc-gen-validate v0.6.3/go.mod h1:dyJXwwfPK2VSqiB9Klm1J6romD608Ba7Hij42vrOBCo=
+github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI=
+github.com/getsentry/sentry-go v0.12.0 h1:era7g0re5iY13bHSdN/xMkyV+5zZppjRVQhZrXCaEIk=
+github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/go-kit/log v0.2.0 h1:7i2K3eKTos3Vc0enKCfnVcgHh2olr/MyfboYq7cAcFw=
+github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA=
+github.com/go-redis/redis/v8 v8.11.4 h1:kHoYkfZP6+pe04aFTnhDH6GDROa5yJdHJVNxV3F46Tg=
+github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
+github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
+github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
+github.com/golang-jwt/jwt/v4 v4.3.0 h1:kHL1vqdqWNfATmA0FNMdmZNMyZI1U6O31X4rlIPoBog=
+github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
+github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
+github.com/golang/gddo v0.0.0-20210115222349-20d68f94ee1f h1:16RtHeWGkJMc80Etb8RPCcKevXGldr57+LOyZt8zOlg=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
+github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
+github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
+github.com/google/wire v0.5.0 h1:I7ELFeVBr3yfPIcc8+MWvrjk+3VjbcSzoXm3JVa+jD8=
+github.com/googleapis/gax-go v2.0.2+incompatible h1:silFMLAnr330+NRuag/VjIGF7TLp/LBrV2CJKFLWEww=
+github.com/googleapis/gax-go/v2 v2.1.1 h1:dp3bWCh+PPO1zjRRiCSczJav13sBvG4UhNyVTa1KqdU=
+github.com/gotnospirit/makeplural v0.0.0-20180622080156-a5f48d94d976 h1:b70jEaX2iaJSPZULSUxKtm73LBfsCrMsIlYCUgNGSIs=
+github.com/gotnospirit/makeplural v0.0.0-20180622080156-a5f48d94d976/go.mod h1:ZGQeOwybjD8lkCjIyJfqR5LD2wMVHJ31d6GdPxoTsWY=
+github.com/gotnospirit/messageformat v0.0.0-20190719172517-c1d0bdacdea2 h1:yUr520KXfjzq/QTGZ2h+DvEydkyBfvifw6ksyDW3Lpg=
+github.com/gotnospirit/messageformat v0.0.0-20190719172517-c1d0bdacdea2/go.mod h1:NO9UUa4C4cSmRsYSfZMAKhI5ifCRzOjSGe/pi7TKRvs=
+github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA=
+github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw=
+github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.10.3 h1:BGNSrTRW4rwfhJiFwvwF4XQ0Y72Jj9YEgxVrtovbD5o=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.10.3/go.mod h1:VHn7KgNsRriXa4mcgtkpR00OXyQY6g67JWMvn+R27A4=
+github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
+github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
+github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
+github.com/jacobsa/crypto v0.0.0-20190317225127-9f44e2d11115 h1:YuDUUFNM21CAbyPOpOP8BicaTD/0klJEKt5p8yuw+uY=
+github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
+github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
+github.com/jstemmer/go-junit-report v0.9.1 h1:6QPYqodiu3GuPL+7mfx+NwDdp2eTkp9IfEUpgAwUN0o=
+github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
+github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
+github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/lyft/protoc-gen-star v0.6.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA=
+github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls=
+github.com/mattn/go-ieproxy v0.0.3 h1:YkaHmK1CzE5C4O7A3hv3TCbfNDPSCf0RKZFX+VhBeYk=
+github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
+github.com/mitchellh/mapstructure v1.4.3 h1:OVowDSCllw/YjdLkam3/sm7wEtOy59d8ndGgCcyj8cs=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/oklog/ulid/v2 v2.0.2 h1:r4fFzBm+bv0wNKNh5eXTwU7i85y5x+uwkxCUTNVQqLc=
+github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
+github.com/pelletier/go-toml v1.9.4 h1:tjENF6MfZAg8e4ZmZTeWaWiT2vXtsoO6+iuOjFhECwM=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/prometheus/client_golang v1.11.0 h1:HNkLOAEQMIDv/K+04rukrLx6ch7msSRwf3/SASFAGtQ=
+github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
+github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4=
+github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU=
+github.com/prometheus/statsd_exporter v0.22.4 h1:bGhC0iI9DM8m9KIUlbcr3uzXnopagrajEKoP790256k=
+github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
+github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg=
+github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
+github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
+github.com/smartystreets/assertions v1.2.1 h1:bKNHfEv7tSIjZ8JbKaFjzFINljxG4lzZvmHUnElzOIg=
+github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4=
+github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
+github.com/spf13/afero v1.8.1 h1:izYHOT71f9iZ7iq37Uqjael60/vYC6vMtzedudZ0zEk=
+github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA=
+github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
+github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
+github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spf13/viper v1.10.1 h1:nuJZuYpG7gTj/XqiUwg8bA0cp1+M2mC3J4g5luUYBKk=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
+github.com/throttled/throttled v2.2.5+incompatible h1:65UB52X0qNTYiT0Sohp8qLYVFwZQPDw85uSa65OljjQ=
+github.com/throttled/throttled/v2 v2.7.1 h1:FnBysDX4Sok55bvfDMI0l2Y71V1vM2wi7O79OW7fNtw=
+github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU=
+github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc=
+github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
+github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
+github.com/wcharczuk/go-chart/v2 v2.1.0 h1:tY2slqVQ6bN+yHSnDYwZebLQFkphK4WNrVwnt7CJZ2I=
+github.com/wcharczuk/go-chart/v2 v2.1.0/go.mod h1:yx7MvAVNcP/kN9lKXM/NTce4au4DFN99j6i1OwDclNA=
+github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M=
+go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
+go.thethings.network/lorawan-stack/v3 v3.22.2 h1:RiXdtbHbG0XC9tMG2kM3df+ntIX4qcHww3G3dfJ6a9E=
+go.thethings.network/lorawan-stack/v3 v3.22.2/go.mod h1:htixulfSnn27vbVmOarvxcM5cu3XJkgdv2KSFDKuwcA=
+go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
+go.uber.org/multierr v1.7.0 h1:zaiO/rmgFjbmCXdSYJWQcdvOCsthmdaHfr3Gm2Kx4Ec=
+go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8=
+gocloud.dev v0.20.0 h1:mbEKMfnyPV7W1Rj35R1xXfjszs9dXkwSOq2KoFr25g8=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d h1:3qF+Z8Hkrw9sOhrFHti9TlB1Hkac1x+DNRkv0XQiFjo=
+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20220706164943-b4a6d9510983 h1:sUweFwmLOje8KNfXAVqGGAsmgJ/F8jJ6wBLJDt4BTKY=
+golang.org/x/exp v0.0.0-20220706164943-b4a6d9510983/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA=
+golang.org/x/image v0.0.0-20200927104501-e162460cd6b5/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
+golang.org/x/image v0.1.0 h1:r8Oj8ZA2Xy12/b5KZYj3tuv7NG/fBz3TwQVvpJ9l8Rk=
+golang.org/x/image v0.1.0/go.mod h1:iyPr49SD/G/TBxYVB/9RRtGUT5eNbo2u4NamWeQcD5c=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 h1:VLliZ0d+/avPrXXH+OakdXhpJuEoBZuwh1m2j7U6Iug=
+golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20191002035440-2ec189313ef0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
+golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 h1:OSnWWcOd/CtWQC2cYSBgbTSJv3ciqd8r54ySIW2y3RE=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64 h1:UiNENfZ8gDvpiWw7IpOMQ27spWmThO1RwwdQVbJahJM=
+golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
+golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/api v0.61.0 h1:TXXKS1slM3b2bZNJwD5DV/Tp6/M2cLzLOLh9PjDhrw8=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
+google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd h1:e0TwkXOdbnH/1x5rc5MZ/VYyiZ4v+RdVfrGMqEwT68I=
+google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
+google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
+google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
+google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
+google.golang.org/grpc v1.46.2 h1:u+MLGgVf7vRdjEYZ8wDFhAVNmhkbJ5hmrA1LMWK1CAQ=
+google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
+google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/ini.v1 v1.66.4 h1:SsAcf+mM7mRZo2nJNGt8mZCjG8ZRaNGMURJw7BsIST4=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
diff --git a/internal/docs/docs.go b/internal/docs/docs.go
new file mode 100644
index 0000000..fee2a18
--- /dev/null
+++ b/internal/docs/docs.go
@@ -0,0 +1,101 @@
+package docs
+
+import (
+ "bytes"
+ "embed"
+ "errors"
+ "fmt"
+ "os"
+ "strings"
+ "text/template"
+
+ "github.com/TheThingsNetwork/lorawan-frequency-plans/internal/model"
+)
+
+//go:embed "*.tmpl"
+var fsys embed.FS
+
+var tmpl = template.Must(template.ParseFS(fsys, "*.tmpl"))
+
+// Generate generates the documentation for the frequency-plans.
+func Generate(sourceFile, destinationFolder string) error {
+ output, err := model.FrequencyPlanDescriptions{}.Parse(sourceFile)
+ if err != nil {
+ return err
+ }
+ descriptions := output.(model.FrequencyPlanDescriptions)
+
+ if err := renderPlans("gateway", descriptions.GatewayDescriptions, model.FrequencyPlanGateway{}); err != nil {
+ return err
+ }
+
+ if err := renderPlans("end-device", descriptions.EndDeviceDescriptions, model.FrequencyPlanEndDevice{}); err != nil {
+ return err
+ }
+
+ var buf bytes.Buffer
+ if err := tmpl.ExecuteTemplate(&buf, "frequency-plans.md.tmpl", output); err != nil {
+ return err
+ }
+ if err := os.WriteFile(destinationFolder+"/frequency-plans.md", buf.Bytes(), 0o644); err != nil {
+ return err
+ }
+ return nil
+}
+
+func formatFrequency(f float64) string {
+ return strings.TrimRight(fmt.Sprintf("%.3f", f/1_000_000), "0.")
+}
+
+func renderPlans(folder string, descriptions []model.FrequencyPlanDescription, definition model.Definition) error {
+ for _, plan := range descriptions {
+ fileName := ""
+ if plan.HasModifiers() {
+ for _, description := range descriptions {
+ if description.ID == *plan.BaseID {
+ fileName = *description.File
+ break
+ }
+ }
+ } else {
+ fileName = *plan.File
+ }
+ basePlan, err := definition.Parse(folder + "/" + fileName)
+ if err != nil {
+ return err
+ }
+ if plan.HasModifiers() {
+ for _, modifierName := range *plan.Modifiers {
+ switch definition.(type) {
+ case model.FrequencyPlanEndDevice:
+ modifier, err := model.FrequencyPlanEndDeviceModifier{}.Parse(folder + "/modifiers/" + modifierName)
+ if err != nil {
+ return err
+ }
+ basePlan = basePlan.(model.FrequencyPlanEndDevice).Modify(modifier.(model.FrequencyPlanEndDeviceModifier))
+ case model.FrequencyPlanGateway:
+ modifier, err := model.FrequencyPlanGatewayModifier{}.Parse(folder + "/modifiers/" + modifierName)
+ if err != nil {
+ return err
+ }
+ basePlan = basePlan.(model.FrequencyPlanGateway).Modify(modifier.(model.FrequencyPlanGatewayModifier))
+ }
+ }
+ }
+ if err := render(plan.ID, basePlan); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func render(id string, definition model.Definition) error {
+ switch mod := definition.(type) {
+ case model.FrequencyPlanGateway:
+ return renderGateway(id, mod)
+ case model.FrequencyPlanEndDevice:
+ return renderEndDevice(id, mod)
+ default:
+ return errors.New("unsupported type")
+ }
+}
diff --git a/internal/docs/end-device.go b/internal/docs/end-device.go
new file mode 100644
index 0000000..07f4807
--- /dev/null
+++ b/internal/docs/end-device.go
@@ -0,0 +1,159 @@
+package docs
+
+import (
+ "bytes"
+ "os"
+ "sort"
+
+ "github.com/TheThingsNetwork/lorawan-frequency-plans/internal/model"
+ "github.com/wcharczuk/go-chart/v2"
+)
+
+func renderEndDevice(id string, plan model.FrequencyPlanEndDevice) error {
+ frequencies := make(map[float64]string)
+
+ graph := chart.Chart{
+ Title: id,
+ Width: 1920,
+ Height: 1080,
+ DPI: 150,
+ }
+
+ annotations := chart.AnnotationSeries{}
+
+ for _, ch := range plan.Channels {
+ freq := float64(*ch.UplinkFrequency)
+ start, end := freq-62500, freq+62500
+ frequencies[freq] = formatFrequency(freq)
+ color := chart.GetDefaultColor(0)
+ color.A = 128
+ graph.Series = append(graph.Series, chart.ContinuousSeries{
+ Style: chart.Style{
+ StrokeColor: color,
+ FillColor: color,
+ },
+ XValues: []float64{start, end},
+ YValues: []float64{float64(1), float64(1)},
+ })
+ annotations.Annotations = append(annotations.Annotations, chart.Value2{
+ XValue: freq,
+ YValue: 1,
+ Label: formatFrequency(freq),
+ })
+
+ if ch.DownlinkFrequency == nil {
+ continue
+ }
+ freq = float64(*ch.DownlinkFrequency)
+ start, end = freq-62500, freq+62500
+ frequencies[freq] = formatFrequency(freq)
+ color = chart.GetDefaultColor(3)
+ color.A = 128
+ graph.Series = append(graph.Series, chart.ContinuousSeries{
+ Style: chart.Style{
+ StrokeColor: color,
+ FillColor: color,
+ },
+ XValues: []float64{start, end},
+ YValues: []float64{float64(-1), float64(-1)},
+ })
+ annotations.Annotations = append(annotations.Annotations, chart.Value2{
+ XValue: freq,
+ YValue: -1,
+ Label: formatFrequency(freq),
+ })
+ }
+
+ if ch := plan.LoRaStandardChannel; ch != nil {
+ freq := float64(*ch.Frequency)
+ start, end := freq-125000, freq+125000
+ frequencies[freq] = formatFrequency(freq)
+ color := chart.GetDefaultColor(1)
+ color.A = 128
+ graph.Series = append(graph.Series, chart.ContinuousSeries{
+ Style: chart.Style{
+ StrokeColor: color,
+ FillColor: color,
+ },
+ XValues: []float64{start, end},
+ YValues: []float64{float64(2), float64(2)},
+ })
+ annotations.Annotations = append(annotations.Annotations, chart.Value2{
+ XValue: freq,
+ YValue: 2,
+ Label: formatFrequency(freq) + " (Std)",
+ })
+ }
+
+ if ch := plan.FSKChannel; ch != nil {
+ freq := float64(*ch.Frequency)
+ start, end := freq-62500, freq+62500
+ frequencies[freq] = formatFrequency(freq)
+ color := chart.GetDefaultColor(2)
+ color.A = 128
+ graph.Series = append(graph.Series, chart.ContinuousSeries{
+ Style: chart.Style{
+ StrokeColor: color,
+ FillColor: color,
+ },
+ XValues: []float64{start, end},
+ YValues: []float64{float64(2), float64(2)},
+ })
+ annotations.Annotations = append(annotations.Annotations, chart.Value2{
+ XValue: freq,
+ YValue: 2,
+ Label: formatFrequency(freq) + " (FSK)",
+ })
+ }
+
+ var frequencySlice []float64
+ for frequency := range frequencies {
+ frequencySlice = append(frequencySlice, frequency)
+ }
+ sort.Float64s(frequencySlice)
+
+ graph.XAxis = chart.XAxis{
+ Range: &chart.ContinuousRange{
+ Min: frequencySlice[0],
+ Max: frequencySlice[len(frequencySlice)-1],
+ },
+ TickStyle: chart.Style{
+ TextRotationDegrees: 45.0,
+ },
+ }
+
+ for _, freq := range frequencySlice {
+ graph.XAxis.Ticks = append(graph.XAxis.Ticks, chart.Tick{
+ Value: freq,
+ Label: frequencies[freq],
+ })
+ }
+
+ graph.YAxis = chart.YAxis{
+ Range: &chart.ContinuousRange{
+ Min: -2,
+ Max: 3,
+ },
+ Ticks: []chart.Tick{
+ {Value: -2},
+ {Value: -1, Label: "Downlink"},
+ {Value: 0, Label: "Radio"},
+ {Value: 1, Label: "Uplink"},
+ {Value: 2, Label: "Std/FSK"},
+ {Value: 3},
+ },
+ }
+
+ graph.Series = append(graph.Series, annotations)
+
+ var buf bytes.Buffer
+
+ if err := graph.Render(chart.SVG, &buf); err != nil {
+ return err
+ }
+ if err := os.WriteFile("./docs/images/end-device/"+id+".svg", buf.Bytes(), 0o644); err != nil {
+ return err
+ }
+
+ return nil
+}
diff --git a/internal/docs/frequency-plans.md.tmpl b/internal/docs/frequency-plans.md.tmpl
new file mode 100644
index 0000000..0d415ca
--- /dev/null
+++ b/internal/docs/frequency-plans.md.tmpl
@@ -0,0 +1,24 @@
+# LoRaWAN Frequency Plans for The Things Stack
+
+# End device frequency plans
+{{- range .EndDeviceDescriptions }}
+{{ if .BaseID }}
+## `{{ .ID }}`: {{ .Name }}
+Based on [{{ .BaseID }}](##{{ .BaseID }}) and modified by {{ range .Modifiers }}[{{ . }}](../end-device/modifiers/{{ . }}){{ end }}{{ else }}
+## [`{{ .ID }}`](../end-device/{{ .File }}): {{ .Name }}{{ end }}
+
+>> {{ .Description }}
+
+![{{ .ID }}](images/end-device/{{ .ID }}.svg){{ end }}
+
+# Gateway frequency plans
+{{- range .GatewayDescriptions }}
+{{ if .BaseID }}
+## `{{ .ID }}`: {{ .Name }}
+Based on {{ .BaseID }} and modified by {{ range .Modifiers }}[{{ . }}](../gateway/modifiers/{{ . }}){{ end }}
+{{ else }}
+## [`{{ .ID }}`](../gateway/{{ .File }}): {{ .Name }}{{ end }}
+
+>> {{ .Description }}
+
+![{{ .ID }}](images/gateway/{{ .ID }}.svg){{ end }}
diff --git a/internal/docs/gateway.go b/internal/docs/gateway.go
new file mode 100644
index 0000000..0a3a4bf
--- /dev/null
+++ b/internal/docs/gateway.go
@@ -0,0 +1,180 @@
+package docs
+
+import (
+ "bytes"
+ "fmt"
+ "os"
+ "sort"
+
+ "github.com/TheThingsNetwork/lorawan-frequency-plans/internal/model"
+ "github.com/wcharczuk/go-chart/v2"
+)
+
+func renderGateway(id string, plan model.FrequencyPlanGateway) error {
+ frequencies := make(map[float64]string)
+
+ graph := chart.Chart{
+ Title: id,
+ Width: 1920,
+ Height: 1080,
+ DPI: 150,
+ }
+
+ annotations := chart.AnnotationSeries{}
+
+ for _, ch := range plan.Channels {
+ freq := float64(*ch.UplinkFrequency)
+ start, end := freq-62500, freq+62500
+ frequencies[freq] = formatFrequency(freq)
+ color := chart.GetDefaultColor(int(*ch.Radio))
+ color.A = 128
+ graph.Series = append(graph.Series, chart.ContinuousSeries{
+ Style: chart.Style{
+ StrokeColor: color,
+ FillColor: color,
+ },
+ XValues: []float64{start, end},
+ YValues: []float64{float64(1), float64(1)},
+ })
+ annotations.Annotations = append(annotations.Annotations, chart.Value2{
+ XValue: freq,
+ YValue: 1,
+ Label: formatFrequency(freq),
+ })
+
+ freq = float64(*ch.DownlinkFrequency)
+ start, end = freq-62500, freq+62500
+ frequencies[freq] = formatFrequency(freq)
+ color = chart.GetDefaultColor(3)
+ color.A = 128
+ graph.Series = append(graph.Series, chart.ContinuousSeries{
+ Style: chart.Style{
+ StrokeColor: color,
+ FillColor: color,
+ },
+ XValues: []float64{start, end},
+ YValues: []float64{float64(-1), float64(-1)},
+ })
+ annotations.Annotations = append(annotations.Annotations, chart.Value2{
+ XValue: freq,
+ YValue: -1,
+ Label: formatFrequency(freq),
+ })
+ }
+
+ if ch := plan.LoRaStandardChannel; ch != nil {
+ freq := float64(*ch.Frequency)
+ start, end := freq-125000, freq+125000
+ frequencies[freq] = formatFrequency(freq)
+ color := chart.GetDefaultColor(int(*ch.Radio))
+ color.A = 128
+ graph.Series = append(graph.Series, chart.ContinuousSeries{
+ Style: chart.Style{
+ StrokeColor: color,
+ FillColor: color,
+ },
+ XValues: []float64{start, end},
+ YValues: []float64{float64(2), float64(2)},
+ })
+ annotations.Annotations = append(annotations.Annotations, chart.Value2{
+ XValue: freq,
+ YValue: 2,
+ Label: formatFrequency(freq) + " (Std)",
+ })
+ }
+
+ if ch := plan.FSKChannel; ch != nil {
+ freq := float64(*ch.Frequency)
+ start, end := freq-62500, freq+62500
+ frequencies[freq] = formatFrequency(freq)
+ color := chart.GetDefaultColor(int(*ch.Radio))
+ color.A = 128
+ graph.Series = append(graph.Series, chart.ContinuousSeries{
+ Style: chart.Style{
+ StrokeColor: color,
+ FillColor: color,
+ },
+ XValues: []float64{start, end},
+ YValues: []float64{float64(2), float64(2)},
+ })
+ annotations.Annotations = append(annotations.Annotations, chart.Value2{
+ XValue: freq,
+ YValue: 2,
+ Label: formatFrequency(freq) + " (FSK)",
+ })
+ }
+
+ for i, radio := range plan.Radios {
+ freq := float64(*radio.Frequency)
+ start, end := freq-462500, freq+462500
+ frequencies[start] = formatFrequency(start)
+ frequencies[freq] = formatFrequency(freq)
+ frequencies[end] = formatFrequency(end)
+ color := chart.GetDefaultColor(i)
+ color.A = 128
+ graph.Series = append(graph.Series, chart.ContinuousSeries{
+ Style: chart.Style{
+ StrokeColor: color,
+ StrokeWidth: 10,
+ },
+ XValues: []float64{start, end},
+ YValues: []float64{float64(0), float64(0)},
+ })
+ annotations.Annotations = append(annotations.Annotations, chart.Value2{
+ XValue: freq,
+ YValue: 0,
+ Label: fmt.Sprintf("Radio %d: %s", i, formatFrequency(freq)),
+ })
+ }
+
+ var frequencySlice []float64
+ for frequency := range frequencies {
+ frequencySlice = append(frequencySlice, frequency)
+ }
+ sort.Float64s(frequencySlice)
+
+ graph.XAxis = chart.XAxis{
+ Range: &chart.ContinuousRange{
+ Min: frequencySlice[0],
+ Max: frequencySlice[len(frequencySlice)-1],
+ },
+ TickStyle: chart.Style{
+ TextRotationDegrees: 45.0,
+ },
+ }
+
+ for _, freq := range frequencySlice {
+ graph.XAxis.Ticks = append(graph.XAxis.Ticks, chart.Tick{
+ Value: freq,
+ Label: frequencies[freq],
+ })
+ }
+
+ graph.YAxis = chart.YAxis{
+ Range: &chart.ContinuousRange{
+ Min: -2,
+ Max: 3,
+ },
+ Ticks: []chart.Tick{
+ {Value: -2},
+ {Value: -1, Label: "Downlink"},
+ {Value: 0, Label: "Radio"},
+ {Value: 1, Label: "Uplink"},
+ {Value: 2, Label: "Std/FSK"},
+ {Value: 3},
+ },
+ }
+
+ graph.Series = append(graph.Series, annotations)
+
+ var buf bytes.Buffer
+
+ if err := graph.Render(chart.SVG, &buf); err != nil {
+ return err
+ }
+ if err := os.WriteFile("./docs/images/gateway/"+id+".svg", buf.Bytes(), 0o644); err != nil {
+ return err
+ }
+
+ return nil
+}
diff --git a/internal/model/end-device-frequency-plan.go b/internal/model/end-device-frequency-plan.go
new file mode 100644
index 0000000..07af927
--- /dev/null
+++ b/internal/model/end-device-frequency-plan.go
@@ -0,0 +1,196 @@
+package model
+
+import (
+ "fmt"
+ "os"
+
+ "gopkg.in/yaml.v2"
+)
+
+type FrequencyPlanEndDevice struct {
+ BandID string `yaml:"band-id"`
+ SubBands []SubBand `yaml:"sub-bands"`
+ Channels []Channel `yaml:"channels"`
+ LoRaStandardChannel *FrequencyDataRate `yaml:"lora-special-channel,omitempty"`
+ FSKChannel *FrequencyDataRate `yaml:"fsk-channel,omitempty"`
+ TimeOffAir *TimeOffAir `yaml:"time-off-air,omitempty"`
+ DwellTime *DwellTime `yaml:"dwell-time,omitempty"`
+ ListenBeforeTalk *ListenBeforeTalk `yaml:"listen-before-talk,omitempty"`
+ PingSlot *FrequencyMinMaxDataRate `yaml:"ping-slot,omitempty"`
+ PingSlotDefaultDataRate *string `yaml:"ping-slot-default-data-rate,omitempty"`
+ RX2Channel *FrequencyMinMaxDataRate `yaml:"rx2-channel,omitempty"`
+ RX2DefaultDataRate *string `yaml:"rx2-default-data-rate,omitempty"`
+ MaxEIRP *float32 `yaml:"max-eirp,omitempty"`
+}
+
+func (f FrequencyPlanEndDevice) Parse(file string) (Definition, error) {
+ indexBytes, err := os.ReadFile(file)
+ if err != nil {
+ return FrequencyPlanEndDevice{}, err
+ }
+ err = yaml.Unmarshal(indexBytes, &f)
+ return f, err
+}
+
+func (f FrequencyPlanEndDevice) Validate() error {
+ for i, subBand := range f.SubBands {
+ if err := subBand.Validate(); err != nil {
+ return fmt.Errorf("SubBand %d: %w", i, err)
+ }
+ }
+ for i, channel := range f.Channels {
+ if err := channel.Validate(); err != nil {
+ return fmt.Errorf("Channel %d: %w", i, err)
+ }
+ }
+ if f.LoRaStandardChannel != nil {
+ if err := f.LoRaStandardChannel.Validate(); err != nil {
+ return fmt.Errorf("LoRaStandardChannel: %w", err)
+ }
+ }
+ if f.FSKChannel != nil {
+ if err := f.FSKChannel.Validate(); err != nil {
+ return fmt.Errorf("FSKChannel: %w", err)
+ }
+ }
+ if f.TimeOffAir != nil {
+ if err := f.TimeOffAir.Validate(); err != nil {
+ return fmt.Errorf("TimeOffAir: %w", err)
+ }
+ }
+ if f.DwellTime != nil {
+ if err := f.DwellTime.Validate(); err != nil {
+ return fmt.Errorf("DwellTime: %w", err)
+ }
+ }
+ if f.ListenBeforeTalk != nil {
+ if err := f.ListenBeforeTalk.Validate(); err != nil {
+ return fmt.Errorf("ListenBeforeTalk: %w", err)
+ }
+ }
+ if f.PingSlot != nil {
+ if err := f.PingSlot.Validate(); err != nil {
+ return fmt.Errorf("PingSlot: %w", err)
+ }
+ }
+ if f.PingSlotDefaultDataRate != nil {
+ if err := validateDataRate(*f.PingSlotDefaultDataRate); err != nil {
+ return fmt.Errorf("PingSlotDefaultDataRate: %w", err)
+ }
+ }
+ if f.RX2Channel != nil {
+ if err := f.RX2Channel.Validate(); err != nil {
+ return fmt.Errorf("RX2Channel: %w", err)
+ }
+ }
+ if f.RX2DefaultDataRate != nil {
+ if err := validateDataRate(*f.RX2DefaultDataRate); err != nil {
+ return fmt.Errorf("RX2DefaultDataRate: %w", err)
+ }
+ }
+ return nil
+}
+
+func (f FrequencyPlanEndDevice) Modify(modifier FrequencyPlanEndDeviceModifier) FrequencyPlanEndDevice {
+ modified := f
+ set(modifier.SubBands, &modified.SubBands)
+ set(modifier.Channels, &modified.Channels)
+ set(modifier.LoRaStandardChannel, modified.LoRaStandardChannel)
+ set(modifier.FSKChannel, modified.FSKChannel)
+ set(modifier.TimeOffAir, modified.TimeOffAir)
+ set(modifier.DwellTime, modified.DwellTime)
+ set(modifier.ListenBeforeTalk, modified.ListenBeforeTalk)
+ set(modifier.PingSlot, modified.PingSlot)
+ set(modifier.PingSlotDefaultDataRate, modified.PingSlotDefaultDataRate)
+ set(modifier.RX2Channel, modified.RX2Channel)
+ set(modifier.RX2DefaultDataRate, modified.RX2DefaultDataRate)
+ set(modifier.MaxEIRP, modified.MaxEIRP)
+ return modified
+}
+
+type FrequencyPlanEndDeviceModifier struct {
+ SubBands *[]SubBand `yaml:"sub-bands,omitempty"`
+ Channels *[]Channel `yaml:"channels,omitempty"`
+ LoRaStandardChannel *FrequencyDataRate `yaml:"lora-special-channel,omitempty"`
+ FSKChannel *FrequencyDataRate `yaml:"fsk-channel,omitempty"`
+ TimeOffAir *TimeOffAir `yaml:"time-off-air,omitempty"`
+ DwellTime *DwellTime `yaml:"dwell-time,omitempty"`
+ ListenBeforeTalk *ListenBeforeTalk `yaml:"listen-before-talk,omitempty"`
+ PingSlot *FrequencyMinMaxDataRate `yaml:"ping-slot,omitempty"`
+ PingSlotDefaultDataRate *string `yaml:"ping-slot-default-data-rate,omitempty"`
+ RX2Channel *FrequencyMinMaxDataRate `yaml:"rx2-channel,omitempty"`
+ RX2DefaultDataRate *string `yaml:"rx2-default-data-rate,omitempty"`
+ MaxEIRP *float32 `yaml:"max-eirp,omitempty"`
+}
+
+func (f FrequencyPlanEndDeviceModifier) Parse(file string) (Definition, error) {
+ indexBytes, err := os.ReadFile(file)
+ if err != nil {
+ return FrequencyPlanEndDeviceModifier{}, err
+ }
+ err = yaml.Unmarshal(indexBytes, &f)
+ return f, err
+}
+
+func (f FrequencyPlanEndDeviceModifier) Validate() error {
+ if f.SubBands != nil {
+ for i, subBand := range *f.SubBands {
+ if err := subBand.Validate(); err != nil {
+ return fmt.Errorf("SubBand %d: %w", i, err)
+ }
+ }
+ }
+ if f.Channels != nil {
+ for i, channel := range *f.Channels {
+ if err := channel.Validate(); err != nil {
+ return fmt.Errorf("Channel %d: %w", i, err)
+ }
+ }
+ }
+ if f.LoRaStandardChannel != nil {
+ if err := f.LoRaStandardChannel.Validate(); err != nil {
+ return fmt.Errorf("LoRaStandardChannel: %w", err)
+ }
+ }
+ if f.FSKChannel != nil {
+ if err := f.FSKChannel.Validate(); err != nil {
+ return fmt.Errorf("FSKChannel: %w", err)
+ }
+ }
+ if f.TimeOffAir != nil {
+ if err := f.TimeOffAir.Validate(); err != nil {
+ return fmt.Errorf("TimeOffAir: %w", err)
+ }
+ }
+ if f.DwellTime != nil {
+ if err := f.DwellTime.Validate(); err != nil {
+ return fmt.Errorf("DwellTime: %w", err)
+ }
+ }
+ if f.ListenBeforeTalk != nil {
+ if err := f.ListenBeforeTalk.Validate(); err != nil {
+ return fmt.Errorf("ListenBeforeTalk: %w", err)
+ }
+ }
+ if f.PingSlot != nil {
+ if err := f.PingSlot.Validate(); err != nil {
+ return fmt.Errorf("PingSlot: %w", err)
+ }
+ }
+ if f.PingSlotDefaultDataRate != nil {
+ if err := validateDataRate(*f.PingSlotDefaultDataRate); err != nil {
+ return fmt.Errorf("PingSlotDefaultDataRate: %w", err)
+ }
+ }
+ if f.RX2Channel != nil {
+ if err := f.RX2Channel.Validate(); err != nil {
+ return fmt.Errorf("RX2Channel: %w", err)
+ }
+ }
+ if f.RX2DefaultDataRate != nil {
+ if err := validateDataRate(*f.RX2DefaultDataRate); err != nil {
+ return fmt.Errorf("RX2DefaultDataRate: %w", err)
+ }
+ }
+ return nil
+}
diff --git a/internal/model/frequency-plan-description.go b/internal/model/frequency-plan-description.go
new file mode 100644
index 0000000..805e041
--- /dev/null
+++ b/internal/model/frequency-plan-description.go
@@ -0,0 +1,117 @@
+package model
+
+import (
+ "fmt"
+ "os"
+
+ "github.com/TheThingsNetwork/lorawan-frequency-plans/internal/utils"
+ "gopkg.in/yaml.v2"
+)
+
+type FrequencyPlanDescriptions struct {
+ EndDeviceDescriptions []FrequencyPlanDescription `yaml:"end-device-descriptions"`
+ GatewayDescriptions []FrequencyPlanDescription `yaml:"gateway-descriptions"`
+}
+
+func (f FrequencyPlanDescriptions) Parse(file string) (Definition, error) {
+ indexBytes, err := os.ReadFile(file)
+ if err != nil {
+ return FrequencyPlanDescriptions{}, err
+ }
+ err = yaml.Unmarshal(indexBytes, &f)
+ if err != nil {
+ return nil, err
+ }
+ return f, err
+}
+
+func (f FrequencyPlanDescriptions) Validate() error {
+ for _, description := range f.EndDeviceDescriptions {
+ if err := description.Validate(EndDeviceFrequencyPlan); err != nil {
+ return err
+ }
+ }
+ for _, description := range f.GatewayDescriptions {
+ if err := description.Validate(GatewayFrequencyPlan); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+type FrequencyPlanType string
+
+var (
+ GatewayFrequencyPlan FrequencyPlanType = "gateway"
+ EndDeviceFrequencyPlan FrequencyPlanType = "end-device"
+)
+
+// EndDeviceFrequencyPlanDescription describes an end device frequency plan in the YAML format.
+type FrequencyPlanDescription struct {
+ ID string `yaml:"id"`
+ BandID string `yaml:"band-id"`
+ BaseID *string `yaml:"base-id,omitempty"`
+ Name string `yaml:"name"`
+ Description string `yaml:"description"`
+ BaseFrequency uint16 `yaml:"base-frequency"`
+ CountryCodes []string `yaml:"country-codes"`
+ File *string `yaml:"file,omitempty"`
+ Modifiers *[]string `yaml:"modifiers,omitempty"`
+ Endorsed bool `yaml:"endorsed"`
+}
+
+func (f FrequencyPlanDescription) Validate(source FrequencyPlanType) error {
+ bandIDs := utils.GetBandIDs()
+ if !bandIDs[f.BandID] {
+ return fmt.Errorf("Frequency plan %s: BandID is invalid", f.ID)
+ }
+ if f.BaseID != nil && f.File != nil {
+ return fmt.Errorf("Frequency plan %s: BaseID may only be defined with Modifiers and not files", f.ID)
+ }
+ if f.File != nil && f.Modifiers != nil {
+ return fmt.Errorf("Frequency plan %s: Either define files or modifiers", f.ID)
+ }
+
+ EndDeviceBaseIDs, GatewayBaseIDs, err := utils.GetBaseIDs()
+ if err != nil {
+ return err
+ }
+ switch source {
+ case EndDeviceFrequencyPlan:
+ if f.BaseID != nil && !EndDeviceBaseIDs[*f.BaseID] {
+ return fmt.Errorf("Frequency plan %s: Set BaseID doesn't exist", f.ID)
+ }
+ case GatewayFrequencyPlan:
+ if f.BaseID != nil && !GatewayBaseIDs[*f.BaseID] {
+ return fmt.Errorf("Frequency plan %s: Set BaseID doesn't exist", f.ID)
+ }
+ }
+
+ if f.Modifiers != nil {
+ modifiers, err := utils.GetYamlFileNames(string(source) + "/modifiers")
+ if err != nil {
+ return err
+ }
+ for _, modifier := range *f.Modifiers {
+ if !modifiers[modifier] {
+ return fmt.Errorf("Frequency plan %s: %s modifier %s doesn't exist", source, f.ID, modifier)
+ }
+ }
+ }
+
+ if f.File != nil {
+ files, err := utils.GetYamlFileNames(string(source))
+ if err != nil {
+ return err
+ }
+ if !files[*f.File] {
+ return fmt.Errorf("Frequency plan %s: %s file %s doesn't exist", source, f.ID, *f.File)
+ }
+ }
+
+ return nil
+}
+
+func (f FrequencyPlanDescription) HasModifiers() bool {
+ return f.BaseID != nil && f.Modifiers != nil
+}
diff --git a/internal/model/gateway-frequency-plan.go b/internal/model/gateway-frequency-plan.go
new file mode 100644
index 0000000..f333fa9
--- /dev/null
+++ b/internal/model/gateway-frequency-plan.go
@@ -0,0 +1,149 @@
+package model
+
+import (
+ "fmt"
+ "os"
+
+ "gopkg.in/yaml.v2"
+)
+
+type FrequencyPlanGateway struct {
+ BandID string `yaml:"band-id"`
+ SubBands []SubBand `yaml:"sub-bands"`
+ Channels []Channel `yaml:"channels"`
+ LoRaStandardChannel *FrequencyDataRate `yaml:"lora-standard-channel,omitempty"`
+ FSKChannel *FrequencyDataRate `yaml:"fsk-channel,omitempty"`
+ TimeOffAir *TimeOffAir `yaml:"time-off-air,omitempty"`
+ DwellTime *DwellTime `yaml:"dwell-time,omitempty"`
+ Radios []Radio `yaml:"radios"`
+ ClockSource int `yaml:"clock-source"`
+ MaxEIRP *float32 `yaml:"max-eirp,omitempty"`
+}
+
+func (f FrequencyPlanGateway) Parse(file string) (Definition, error) {
+ indexBytes, err := os.ReadFile(file)
+ if err != nil {
+ return FrequencyPlanGateway{}, err
+ }
+ err = yaml.Unmarshal(indexBytes, &f)
+ return f, err
+}
+
+func (f FrequencyPlanGateway) Validate() error {
+ for i, subBand := range f.SubBands {
+ if err := subBand.Validate(); err != nil {
+ return fmt.Errorf("SubBand %d: %w", i, err)
+ }
+ }
+ for i, channel := range f.Channels {
+ if err := channel.Validate(); err != nil {
+ return fmt.Errorf("Channel %d: %w", i, err)
+ }
+ }
+ if f.LoRaStandardChannel != nil {
+ if err := f.LoRaStandardChannel.Validate(); err != nil {
+ return fmt.Errorf("LoRaStandardChannel: %w", err)
+ }
+ }
+ if f.FSKChannel != nil {
+ if err := f.FSKChannel.Validate(); err != nil {
+ return fmt.Errorf("FSKChannel: %w", err)
+ }
+ }
+ if f.TimeOffAir != nil {
+ if err := f.TimeOffAir.Validate(); err != nil {
+ return fmt.Errorf("TimeOffAir: %w", err)
+ }
+ }
+ if f.DwellTime != nil {
+ if err := f.DwellTime.Validate(); err != nil {
+ return fmt.Errorf("DwellTime: %w", err)
+ }
+ }
+ for i, radio := range f.Radios {
+ if err := radio.Validate(); err != nil {
+ return fmt.Errorf("Radio %d: %w", i, err)
+ }
+ }
+ return nil
+}
+
+func (f FrequencyPlanGateway) Modify(modifier FrequencyPlanGatewayModifier) FrequencyPlanGateway {
+ modified := f
+ set(modifier.SubBands, &modified.SubBands)
+ set(modifier.Channels, &modified.Channels)
+ set(modifier.Radios, &modified.Radios)
+ set(modifier.ClockSource, &modified.ClockSource)
+ set(modifier.LoRaStandardChannel, modified.LoRaStandardChannel)
+ set(modifier.FSKChannel, modified.FSKChannel)
+ set(modifier.TimeOffAir, modified.TimeOffAir)
+ set(modifier.DwellTime, modified.DwellTime)
+ set(modifier.MaxEIRP, modified.MaxEIRP)
+ return modified
+}
+
+type FrequencyPlanGatewayModifier struct {
+ SubBands *[]SubBand `yaml:"sub-bands,omitempty"`
+ Channels *[]Channel `yaml:"channels,omitempty"`
+ LoRaStandardChannel *FrequencyDataRate `yaml:"lora-standard-channel,omitempty"`
+ FSKChannel *FrequencyDataRate `yaml:"fsk-channel,omitempty"`
+ TimeOffAir *TimeOffAir `yaml:"time-off-air,omitempty"`
+ DwellTime *DwellTime `yaml:"dwell-time,omitempty"`
+ Radios *[]Radio `yaml:"radios,omitempty"`
+ ClockSource *int `yaml:"clock-source,omitempty"`
+ MaxEIRP *float32 `yaml:"max-eirp,omitempty"`
+}
+
+func (f FrequencyPlanGatewayModifier) Parse(file string) (Definition, error) {
+ indexBytes, err := os.ReadFile(file)
+ if err != nil {
+ return FrequencyPlanGatewayModifier{}, err
+ }
+ err = yaml.Unmarshal(indexBytes, &f)
+ return f, err
+}
+
+func (f FrequencyPlanGatewayModifier) Validate() error {
+ if f.SubBands != nil {
+ for i, subBand := range *f.SubBands {
+ if err := subBand.Validate(); err != nil {
+ return fmt.Errorf("SubBand %d: %w", i, err)
+ }
+ }
+ }
+ if f.Channels != nil {
+ for i, channel := range *f.Channels {
+ if err := channel.Validate(); err != nil {
+ return fmt.Errorf("Channel %d: %w", i, err)
+ }
+ }
+ }
+ if f.LoRaStandardChannel != nil {
+ if err := f.LoRaStandardChannel.Validate(); err != nil {
+ return fmt.Errorf("LoRaStandardChannel: %w", err)
+ }
+ }
+ if f.FSKChannel != nil {
+ if err := f.FSKChannel.Validate(); err != nil {
+ return fmt.Errorf("FSKChannel: %w", err)
+ }
+ }
+ if f.TimeOffAir != nil {
+ if err := f.TimeOffAir.Validate(); err != nil {
+ return fmt.Errorf("TimeOffAir: %w", err)
+ }
+ }
+ if f.DwellTime != nil {
+ if err := f.DwellTime.Validate(); err != nil {
+ return fmt.Errorf("DwellTime: %w", err)
+ }
+ }
+ if f.Radios != nil {
+ for i, radio := range *f.Radios {
+ if err := radio.Validate(); err != nil {
+ return fmt.Errorf("Radio %d: %w", i, err)
+ }
+ }
+ }
+ return nil
+}
diff --git a/internal/model/model.go b/internal/model/model.go
new file mode 100644
index 0000000..e5ef00b
--- /dev/null
+++ b/internal/model/model.go
@@ -0,0 +1,248 @@
+package model
+
+import (
+ "errors"
+ "fmt"
+ "strconv"
+ "time"
+)
+
+var (
+ dataRates = map[string]bool{
+ "SF7BW125": true,
+ "SF7BW250": true,
+ "SF8BW125": true,
+ "SF8BW500": true,
+ "SF9BW125": true,
+ "SF9BW500": true,
+ "SF10BW125": true,
+ "SF10BW500": true,
+ "SF11BW125": true,
+ "SF11BW500": true,
+ "SF12BW125": true,
+ "SF12BW500": true,
+ "FSK50": true,
+ "M0CW137CR1/3": true,
+ "M0CW137CR2/3": true,
+ "M0CW336CR1/3": true,
+ "M0CW336CR2/3": true,
+ "M0CW1523CR1/3": true,
+ "M0CW1523CR2/3": true,
+ }
+ chipTypes = map[string]bool{
+ "SX1255": true,
+ "SX1257": true,
+ }
+ mandatoryFieldsError = errors.New("all mandatory fields should be set")
+)
+
+type Definition interface {
+ Parse(file string) (Definition, error)
+ Validate() error
+}
+
+type SubBand struct {
+ MinFrequency *int `yaml:"min-frequency,omitempty"`
+ MaxFrequency *int `yaml:"max-frequency,omitempty"`
+ DutyCyle *float32 `yaml:"duty-cycle,omitempty"`
+ MaxEIRP *float32 `yaml:"max-eirp,omitempty"`
+}
+
+func (s SubBand) Validate() error {
+ if s.MinFrequency == nil || s.MaxFrequency == nil {
+ return mandatoryFieldsError
+ }
+ if err := validateFrequency(*s.MinFrequency); err != nil {
+ return err
+ }
+ if err := validateFrequency(*s.MaxFrequency); err != nil {
+ return err
+ }
+ if *s.MinFrequency >= *s.MaxFrequency {
+ return errors.New("min-frequency should not be the same as or bigger than max-frequency")
+ }
+ return nil
+}
+
+type Channel struct {
+ UplinkFrequency *int `yaml:"uplink-frequency"`
+ DownlinkFrequency *int `yaml:"downlink-frequency,omitempty"`
+ MinDataRate *string `yaml:"min-data-rate"`
+ MaxDataRate *string `yaml:"max-data-rate"`
+ Radio *int `yaml:"radio,omitempty"`
+ Default *bool `yaml:"default"`
+}
+
+func (c Channel) Validate() error {
+ if c.UplinkFrequency == nil || c.MinDataRate == nil || c.MaxDataRate == nil {
+ return mandatoryFieldsError
+ }
+ if err := validateFrequency(*c.UplinkFrequency); err != nil {
+ return err
+ }
+ if c.DownlinkFrequency != nil {
+ if err := validateFrequency(*c.DownlinkFrequency); err != nil {
+ return err
+ }
+ }
+ if err := validateDataRate(*c.MinDataRate); err != nil {
+ return err
+ }
+ if err := validateDataRate(*c.MaxDataRate); err != nil {
+ return err
+ }
+ return nil
+}
+
+type FrequencyDataRate struct {
+ Frequency *int `yaml:"frequency,omitempty"`
+ DataRate *string `yaml:"data-rate,omitempty"`
+ Radio *int `yaml:"radio,omitempty"`
+}
+
+func (f FrequencyDataRate) Validate() error {
+ if f.Frequency == nil || f.DataRate == nil {
+ return mandatoryFieldsError
+ }
+ if err := validateFrequency(*f.Frequency); err != nil {
+ return err
+ }
+ if err := validateDataRate(*f.DataRate); err != nil {
+ return err
+ }
+ return nil
+}
+
+type TimeOffAir struct {
+ Fraction *float32 `yaml:"fraction,omitempty"`
+ Duration *time.Duration `yaml:"duration,omitempty"`
+}
+
+func (t TimeOffAir) Validate() error {
+ if t.Fraction == nil || t.Duration == nil {
+ return mandatoryFieldsError
+ }
+ return nil
+}
+
+type DwellTime struct {
+ Uplinks *bool `yaml:"uplinks,omitempty"`
+ Downlinks *bool `yaml:"downlinks,omitempty"`
+ Duration *time.Duration `yaml:"duration,omitempty"`
+}
+
+func (d DwellTime) Validate() error {
+ if d.Uplinks == nil || d.Downlinks == nil || d.Duration == nil {
+ return mandatoryFieldsError
+ }
+ return nil
+}
+
+type ListenBeforeTalk struct {
+ RSSIOffset *int `yaml:"rssi-offset,omitempty"`
+ RSSITarget *int `yaml:"rssi-target,omitempty"`
+ ScanTime *int `yaml:"scan-time,omitempty"`
+}
+
+func (l ListenBeforeTalk) Validate() error {
+ if l.RSSIOffset == nil || l.RSSITarget == nil || l.ScanTime == nil {
+ return mandatoryFieldsError
+ }
+ return nil
+}
+
+type Radio struct {
+ Enable *bool `yaml:"enable,omitempty"`
+ ChipType *string `yaml:"chip-type,omitempty"`
+ Frequency *int `yaml:"frequency,omitempty"`
+ RSSIOffset *int `yaml:"rssi-offset,omitempty"`
+ TX *TX `yaml:"tx,omitempty"`
+}
+
+func (r Radio) Validate() error {
+ if r.Enable == nil || r.ChipType == nil || r.Frequency == nil || r.RSSIOffset == nil {
+ return mandatoryFieldsError
+ }
+ if !chipTypes[*r.ChipType] {
+ return errors.New("Unknown chip type")
+ }
+ if err := validateFrequency(*r.Frequency); err != nil {
+ return err
+ }
+ return nil
+}
+
+type TX struct {
+ MinFrequency *int `yaml:"min-frequency,omitempty"`
+ MaxFrequency *int `yaml:"max-frequency,omitempty"`
+ NotchFrequency *int `yaml:"notch-frequency,omitempty"`
+}
+
+func (tx TX) Validate() error {
+ if tx.MinFrequency == nil || tx.MaxFrequency == nil {
+ return mandatoryFieldsError
+ }
+ if err := validateFrequency(*tx.MinFrequency); err != nil {
+ return err
+ }
+ if err := validateFrequency(*tx.MaxFrequency); err != nil {
+ return err
+ }
+ if err := validateFrequencyRange(*tx.NotchFrequency, 126000, 250000); err != nil {
+ return err
+ }
+ return nil
+}
+
+type FrequencyMinMaxDataRate struct {
+ Frequency *int `yaml:"frequency,omitempty"`
+ MinDataRate *string `yaml:"min-data-rate,omitempty"`
+ MaxDataRate *string `yaml:"max-data-rate,omitempty"`
+}
+
+func (f FrequencyMinMaxDataRate) Validate() error {
+ if f.Frequency == nil || f.MaxDataRate == nil || f.MinDataRate == nil {
+ return mandatoryFieldsError
+ }
+ if err := validateFrequency(*f.Frequency); err != nil {
+ return err
+ }
+ if err := validateDataRate(*f.MinDataRate); err != nil {
+ return err
+ }
+ if err := validateDataRate(*f.MaxDataRate); err != nil {
+ return err
+ }
+ return nil
+}
+
+func validateDataRate(dataRate string) error {
+ if _, err := strconv.Atoi(dataRate); err == nil || dataRates[dataRate] {
+ return nil
+ }
+ return fmt.Errorf("data rate %s is neither a known datarate nor an integer", dataRate)
+}
+
+func validateFrequency(frequency int) error {
+ if frequency < 0 {
+ return errors.New("frequencies can't be negative")
+ }
+ return nil
+}
+
+func validateFrequencyRange(frequency int, min, max int) error {
+ if err := validateFrequency(frequency); err != nil {
+ return err
+ }
+ if frequency < min || frequency > max {
+ return fmt.Errorf("frequency should be between %d and %d Hz", min, max)
+ }
+ return nil
+}
+
+func set[T any](modifier *T, base *T) {
+ if modifier == nil {
+ return
+ }
+ *base = *modifier
+}
diff --git a/internal/schema/frequency-plans-description-schema.json.tmpl b/internal/schema/frequency-plans-description-schema.json.tmpl
new file mode 100644
index 0000000..ba2e366
--- /dev/null
+++ b/internal/schema/frequency-plans-description-schema.json.tmpl
@@ -0,0 +1,678 @@
+{
+ "title": "Frequency Plan",
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "object",
+ "properties": {
+ "end-device-descriptions": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/EndDeviceFrequencyPlan"
+ }
+ },
+ "gateway-descriptions": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/GatewayFrequencyPlan"
+ }
+ }
+ },
+ "required": ["end-device-descriptions", "gateway-descriptions"],
+ "definitions": {
+ "EndDeviceFrequencyPlan": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["id", "band-id", "name", "description", "country-codes", "endorsed"],
+ "oneOf": [
+ {
+ "required": ["file"],
+ "not": {
+ "anyOf": [{ "required": ["base-id"] }, { "required": ["modifiers"] }]
+ }
+ },
+ {
+ "required": ["base-id", "modifiers"],
+ "not": {
+ "anyOf": [{ "required": ["file"] }]
+ }
+ }
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "ID of the frequency plan."
+ },
+ "band-id": {
+ "type": "string",
+ "description": "ID of the LoRaWAN band (needs to match band-id in the definition)",
+ "enum" : [{{.BandIDs}}]
+ },
+ "name": {
+ "type": "string",
+ "description": "Name of the frequency plan. In the format '{Region} {Frequency range} MHz'."
+ },
+ "description": {
+ "type": "string",
+ "description": "Description of the frequency plan."
+ },
+ "base-frequency": {
+ "type": "integer",
+ "description": "Base frequency in MHz for hardware support (433, 470, 868 or 915) [MHz]",
+ "enum": [433, 470, 868, 915, 2450]
+ },
+ "file": {
+ "type": "string",
+ "description": "File of the frequency plan definition.",
+ "enum": [{{.EndDeviceFiles}}]
+ },
+ "base-id": {
+ "type": "string",
+ "description": "ID that this frequency plan extends (refers to id of another frequency plan).",
+ "enum" : [{{.EndDeviceBaseIDs}}]
+ },
+ "modifiers": {
+ "type": "array",
+ "uniqueItems": true,
+ "items": {
+ "type": "string",
+ "enum":[{{.EndDeviceModifiers}}],
+ "description": "Modifier addressed by filename."
+ },
+ "description": "List of files containing the modifiers used to extend the `base-id` end-device."
+ },
+ "endorsed": {
+ "type": "boolean",
+ "description": "This is one of the endorsed frequency plans for this band-id."
+ },
+ "country-codes": {
+ "type": "array",
+ "uniqueItems": true,
+ "description": "Countries that this frequency plan can be used in.",
+ "items": {
+ "type": "string",
+ "enum": [
+ "worldwide",
+ "af",
+ "ax",
+ "al",
+ "dz",
+ "as",
+ "ad",
+ "ao",
+ "ai",
+ "aq",
+ "ag",
+ "ar",
+ "am",
+ "an",
+ "aw",
+ "au",
+ "at",
+ "az",
+ "bh",
+ "bs",
+ "bd",
+ "bb",
+ "by",
+ "be",
+ "bz",
+ "bj",
+ "bm",
+ "bt",
+ "bo",
+ "bq",
+ "ba",
+ "bw",
+ "bv",
+ "br",
+ "io",
+ "bn",
+ "bg",
+ "bf",
+ "bi",
+ "kh",
+ "cm",
+ "ca",
+ "cv",
+ "ky",
+ "cf",
+ "td",
+ "cl",
+ "cn",
+ "cx",
+ "cc",
+ "co",
+ "km",
+ "cg",
+ "cd",
+ "ck",
+ "cr",
+ "ci",
+ "hr",
+ "cu",
+ "cw",
+ "cy",
+ "cz",
+ "dk",
+ "dj",
+ "dm",
+ "do",
+ "ec",
+ "eg",
+ "sv",
+ "gq",
+ "er",
+ "ee",
+ "et",
+ "fk",
+ "fo",
+ "fj",
+ "fi",
+ "fr",
+ "gf",
+ "pf",
+ "tf",
+ "ga",
+ "gm",
+ "ge",
+ "de",
+ "gh",
+ "gi",
+ "gr",
+ "gl",
+ "gd",
+ "gp",
+ "gu",
+ "gt",
+ "gg",
+ "gn",
+ "gw",
+ "gy",
+ "ht",
+ "hm",
+ "va",
+ "hn",
+ "hk",
+ "hu",
+ "is",
+ "in",
+ "id",
+ "ir",
+ "iq",
+ "ie",
+ "im",
+ "il",
+ "it",
+ "jm",
+ "jp",
+ "je",
+ "jo",
+ "kz",
+ "ke",
+ "ki",
+ "kp",
+ "kr",
+ "kw",
+ "kg",
+ "la",
+ "lv",
+ "lb",
+ "ls",
+ "lr",
+ "ly",
+ "li",
+ "lt",
+ "lu",
+ "mo",
+ "mk",
+ "mg",
+ "mw",
+ "my",
+ "mv",
+ "ml",
+ "mt",
+ "mh",
+ "mq",
+ "mr",
+ "mu",
+ "yt",
+ "mx",
+ "fm",
+ "md",
+ "mc",
+ "mn",
+ "me",
+ "ms",
+ "ma",
+ "mz",
+ "mm",
+ "na",
+ "nr",
+ "np",
+ "nl",
+ "nc",
+ "nz",
+ "ni",
+ "ne",
+ "ng",
+ "nu",
+ "nf",
+ "mp",
+ "no",
+ "om",
+ "pk",
+ "pw",
+ "ps",
+ "pa",
+ "pg",
+ "py",
+ "pe",
+ "ph",
+ "pn",
+ "pl",
+ "pt",
+ "pr",
+ "qa",
+ "re",
+ "ro",
+ "ru",
+ "rw",
+ "bl",
+ "sh",
+ "kn",
+ "lc",
+ "mf",
+ "pm",
+ "vc",
+ "ws",
+ "sm",
+ "st",
+ "sa",
+ "sn",
+ "rs",
+ "sc",
+ "sl",
+ "sg",
+ "sx",
+ "sk",
+ "si",
+ "sb",
+ "so",
+ "za",
+ "gs",
+ "ss",
+ "es",
+ "lk",
+ "sd",
+ "sr",
+ "sj",
+ "sz",
+ "se",
+ "ch",
+ "sy",
+ "tw",
+ "tj",
+ "tz",
+ "th",
+ "tl",
+ "tg",
+ "tk",
+ "to",
+ "tt",
+ "tn",
+ "tr",
+ "tm",
+ "tc",
+ "tv",
+ "ug",
+ "ua",
+ "ae",
+ "gb",
+ "us",
+ "um",
+ "uy",
+ "uz",
+ "vu",
+ "ve",
+ "vn",
+ "vg",
+ "vi",
+ "wf",
+ "eh",
+ "ye",
+ "zm",
+ "zw"
+ ]
+ }
+ }
+ }
+ },
+ "GatewayFrequencyPlan": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["id", "band-id", "name", "description", "country-codes", "endorsed"],
+ "oneOf": [
+ {
+ "required": ["file"],
+ "not": {
+ "anyOf": [{ "required": ["base-id"] }, { "required": ["modifiers"] }]
+ }
+ },
+ {
+ "required": ["base-id", "modifiers"],
+ "not": {
+ "anyOf": [{ "required": ["file"] }]
+ }
+ }
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "ID of the frequency plan."
+ },
+ "band-id": {
+ "type": "string",
+ "description": "ID of the LoRaWAN band (needs to match band-id in the definition)",
+ "enum" : [{{.BandIDs}}]
+ },
+ "name": {
+ "type": "string",
+ "description": "Name of the frequency plan. In the format '{Region} {Frequency range} MHz'."
+ },
+ "description": {
+ "type": "string",
+ "description": "Description of the frequency plan."
+ },
+ "base-frequency": {
+ "type": "integer",
+ "description": "Base frequency in MHz for hardware support (433, 470, 868 or 915) [MHz]",
+ "enum": [433, 470, 868, 915, 2450]
+ },
+ "file": {
+ "type": "string",
+ "description": "File of the frequency plan definition.",
+ "enum": [{{.GatewayFiles}}]
+ },
+ "base-id": {
+ "type": "string",
+ "description": "ID that this frequency plan extends (refers to id of another frequency plan).",
+ "enum" : [{{.GatewayBaseIDs}}]
+ },
+ "modifiers": {
+ "type": "array",
+ "uniqueItems": true,
+ "items": {
+ "type": "string",
+ "enum":[{{.GatewayModifiers}}],
+ "description": "Modifier addressed by filename."
+ },
+ "description": "List of files containing the modifiers used to extend the `base-id` end-device."
+ },
+ "endorsed": {
+ "type": "boolean",
+ "description": "This is one of the endorsed frequency plans for this band-id."
+ },
+ "country-codes": {
+ "type": "array",
+ "uniqueItems": true,
+ "description": "Countries that this frequency plan can be used in.",
+ "items": {
+ "type": "string",
+ "enum": [
+ "worldwide",
+ "af",
+ "ax",
+ "al",
+ "dz",
+ "as",
+ "ad",
+ "ao",
+ "ai",
+ "aq",
+ "ag",
+ "ar",
+ "am",
+ "an",
+ "aw",
+ "au",
+ "at",
+ "az",
+ "bh",
+ "bs",
+ "bd",
+ "bb",
+ "by",
+ "be",
+ "bz",
+ "bj",
+ "bm",
+ "bt",
+ "bo",
+ "bq",
+ "ba",
+ "bw",
+ "bv",
+ "br",
+ "io",
+ "bn",
+ "bg",
+ "bf",
+ "bi",
+ "kh",
+ "cm",
+ "ca",
+ "cv",
+ "ky",
+ "cf",
+ "td",
+ "cl",
+ "cn",
+ "cx",
+ "cc",
+ "co",
+ "km",
+ "cg",
+ "cd",
+ "ck",
+ "cr",
+ "ci",
+ "hr",
+ "cu",
+ "cw",
+ "cy",
+ "cz",
+ "dk",
+ "dj",
+ "dm",
+ "do",
+ "ec",
+ "eg",
+ "sv",
+ "gq",
+ "er",
+ "ee",
+ "et",
+ "fk",
+ "fo",
+ "fj",
+ "fi",
+ "fr",
+ "gf",
+ "pf",
+ "tf",
+ "ga",
+ "gm",
+ "ge",
+ "de",
+ "gh",
+ "gi",
+ "gr",
+ "gl",
+ "gd",
+ "gp",
+ "gu",
+ "gt",
+ "gg",
+ "gn",
+ "gw",
+ "gy",
+ "ht",
+ "hm",
+ "va",
+ "hn",
+ "hk",
+ "hu",
+ "is",
+ "in",
+ "id",
+ "ir",
+ "iq",
+ "ie",
+ "im",
+ "il",
+ "it",
+ "jm",
+ "jp",
+ "je",
+ "jo",
+ "kz",
+ "ke",
+ "ki",
+ "kp",
+ "kr",
+ "kw",
+ "kg",
+ "la",
+ "lv",
+ "lb",
+ "ls",
+ "lr",
+ "ly",
+ "li",
+ "lt",
+ "lu",
+ "mo",
+ "mk",
+ "mg",
+ "mw",
+ "my",
+ "mv",
+ "ml",
+ "mt",
+ "mh",
+ "mq",
+ "mr",
+ "mu",
+ "yt",
+ "mx",
+ "fm",
+ "md",
+ "mc",
+ "mn",
+ "me",
+ "ms",
+ "ma",
+ "mz",
+ "mm",
+ "na",
+ "nr",
+ "np",
+ "nl",
+ "nc",
+ "nz",
+ "ni",
+ "ne",
+ "ng",
+ "nu",
+ "nf",
+ "mp",
+ "no",
+ "om",
+ "pk",
+ "pw",
+ "ps",
+ "pa",
+ "pg",
+ "py",
+ "pe",
+ "ph",
+ "pn",
+ "pl",
+ "pt",
+ "pr",
+ "qa",
+ "re",
+ "ro",
+ "ru",
+ "rw",
+ "bl",
+ "sh",
+ "kn",
+ "lc",
+ "mf",
+ "pm",
+ "vc",
+ "ws",
+ "sm",
+ "st",
+ "sa",
+ "sn",
+ "rs",
+ "sc",
+ "sl",
+ "sg",
+ "sx",
+ "sk",
+ "si",
+ "sb",
+ "so",
+ "za",
+ "gs",
+ "ss",
+ "es",
+ "lk",
+ "sd",
+ "sr",
+ "sj",
+ "sz",
+ "se",
+ "ch",
+ "sy",
+ "tw",
+ "tj",
+ "tz",
+ "th",
+ "tl",
+ "tg",
+ "tk",
+ "to",
+ "tt",
+ "tn",
+ "tr",
+ "tm",
+ "tc",
+ "tv",
+ "ug",
+ "ua",
+ "ae",
+ "gb",
+ "us",
+ "um",
+ "uy",
+ "uz",
+ "vu",
+ "ve",
+ "vn",
+ "vg",
+ "vi",
+ "wf",
+ "eh",
+ "ye",
+ "zm",
+ "zw"
+ ]
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/internal/schema/frequency-plans-end-device-modifiers-schema.json.tmpl b/internal/schema/frequency-plans-end-device-modifiers-schema.json.tmpl
new file mode 100644
index 0000000..8d39039
--- /dev/null
+++ b/internal/schema/frequency-plans-end-device-modifiers-schema.json.tmpl
@@ -0,0 +1,204 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "title": "Frequency Plan",
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "sub-bands": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "min-frequency": {
+ "type": "integer",
+ "description": "Minimum frequency [Hz] (inclusive)"
+ },
+ "max-frequency": {
+ "type": "integer",
+ "description": "Maximum frequency [Hz] (inclusive)"
+ },
+ "duty-cycle": {
+ "type": "number",
+ "description": "Duty cycle for this sub-band (optional; default: 1)"
+ },
+ "max-eirp": {
+ "type": "number",
+ "description": "Maximum EIRP for this sub-band (optional; takes precedence over frequency plan's max-eirp)"
+ }
+ },
+ "required": ["min-frequency", "max-frequency"]
+ }
+ },
+ "channels": {
+ "type": "array",
+ "description": "List of uplink channels (zero indexed)",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "uplink-frequency": {
+ "type": "integer",
+ "description": "Uplink frequency [Hz]"
+ },
+ "downlink-frequency": {
+ "type": "integer",
+ "description": "Downlink frequency [Hz] (optional)"
+ },
+ "min-data-rate": {
+ "type": "integer",
+ "description": "Minimum data rate index"
+ },
+ "max-data-rate": {
+ "type": "integer",
+ "description": "Maximum data rate index"
+ },
+ "default": {
+ "type": "boolean",
+ "description": "Channel is defined by RP"
+ }
+ },
+ "required": ["uplink-frequency", "min-data-rate", "max-data-rate"]
+ }
+ },
+ "lora-standard-channel": {
+ "type": "object",
+ "description": "LoRa standard channel (optional)",
+ "additionalProperties": false,
+ "properties": {
+ "frequency": {
+ "type": "integer",
+ "description": "Frequency [Hz]"
+ },
+ "data-rate": {
+ "type": "integer",
+ "description": "Data rate index"
+ }
+ },
+ "required": ["frequency", "data-rate"]
+ },
+ "fsk-channel": {
+ "type": "object",
+ "description": "FSK channel (optional)",
+ "additionalProperties": false,
+ "properties": {
+ "frequency": {
+ "type": "integer",
+ "description": "Frequency [Hz]"
+ },
+ "data-rate": {
+ "type": "integer",
+ "description": "Data rate index"
+ }
+ },
+ "required": ["frequency", "data-rate"]
+ },
+ "time-off-air": {
+ "type": "object",
+ "description": "Time-off-air (optional)",
+ "additionalProperties": false,
+ "properties": {
+ "fraction": {
+ "type": "number",
+ "description": "Minimum fraction of the emission time (optional)"
+ },
+ "duration": {
+ "type": "string",
+ "description": "Minimum duration (optional)"
+ }
+ }
+ },
+ "dwell-time": {
+ "type": "object",
+ "description": "Dwell time (optional)",
+ "additionalProperties": false,
+ "properties": {
+ "uplinks": {
+ "type": "boolean",
+ "description": "Enabled for uplink"
+ },
+ "downlinks": {
+ "type": "boolean",
+ "description": "Enabled for downlink"
+ },
+ "duration": {
+ "type": "string",
+ "description": "Duration"
+ }
+ },
+ "required": ["uplinks", "downlinks", "duration"]
+ },
+ "listen-before-talk": {
+ "type": "object",
+ "description": "Listen-before-talk (optional)",
+ "additionalProperties": false,
+ "properties": {
+ "rssi-offset": {
+ "type": "integer",
+ "description": "RSSI offset [dbm]"
+ },
+ "rssi-target": {
+ "type": "integer",
+ "description": "RSSI target [dbm]"
+ },
+ "scan-time": {
+ "type": "integer",
+ "description": "Scan time [nanoseconds]"
+ }
+ },
+ "required": ["rssi-offset", "rssi-target", "scan-time"]
+ },
+ "ping-slot": {
+ "type": "object",
+ "description": "Class B ping slot settings (optional)",
+ "additionalProperties": false,
+ "properties": {
+ "frequency": {
+ "type": "integer",
+ "description": "Frequency [Hz]"
+ },
+ "min-data-rate": {
+ "type": "integer",
+ "description": "Minimum data rate index"
+ },
+ "max-data-rate": {
+ "type": "integer",
+ "description": "Maximum data rate index"
+ }
+ },
+ "required": ["frequency", "min-data-rate", "max-data-rate"]
+ },
+ "ping-slot-default-data-rate": {
+ "type": "integer",
+ "description": "Default data rate index of class B ping slot (optional)"
+ },
+ "rx2-channel": {
+ "type": "object",
+ "description": "Rx2 channel (optional)",
+ "additionalProperties": false,
+ "properties": {
+ "frequency": {
+ "type": "integer",
+ "description": "Frequency [Hz]"
+ },
+ "min-data-rate": {
+ "type": "integer",
+ "description": "Minimum data rate index"
+ },
+ "max-data-rate": {
+ "type": "integer",
+ "description": "Maximum data rate index"
+ }
+ },
+ "required": ["frequency", "min-data-rate", "max-data-rate"]
+ },
+ "rx2-default-data-rate": {
+ "type": "integer",
+ "description": "Default data rate index of Rx2 (optional)"
+ },
+ "max-eirp": {
+ "type": "number",
+ "description": "Maximum EIRP (optional; used when sub-bands do not have max-eirp, takes precedence over band's default)"
+ }
+ }
+}
diff --git a/internal/schema/frequency-plans-end-device-schema.json.tmpl b/internal/schema/frequency-plans-end-device-schema.json.tmpl
new file mode 100644
index 0000000..807380b
--- /dev/null
+++ b/internal/schema/frequency-plans-end-device-schema.json.tmpl
@@ -0,0 +1,210 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "title": "Frequency Plan",
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "band-id": {
+ "type": "string",
+ "description": "ID of the band (needs to match band-id in the index)",
+ "enum": [{{.BandIDs}}]
+ },
+ "sub-bands": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "min-frequency": {
+ "type": "integer",
+ "description": "Minimum frequency [Hz] (inclusive)"
+ },
+ "max-frequency": {
+ "type": "integer",
+ "description": "Maximum frequency [Hz] (inclusive)"
+ },
+ "duty-cycle": {
+ "type": "number",
+ "description": "Duty cycle for this sub-band (optional; default: 1)"
+ },
+ "max-eirp": {
+ "type": "number",
+ "description": "Maximum EIRP for this sub-band (optional; takes precedence over frequency plan's max-eirp)"
+ }
+ },
+ "required": ["min-frequency", "max-frequency"]
+ }
+ },
+ "channels": {
+ "type": "array",
+ "description": "List of uplink channels (zero indexed)",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "uplink-frequency": {
+ "type": "integer",
+ "description": "Uplink frequency [Hz]"
+ },
+ "downlink-frequency": {
+ "type": "integer",
+ "description": "Downlink frequency [Hz]"
+ },
+ "min-data-rate": {
+ "type": "integer",
+ "description": "Minimum data rate index"
+ },
+ "max-data-rate": {
+ "type": "integer",
+ "description": "Maximum data rate index"
+ },
+ "default": {
+ "type": "boolean",
+ "description": "Channel is defined by RP"
+ }
+ },
+ "required": ["uplink-frequency", "min-data-rate", "max-data-rate"]
+ }
+ },
+ "lora-standard-channel": {
+ "type": "object",
+ "description": "LoRa standard channel (optional)",
+ "additionalProperties": false,
+ "properties": {
+ "frequency": {
+ "type": "integer",
+ "description": "Frequency [Hz]"
+ },
+ "data-rate": {
+ "type": "integer",
+ "description": "Data rate index"
+ }
+ },
+ "required": ["frequency", "data-rate"]
+ },
+ "fsk-channel": {
+ "type": "object",
+ "description": "FSK channel (optional)",
+ "additionalProperties": false,
+ "properties": {
+ "frequency": {
+ "type": "integer",
+ "description": "Frequency [Hz]"
+ },
+ "data-rate": {
+ "type": "integer",
+ "description": "Data rate index"
+ }
+ },
+ "required": ["frequency", "data-rate"]
+ },
+ "time-off-air": {
+ "type": "object",
+ "description": "Time-off-air (optional)",
+ "additionalProperties": false,
+ "properties": {
+ "fraction": {
+ "type": "number",
+ "description": "Minimum fraction of the emission time (optional)"
+ },
+ "duration": {
+ "type": "string",
+ "description": "Minimum duration (optional)"
+ }
+ }
+ },
+ "dwell-time": {
+ "type": "object",
+ "description": "Dwell time (optional)",
+ "additionalProperties": false,
+ "properties": {
+ "uplinks": {
+ "type": "boolean",
+ "description": "Enabled for uplink"
+ },
+ "downlinks": {
+ "type": "boolean",
+ "description": "Enabled for downlink"
+ },
+ "duration": {
+ "type": "string",
+ "description": "Duration"
+ }
+ },
+ "required": ["uplinks", "downlinks", "duration"]
+ },
+ "listen-before-talk": {
+ "type": "object",
+ "description": "Listen-before-talk (optional)",
+ "additionalProperties": false,
+ "properties": {
+ "rssi-offset": {
+ "type": "integer",
+ "description": "RSSI offset [dbm]"
+ },
+ "rssi-target": {
+ "type": "integer",
+ "description": "RSSI target [dbm]"
+ },
+ "scan-time": {
+ "type": "integer",
+ "description": "Scan time [nanoseconds]"
+ }
+ },
+ "required": ["rssi-offset", "rssi-target", "scan-time"]
+ },
+ "ping-slot": {
+ "type": "object",
+ "description": "Class B ping slot settings (optional)",
+ "additionalProperties": false,
+ "properties": {
+ "frequency": {
+ "type": "integer",
+ "description": "Frequency [Hz]"
+ },
+ "min-data-rate": {
+ "type": "integer",
+ "description": "Minimum data rate index"
+ },
+ "max-data-rate": {
+ "type": "integer",
+ "description": "Maximum data rate index"
+ }
+ },
+ "required": ["frequency", "min-data-rate", "max-data-rate"]
+ },
+ "ping-slot-default-data-rate": {
+ "type": "integer",
+ "description": "Default data rate index of class B ping slot (optional)"
+ },
+ "rx2-channel": {
+ "type": "object",
+ "description": "Rx2 channel (optional)",
+ "additionalProperties": false,
+ "properties": {
+ "frequency": {
+ "type": "integer",
+ "description": "Frequency [Hz]"
+ },
+ "min-data-rate": {
+ "type": "integer",
+ "description": "Minimum data rate index"
+ },
+ "max-data-rate": {
+ "type": "integer",
+ "description": "Maximum data rate index"
+ }
+ },
+ "required": ["frequency", "min-data-rate", "max-data-rate"]
+ },
+ "rx2-default-data-rate": {
+ "type": "integer",
+ "description": "Default data rate index of Rx2 (optional)"
+ },
+ "max-eirp": {
+ "type": "number",
+ "description": "Maximum EIRP (optional; used when sub-bands do not have max-eirp, takes precedence over band's default)"
+ }
+ },
+ "required": ["band-id", "sub-bands", "channels"]
+}
diff --git a/internal/schema/frequency-plans-gateway-modifiers-schema.json.tmpl b/internal/schema/frequency-plans-gateway-modifiers-schema.json.tmpl
new file mode 100644
index 0000000..db386a6
--- /dev/null
+++ b/internal/schema/frequency-plans-gateway-modifiers-schema.json.tmpl
@@ -0,0 +1,225 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "title": "Frequency Plan",
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "sub-bands": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "min-frequency": {
+ "type": "integer",
+ "description": "Minimum frequency [Hz] (inclusive)"
+ },
+ "max-frequency": {
+ "type": "integer",
+ "description": "Maximum frequency [Hz] (inclusive)"
+ },
+ "duty-cycle": {
+ "type": "number",
+ "description": "Duty cycle for this sub-band (optional; default: 1)"
+ },
+ "max-eirp": {
+ "type": "number",
+ "description": "Maximum EIRP for this sub-band (optional; takes precedence over frequency plan's max-eirp)"
+ }
+ },
+ "required": ["min-frequency", "max-frequency"]
+ }
+ },
+ "channels": {
+ "type": "array",
+ "description": "List of uplink channels (zero indexed)",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "uplink-frequency": {
+ "type": "integer",
+ "description": "Uplink frequency [Hz]"
+ },
+ "downlink-frequency": {
+ "type": "integer",
+ "description": "Downlink frequency [Hz] (optional)"
+ },
+ "min-data-rate": {
+ "type": "string",
+ "description": "Minimum data rate index",
+ "enum": ["SF7BW125","SF7BW250","SF8BW125","SF8BW500","SF9BW125","SF9BW500","SF10BW125","SF10BW500","SF11BW125","SF11BW500","SF12BW125","SF12BW500"]
+ },
+ "max-data-rate": {
+ "type": "string",
+ "description": "Maximum data rate index",
+ "enum": ["SF7BW125","SF7BW250","SF8BW125","SF8BW500","SF9BW125","SF9BW500","SF10BW125","SF10BW500","SF11BW125","SF11BW500","SF12BW125","SF12BW500"]
+ },
+ "radio": {
+ "type": "integer",
+ "description": "Radio index"
+ },
+ "default": {
+ "type": "boolean",
+ "description": "Channel is defined by RP"
+ }
+ },
+ "required": ["uplink-frequency", "min-data-rate", "max-data-rate", "radio"]
+ }
+ },
+ "lora-standard-channel": {
+ "type": "object",
+ "description": "LoRa standard channel (optional)",
+ "additionalProperties": false,
+ "properties": {
+ "frequency": {
+ "type": "integer",
+ "description": "Frequency [Hz]"
+ },
+ "data-rate": {
+ "type": "string",
+ "description": "Data rate index",
+ "enum": ["SF7BW125","SF7BW250","SF8BW125","SF8BW500","SF9BW125","SF9BW500","SF10BW125","SF10BW500","SF11BW125","SF11BW500","SF12BW125","SF12BW500"]
+ },
+ "radio": {
+ "type": "integer",
+ "description": "Radio index"
+ }
+ },
+ "required": ["frequency", "data-rate", "radio"]
+ },
+ "fsk-channel": {
+ "type": "object",
+ "description": "FSK channel (optional)",
+ "additionalProperties": false,
+ "properties": {
+ "frequency": {
+ "type": "integer",
+ "description": "Frequency [Hz]"
+ },
+ "data-rate": {
+ "type": "string",
+ "description": "Data rate index",
+ "enum": ["FSK50"]
+ },
+ "radio": {
+ "type": "integer",
+ "description": "Radio index"
+ }
+ },
+ "required": ["frequency", "data-rate", "radio"]
+ },
+ "time-off-air": {
+ "type": "object",
+ "description": "Time-off-air (optional)",
+ "additionalProperties": false,
+ "properties": {
+ "fraction": {
+ "type": "number",
+ "description": "Minimum fraction of the emission time (optional)"
+ },
+ "duration": {
+ "type": "string",
+ "description": "Minimum duration (optional)"
+ }
+ }
+ },
+ "dwell-time": {
+ "type": "object",
+ "description": "Dwell time (optional)",
+ "additionalProperties": false,
+ "properties": {
+ "uplinks": {
+ "type": "boolean",
+ "description": "Enabled for uplink"
+ },
+ "downlinks": {
+ "type": "boolean",
+ "description": "Enabled for downlink"
+ },
+ "duration": {
+ "type": "string",
+ "description": "Duration"
+ }
+ },
+ "required": ["uplinks", "downlinks", "duration"]
+ },
+ "listen-before-talk": {
+ "type": "object",
+ "description": "Listen-before-talk (optional)",
+ "additionalProperties": false,
+ "properties": {
+ "rssi-offset": {
+ "type": "integer",
+ "description": "RSSI offset [dbm]"
+ },
+ "rssi-target": {
+ "type": "integer",
+ "description": "RSSI target [dbm]"
+ },
+ "scan-time": {
+ "type": "integer",
+ "description": "Scan time [nanoseconds]"
+ }
+ },
+ "required": ["rssi-offset", "rssi-target", "scan-time"]
+ },
+ "radios": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "enable": {
+ "type": "boolean",
+ "description": "Enable the radio"
+ },
+ "chip-type": {
+ "type": "string",
+ "description": "Chip type",
+ "enum": ["SX1255", "SX1257"]
+ },
+ "frequency": {
+ "type": "integer",
+ "description": "Center frequency [Hz]"
+ },
+ "rssi-offset": {
+ "type": "integer",
+ "description": "RSSI offset [dbm]"
+ },
+ "tx": {
+ "type": "object",
+ "description": "Radio transmission configuration (optional)",
+ "additionalProperties": false,
+ "properties": {
+ "min-frequency": {
+ "type": "integer",
+ "description": "Minimum frequency [Hz]"
+ },
+ "max-frequency": {
+ "type": "integer",
+ "description": "Maximum frequency [Hz]"
+ },
+ "notch-frequency": {
+ "type": "integer",
+ "description": "Notch frequency 126000..250000 [Hz] (optional)",
+ "minimum": 126000,
+ "maximum": 250000
+ }
+ },
+ "required": ["min-frequency", "max-frequency"]
+ }
+ },
+ "required": ["enable", "chip-type", "frequency", "rssi-offset"]
+ }
+ },
+ "clock-source": {
+ "type": "integer",
+ "description": "Gateway clock source"
+ },
+ "max-eirp": {
+ "type": "number",
+ "description": "Maximum EIRP (optional; used when sub-bands do not have max-eirp, takes precedence over band's default)"
+ }
+ }
+}
diff --git a/internal/schema/frequency-plans-gateway-schema.json.tmpl b/internal/schema/frequency-plans-gateway-schema.json.tmpl
new file mode 100644
index 0000000..1faeb9b
--- /dev/null
+++ b/internal/schema/frequency-plans-gateway-schema.json.tmpl
@@ -0,0 +1,231 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "title": "Frequency Plan",
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "band-id": {
+ "type": "string",
+ "description": "ID of the band (needs to match band-id in the index)",
+ "enum": [{{.BandIDs}}]
+ },
+ "sub-bands": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "min-frequency": {
+ "type": "integer",
+ "description": "Minimum frequency [Hz] (inclusive)"
+ },
+ "max-frequency": {
+ "type": "integer",
+ "description": "Maximum frequency [Hz] (inclusive)"
+ },
+ "duty-cycle": {
+ "type": "number",
+ "description": "Duty cycle for this sub-band (optional; default: 1)"
+ },
+ "max-eirp": {
+ "type": "number",
+ "description": "Maximum EIRP for this sub-band (optional; takes precedence over frequency plan's max-eirp)"
+ }
+ },
+ "required": ["min-frequency", "max-frequency"]
+ }
+ },
+ "channels": {
+ "type": "array",
+ "description": "List of uplink channels (zero indexed)",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "uplink-frequency": {
+ "type": "integer",
+ "description": "Uplink frequency [Hz]"
+ },
+ "downlink-frequency": {
+ "type": "integer",
+ "description": "Downlink frequency [Hz]"
+ },
+ "min-data-rate": {
+ "type": "string",
+ "description": "Minimum data rate index",
+ "enum": ["SF7BW125","SF7BW250","SF8BW125","SF8BW500","SF9BW125","SF9BW500","SF10BW125","SF10BW500","SF11BW125","SF11BW500","SF12BW125","SF12BW500"]
+ },
+ "max-data-rate": {
+ "type": "string",
+ "description": "Maximum data rate index",
+ "enum": ["SF7BW125","SF7BW250","SF8BW125","SF8BW500","SF9BW125","SF9BW500","SF10BW125","SF10BW500","SF11BW125","SF11BW500","SF12BW125","SF12BW500"]
+ },
+ "radio": {
+ "type": "integer",
+ "description": "Radio index"
+ },
+ "default": {
+ "type": "boolean",
+ "description": "Channel is defined by RP"
+ }
+ },
+ "required": ["uplink-frequency", "min-data-rate", "max-data-rate", "radio"]
+ }
+ },
+ "lora-standard-channel": {
+ "type": "object",
+ "description": "LoRa standard channel (optional)",
+ "additionalProperties": false,
+ "properties": {
+ "frequency": {
+ "type": "integer",
+ "description": "Frequency [Hz]"
+ },
+ "data-rate": {
+ "type": "string",
+ "description": "Data rate index",
+ "enum": ["SF7BW125","SF7BW250","SF8BW125","SF8BW500","SF9BW125","SF9BW500","SF10BW125","SF10BW500","SF11BW125","SF11BW500","SF12BW125","SF12BW500"]
+ },
+ "radio": {
+ "type": "integer",
+ "description": "Radio index"
+ }
+ },
+ "required": ["frequency", "data-rate", "radio"]
+ },
+ "fsk-channel": {
+ "type": "object",
+ "description": "FSK channel (optional)",
+ "additionalProperties": false,
+ "properties": {
+ "frequency": {
+ "type": "integer",
+ "description": "Frequency [Hz]"
+ },
+ "data-rate": {
+ "type": "string",
+ "description": "Data rate index",
+ "enum": ["FSK50"]
+ },
+ "radio": {
+ "type": "integer",
+ "description": "Radio index"
+ }
+ },
+ "required": ["frequency", "data-rate", "radio"]
+ },
+ "time-off-air": {
+ "type": "object",
+ "description": "Time-off-air (optional)",
+ "additionalProperties": false,
+ "properties": {
+ "fraction": {
+ "type": "number",
+ "description": "Minimum fraction of the emission time (optional)"
+ },
+ "duration": {
+ "type": "string",
+ "description": "Minimum duration (optional)"
+ }
+ }
+ },
+ "dwell-time": {
+ "type": "object",
+ "description": "Dwell time (optional)",
+ "additionalProperties": false,
+ "properties": {
+ "uplinks": {
+ "type": "boolean",
+ "description": "Enabled for uplink"
+ },
+ "downlinks": {
+ "type": "boolean",
+ "description": "Enabled for downlink"
+ },
+ "duration": {
+ "type": "string",
+ "description": "Duration"
+ }
+ },
+ "required": ["uplinks", "downlinks", "duration"]
+ },
+ "listen-before-talk": {
+ "type": "object",
+ "description": "Listen-before-talk (optional)",
+ "additionalProperties": false,
+ "properties": {
+ "rssi-offset": {
+ "type": "integer",
+ "description": "RSSI offset [dbm]"
+ },
+ "rssi-target": {
+ "type": "integer",
+ "description": "RSSI target [dbm]"
+ },
+ "scan-time": {
+ "type": "integer",
+ "description": "Scan time [nanoseconds]"
+ }
+ },
+ "required": ["rssi-offset", "rssi-target", "scan-time"]
+ },
+ "radios": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "enable": {
+ "type": "boolean",
+ "description": "Enable the radio"
+ },
+ "chip-type": {
+ "type": "string",
+ "description": "Chip type",
+ "enum": ["SX1255", "SX1257"]
+ },
+ "frequency": {
+ "type": "integer",
+ "description": "Center frequency [Hz]"
+ },
+ "rssi-offset": {
+ "type": "integer",
+ "description": "RSSI offset [dbm]"
+ },
+ "tx": {
+ "type": "object",
+ "description": "Radio transmission configuration (optional)",
+ "additionalProperties": false,
+ "properties": {
+ "min-frequency": {
+ "type": "integer",
+ "description": "Minimum frequency [Hz]"
+ },
+ "max-frequency": {
+ "type": "integer",
+ "description": "Maximum frequency [Hz]"
+ },
+ "notch-frequency": {
+ "type": "integer",
+ "description": "Notch frequency 126000..250000 [Hz] (optional)",
+ "minimum": 126000,
+ "maximum": 250000
+ }
+ },
+ "required": ["min-frequency", "max-frequency"]
+ }
+ },
+ "required": ["enable", "chip-type", "frequency", "rssi-offset"]
+ }
+ },
+ "clock-source": {
+ "type": "integer",
+ "description": "Gateway clock source"
+ },
+ "max-eirp": {
+ "type": "number",
+ "description": "Maximum EIRP (optional; used when sub-bands do not have max-eirp, takes precedence over band's default)"
+ }
+ },
+ "required": ["band-id", "sub-bands", "channels"]
+}
diff --git a/internal/schema/schema.go b/internal/schema/schema.go
new file mode 100644
index 0000000..8c0e732
--- /dev/null
+++ b/internal/schema/schema.go
@@ -0,0 +1,117 @@
+package schema
+
+import (
+ "bytes"
+ "embed"
+ "os"
+ "sort"
+ "text/template"
+
+ "github.com/TheThingsNetwork/lorawan-frequency-plans/internal/utils"
+)
+
+//go:embed "*.tmpl"
+var fsys embed.FS
+
+var (
+ tmpl = template.Must(template.ParseFS(fsys, "*.tmpl"))
+ schema = Schema{
+ BandIDs: getBandIDs(),
+ PhyVersions: getPhyVersions(),
+ }
+)
+
+type Schema struct {
+ BandIDs string
+ EndDeviceBaseIDs string
+ GatewayBaseIDs string
+ EndDeviceModifiers string
+ GatewayModifiers string
+ EndDeviceFiles string
+ GatewayFiles string
+ PhyVersions string
+}
+
+func Generate() error {
+ var err error
+ schema.EndDeviceBaseIDs, schema.GatewayBaseIDs, err = getBaseIDs()
+ if err != nil {
+ return err
+ }
+ schema.EndDeviceModifiers, err = getYamlFileNames("end-device/modifiers")
+ if err != nil {
+ return err
+ }
+ schema.GatewayModifiers, err = getYamlFileNames("gateway/modifiers")
+ if err != nil {
+ return err
+ }
+ schema.EndDeviceFiles, err = getYamlFileNames("end-device")
+ if err != nil {
+ return err
+ }
+ schema.GatewayFiles, err = getYamlFileNames("gateway")
+ if err != nil {
+ return err
+ }
+
+ executeTemplate("frequency-plans-description-schema.json.tmpl", "schema.json")
+ executeTemplate("frequency-plans-end-device-schema.json.tmpl", "end-device/schema.json")
+ executeTemplate("frequency-plans-end-device-modifiers-schema.json.tmpl", "end-device/modifiers/schema.json")
+ executeTemplate("frequency-plans-gateway-schema.json.tmpl", "gateway/schema.json")
+ executeTemplate("frequency-plans-gateway-modifiers-schema.json.tmpl", "gateway/modifiers/schema.json")
+
+ return nil
+}
+
+func executeTemplate(file, output string) error {
+ var buf bytes.Buffer
+ if err := tmpl.ExecuteTemplate(&buf, file, schema); err != nil {
+ return err
+ }
+ if err := os.WriteFile(output, buf.Bytes(), 0o644); err != nil {
+ return err
+ }
+ return nil
+}
+
+func getBaseIDs() (string, string, error) {
+ endDevice, gateway, err := utils.GetBaseIDs()
+ if err != nil {
+ return "", "", err
+ }
+ return buildCSV(endDevice), buildCSV(gateway), nil
+}
+
+func getBandIDs() string {
+ return buildCSV(utils.GetBandIDs())
+}
+
+func getYamlFileNames(folder string) (string, error) {
+ filenames, err := utils.GetYamlFileNames(folder)
+ if err != nil {
+ return "", err
+ }
+ return buildCSV(filenames), nil
+}
+
+func getPhyVersions() string {
+ return buildCSV(utils.GetPhyVersions())
+}
+
+func buildCSV(inputs map[string]bool) (output string) {
+ keys := []string{}
+ for input := range inputs {
+ keys = append(keys, input)
+ }
+ sort.Strings(keys)
+ first := true
+ for _, key := range keys {
+ if !first {
+ output += ","
+ }
+ output += "\"" + key + "\""
+ first = false
+ }
+ return output
+}
diff --git a/internal/utils/utils.go b/internal/utils/utils.go
new file mode 100644
index 0000000..46d5196
--- /dev/null
+++ b/internal/utils/utils.go
@@ -0,0 +1,77 @@
+package utils
+
+import (
+ "os"
+ "strings"
+
+ "go.thethings.network/lorawan-stack/v3/pkg/band"
+ "go.thethings.network/lorawan-stack/v3/pkg/ttnpb"
+ "gopkg.in/yaml.v2"
+)
+
+// GetBaseIDs returns the base ids for end-devices and gateways
+func GetBaseIDs() (map[string]bool, map[string]bool, error) {
+ plans := struct {
+ EndDevices []struct {
+ ID string `yaml:"id"`
+ } `yaml:"end-device-descriptions"`
+ Gateways []struct {
+ ID string `yaml:"id"`
+ } `yaml:"gateway-descriptions"`
+ }{}
+ indexBytes, err := os.ReadFile("frequency-plans.yml")
+ if err != nil {
+ return nil, nil, err
+ }
+ err = yaml.Unmarshal(indexBytes, &plans)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ EndDeviceBaseIDs := make(map[string]bool)
+ for _, description := range plans.EndDevices {
+ if description.ID == "" {
+ continue
+ }
+ EndDeviceBaseIDs[description.ID] = true
+ }
+ GatewayBaseIDs := make(map[string]bool)
+ for _, description := range plans.EndDevices {
+ if description.ID == "" {
+ continue
+ }
+ GatewayBaseIDs[description.ID] = true
+ }
+ return EndDeviceBaseIDs, GatewayBaseIDs, nil
+}
+
+func GetBandIDs() map[string]bool {
+ bandIDs := make(map[string]bool)
+ for band := range band.All {
+ bandIDs[band] = true
+ }
+ return bandIDs
+}
+
+func GetYamlFileNames(folder string) (map[string]bool, error) {
+ fileNames := make(map[string]bool)
+ files, err := os.ReadDir(folder)
+ if err != nil {
+ return nil, err
+ }
+ for _, file := range files {
+ if !strings.HasSuffix(file.Name(), ".yml") {
+ continue
+ }
+ fileNames[file.Name()] = true
+ }
+ return fileNames, nil
+}
+
+func GetPhyVersions() map[string]bool {
+ phyVersions := make(map[string]bool)
+ for _, phyVersion := range ttnpb.PHYVersion_name {
+ phyVersions[phyVersion] = true
+ }
+ return phyVersions
+}
diff --git a/internal/validate/validate.go b/internal/validate/validate.go
new file mode 100644
index 0000000..08bb5b1
--- /dev/null
+++ b/internal/validate/validate.go
@@ -0,0 +1,57 @@
+package validate
+
+import (
+ "fmt"
+
+ "github.com/TheThingsNetwork/lorawan-frequency-plans/internal/model"
+ "github.com/TheThingsNetwork/lorawan-frequency-plans/internal/utils"
+)
+
+// Validate validates the defined frequency-plans.
+func Validate() error {
+ if err := validateFolder(model.FrequencyPlanEndDevice{}, "end-device"); err != nil {
+ return err
+ }
+ if err := validateFolder(model.FrequencyPlanEndDeviceModifier{}, "end-device/modifiers"); err != nil {
+ return err
+ }
+ if err := validateFolder(model.FrequencyPlanGateway{}, "gateway"); err != nil {
+ return err
+ }
+ if err := validateFolder(model.FrequencyPlanGatewayModifier{}, "gateway/modifiers"); err != nil {
+ return err
+ }
+ if err := validateFile(model.FrequencyPlanDescriptions{}, "frequency-plans.yml"); err != nil {
+ return err
+ }
+ return nil
+}
+
+func validateFile(def model.Definition, file string) error {
+ definition, err := def.Parse(file)
+ if err != nil {
+ return err
+ }
+ return definition.Validate()
+}
+
+func validateFolder(def model.Definition, folder string) error {
+ files, err := utils.GetYamlFileNames(folder)
+ if err != nil {
+ return err
+ }
+ for file := range files {
+ if file == "" {
+ continue
+ }
+ definition, err := def.Parse(folder + "/" + file)
+ if err != nil {
+ return err
+ }
+ err = definition.Validate()
+ if err != nil {
+ return fmt.Errorf("%s/%s: %s", folder, file, err)
+ }
+ }
+ return nil
+}
diff --git a/AS_920_923.yml b/legacy/AS_920_923.yml
similarity index 100%
rename from AS_920_923.yml
rename to legacy/AS_920_923.yml
diff --git a/AS_920_923_TTN_AU.yml b/legacy/AS_920_923_TTN_AU.yml
similarity index 100%
rename from AS_920_923_TTN_AU.yml
rename to legacy/AS_920_923_TTN_AU.yml
diff --git a/AS_920_923_TTN_JP_1.yml b/legacy/AS_920_923_TTN_JP_1.yml
similarity index 100%
rename from AS_920_923_TTN_JP_1.yml
rename to legacy/AS_920_923_TTN_JP_1.yml
diff --git a/AS_920_923_TTN_JP_1_LAND_MOBILE.yml b/legacy/AS_920_923_TTN_JP_1_LAND_MOBILE.yml
similarity index 100%
rename from AS_920_923_TTN_JP_1_LAND_MOBILE.yml
rename to legacy/AS_920_923_TTN_JP_1_LAND_MOBILE.yml
diff --git a/AS_920_923_TTN_JP_2.yml b/legacy/AS_920_923_TTN_JP_2.yml
similarity index 100%
rename from AS_920_923_TTN_JP_2.yml
rename to legacy/AS_920_923_TTN_JP_2.yml
diff --git a/AS_920_923_TTN_JP_3.yml b/legacy/AS_920_923_TTN_JP_3.yml
similarity index 100%
rename from AS_920_923_TTN_JP_3.yml
rename to legacy/AS_920_923_TTN_JP_3.yml
diff --git a/AS_920_923_TTN_JP_3_LAND_MOBILE.yml b/legacy/AS_920_923_TTN_JP_3_LAND_MOBILE.yml
similarity index 100%
rename from AS_920_923_TTN_JP_3_LAND_MOBILE.yml
rename to legacy/AS_920_923_TTN_JP_3_LAND_MOBILE.yml
diff --git a/AS_923.yml b/legacy/AS_923.yml
similarity index 100%
rename from AS_923.yml
rename to legacy/AS_923.yml
diff --git a/AS_923_2.yml b/legacy/AS_923_2.yml
similarity index 100%
rename from AS_923_2.yml
rename to legacy/AS_923_2.yml
diff --git a/AS_923_3.yml b/legacy/AS_923_3.yml
similarity index 100%
rename from AS_923_3.yml
rename to legacy/AS_923_3.yml
diff --git a/AS_923_4.yml b/legacy/AS_923_4.yml
similarity index 100%
rename from AS_923_4.yml
rename to legacy/AS_923_4.yml
diff --git a/AS_923_925.yml b/legacy/AS_923_925.yml
similarity index 100%
rename from AS_923_925.yml
rename to legacy/AS_923_925.yml
diff --git a/AS_923_925_TTN_AU.yml b/legacy/AS_923_925_TTN_AU.yml
similarity index 100%
rename from AS_923_925_TTN_AU.yml
rename to legacy/AS_923_925_TTN_AU.yml
diff --git a/AU_915_928_FSB_1.yml b/legacy/AU_915_928_FSB_1.yml
similarity index 100%
rename from AU_915_928_FSB_1.yml
rename to legacy/AU_915_928_FSB_1.yml
diff --git a/AU_915_928_FSB_2.yml b/legacy/AU_915_928_FSB_2.yml
similarity index 100%
rename from AU_915_928_FSB_2.yml
rename to legacy/AU_915_928_FSB_2.yml
diff --git a/AU_915_928_FSB_3.yml b/legacy/AU_915_928_FSB_3.yml
similarity index 100%
rename from AU_915_928_FSB_3.yml
rename to legacy/AU_915_928_FSB_3.yml
diff --git a/AU_915_928_FSB_4.yml b/legacy/AU_915_928_FSB_4.yml
similarity index 100%
rename from AU_915_928_FSB_4.yml
rename to legacy/AU_915_928_FSB_4.yml
diff --git a/AU_915_928_FSB_5.yml b/legacy/AU_915_928_FSB_5.yml
similarity index 100%
rename from AU_915_928_FSB_5.yml
rename to legacy/AU_915_928_FSB_5.yml
diff --git a/AU_915_928_FSB_6.yml b/legacy/AU_915_928_FSB_6.yml
similarity index 100%
rename from AU_915_928_FSB_6.yml
rename to legacy/AU_915_928_FSB_6.yml
diff --git a/AU_915_928_FSB_7.yml b/legacy/AU_915_928_FSB_7.yml
similarity index 100%
rename from AU_915_928_FSB_7.yml
rename to legacy/AU_915_928_FSB_7.yml
diff --git a/AU_915_928_FSB_8.yml b/legacy/AU_915_928_FSB_8.yml
similarity index 100%
rename from AU_915_928_FSB_8.yml
rename to legacy/AU_915_928_FSB_8.yml
diff --git a/CN_470_510_FSB_1.yml b/legacy/CN_470_510_FSB_1.yml
similarity index 100%
rename from CN_470_510_FSB_1.yml
rename to legacy/CN_470_510_FSB_1.yml
diff --git a/CN_470_510_FSB_11.yml b/legacy/CN_470_510_FSB_11.yml
similarity index 100%
rename from CN_470_510_FSB_11.yml
rename to legacy/CN_470_510_FSB_11.yml
diff --git a/EU_433.yml b/legacy/EU_433.yml
similarity index 100%
rename from EU_433.yml
rename to legacy/EU_433.yml
diff --git a/EU_863_870.yml b/legacy/EU_863_870.yml
similarity index 100%
rename from EU_863_870.yml
rename to legacy/EU_863_870.yml
diff --git a/EU_863_870_ROAMING_DRAFT.yml b/legacy/EU_863_870_ROAMING_DRAFT.yml
similarity index 100%
rename from EU_863_870_ROAMING_DRAFT.yml
rename to legacy/EU_863_870_ROAMING_DRAFT.yml
diff --git a/legacy/EU_863_870_TTN.yml b/legacy/EU_863_870_TTN.yml
new file mode 100644
index 0000000..ca5239f
--- /dev/null
+++ b/legacy/EU_863_870_TTN.yml
@@ -0,0 +1 @@
+rx2-default-data-rate: 3
diff --git a/IN_865_867.yml b/legacy/IN_865_867.yml
similarity index 100%
rename from IN_865_867.yml
rename to legacy/IN_865_867.yml
diff --git a/ISM_2400_3CH_DRAFT2.yml b/legacy/ISM_2400_3CH_DRAFT2.yml
similarity index 100%
rename from ISM_2400_3CH_DRAFT2.yml
rename to legacy/ISM_2400_3CH_DRAFT2.yml
diff --git a/KR_920_923_TTN.yml b/legacy/KR_920_923_TTN.yml
similarity index 100%
rename from KR_920_923_TTN.yml
rename to legacy/KR_920_923_TTN.yml
diff --git a/MA_869_870_DRAFT.yml b/legacy/MA_869_870_DRAFT.yml
similarity index 100%
rename from MA_869_870_DRAFT.yml
rename to legacy/MA_869_870_DRAFT.yml
diff --git a/RU_864_870_TTN.yml b/legacy/RU_864_870_TTN.yml
similarity index 100%
rename from RU_864_870_TTN.yml
rename to legacy/RU_864_870_TTN.yml
diff --git a/US_902_928_FSB_1.yml b/legacy/US_902_928_FSB_1.yml
similarity index 100%
rename from US_902_928_FSB_1.yml
rename to legacy/US_902_928_FSB_1.yml
diff --git a/US_902_928_FSB_2.yml b/legacy/US_902_928_FSB_2.yml
similarity index 100%
rename from US_902_928_FSB_2.yml
rename to legacy/US_902_928_FSB_2.yml
diff --git a/US_902_928_FSB_3.yml b/legacy/US_902_928_FSB_3.yml
similarity index 100%
rename from US_902_928_FSB_3.yml
rename to legacy/US_902_928_FSB_3.yml
diff --git a/US_902_928_FSB_4.yml b/legacy/US_902_928_FSB_4.yml
similarity index 100%
rename from US_902_928_FSB_4.yml
rename to legacy/US_902_928_FSB_4.yml
diff --git a/US_902_928_FSB_5.yml b/legacy/US_902_928_FSB_5.yml
similarity index 100%
rename from US_902_928_FSB_5.yml
rename to legacy/US_902_928_FSB_5.yml
diff --git a/US_902_928_FSB_6.yml b/legacy/US_902_928_FSB_6.yml
similarity index 100%
rename from US_902_928_FSB_6.yml
rename to legacy/US_902_928_FSB_6.yml
diff --git a/US_902_928_FSB_7.yml b/legacy/US_902_928_FSB_7.yml
similarity index 100%
rename from US_902_928_FSB_7.yml
rename to legacy/US_902_928_FSB_7.yml
diff --git a/US_902_928_FSB_8.yml b/legacy/US_902_928_FSB_8.yml
similarity index 100%
rename from US_902_928_FSB_8.yml
rename to legacy/US_902_928_FSB_8.yml
diff --git a/legacy/frequency-plans.yml b/legacy/frequency-plans.yml
new file mode 100644
index 0000000..255fadd
--- /dev/null
+++ b/legacy/frequency-plans.yml
@@ -0,0 +1,649 @@
+- id: EU_863_870
+ band-id: EU_863_870
+ name: Europe 863-870 MHz (SF12 for RX2)
+ description: Default frequency plan for Europe
+ base-frequency: 868
+ country-codes: [al, ad, ao, at, bh, be, ba, bw, bg, cg, hr, cy, cz, dk, ee, sz, fi, fr, gr, hu, is, ie, it, lv, ls, li, lt, lu, mg, mw, mt, mu, md, me, mz, na, nl, mk, ph, pl, pt, ro, ru, sa, rs, sc, sk, si, za, es, se, ch, tz, tr, ae, gb, va, zm, zw]
+ file: EU_863_870.yml
+
+- id: EU_863_870_TTN
+ band-id: EU_863_870
+ name: Europe 863-870 MHz (SF9 for RX2 - recommended)
+ description: TTN Community Network frequency plan for Europe, using SF9 for RX2
+ base-frequency: 868
+ base-id: EU_863_870
+ country-codes: [al, ad, ao, at, bh, be, ba, bw, bg, cg, hr, cy, cz, dk, ee, sz, fi, fr, gr, hu, is, ie, it, lv, ls, li, lt, lu, mg, mw, mt, mu, md, me, mz, na, nl, mk, ph, pl, pt, ro, ru, sa, rs, sc, sk, si, za, es, se, ch, tz, tr, ae, gb, va, zm, zw]
+ file: EU_863_870_TTN.yml
+
+- id: EU_863_870_ROAMING_DRAFT
+ band-id: EU_863_870
+ name: Europe 863-870 MHz, 6 channels for roaming (Draft)
+ description: European 6 channel plan used by major operators to support LoRaWAN Passive Roaming
+ base-frequency: 868
+ base-id: EU_863_870
+ country-codes: [al, ad, ao, at, bh, be, ba, bw, bg, cg, hr, cy, cz, dk, ee, sz, fi, fr, gr, hu, is, ie, it, lv, ls, li, lt, lu, mg, mw, mt, mu, md, me, mz, na, nl, mk, ph, pl, pt, ro, ru, sa, rs, sc, sk, si, za, es, se, ch, tz, tr, ae, gb, va, zm, zw]
+ file: EU_863_870_ROAMING_DRAFT.yml
+
+- id: EU_433
+ band-id: EU_433
+ name: Europe 433 MHz (ITU region 1)
+ description: Default frequency plan for worldwide 433MHz
+ base-frequency: 433
+ file: EU_433.yml
+
+- id: US_902_928_FSB_1
+ band-id: US_902_928
+ name: United States 902-928 MHz, FSB 1
+ description: Default frequency plan for the United States and Canada, using sub-band 1
+ base-frequency: 915
+ country-codes: [ca, cr, ec, gy, mx, pa, pr, us]
+ file: US_902_928_FSB_1.yml
+
+- id: US_902_928_FSB_2
+ band-id: US_902_928
+ name: United States 902-928 MHz, FSB 2 (used by TTN)
+ description: TTN Community Network frequency plan for the United States and Canada, using sub-band 2
+ base-frequency: 915
+ country-codes: [ca, cr, ec, gy, mx, pa, pr, us]
+ file: US_902_928_FSB_2.yml
+
+- id: US_902_928_FSB_3
+ band-id: US_902_928
+ name: United States 902-928 MHz, FSB 3
+ description: Default frequency plan for the United States and Canada, using sub-band 3
+ base-frequency: 915
+ country-codes: [ca, cr, ec, gy, mx, pa, pr, us]
+ file: US_902_928_FSB_3.yml
+
+- id: US_902_928_FSB_4
+ band-id: US_902_928
+ name: United States 902-928 MHz, FSB 4
+ description: Default frequency plan for the United States and Canada, using sub-band 4
+ base-frequency: 915
+ country-codes: [ca, cr, ec, gy, mx, pa, pr, us]
+ file: US_902_928_FSB_4.yml
+
+- id: US_902_928_FSB_5
+ band-id: US_902_928
+ name: United States 902-928 MHz, FSB 5
+ description: Default frequency plan for the United States and Canada, using sub-band 5
+ base-frequency: 915
+ country-codes: [ca, cr, ec, gy, mx, pa, pr, us]
+ file: US_902_928_FSB_5.yml
+
+- id: US_902_928_FSB_6
+ band-id: US_902_928
+ name: United States 902-928 MHz, FSB 6
+ description: Default frequency plan for the United States and Canada, using sub-band 6
+ base-frequency: 915
+ country-codes: [ca, cr, ec, gy, mx, pa, pr, us]
+ file: US_902_928_FSB_6.yml
+
+- id: US_902_928_FSB_7
+ band-id: US_902_928
+ name: United States 902-928 MHz, FSB 7
+ description: Default frequency plan for the United States and Canada, using sub-band 7
+ base-frequency: 915
+ country-codes: [ca, cr, ec, gy, mx, pa, pr, us]
+ file: US_902_928_FSB_7.yml
+
+- id: US_902_928_FSB_8
+ band-id: US_902_928
+ name: United States 902-928 MHz, FSB 8
+ description: Default frequency plan for the United States and Canada, using sub-band 8
+ base-frequency: 915
+ country-codes: [ca, cr, ec, gy, mx, pa, pr, us]
+ file: US_902_928_FSB_8.yml
+
+- id: AU_915_928_FSB_1
+ band-id: AU_915_928
+ name: Australia 915-928 MHz, FSB 1
+ description: Default frequency plan for Australia, using sub-band 1
+ base-frequency: 915
+ country-codes: [ar, au, bo, br, ca, cl, co, do, nz, py, pe, sr, uy]
+ file: AU_915_928_FSB_1.yml
+
+- id: AU_915_928_FSB_2
+ band-id: AU_915_928
+ name: Australia 915-928 MHz, FSB 2 (used by TTN)
+ description: TTN Community Network frequency plan for Australia, using sub-band 2
+ base-frequency: 915
+ country-codes: [ar, au, bo, br, ca, cl, co, do, nz, py, pe, sr, uy]
+ file: AU_915_928_FSB_2.yml
+
+- id: AU_915_928_FSB_3
+ band-id: AU_915_928
+ name: Australia 915-928 MHz, FSB 3
+ description: Default frequency plan for Australia, using sub-band 3
+ base-frequency: 915
+ country-codes: [ar, au, bo, br, ca, cl, co, do, nz, py, pe, sr, uy]
+ file: AU_915_928_FSB_3.yml
+
+- id: AU_915_928_FSB_4
+ band-id: AU_915_928
+ name: Australia 915-928 MHz, FSB 4
+ description: Default frequency plan for Australia, using sub-band 4
+ base-frequency: 915
+ country-codes: [ar, au, bo, br, ca, cl, co, do, nz, py, pe, sr, uy]
+ file: AU_915_928_FSB_4.yml
+
+- id: AU_915_928_FSB_5
+ band-id: AU_915_928
+ name: Australia 915-928 MHz, FSB 5
+ description: Default frequency plan for Australia, using sub-band 5
+ base-frequency: 915
+ country-codes: [ar, au, bo, br, ca, cl, co, do, nz, py, pe, sr, uy]
+ file: AU_915_928_FSB_5.yml
+
+- id: AU_915_928_FSB_6
+ band-id: AU_915_928
+ name: Australia 915-928 MHz, FSB 6
+ description: Frequency plan for Australia, using sub-band 6, which overlaps with Asia 923-925 MHz
+ base-frequency: 915
+ country-codes: [ar, au, bo, br, ca, cl, co, do, nz, py, pe, sr, uy]
+ file: AU_915_928_FSB_6.yml
+
+- id: AU_915_928_FSB_7
+ band-id: AU_915_928
+ name: Australia 915-928 MHz, FSB 7
+ description: Default frequency plan for Australia, using sub-band 7
+ base-frequency: 915
+ country-codes: [ar, au, bo, br, ca, cl, co, do, nz, py, pe, sr, uy]
+ file: AU_915_928_FSB_7.yml
+
+- id: AU_915_928_FSB_8
+ band-id: AU_915_928
+ name: Australia 915-928 MHz, FSB 8
+ description: Default frequency plan for Australia, using sub-band 8
+ base-frequency: 915
+ country-codes: [ar, au, bo, br, ca, cl, co, do, nz, py, pe, sr, uy]
+ file: AU_915_928_FSB_8.yml
+
+- id: CN_470_510_FSB_1
+ band-id: CN_470_510
+ name: China 470-510 MHz, FSB 1
+ description: Default frequency plan for China, using sub-band 1
+ base-frequency: 470
+ country-codes: [cn]
+ file: CN_470_510_FSB_1.yml
+
+- id: CN_470_510_FSB_11
+ band-id: CN_470_510
+ name: China 470-510 MHz, FSB 11 (used by TTN)
+ description: TTN Community Network frequency plan for China, using sub-band 11
+ base-frequency: 470
+ country-codes: [cn]
+ file: CN_470_510_FSB_11.yml
+
+- id: AS_920_923
+ band-id: AS_920_923
+ name: Asia 920-923 MHz
+ description: TTN Community Network frequency plan for Asian countries, using frequencies ≤ 923 MHz
+ base-frequency: 915
+ country-codes: [my, sg]
+ file: AS_920_923.yml
+
+- id: AS_920_923_LBT
+ band-id: AS_920_923
+ name: Asia 920-923 MHz with LBT
+ description: TTN Community Network frequency plan for Asian countries, using frequencies ≤ 923 MHz with listen-before-talk
+ base-frequency: 915
+ base-id: AS_920_923
+ country-codes: [jp, my, sg]
+ file: lbt_80_over_128.yml
+
+- id: AS_920_923_TTN_JP_1
+ name: Japan 920-923 MHz with LBT (channels 31-38)
+ description: Frequency plan for Japan, using continuous frequencies up to 923.4MHz with LBT.
+ base-frequency: 915
+ country-codes: [jp]
+ file: AS_920_923_TTN_JP_1.yml
+
+- id: AS_920_923_TTN_JP_1_LAND_MOBILE
+ name: Japan 920-923 MHz with LBT (channels 31-38), Max EIRP 27 dBm
+ description: |
+ Frequency plan for Japanese land mobile station, using continuous frequencies up to 923.4MHz with MAX EIRP 27 dBm and LBT.
+ (note) A user who installs land mobile station in Japan must apply to Japanese Ministry of Internal Affairs and Communications.
+ base-frequency: 915
+ base-id: AS_920_923_TTN_JP_1
+ country-codes: [jp]
+ file: AS_920_923_TTN_JP_1_LAND_MOBILE.yml
+
+- id: AS_920_923_TTN_JP_2
+ name: Japan 920-923 MHz with LBT (channels 24-27 and 35-38)
+ description: Frequency plan for Japan, using top and bottom frequencies ≤ 923.4 MHz with LBT.
+ base-frequency: 915
+ country-codes: [jp]
+ file: AS_920_923_TTN_JP_2.yml
+
+- id: AS_920_923_TTN_JP_3
+ name: Japan 920-923 MHz with LBT (channels 24-31)
+ description: Frequency plan for Japan (16 channels), using continuous frequencies from 920.6 MHz, expected to be used with AS_920_923_TTN_JP_1.
+ base-frequency: 915
+ country-codes: [jp]
+ file: AS_920_923_TTN_JP_3.yml
+
+- id: AS_920_923_TTN_JP_3_LAND_MOBILE
+ name: Japan 920-923 MHz with LBT (channels 24-31), Max EIRP 27 dBm
+ description: |
+ Frequency plan for Japanese land mobile station (16 channels), using continuous frequencies from 920.6 MHz with MAX EIRP 27 dBm and LBT, expected to be used with AS_920_923_TTN_JP_1.
+ (note) A user who installs land mobile station in Japan must apply to Japanese Ministry of Internal Affairs and Communications.
+ base-frequency: 915
+ base-id: AS_920_923_TTN_JP_3
+ country-codes: [jp]
+ file: AS_920_923_TTN_JP_3_LAND_MOBILE.yml
+
+- id: AS_923
+ band-id: AS_923
+ name: Asia 915-928 MHz (AS923 Group 1) with only default channels
+ description: Compatibility frequency plan for Asian countries with common channels in the 923.0-923.5 MHz sub-band
+ base-frequency: 915
+ file: AS_923.yml
+
+- id: AS_923_2
+ band-id: AS_923_2
+ name: Asia 920-923 MHz (AS923 Group 2) with only default channels
+ description: Compatibility frequency plan for Asian countries with common channels in the 921.4-922.0 MHz sub-band
+ base-frequency: 915
+ file: AS_923_2.yml
+
+- id: AS_923_3
+ band-id: AS_923_3
+ name: Asia 915-921 MHz (AS923 Group 3) with only default channels
+ description: Compatibility frequency plan for Asian countries with common channels in the 916.5-917.0 MHz sub-band
+ base-frequency: 915
+ file: AS_923_3.yml
+
+- id: AS_923_4
+ band-id: AS_923_4
+ name: Asia 917-920 MHz (AS923 Group 4) with only default channels
+ description: Compatibility frequency plan for Asian countries with common channels in the 917.3-917.5 MHz sub-band
+ base-frequency: 915
+ file: AS_923_4.yml
+
+- id: AS_923_NDT
+ band-id: AS_923
+ name: Asia 915-928 MHz (AS923 Group 1) with only default channels and dwell time disabled
+ description: Compatibility frequency plan for Asian countries with common channels in the 923.0-923.5 MHz sub-band and dwell time disabled
+ base-frequency: 915
+ base-id: AS_923
+ file: disable_dwell_time.yml
+
+- id: AS_923_DT
+ band-id: AS_923
+ name: Asia 915-928 MHz (AS923 Group 1) with only default channels and dwell time enabled
+ description: Compatibility frequency plan for Asian countries with common channels in the 923.0-923.5 MHz sub-band and dwell time enabled
+ base-frequency: 915
+ base-id: AS_923
+ file: enable_dwell_time_400ms.yml
+
+- id: AS_923_2_NDT
+ band-id: AS_923_2
+ name: Asia 920-923 MHz (AS923 Group 2) with only default channels and dwell time disabled
+ description: Compatibility frequency plan for Asian countries with common channels in the 921.4-922.0 MHz sub-band and dwell time disabled
+ base-frequency: 915
+ base-id: AS_923_2
+ file: disable_dwell_time.yml
+
+- id: AS_923_2_DT
+ band-id: AS_923_2
+ name: Asia 920-923 MHz (AS923 Group 2) with only default channels and dwell time enabled
+ description: Compatibility frequency plan for Asian countries with common channels in the 921.4-922.0 MHz sub-band and dwell time enabled
+ base-frequency: 915
+ base-id: AS_923_2
+ file: enable_dwell_time_400ms.yml
+
+- id: AS_923_3_NDT
+ band-id: AS_923_3
+ name: Asia 920-923 MHz (AS923 Group 3) with only default channels and dwell time disabled
+ description: Compatibility frequency plan for Asian countries with common channels in the 921.4-922.0 MHz sub-band and dwell time disabled
+ base-frequency: 915
+ base-id: AS_923_3
+ file: disable_dwell_time.yml
+
+- id: AS_923_3_DT
+ band-id: AS_923_3
+ name: Asia 920-923 MHz (AS923 Group 3) with only default channels and dwell time enabled
+ description: Compatibility frequency plan for Asian countries with common channels in the 921.4-922.0 MHz sub-band and dwell time enabled
+ base-frequency: 915
+ base-id: AS_923_3
+ file: enable_dwell_time_400ms.yml
+
+- id: AS_923_4_NDT
+ band-id: AS_923_4
+ name: Asia 920-923 MHz (AS923 Group 4) with only default channels and dwell time disabled
+ description: Compatibility frequency plan for Asian countries with common channels in the 921.4-922.0 MHz sub-band and dwell time disabled
+ base-frequency: 915
+ base-id: AS_923_4
+ file: disable_dwell_time.yml
+
+- id: AS_923_4_DT
+ band-id: AS_923_4
+ name: Asia 920-923 MHz (AS923 Group 4) with only default channels and dwell time enabled
+ description: Compatibility frequency plan for Asian countries with common channels in the 921.4-922.0 MHz sub-band and dwell time enabled
+ base-frequency: 915
+ base-id: AS_923_4
+ file: enable_dwell_time_400ms.yml
+
+- id: AS_923_925
+ band-id: AS_923_925
+ name: Asia 923-925 MHz
+ description: TTN Community Network frequency plan for Asian countries, using frequencies ≥ 923 MHz
+ base-frequency: 915
+ country-codes: [bn, kh, hk, id, la, tw, th, vn]
+ file: AS_923_925.yml
+
+- id: AS_923_925_LBT
+ band-id: AS_923_925
+ name: Asia 923-925 MHz with LBT
+ description: TTN Community Network frequency plan for Asian countries, using frequencies ≥ 923 MHz with listen-before-talk
+ base-frequency: 915
+ base-id: AS_923_925
+ country-codes: [bn, kh, hk, id, la, tw, th, vn]
+ file: lbt_80_over_128.yml
+
+- id: AS_920_923_TTN_AU
+ band-id: AS_923_925
+ name: Asia 920-923 MHz (used by TTN Australia)
+ description: TTN Community Network frequency plan for Asia 920-923 MHz in Australia
+ base-frequency: 915
+ base-id: AS_920_923
+ country-codes: [au]
+ file: AS_920_923_TTN_AU.yml
+
+- id: AS_923_925_TTN_AU
+ band-id: AS_923_925
+ name: Asia 923-925 MHz (used by TTN Australia - secondary channels)
+ description: TTN Community Network frequency plan for Asia 923-925 MHz in Australia. Secondary channels for 16 channel gateways.
+ base-frequency: 915
+ base-id: AS_923_925
+ country-codes: [au]
+ file: AS_923_925_TTN_AU.yml
+
+- id: KR_920_923_TTN
+ band-id: KR_920_923
+ name: South Korea 920-923 MHz
+ description: TTN Community Network frequency plan for South Korea
+ base-frequency: 915
+ country-codes: [kr]
+ file: KR_920_923_TTN.yml
+
+- id: MA_869_870_DRAFT
+ band-id: MA_869_870_DRAFT
+ name: Morocco 869-870 MHz
+ description: Draft frequency plan for Morocco, with 4 channels
+ base-frequency: 868
+ country-codes: [ma]
+ file: MA_869_870_DRAFT.yml
+
+- id: IN_865_867
+ band-id: IN_865_867
+ name: India 865-867 MHz
+ description: Default frequency plan for India
+ base-frequency: 868
+ country-codes: [in]
+ file: IN_865_867.yml
+
+- id: RU_864_870_TTN
+ band-id: RU_864_870
+ name: Russia 864-870 MHz
+ description: TTN Community Network frequency plan for Russia
+ base-frequency: 868
+ country-codes: [ru]
+ file: RU_864_870_TTN.yml
+
+- id: ISM_2400_3CH_DRAFT2
+ band-id: ISM_2400
+ name: LoRa 2.4 GHz with 3 channels (Draft 2)
+ description: Global 3 channel plan for LoRa 2.4 GHz (Draft 2)
+ base-frequency: 2450
+ country-codes:
+ [
+ af,
+ ax,
+ al,
+ dz,
+ as,
+ ad,
+ ao,
+ ai,
+ aq,
+ ag,
+ ar,
+ am,
+ aw,
+ au,
+ at,
+ az,
+ bs,
+ bh,
+ bd,
+ bb,
+ by,
+ be,
+ bz,
+ bj,
+ bm,
+ bt,
+ bo,
+ ba,
+ bw,
+ bv,
+ br,
+ io,
+ bn,
+ bg,
+ bf,
+ bi,
+ kh,
+ cm,
+ ca,
+ cv,
+ ky,
+ cf,
+ td,
+ cl,
+ cn,
+ cx,
+ cc,
+ co,
+ km,
+ cg,
+ cd,
+ ck,
+ cr,
+ ci,
+ hr,
+ cu,
+ cy,
+ cz,
+ dk,
+ dj,
+ dm,
+ do,
+ ec,
+ eg,
+ sv,
+ gq,
+ er,
+ ee,
+ et,
+ fk,
+ fo,
+ fj,
+ fi,
+ fr,
+ gf,
+ pf,
+ tf,
+ ga,
+ gm,
+ ge,
+ de,
+ gh,
+ gi,
+ gr,
+ gl,
+ gd,
+ gp,
+ gu,
+ gt,
+ gg,
+ gn,
+ gw,
+ gy,
+ ht,
+ hm,
+ va,
+ hn,
+ hk,
+ hu,
+ is,
+ in,
+ id,
+ ir,
+ iq,
+ ie,
+ im,
+ il,
+ it,
+ jm,
+ jp,
+ je,
+ jo,
+ kz,
+ ke,
+ ki,
+ kp,
+ kr,
+ kw,
+ kg,
+ la,
+ lv,
+ lb,
+ ls,
+ lr,
+ ly,
+ li,
+ lt,
+ lu,
+ mo,
+ mk,
+ mg,
+ mw,
+ my,
+ mv,
+ ml,
+ mt,
+ mh,
+ mq,
+ mr,
+ mu,
+ yt,
+ mx,
+ fm,
+ md,
+ mc,
+ mn,
+ me,
+ ms,
+ ma,
+ mz,
+ mm,
+ na,
+ nr,
+ np,
+ nl,
+ an,
+ nc,
+ nz,
+ ni,
+ ne,
+ ng,
+ nu,
+ nf,
+ mp,
+ no,
+ om,
+ pk,
+ pw,
+ ps,
+ pa,
+ pg,
+ py,
+ pe,
+ ph,
+ pn,
+ pl,
+ pt,
+ pr,
+ qa,
+ re,
+ ro,
+ ru,
+ rw,
+ bl,
+ sh,
+ kn,
+ lc,
+ mf,
+ pm,
+ vc,
+ ws,
+ sm,
+ st,
+ sa,
+ sn,
+ rs,
+ sc,
+ sl,
+ sg,
+ sk,
+ si,
+ sb,
+ so,
+ za,
+ gs,
+ es,
+ lk,
+ sd,
+ sr,
+ sj,
+ sz,
+ se,
+ ch,
+ sy,
+ tw,
+ tj,
+ tz,
+ th,
+ tl,
+ tg,
+ tk,
+ to,
+ tt,
+ tn,
+ tr,
+ tm,
+ tc,
+ tv,
+ ug,
+ ua,
+ ae,
+ gb,
+ us,
+ um,
+ uy,
+ uz,
+ vu,
+ ve,
+ vn,
+ vg,
+ vi,
+ wf,
+ eh,
+ ye,
+ zm,
+ zw,
+ ]
+ file: ISM_2400_3CH_DRAFT2.yml
diff --git a/schema.json b/schema.json
new file mode 100644
index 0000000..aff0b51
--- /dev/null
+++ b/schema.json
@@ -0,0 +1,678 @@
+{
+ "title": "Frequency Plan",
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "type": "object",
+ "properties": {
+ "end-device-descriptions": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/EndDeviceFrequencyPlan"
+ }
+ },
+ "gateway-descriptions": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/GatewayFrequencyPlan"
+ }
+ }
+ },
+ "required": ["end-device-descriptions", "gateway-descriptions"],
+ "definitions": {
+ "EndDeviceFrequencyPlan": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["id", "band-id", "name", "description", "country-codes", "endorsed"],
+ "oneOf": [
+ {
+ "required": ["file"],
+ "not": {
+ "anyOf": [{ "required": ["base-id"] }, { "required": ["modifiers"] }]
+ }
+ },
+ {
+ "required": ["base-id", "modifiers"],
+ "not": {
+ "anyOf": [{ "required": ["file"] }]
+ }
+ }
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "ID of the frequency plan."
+ },
+ "band-id": {
+ "type": "string",
+ "description": "ID of the LoRaWAN band (needs to match band-id in the definition)",
+ "enum" : ["AS_923","AS_923_2","AS_923_3","AS_923_4","AU_915_928","CN_470_510","CN_470_510_20_A","CN_470_510_20_B","CN_470_510_26_A","CN_470_510_26_B","CN_779_787","EU_433","EU_863_870","IN_865_867","ISM_2400","KR_920_923","MA_869_870_DRAFT","RU_864_870","US_902_928"]
+ },
+ "name": {
+ "type": "string",
+ "description": "Name of the frequency plan. In the format '{Region} {Frequency range} MHz'."
+ },
+ "description": {
+ "type": "string",
+ "description": "Description of the frequency plan."
+ },
+ "base-frequency": {
+ "type": "integer",
+ "description": "Base frequency in MHz for hardware support (433, 470, 868 or 915) [MHz]",
+ "enum": [433, 470, 868, 915, 2450]
+ },
+ "file": {
+ "type": "string",
+ "description": "File of the frequency plan definition.",
+ "enum": ["AS_920_923.yml","EU_433.yml","EU_863_870.yml","EU_863_870_ROAMING_DRAFT.yml","US_902_928_FSB_1.yml"]
+ },
+ "base-id": {
+ "type": "string",
+ "description": "ID that this frequency plan extends (refers to id of another frequency plan).",
+ "enum" : ["AS_920_923","AS_920_923_LBT","EU_433","EU_863_870","EU_863_870_ROAMING_DRAFT","EU_863_870_TTN","US_902_928_FSB_1"]
+ },
+ "modifiers": {
+ "type": "array",
+ "uniqueItems": true,
+ "items": {
+ "type": "string",
+ "enum":["disable_dwell_time.yml","enable_dwell_time_400ms.yml","lbt_80_over_128.yml","rx2_default_data_rata_3.yml"],
+ "description": "Modifier addressed by filename."
+ },
+ "description": "List of files containing the modifiers used to extend the `base-id` end-device."
+ },
+ "endorsed": {
+ "type": "boolean",
+ "description": "This is one of the endorsed frequency plans for this band-id."
+ },
+ "country-codes": {
+ "type": "array",
+ "uniqueItems": true,
+ "description": "Countries that this frequency plan can be used in.",
+ "items": {
+ "type": "string",
+ "enum": [
+ "worldwide",
+ "af",
+ "ax",
+ "al",
+ "dz",
+ "as",
+ "ad",
+ "ao",
+ "ai",
+ "aq",
+ "ag",
+ "ar",
+ "am",
+ "an",
+ "aw",
+ "au",
+ "at",
+ "az",
+ "bh",
+ "bs",
+ "bd",
+ "bb",
+ "by",
+ "be",
+ "bz",
+ "bj",
+ "bm",
+ "bt",
+ "bo",
+ "bq",
+ "ba",
+ "bw",
+ "bv",
+ "br",
+ "io",
+ "bn",
+ "bg",
+ "bf",
+ "bi",
+ "kh",
+ "cm",
+ "ca",
+ "cv",
+ "ky",
+ "cf",
+ "td",
+ "cl",
+ "cn",
+ "cx",
+ "cc",
+ "co",
+ "km",
+ "cg",
+ "cd",
+ "ck",
+ "cr",
+ "ci",
+ "hr",
+ "cu",
+ "cw",
+ "cy",
+ "cz",
+ "dk",
+ "dj",
+ "dm",
+ "do",
+ "ec",
+ "eg",
+ "sv",
+ "gq",
+ "er",
+ "ee",
+ "et",
+ "fk",
+ "fo",
+ "fj",
+ "fi",
+ "fr",
+ "gf",
+ "pf",
+ "tf",
+ "ga",
+ "gm",
+ "ge",
+ "de",
+ "gh",
+ "gi",
+ "gr",
+ "gl",
+ "gd",
+ "gp",
+ "gu",
+ "gt",
+ "gg",
+ "gn",
+ "gw",
+ "gy",
+ "ht",
+ "hm",
+ "va",
+ "hn",
+ "hk",
+ "hu",
+ "is",
+ "in",
+ "id",
+ "ir",
+ "iq",
+ "ie",
+ "im",
+ "il",
+ "it",
+ "jm",
+ "jp",
+ "je",
+ "jo",
+ "kz",
+ "ke",
+ "ki",
+ "kp",
+ "kr",
+ "kw",
+ "kg",
+ "la",
+ "lv",
+ "lb",
+ "ls",
+ "lr",
+ "ly",
+ "li",
+ "lt",
+ "lu",
+ "mo",
+ "mk",
+ "mg",
+ "mw",
+ "my",
+ "mv",
+ "ml",
+ "mt",
+ "mh",
+ "mq",
+ "mr",
+ "mu",
+ "yt",
+ "mx",
+ "fm",
+ "md",
+ "mc",
+ "mn",
+ "me",
+ "ms",
+ "ma",
+ "mz",
+ "mm",
+ "na",
+ "nr",
+ "np",
+ "nl",
+ "nc",
+ "nz",
+ "ni",
+ "ne",
+ "ng",
+ "nu",
+ "nf",
+ "mp",
+ "no",
+ "om",
+ "pk",
+ "pw",
+ "ps",
+ "pa",
+ "pg",
+ "py",
+ "pe",
+ "ph",
+ "pn",
+ "pl",
+ "pt",
+ "pr",
+ "qa",
+ "re",
+ "ro",
+ "ru",
+ "rw",
+ "bl",
+ "sh",
+ "kn",
+ "lc",
+ "mf",
+ "pm",
+ "vc",
+ "ws",
+ "sm",
+ "st",
+ "sa",
+ "sn",
+ "rs",
+ "sc",
+ "sl",
+ "sg",
+ "sx",
+ "sk",
+ "si",
+ "sb",
+ "so",
+ "za",
+ "gs",
+ "ss",
+ "es",
+ "lk",
+ "sd",
+ "sr",
+ "sj",
+ "sz",
+ "se",
+ "ch",
+ "sy",
+ "tw",
+ "tj",
+ "tz",
+ "th",
+ "tl",
+ "tg",
+ "tk",
+ "to",
+ "tt",
+ "tn",
+ "tr",
+ "tm",
+ "tc",
+ "tv",
+ "ug",
+ "ua",
+ "ae",
+ "gb",
+ "us",
+ "um",
+ "uy",
+ "uz",
+ "vu",
+ "ve",
+ "vn",
+ "vg",
+ "vi",
+ "wf",
+ "eh",
+ "ye",
+ "zm",
+ "zw"
+ ]
+ }
+ }
+ }
+ },
+ "GatewayFrequencyPlan": {
+ "type": "object",
+ "additionalProperties": false,
+ "required": ["id", "band-id", "name", "description", "country-codes", "endorsed"],
+ "oneOf": [
+ {
+ "required": ["file"],
+ "not": {
+ "anyOf": [{ "required": ["base-id"] }, { "required": ["modifiers"] }]
+ }
+ },
+ {
+ "required": ["base-id", "modifiers"],
+ "not": {
+ "anyOf": [{ "required": ["file"] }]
+ }
+ }
+ ],
+ "properties": {
+ "id": {
+ "type": "string",
+ "description": "ID of the frequency plan."
+ },
+ "band-id": {
+ "type": "string",
+ "description": "ID of the LoRaWAN band (needs to match band-id in the definition)",
+ "enum" : ["AS_923","AS_923_2","AS_923_3","AS_923_4","AU_915_928","CN_470_510","CN_470_510_20_A","CN_470_510_20_B","CN_470_510_26_A","CN_470_510_26_B","CN_779_787","EU_433","EU_863_870","IN_865_867","ISM_2400","KR_920_923","MA_869_870_DRAFT","RU_864_870","US_902_928"]
+ },
+ "name": {
+ "type": "string",
+ "description": "Name of the frequency plan. In the format '{Region} {Frequency range} MHz'."
+ },
+ "description": {
+ "type": "string",
+ "description": "Description of the frequency plan."
+ },
+ "base-frequency": {
+ "type": "integer",
+ "description": "Base frequency in MHz for hardware support (433, 470, 868 or 915) [MHz]",
+ "enum": [433, 470, 868, 915, 2450]
+ },
+ "file": {
+ "type": "string",
+ "description": "File of the frequency plan definition.",
+ "enum": ["AS_920_923.yml","EU_433.yml","EU_863_870.yml"]
+ },
+ "base-id": {
+ "type": "string",
+ "description": "ID that this frequency plan extends (refers to id of another frequency plan).",
+ "enum" : ["AS_920_923","AS_920_923_LBT","EU_433","EU_863_870","EU_863_870_ROAMING_DRAFT","EU_863_870_TTN","US_902_928_FSB_1"]
+ },
+ "modifiers": {
+ "type": "array",
+ "uniqueItems": true,
+ "items": {
+ "type": "string",
+ "enum":["disable_dwell_time.yml","enable_dwell_time_400ms.yml","lbt_80_over_128.yml"],
+ "description": "Modifier addressed by filename."
+ },
+ "description": "List of files containing the modifiers used to extend the `base-id` end-device."
+ },
+ "endorsed": {
+ "type": "boolean",
+ "description": "This is one of the endorsed frequency plans for this band-id."
+ },
+ "country-codes": {
+ "type": "array",
+ "uniqueItems": true,
+ "description": "Countries that this frequency plan can be used in.",
+ "items": {
+ "type": "string",
+ "enum": [
+ "worldwide",
+ "af",
+ "ax",
+ "al",
+ "dz",
+ "as",
+ "ad",
+ "ao",
+ "ai",
+ "aq",
+ "ag",
+ "ar",
+ "am",
+ "an",
+ "aw",
+ "au",
+ "at",
+ "az",
+ "bh",
+ "bs",
+ "bd",
+ "bb",
+ "by",
+ "be",
+ "bz",
+ "bj",
+ "bm",
+ "bt",
+ "bo",
+ "bq",
+ "ba",
+ "bw",
+ "bv",
+ "br",
+ "io",
+ "bn",
+ "bg",
+ "bf",
+ "bi",
+ "kh",
+ "cm",
+ "ca",
+ "cv",
+ "ky",
+ "cf",
+ "td",
+ "cl",
+ "cn",
+ "cx",
+ "cc",
+ "co",
+ "km",
+ "cg",
+ "cd",
+ "ck",
+ "cr",
+ "ci",
+ "hr",
+ "cu",
+ "cw",
+ "cy",
+ "cz",
+ "dk",
+ "dj",
+ "dm",
+ "do",
+ "ec",
+ "eg",
+ "sv",
+ "gq",
+ "er",
+ "ee",
+ "et",
+ "fk",
+ "fo",
+ "fj",
+ "fi",
+ "fr",
+ "gf",
+ "pf",
+ "tf",
+ "ga",
+ "gm",
+ "ge",
+ "de",
+ "gh",
+ "gi",
+ "gr",
+ "gl",
+ "gd",
+ "gp",
+ "gu",
+ "gt",
+ "gg",
+ "gn",
+ "gw",
+ "gy",
+ "ht",
+ "hm",
+ "va",
+ "hn",
+ "hk",
+ "hu",
+ "is",
+ "in",
+ "id",
+ "ir",
+ "iq",
+ "ie",
+ "im",
+ "il",
+ "it",
+ "jm",
+ "jp",
+ "je",
+ "jo",
+ "kz",
+ "ke",
+ "ki",
+ "kp",
+ "kr",
+ "kw",
+ "kg",
+ "la",
+ "lv",
+ "lb",
+ "ls",
+ "lr",
+ "ly",
+ "li",
+ "lt",
+ "lu",
+ "mo",
+ "mk",
+ "mg",
+ "mw",
+ "my",
+ "mv",
+ "ml",
+ "mt",
+ "mh",
+ "mq",
+ "mr",
+ "mu",
+ "yt",
+ "mx",
+ "fm",
+ "md",
+ "mc",
+ "mn",
+ "me",
+ "ms",
+ "ma",
+ "mz",
+ "mm",
+ "na",
+ "nr",
+ "np",
+ "nl",
+ "nc",
+ "nz",
+ "ni",
+ "ne",
+ "ng",
+ "nu",
+ "nf",
+ "mp",
+ "no",
+ "om",
+ "pk",
+ "pw",
+ "ps",
+ "pa",
+ "pg",
+ "py",
+ "pe",
+ "ph",
+ "pn",
+ "pl",
+ "pt",
+ "pr",
+ "qa",
+ "re",
+ "ro",
+ "ru",
+ "rw",
+ "bl",
+ "sh",
+ "kn",
+ "lc",
+ "mf",
+ "pm",
+ "vc",
+ "ws",
+ "sm",
+ "st",
+ "sa",
+ "sn",
+ "rs",
+ "sc",
+ "sl",
+ "sg",
+ "sx",
+ "sk",
+ "si",
+ "sb",
+ "so",
+ "za",
+ "gs",
+ "ss",
+ "es",
+ "lk",
+ "sd",
+ "sr",
+ "sj",
+ "sz",
+ "se",
+ "ch",
+ "sy",
+ "tw",
+ "tj",
+ "tz",
+ "th",
+ "tl",
+ "tg",
+ "tk",
+ "to",
+ "tt",
+ "tn",
+ "tr",
+ "tm",
+ "tc",
+ "tv",
+ "ug",
+ "ua",
+ "ae",
+ "gb",
+ "us",
+ "um",
+ "uy",
+ "uz",
+ "vu",
+ "ve",
+ "vn",
+ "vg",
+ "vi",
+ "wf",
+ "eh",
+ "ye",
+ "zm",
+ "zw"
+ ]
+ }
+ }
+ }
+ }
+ }
+}