Skip to content
2 changes: 1 addition & 1 deletion cmd/smartlabel.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
2 changes: 1 addition & 1 deletion cmd/tagxl.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
67 changes: 54 additions & 13 deletions pkg/solver/loracloud/loracloud.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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
}
Expand All @@ -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
}
Expand All @@ -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")
}
Expand Down Expand Up @@ -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
Expand Down
32 changes: 6 additions & 26 deletions pkg/solver/loracloud/loracloud_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -347,44 +347,24 @@ 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,
},
{
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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
Loading