Skip to content

Commit 6283d81

Browse files
authored
Merge pull request #112 from TheThingsNetwork/chirpstack-v4
Support ChirpStack v4
2 parents e1c046f + d4f338a commit 6283d81

File tree

11 files changed

+307
-224
lines changed

11 files changed

+307
-224
lines changed

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,24 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
88

99
### Added
1010

11+
- Support for ChirpStack v4.
12+
1113
### Changed
1214

1315
### Deprecated
1416

1517
### Removed
1618

19+
- Support for ChirpStack v3. Use versions `0.11.x` for ChirpStack v3.
20+
21+
### Fixed
22+
23+
## [v0.11.2] (2024-03-04)
24+
1725
### Fixed
1826

27+
- Exporting devices from ChirpStack v3.
28+
1929
## [v0.11.1] (2024-01-20)
2030

2131
### Fixed

README.md

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ Binaries are available on [GitHub](https://github.com/TheThingsNetwork/lorawan-s
1111
## Support
1212

1313
- [x] The Things Network Stack V2
14-
- [x] [ChirpStack Network Server](https://www.chirpstack.io/)
14+
- [x] [ChirpStack Network Server v4](https://www.chirpstack.io/)
15+
- [x] [ChirpStack Network Server v3](https://www.chirpstack.io/docs/v3-documentation.html) (only versions `v0.11.x`).
1516
- [x] [The Things Stack](https://www.github.com/TheThingsNetwork/lorawan-stack/)
1617
- [x] [Firefly](https://fireflyiot.com/)
1718
- [ ] [LORIOT Network Server](https://www.loriot.io/)
@@ -106,7 +107,9 @@ $ ttn-lw-migrate ttnv2 application 'my-app-id' --dry-run --verbose > devices.jso
106107
$ ttn-lw-migrate ttnv2 application 'my-app-id' > devices.json
107108
```
108109

109-
## ChirpStack
110+
## ChirpStack v3
111+
112+
> Note: ChirpStack v3 support is removed from versions `v0.12.0` onwards. Use `v0.11.x` for ChirpStack v3.
110113
111114
### Configuration
112115

@@ -176,6 +179,81 @@ And export with:
176179
$ ttn-lw-migrate chirpstack application < application_names.txt > devices.json
177180
```
178181

182+
## ChirpStack v4
183+
184+
> Minimum supported version: `v0.12.0`
185+
186+
### Configuration
187+
188+
Configure with environment variables, or command-line arguments. See `--help` for more details:
189+
190+
```bash
191+
$ export CHIRPSTACK_API_URL="localhost:8080" # ChirpStack Application Server URL
192+
$ export CHIRPSTACK_API_KEY="eyJ0eX........" # Generate from ChirpStack GUI
193+
$ export JOIN_EUI="0101010102020203" # JoinEUI for exported devices
194+
$ export FREQUENCY_PLAN_ID="EU_863_870" # Frequency Plan for exported devices
195+
$ export CHIRPSTACK_EXPORT_SESSION="true" # Set to true for session migration.
196+
```
197+
198+
See [Frequency Plans](https://thethingsstack.io/reference/frequency-plans/) for the list of frequency plans available on The Things Stack. For example, to use `United States 902-928 MHz, FSB 1`, you need to specify the `US_902_928_FSB_1` frequency plan ID.
199+
200+
> _NOTE_: `JoinEUI` and `FrequencyPlanID` are required because ChirpStack does not store these fields.
201+
202+
### Notes
203+
204+
- ABP devices without an active session are successfully exported from ChirpStack, but cannot be imported into The Things Stack.
205+
- MaxEIRP may not be always set properly.
206+
- ChirpStack payload formatters also accept a `variables` parameter. This will always be `null` on The Things Stack.
207+
- ChirpStack v4 uses UUIDs as application ID. The migration tool uses the appends the last index of the UUID to application ID.
208+
- Ex: If the ChirpStack v4 application ID is `59459ffa-bfd3-4ef3-9cee-e1ca219397f2`, the tool generates `chirpstack-e1ca219397f2` as the application ID.
209+
210+
### Export Devices
211+
212+
To export a single device using its DevEUI (e.g. `0102030405060708`):
213+
214+
```
215+
$ ttn-lw-migrate chirpstack device '0102030405060708' > devices.json
216+
```
217+
218+
In order to export a large number of devices, create a file named `device_euis.txt` with one DevEUI per line:
219+
220+
```
221+
0102030405060701
222+
0102030405060702
223+
0102030405060703
224+
0102030405060704
225+
0102030405060705
226+
0102030405060706
227+
```
228+
229+
And then export with:
230+
231+
```bash
232+
$ ttn-lw-migrate chirpstack device < device_euis.txt > devices.json
233+
```
234+
235+
### Export Applications
236+
237+
Similarly, to export all devices of application `chirpstack-app-1`:
238+
239+
```bash
240+
$ ttn-lw-migrate chirpstack application 'chirpstack-app-1' > devices.json
241+
```
242+
243+
In order to export multiple applications, create a file named `application_names.txt` with one Application name per line:
244+
245+
```
246+
chirpstack-app-1
247+
chirpstack-app-2
248+
chirpstack-app-3
249+
```
250+
251+
And export with:
252+
253+
```bash
254+
$ ttn-lw-migrate chirpstack application < application_names.txt > devices.json
255+
```
256+
179257
## The Things Stack
180258

181259
### Configuration

cmd/chirpstack/chirpstack.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ const sourceName = "chirpstack"
2323

2424
// ChirpStackCmd represents the chirpstack source.
2525
var ChirpStackCmd = commands.Source(sourceName,
26-
"Export devices from ChirpStack V3",
26+
"Export devices from ChirpStack v4",
2727
commands.WithDevicesOptions(
2828
commands.WithShort("Export devices by DevEUI"),
2929
),

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ require (
1111
github.com/TheThingsNetwork/ttn/core/types v0.0.0-20190516112328-fcd38e2b9dc6
1212
github.com/apex/log v1.9.0
1313
github.com/brocaar/chirpstack-api/go/v3 v3.12.5
14+
github.com/chirpstack/chirpstack/api/go/v4 v4.6.0
1415
github.com/mdempsky/unconvert v0.0.0-20230125054757-2661c2c99a9b
1516
github.com/mgechev/revive v1.3.7
1617
github.com/smarty/assertions v1.15.1

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,8 @@ github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj
177177
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
178178
github.com/chavacava/garif v0.1.0 h1:2JHa3hbYf5D9dsgseMKAmc/MZ109otzgNFk5s87H9Pc=
179179
github.com/chavacava/garif v0.1.0/go.mod h1:XMyYCkEL58DF0oyW4qDjjnPWONs2HBqYKI+UIPD+Gww=
180+
github.com/chirpstack/chirpstack/api/go/v4 v4.6.0 h1:l+nr/QhFab1y9E8LVOJq/lDG+o0+mShcZOCNBvFYXUA=
181+
github.com/chirpstack/chirpstack/api/go/v4 v4.6.0/go.mod h1:6+68s1PGHq2QWZ216RTwXhp7h1vCiMc6kX3f4s74ZzQ=
180182
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
181183
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
182184
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=

pkg/source/chirpstack/chirpstack.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,12 @@ import (
2020
)
2121

2222
func init() {
23-
cfg, flags := config.New()
23+
cfg := config.New()
2424

2525
source.RegisterSource(source.Registration{
2626
Name: "chirpstack",
27-
Description: "Migrate from ChirpStack LoRaWAN Network Server",
28-
FlagSet: flags,
27+
Description: "Migrate from ChirpStack LoRaWAN Network Server v4",
28+
FlagSet: cfg.Flags(),
2929
Create: createNewSource(cfg),
3030
})
3131
}

pkg/source/chirpstack/config/config.go

Lines changed: 68 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ import (
2424

2525
"github.com/spf13/pflag"
2626
"go.thethings.network/lorawan-stack-migrate/pkg/source"
27+
"go.thethings.network/lorawan-stack/v3/pkg/fetch"
28+
"go.thethings.network/lorawan-stack/v3/pkg/frequencyplans"
2729
"go.thethings.network/lorawan-stack/v3/pkg/types"
2830
"google.golang.org/grpc"
2931
"google.golang.org/grpc/credentials"
@@ -32,96 +34,113 @@ import (
3234

3335
const dialTimeout = 10 * time.Second
3436

35-
func New() (*Config, *pflag.FlagSet) {
36-
var (
37-
config = &Config{}
38-
flags = &pflag.FlagSet{}
39-
)
37+
type Config struct {
38+
src source.Config
39+
40+
apiKey, caCertPath, url, joinEUI string
41+
flags *pflag.FlagSet
42+
FPStore *frequencyplans.Store
43+
insecure bool
44+
45+
ClientConn *grpc.ClientConn
4046

41-
flags.StringVar(&config.url,
47+
ExportVars,
48+
ExportSession bool
49+
FrequencyPlanID string
50+
JoinEUI *types.EUI64
51+
}
52+
53+
func New() *Config {
54+
config := &Config{
55+
flags: &pflag.FlagSet{},
56+
}
57+
58+
config.flags.StringVar(&config.url,
4259
"api-url",
43-
os.Getenv("CHIRPSTACK_API_URL"),
60+
"",
4461
"ChirpStack API URL")
45-
flags.StringVar(&config.token,
46-
"api-token",
47-
os.Getenv("CHIRPSTACK_API_TOKEN"),
48-
"ChirpStack API Token")
49-
flags.StringVar(&config.caPath,
50-
"api-ca",
51-
os.Getenv("CHIRPSTACK_API_CA"),
52-
"(optional) CA for TLS")
53-
flags.BoolVar(&config.insecure,
54-
"api-insecure",
55-
os.Getenv("CHIRPSTACK_API_INSECURE") == "1",
62+
config.flags.StringVar(&config.apiKey,
63+
"api-key",
64+
"",
65+
"ChirpStack API key")
66+
config.flags.StringVar(&config.caCertPath,
67+
"ca-cert-path",
68+
"",
69+
"(optional) Path to the CA certificate file for ChirpStack API TLS connections")
70+
config.flags.BoolVar(&config.insecure,
71+
"insecure",
72+
false,
5673
"Do not connect to ChirpStack over TLS")
57-
flags.BoolVar(&config.ExportVars,
74+
config.flags.BoolVar(&config.ExportVars,
5875
"export-vars",
5976
false,
6077
"Export device variables from ChirpStack")
61-
flags.BoolVar(&config.ExportSession,
78+
config.flags.BoolVar(&config.ExportSession,
6279
"export-session",
63-
true,
80+
false,
6481
"Export device session keys from ChirpStack")
65-
flags.StringVar(&config.joinEUI,
82+
config.flags.StringVar(&config.joinEUI,
6683
"join-eui",
67-
os.Getenv("JOIN_EUI"),
84+
"",
6885
"JoinEUI of exported devices")
69-
flags.StringVar(&config.FrequencyPlanID,
86+
config.flags.StringVar(&config.FrequencyPlanID,
7087
"frequency-plan-id",
71-
os.Getenv("FREQUENCY_PLAN_ID"),
88+
"",
7289
"Frequency Plan ID of exported devices")
7390

74-
return config, flags
91+
return config
7592
}
7693

77-
type Config struct {
78-
source.Config
79-
80-
ClientConn *grpc.ClientConn
94+
func (c *Config) Initialize(src source.Config) error {
95+
c.src = src
8196

82-
token, caPath, url,
83-
FrequencyPlanID string
84-
85-
joinEUI string
86-
JoinEUI *types.EUI64
87-
88-
insecure,
89-
ExportVars,
90-
ExportSession bool
91-
}
92-
93-
func (c *Config) Initialize() error {
94-
if c.token == "" {
97+
if c.apiKey = os.Getenv("CHIRPSTACK_API_KEY"); c.apiKey == "" {
9598
return errNoAPIToken.New()
9699
}
97-
if c.url == "" {
100+
if c.url = os.Getenv("CHIRPSTACK_API_URL"); c.url == "" {
98101
return errNoAPIURL.New()
99102
}
100-
if c.FrequencyPlanID == "" {
103+
if c.FrequencyPlanID = os.Getenv("FREQUENCY_PLAN_ID"); c.FrequencyPlanID == "" {
101104
return errNoFrequencyPlan.New()
102105
}
103-
106+
if c.joinEUI = os.Getenv("JOIN_EUI"); c.joinEUI == "" {
107+
return errNoJoinEUI.New()
108+
}
104109
c.JoinEUI = &types.EUI64{}
105110
if err := c.JoinEUI.UnmarshalText([]byte(c.joinEUI)); err != nil {
106111
return errInvalidJoinEUI.WithAttributes("join_eui", c.joinEUI)
107112
}
113+
c.caCertPath = os.Getenv("CHIRPSTACK_CA_CERT_PATH")
114+
c.insecure = os.Getenv("CHIRPSTACK_INSECURE") == "true"
115+
c.ExportSession = os.Getenv("EXPORT_SESSION") == "true"
116+
c.ExportVars = os.Getenv("EXPORT_VARS") == "true"
117+
108118
err := c.dialGRPC(
109119
grpc.FailOnNonTempDialError(true),
110120
grpc.WithBlock(),
111-
grpc.WithPerRPCCredentials(token(c.token)),
121+
grpc.WithPerRPCCredentials(token(c.apiKey)),
112122
)
113123
if err != nil {
114124
return err
115125
}
116-
126+
fpFetcher, err := fetch.FromHTTP(http.DefaultClient, src.FrequencyPlansURL)
127+
if err != nil {
128+
return err
129+
}
130+
c.FPStore = frequencyplans.NewStore(fpFetcher)
117131
return nil
118132
}
119133

134+
// Flags returns the flags for the configuration.
135+
func (c *Config) Flags() *pflag.FlagSet {
136+
return c.flags
137+
}
138+
120139
func (c *Config) dialGRPC(opts ...grpc.DialOption) error {
121140
if c.insecure {
122141
opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials()))
123142
} else {
124-
tlsConfig, err := generateTLSConfig(c.caPath)
143+
tlsConfig, err := generateTLSConfig(c.caCertPath)
125144
if err != nil {
126145
return err
127146
}

pkg/source/chirpstack/config/errors.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ var (
2020
errNoAPIToken = errors.DefineInvalidArgument("no_api_token", "no API token")
2121
errNoAPIURL = errors.DefineInvalidArgument("no_api_url", "no API URL")
2222
errNoFrequencyPlan = errors.DefineInvalidArgument("no_frequency_plan", "no Frequency Plan")
23+
errNoJoinEUI = errors.DefineInvalidArgument("no_join_eui", "no join eui")
2324

2425
errInvalidJoinEUI = errors.DefineInvalidArgument("invalid_join_eui", "invalid JoinEUI `{join_eui}`")
2526
)

pkg/source/chirpstack/errors.go

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,12 @@ var (
2525

2626
errAPI = errors.Define("api", "API error")
2727

28-
errAppNotFound = errors.DefineNotFound("app_not_found", "app `{app}` not found")
29-
errInvalidDevAddr = errors.DefineInvalidArgument("invalid_dev_addr", "invalid DevAddr `{dev_addr}`")
30-
errInvalidDevEUI = errors.DefineInvalidArgument("invalid_dev_eui", "invalid DevEUI `{dev_eui}`")
31-
errInvalidJoinEUI = errors.DefineInvalidArgument("invalid_join_eui", "invalid JoinEUI `{join_eui}`")
32-
errInvalidPHYVersion = errors.DefineInvalidArgument("invalid_phy_version", "invalid PHY version `{phy_version}`")
33-
errInvalidMACVersion = errors.DefineInvalidArgument("invalid_mac_version", "invalid MAC version `{mac_version}`")
34-
errInvalidKey = errors.DefineInvalidArgument("invalid_key", "invalid key `{key}`")
28+
errAppNotFound = errors.DefineNotFound("app_not_found", "app `{app}` not found")
29+
errInvalidDevAddr = errors.DefineInvalidArgument("invalid_dev_addr", "invalid DevAddr `{dev_addr}`")
30+
errInvalidDevEUI = errors.DefineInvalidArgument("invalid_dev_eui", "invalid DevEUI `{dev_eui}`")
31+
errInvalidJoinEUI = errors.DefineInvalidArgument("invalid_join_eui", "invalid JoinEUI `{join_eui}`")
32+
errInvalidPHYVersion = errors.DefineInvalidArgument("invalid_phy_version", "invalid PHY version `{phy_version}`")
33+
errInvalidMACVersion = errors.DefineInvalidArgument("invalid_mac_version", "invalid MAC version `{mac_version}`")
34+
errInvalidKey = errors.DefineInvalidArgument("invalid_key", "invalid key `{key}`")
35+
errInvalidApplicationID = errors.DefineInvalidArgument("invalid_application_id", "invalid application ID `{application_id}`")
3536
)

0 commit comments

Comments
 (0)