diff --git a/docs/frequency-plans.md b/docs/frequency-plans.md new file mode 100644 index 0000000..754bd33 --- /dev/null +++ b/docs/frequency-plans.md @@ -0,0 +1,34 @@ +# LoRaWAN Frequency Plans for The Things Stack + + +## [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](../gateway/EU_863_870.yml): Europe 863-870 MHz + + +>> Default frequency plan for Europe + +![EU_863_870](images/gateway/EU_863_870.svg) + + 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 @@ +\n867.1867.3867.5867.7867.9868.1868.3868.5868.8DownlinkRadioUplinkStd/FSK0.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.00868.1868.1868.3868.3868.5868.5867.1867.1867.3867.3867.5867.5867.7867.7867.9867.9868.8 (FSK)EU_863_870 \ 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 @@ +\n867.1867.3867.5867.7867.9868.1868.3868.5868.8DownlinkRadioUplinkStd/FSK0.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.00868.1868.1868.3868.3868.5868.5867.1867.1867.3867.3867.5867.5867.7867.7867.9867.9868.8 (FSK)EU_863_870_TTN \ 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 @@ +\n867.038867.1867.3867.5867.7867.9867.962868.038868.1868.3868.5868.8868.962DownlinkRadioUplinkStd/FSK0.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.000.00868.1868.1868.3868.3868.5868.5867.1867.1867.3867.3867.5867.5867.7867.7867.9867.9868.3 (Std)868.8 (FSK)Radio 0: 867.5Radio 1: 868.5EU_863_870 \ No newline at end of file diff --git a/internal/docs/docs.go b/internal/docs/docs.go index 6d7e52b..fee2a18 100644 --- a/internal/docs/docs.go +++ b/internal/docs/docs.go @@ -3,16 +3,12 @@ package docs import ( "bytes" "embed" + "errors" "fmt" - "log" "os" - "sort" "strings" "text/template" - "github.com/wcharczuk/go-chart/v2" - "go.thethings.network/lorawan-stack/v3/pkg/frequencyplans" - "github.com/TheThingsNetwork/lorawan-frequency-plans/internal/model" ) @@ -23,35 +19,22 @@ var tmpl = template.Must(template.ParseFS(fsys, "*.tmpl")) // Generate generates the documentation for the frequency-plans. func Generate(sourceFile, destinationFolder string) error { - plans := model.FrequencyPlanDescriptions{} - output, err := plans.Parse(sourceFile) + output, err := model.FrequencyPlanDescriptions{}.Parse(sourceFile) if err != nil { return err } + descriptions := output.(model.FrequencyPlanDescriptions) - for _, plan := range output.(model.FrequencyPlanDescriptions).EndDeviceDescriptions { - // TODO: Support extensions - if plan.BaseID != nil { - log.Printf("Skipping %s: extending a base plan not supported yet", plan.ID) - continue - } - if err := renderEndDevice(plan.ID, *plan.File); err != nil { - return err - } + if err := renderPlans("gateway", descriptions.GatewayDescriptions, model.FrequencyPlanGateway{}); err != nil { + return err } - for _, plan := range output.(model.FrequencyPlanDescriptions).GatewayDescriptions { - // TODO: Support extensions - if plan.BaseID != nil { - log.Printf("Skipping %s: extending a base plan not supported yet", plan.ID) - continue - } - if err := renderEndDevice(plan.ID, *plan.File); 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", plans); err != nil { + 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 { @@ -64,195 +47,55 @@ func formatFrequency(f float64) string { return strings.TrimRight(fmt.Sprintf("%.3f", f/1_000_000), "0.") } -func renderEndDevice(id, file string) error { - plan := model.FrequencyPlanEndDevice{} - _, err := plan.Parse("./end-device/" + file) - if err != nil { - return err - } - - // return render(id, "./docs/images/end-device/", plan) - return nil -} - -func renderGateway(id, file string) error { - plan := model.FrequencyPlanGateway{} - _, err := plan.Parse("./gateway/" + file) - if err != nil { - return err +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 render(id, "./docs/images/gateway/", plan) return nil } -func render(id string, folder string, plan frequencyplans.FrequencyPlan) error { - frequencies := make(map[float64]string) - - graph := chart.Chart{ - Title: id, - Width: 1920, - Height: 1080, - DPI: 150, - } - - annotations := chart.AnnotationSeries{} - - for _, ch := range plan.UplinkChannels { - 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(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 _, ch := range plan.DownlinkChannels { - freq := float64(ch.Frequency) - 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), - }) - } - - 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, - }, +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") } - - 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(folder+id+".svg", buf.Bytes(), 0o644); err != nil { - return err - } - - return nil } diff --git a/internal/docs/end-device.go b/internal/docs/end-device.go new file mode 100644 index 0000000..393b074 --- /dev/null +++ b/internal/docs/end-device.go @@ -0,0 +1,156 @@ +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), + }) + + 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 index a422d33..9908446 100644 --- a/internal/docs/frequency-plans.md.tmpl +++ b/internal/docs/frequency-plans.md.tmpl @@ -1,11 +1,32 @@ # LoRaWAN Frequency Plans for The Things Stack -{{- range . }} +{{- 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/{{ .ID }}.yml.svg) +![{{ .ID }}](images/end-device/{{ .ID }}.svg) + + +{{ end }} + +{{- 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 index 6e00336..3f98366 100644 --- a/internal/model/end-device-frequency-plan.go +++ b/internal/model/end-device-frequency-plan.go @@ -93,42 +93,18 @@ func (f FrequencyPlanEndDevice) Validate() error { func (f FrequencyPlanEndDevice) Modify(modifier FrequencyPlanEndDeviceModifier) FrequencyPlanEndDevice { modified := f - if modifier.SubBands != nil { - modified.SubBands = *modifier.SubBands - } - if modifier.Channels != nil { - modified.Channels = *modifier.Channels - } - if modifier.LoRaStandardChannel != nil { - *modified.LoRaStandardChannel = *modifier.LoRaStandardChannel - } - if modifier.FSKChannel != nil { - *modified.FSKChannel = *modifier.FSKChannel - } - if modifier.TimeOffAir != nil { - *modified.TimeOffAir = *modifier.TimeOffAir - } - if modifier.DwellTime != nil { - *modified.DwellTime = *modifier.DwellTime - } - if modifier.ListenBeforeTalk != nil { - *modified.ListenBeforeTalk = *modifier.ListenBeforeTalk - } - if modifier.PingSlot != nil { - *modified.PingSlot = *modifier.PingSlot - } - if modifier.PingSlotDefaultDataRate != nil { - *modified.PingSlotDefaultDataRate = *modifier.PingSlotDefaultDataRate - } - if modifier.RX2Channel != nil { - *modified.RX2Channel = *modifier.RX2Channel - } - if modifier.RX2DefaultDataRate != nil { - *modified.RX2DefaultDataRate = *modifier.RX2DefaultDataRate - } - if modifier.MaxEIRP != nil { - *modified.MaxEIRP = *modifier.MaxEIRP - } + set(modifier.SubBands, modified.SubBands) + set(modifier.Channels, modified.Channels) + setPointer(modifier.LoRaStandardChannel, modified.LoRaStandardChannel) + setPointer(modifier.FSKChannel, modified.FSKChannel) + setPointer(modifier.TimeOffAir, modified.TimeOffAir) + setPointer(modifier.DwellTime, modified.DwellTime) + setPointer(modifier.ListenBeforeTalk, modified.ListenBeforeTalk) + setPointer(modifier.PingSlot, modified.PingSlot) + setPointer(modifier.PingSlotDefaultDataRate, modified.PingSlotDefaultDataRate) + setPointer(modifier.RX2Channel, modified.RX2Channel) + setPointer(modifier.RX2DefaultDataRate, modified.RX2DefaultDataRate) + setPointer(modifier.MaxEIRP, modified.MaxEIRP) return modified } diff --git a/internal/model/frequency-plan-description.go b/internal/model/frequency-plan-description.go index fc43bea..805e041 100644 --- a/internal/model/frequency-plan-description.go +++ b/internal/model/frequency-plan-description.go @@ -111,3 +111,7 @@ func (f FrequencyPlanDescription) Validate(source FrequencyPlanType) error { 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 index d0f2fea..a5ff6cf 100644 --- a/internal/model/gateway-frequency-plan.go +++ b/internal/model/gateway-frequency-plan.go @@ -70,33 +70,15 @@ func (f FrequencyPlanGateway) Validate() error { func (f FrequencyPlanGateway) Modify(modifier FrequencyPlanGatewayModifier) FrequencyPlanGateway { modified := f - if modifier.SubBands != nil { - modified.SubBands = *modifier.SubBands - } - if modifier.Channels != nil { - modified.Channels = *modifier.Channels - } - if modifier.LoRaStandardChannel != nil { - *modified.LoRaStandardChannel = *modifier.LoRaStandardChannel - } - if modifier.FSKChannel != nil { - *modified.FSKChannel = *modifier.FSKChannel - } - if modifier.TimeOffAir != nil { - *modified.TimeOffAir = *modifier.TimeOffAir - } - if modifier.DwellTime != nil { - *modified.DwellTime = *modifier.DwellTime - } - if modifier.Radios != nil { - modified.Radios = *modifier.Radios - } - if modifier.ClockSource != nil { - modified.ClockSource = *modifier.ClockSource - } - if modifier.MaxEIRP != nil { - *modified.MaxEIRP = *modifier.MaxEIRP - } + set(modifier.SubBands, modified.SubBands) + set(modifier.Channels, modified.Channels) + set(modifier.Radios, modified.Radios) + set(modifier.ClockSource, modified.ClockSource) + setPointer(modifier.LoRaStandardChannel, modified.LoRaStandardChannel) + setPointer(modifier.FSKChannel, modified.FSKChannel) + setPointer(modifier.TimeOffAir, modified.TimeOffAir) + setPointer(modifier.DwellTime, modified.DwellTime) + setPointer(modifier.MaxEIRP, modified.MaxEIRP) return modified } diff --git a/internal/model/model.go b/internal/model/model.go index 49eb089..0546380 100644 --- a/internal/model/model.go +++ b/internal/model/model.go @@ -239,3 +239,18 @@ func validateFrequencyRange(frequency int, min, max int) error { } return nil } + +func set[T any](modifier *T, base T) { + if modifier != nil { + base = *modifier + } +} + +func setPointer[T any](modifier *T, base *T) { + if modifier != nil { + if base == nil { + base = new(T) + } + *base = *modifier + } +}