diff --git a/cmd/smartlabel.go b/cmd/smartlabel.go index 3ffa2ff..2e79fb5 100644 --- a/cmd/smartlabel.go +++ b/cmd/smartlabel.go @@ -73,7 +73,7 @@ var smartlabelCmd = &cobra.Command{ } ctx = context.WithValue(ctx, decoder.DEVEUI_CONTEXT_KEY, smartlabelDevEui) - ctx = context.WithValue(ctx, decoder.PORT_CONTEXT_KEY, port) + ctx = context.WithValue(ctx, decoder.PORT_CONTEXT_KEY, uint8(port)) ctx = context.WithValue(ctx, decoder.FCNT_CONTEXT_KEY, 1) // Default frame count, can be adjusted as needed data, err := d.Decode(ctx, args[1], uint8(port)) diff --git a/cmd/tagxl.go b/cmd/tagxl.go index ea417fc..f34cb99 100644 --- a/cmd/tagxl.go +++ b/cmd/tagxl.go @@ -68,7 +68,7 @@ var tagxlCmd = &cobra.Command{ } ctx = context.WithValue(ctx, decoder.DEVEUI_CONTEXT_KEY, tagXlDevEui) - ctx = context.WithValue(ctx, decoder.PORT_CONTEXT_KEY, port) + ctx = context.WithValue(ctx, decoder.PORT_CONTEXT_KEY, uint8(port)) ctx = context.WithValue(ctx, decoder.FCNT_CONTEXT_KEY, 1) // Default frame count, can be adjusted as needed data, err := d.Decode(ctx, args[1], uint8(port)) diff --git a/pkg/solver/loracloud/loracloud.go b/pkg/solver/loracloud/loracloud.go index a6c7fda..3005c2e 100644 --- a/pkg/solver/loracloud/loracloud.go +++ b/pkg/solver/loracloud/loracloud.go @@ -22,23 +22,36 @@ type LoracloudClient struct { BaseUrl string } +const ( + SemtechLoRaCloudBaseUrl = "https://mgs.loracloud.com" + TraxmateLoRaCloudBaseUrl = "https://lw.traxmate.io" +) + var _ solver.SolverV1 = &LoracloudClient{} -func NewLoracloudClient(ctx context.Context, accessToken string, logger *zap.Logger, options ...LoracloudClientOptions) LoracloudClient { +func (m LoracloudClient) checkForSemtechLoRaCloudShutdown() { + if m.BaseUrl != SemtechLoRaCloudBaseUrl { + return + } + if time.Now().After(time.Date(2025, 7, 31, 0, 0, 0, 0, time.UTC)) { - logger.Fatal("LoRa Cloud is no longer available after 31.07.2025", zap.String("url", "https://www.semtech.com/loracloud-shutdown")) + m.logger.Fatal("LoRa Cloud is no longer available after 31.07.2025", zap.String("url", "https://www.semtech.com/loracloud-shutdown")) } - logger.Warn("LoRa Cloud is Sunsetting on 31.07.2025", zap.String("url", "https://www.semtech.com/loracloud-shutdown")) + m.logger.Warn("LoRa Cloud is Sunsetting on 31.07.2025", zap.String("url", "https://www.semtech.com/loracloud-shutdown")) +} +func NewLoracloudClient(ctx context.Context, accessToken string, logger *zap.Logger, options ...LoracloudClientOptions) LoracloudClient { client := LoracloudClient{ accessToken: accessToken, - BaseUrl: "https://mgs.loracloud.com", + BaseUrl: TraxmateLoRaCloudBaseUrl, logger: logger, } for _, option := range options { option(&client) } + // Check this after all options have been applied + client.checkForSemtechLoRaCloudShutdown() return client } @@ -52,7 +65,7 @@ func WithBaseUrl(baseUrl string) LoracloudClientOptions { } func validateContext(ctx context.Context) error { - port, ok := ctx.Value(decoder.PORT_CONTEXT_KEY).(int) + _, ok := ctx.Value(decoder.PORT_CONTEXT_KEY).(uint8) if !ok { return ErrContextPortNotFound } @@ -64,9 +77,6 @@ func validateContext(ctx context.Context) error { if !ok { return ErrContextFCountNotFound } - if port < 0 || port > 255 { - return ErrContextInvalidPort - } if len(devEui) != 16 { return ErrContextInvalidDevEui } @@ -86,7 +96,7 @@ func (m LoracloudClient) Solve(ctx context.Context, payload string) (*decoder.De return nil, fmt.Errorf("context validation failed: %v", err) } - port, ok := ctx.Value(decoder.PORT_CONTEXT_KEY).(int) + port, ok := ctx.Value(decoder.PORT_CONTEXT_KEY).(uint8) if !ok { return nil, fmt.Errorf("port not found in context") } @@ -219,10 +229,41 @@ func (m LoracloudClient) DeliverUplinkMessage(devEui string, uplinkMsg UplinkMsg return nil, fmt.Errorf("unexpected status code returned by loracloud: HTTP %v, %v", response.StatusCode, responseJson) } - uplinkResponse := UplinkMsgResponse{} - err = json.NewDecoder(response.Body).Decode(&uplinkResponse) - if err != nil { - return nil, fmt.Errorf("error decoding loracloud response: %v", err) + var uplinkResponse UplinkMsgResponse + + if m.BaseUrl == TraxmateLoRaCloudBaseUrl { + // NOTE: Traxmate LoRaCloud returns a nested response structure + // { + // "result": { + // "10-CE-45-FF-FE-01-CE-53": { + // UplinkMsgResponse + // } + // } + // } + var traxmateResponse struct { + Result map[string]UplinkMsgResponse `json:"result"` + } + + err = json.NewDecoder(response.Body).Decode(&traxmateResponse) + if err != nil { + return nil, fmt.Errorf("error decoding Traxmate LoRaCloud response: %v", err) + } + + if len(traxmateResponse.Result) != 1 { + return nil, fmt.Errorf("expected exactly one device EUI in the Traxmate LoRaCloud response, got %d", len(traxmateResponse.Result)) + } + + nestedUplinkResponse, ok := traxmateResponse.Result[devEui] + if !ok { + return nil, fmt.Errorf("device EUI %s not found in Traxmate LoRaCloud response", devEui) + } + + uplinkResponse = nestedUplinkResponse + } else { + err = json.NewDecoder(response.Body).Decode(&uplinkResponse) + if err != nil { + return nil, fmt.Errorf("error decoding response: %v", err) + } } // remove the '-' from the devEui diff --git a/pkg/solver/loracloud/loracloud_test.go b/pkg/solver/loracloud/loracloud_test.go index 4a5bddf..7f63006 100644 --- a/pkg/solver/loracloud/loracloud_test.go +++ b/pkg/solver/loracloud/loracloud_test.go @@ -347,7 +347,7 @@ func TestValidateContext(t *testing.T) { { name: "missing devEui", ctx: func() context.Context { - ctx := context.WithValue(context.Background(), decoder.PORT_CONTEXT_KEY, 1) + ctx := context.WithValue(context.Background(), decoder.PORT_CONTEXT_KEY, uint8(1)) return ctx }(), wantErr: ErrContextDevEuiNotFound, @@ -355,36 +355,16 @@ func TestValidateContext(t *testing.T) { { name: "missing fCount", ctx: func() context.Context { - ctx := context.WithValue(context.Background(), decoder.PORT_CONTEXT_KEY, 1) + ctx := context.WithValue(context.Background(), decoder.PORT_CONTEXT_KEY, uint8(1)) ctx = context.WithValue(ctx, decoder.DEVEUI_CONTEXT_KEY, "0123456789abcdef") return ctx }(), wantErr: ErrContextFCountNotFound, }, - { - name: "invalid port low", - ctx: func() context.Context { - ctx := context.WithValue(context.Background(), decoder.PORT_CONTEXT_KEY, -1) - ctx = context.WithValue(ctx, decoder.DEVEUI_CONTEXT_KEY, "0123456789abcdef") - ctx = context.WithValue(ctx, decoder.FCNT_CONTEXT_KEY, 0) - return ctx - }(), - wantErr: ErrContextInvalidPort, - }, - { - name: "invalid port high", - ctx: func() context.Context { - ctx := context.WithValue(context.Background(), decoder.PORT_CONTEXT_KEY, 256) - ctx = context.WithValue(ctx, decoder.DEVEUI_CONTEXT_KEY, "0123456789abcdef") - ctx = context.WithValue(ctx, decoder.FCNT_CONTEXT_KEY, 0) - return ctx - }(), - wantErr: ErrContextInvalidPort, - }, { name: "invalid devEui length", ctx: func() context.Context { - ctx := context.WithValue(context.Background(), decoder.PORT_CONTEXT_KEY, 1) + ctx := context.WithValue(context.Background(), decoder.PORT_CONTEXT_KEY, uint8(1)) ctx = context.WithValue(ctx, decoder.DEVEUI_CONTEXT_KEY, "0123456789abcde") // 15 chars ctx = context.WithValue(ctx, decoder.FCNT_CONTEXT_KEY, 0) return ctx @@ -394,7 +374,7 @@ func TestValidateContext(t *testing.T) { { name: "invalid devEui non-hex", ctx: func() context.Context { - ctx := context.WithValue(context.Background(), decoder.PORT_CONTEXT_KEY, 1) + ctx := context.WithValue(context.Background(), decoder.PORT_CONTEXT_KEY, uint8(1)) ctx = context.WithValue(ctx, decoder.DEVEUI_CONTEXT_KEY, "0123456789abcdeg") // 'g' is not hex ctx = context.WithValue(ctx, decoder.FCNT_CONTEXT_KEY, 0) return ctx @@ -404,7 +384,7 @@ func TestValidateContext(t *testing.T) { { name: "invalid fCount negative", ctx: func() context.Context { - ctx := context.WithValue(context.Background(), decoder.PORT_CONTEXT_KEY, 1) + ctx := context.WithValue(context.Background(), decoder.PORT_CONTEXT_KEY, uint8(1)) ctx = context.WithValue(ctx, decoder.DEVEUI_CONTEXT_KEY, "0123456789abcdef") ctx = context.WithValue(ctx, decoder.FCNT_CONTEXT_KEY, -1) return ctx @@ -414,7 +394,7 @@ func TestValidateContext(t *testing.T) { { name: "valid context", ctx: func() context.Context { - ctx := context.WithValue(context.Background(), decoder.PORT_CONTEXT_KEY, 10) + ctx := context.WithValue(context.Background(), decoder.PORT_CONTEXT_KEY, uint8(10)) ctx = context.WithValue(ctx, decoder.DEVEUI_CONTEXT_KEY, "0123456789abcdef") ctx = context.WithValue(ctx, decoder.FCNT_CONTEXT_KEY, 42) return ctx