Repository: edgexfoundry/device-sdk-go (go)

Issue:
Apply mask and shift transformations to incoming data
# 🐞 Bug Report

### Affected Services [**REQUIRED**]
<!-- Can you pin-point one or more EdgeX services (Data, Metadata, Command, etc...) as the source of the bug? -->
<!-- ✍️edit: --> The issue is located in: device-sdk


### Is this a regression?

<!-- Did this behavior work correctly in the previous version? -->
<!-- ✍️--> No


### Description and Minimal Reproduction [**REQUIRED**]
https://docs.edgexfoundry.org/4.0/design/adr/device-service/0011-DeviceService-Rest-API/#data-transformations
According to the ADR, we should also apply mask and shift transformations to incoming data.

## 🌍  Your Environment
**EdgeX Version [**REQUIRED**]: v4 dev**




Pull request:
fix: Apply mask and shift transformations to incoming data
Apply mask and shift transformations to incoming data according to the ADR https://docs.edgexfoundry.org/4.0/design/adr/device-service/0011-DeviceService-Rest-API/#data-transformations

Closes #1703

<!-- Expected Commit Message Description (imported automatically by GitHub) -->
<!-- Must conform to [conventional commits guidelines](https://github.com/edgexfoundry/device-sdk-go/blob/main/.github/Contributing.md) -->
<!-- Expected Commit message must contain Closes/Fixes #IssueNumber statement when there is a related issue -->

<!-- Add additional detailed description of need for change if no related issue -->

**If your build fails** due to your commit message not passing the build checks, please review the guidelines here: https://github.com/edgexfoundry/device-sdk-go/blob/main/.github/Contributing.md

## PR Checklist
Please check if your PR fulfills the following requirements:

- [x] I am not introducing a breaking change (if you are, flag in conventional commit message with `BREAKING CHANGE:` describing the break)
- [x] I am not introducing a new dependency (add notes below if you are)
- [x] I have added unit tests for the new feature or bug fix (if not, why?)
- [x] I have fully tested (add details below) this the new feature or bug fix (if not, why?)
- [ ] I have opened a PR for the related docs change (if not, why?) not impact
  <link to docs PR>

## Testing Instructions
<!-- How can the reviewers test your change? -->

## New Dependency Instructions (If applicable)
<!-- Please follow [vetting instructions](https://wiki.edgexfoundry.org/display/FA/Vetting+Process+for+3rd+Party+Dependencies) and place results here -->

Patch:
diff --git a/example/cmd/device-simple/Attribution.txt b/example/cmd/device-simple/Attribution.txt
index 549a1fa5..d6dffef5 100644
--- a/example/cmd/device-simple/Attribution.txt
+++ b/example/cmd/device-simple/Attribution.txt
@@ -428,3 +428,9 @@ https://github.com/nhooyr/websocket/blob/master/LICENSE.txt
 
 gopkg.in/go-jose/go-jose.v2 (Apache 2.0) - https://github.com/go-jose/go-jose
 https://github.com/go-jose/go-jose/blob/v2.6.3/LICENSE
+
+github.com/emirpasic/gods (Unspecified) https://github.com/emirpasic/gods
+https://github.com/emirpasic/gods/blob/master/LICENSE
+
+github.com/openziti/channel/v4 (Apache 2.0) - https://github.com/openziti/channel
+https://github.com/openziti/channel/blob/main/LICENSE
diff --git a/internal/transformer/transformparam.go b/internal/transformer/transformparam.go
index e1c8dca2..aa570d37 100644
--- a/internal/transformer/transformparam.go
+++ b/internal/transformer/transformparam.go
@@ -1,6 +1,6 @@
 // -*- Mode: Go; indent-tabs-mode: t -*-
 //
-// Copyright (C) 2018-2023 IOTech Ltd
+// Copyright (C) 2018-2025 IOTech Ltd
 //
 // SPDX-License-Identifier: Apache-2.0
 
@@ -8,12 +8,16 @@ package transformer
 
 import (
 	"fmt"
+
+	"github.com/edgexfoundry/go-mod-core-contracts/v4/common"
 	"github.com/edgexfoundry/go-mod-core-contracts/v4/errors"
 	"github.com/edgexfoundry/go-mod-core-contracts/v4/models"
 
 	dsModels "github.com/edgexfoundry/device-sdk-go/v4/pkg/models"
 )
 
+// TransformWriteParameter performs the data transformation on incoming data
+// the incoming data transformations order can refer to https://docs.edgexfoundry.org/4.0/design/adr/device-service/0011-DeviceService-Rest-API/#data-transformations
 func TransformWriteParameter(cv *dsModels.CommandValue, pv models.ResourceProperties) errors.EdgeX {
 	if cv.Value == nil {
 		return nil
@@ -58,6 +62,17 @@ func TransformWriteParameter(cv *dsModels.CommandValue, pv models.ResourceProper
 			return errors.NewCommonEdgeXWrapper(err)
 		}
 	}
+	if pv.Shift != nil && *pv.Shift != defaultShift &&
+		(cv.Type == common.ValueTypeUint8 || cv.Type == common.ValueTypeUint16 || cv.Type == common.ValueTypeUint32 || cv.Type == common.ValueTypeUint64 ||
+			cv.Type == common.ValueTypeInt8 || cv.Type == common.ValueTypeInt16 || cv.Type == common.ValueTypeInt32 || cv.Type == common.ValueTypeInt64) {
+		// use negative value to reuse the shift function to perform reversed operation
+		newValue = transformShift(newValue, -*pv.Shift)
+	}
+	if pv.Mask != nil && *pv.Mask != defaultMask &&
+		(cv.Type == common.ValueTypeUint8 || cv.Type == common.ValueTypeUint16 || cv.Type == common.ValueTypeUint32 || cv.Type == common.ValueTypeUint64 ||
+			cv.Type == common.ValueTypeInt8 || cv.Type == common.ValueTypeInt16 || cv.Type == common.ValueTypeInt32 || cv.Type == common.ValueTypeInt64) {
+		newValue = transformMask(newValue, *pv.Mask)
+	}
 
 	if value != newValue {
 		cv.Value = newValue
diff --git a/internal/transformer/transformresult.go b/internal/transformer/transformresult.go
index a70046c3..800606c2 100644
--- a/internal/transformer/transformresult.go
+++ b/internal/transformer/transformresult.go
@@ -1,6 +1,6 @@
 // -*- Mode: Go; indent-tabs-mode: t -*-
 //
-// Copyright (C) 2019-2023 IOTech Ltd
+// Copyright (C) 2019-2025 IOTech Ltd
 //
 // SPDX-License-Identifier: Apache-2.0
 
@@ -31,6 +31,8 @@ const (
 	NaN      = "NaN"
 )
 
+// TransformReadResult performs the data transformation on outgoing data
+// the outgoing data transformations order can refer to https://docs.edgexfoundry.org/4.0/design/adr/device-service/0011-DeviceService-Rest-API/#data-transformations
 func TransformReadResult(cv *sdkModels.CommandValue, pv models.ResourceProperties) errors.EdgeX {
 	if !isNumericValueType(cv) {
 		return nil
@@ -50,18 +52,14 @@ func TransformReadResult(cv *sdkModels.CommandValue, pv models.ResourcePropertie
 	newValue := value
 
 	if pv.Mask != nil && *pv.Mask != defaultMask &&
-		(cv.Type == common.ValueTypeUint8 || cv.Type == common.ValueTypeUint16 || cv.Type == common.ValueTypeUint32 || cv.Type == common.ValueTypeUint64) {
-		newValue, err = transformReadMask(newValue, *pv.Mask)
-		if err != nil {
-			return errors.NewCommonEdgeXWrapper(err)
-		}
+		(cv.Type == common.ValueTypeUint8 || cv.Type == common.ValueTypeUint16 || cv.Type == common.ValueTypeUint32 || cv.Type == common.ValueTypeUint64 ||
+			cv.Type == common.ValueTypeInt8 || cv.Type == common.ValueTypeInt16 || cv.Type == common.ValueTypeInt32 || cv.Type == common.ValueTypeInt64) {
+		newValue = transformMask(newValue, *pv.Mask)
 	}
 	if pv.Shift != nil && *pv.Shift != defaultShift &&
-		(cv.Type == common.ValueTypeUint8 || cv.Type == common.ValueTypeUint16 || cv.Type == common.ValueTypeUint32 || cv.Type == common.ValueTypeUint64) {
-		newValue, err = transformReadShift(newValue, *pv.Shift)
-		if err != nil {
-			return errors.NewCommonEdgeXWrapper(err)
-		}
+		(cv.Type == common.ValueTypeUint8 || cv.Type == common.ValueTypeUint16 || cv.Type == common.ValueTypeUint32 || cv.Type == common.ValueTypeUint64 ||
+			cv.Type == common.ValueTypeInt8 || cv.Type == common.ValueTypeInt16 || cv.Type == common.ValueTypeInt32 || cv.Type == common.ValueTypeInt64) {
+		newValue = transformShift(newValue, *pv.Shift)
 	}
 	if pv.Base != nil && *pv.Base != defaultBase {
 		newValue, err = transformBase(newValue, *pv.Base, true)
@@ -351,7 +349,7 @@ func transformOffset(value any, offset float64, read bool) (any, errors.EdgeX) {
 	return value, nil
 }
 
-func transformReadMask(value any, mask uint64) (any, errors.EdgeX) {
+func transformMask(value any, mask uint64) any {
 	switch v := value.(type) {
 	case uint8:
 		value = v & uint8(mask)
@@ -361,40 +359,73 @@ func transformReadMask(value any, mask uint64) (any, errors.EdgeX) {
 		value = v & uint32(mask)
 	case uint64:
 		value = v & mask
+	case int8:
+		value = v & int8(mask)
+	case int16:
+		value = v & int16(mask)
+	case int32:
+		value = v & int32(mask)
+	case int64:
+		value = v & int64(mask)
 	}
-
-	return value, nil
+	return value
 }
 
-func transformReadShift(value any, shift int64) (any, errors.EdgeX) {
+// transformShift returns the bit-shifted value
+// Positive values indicate right-shift, negative for left.
+func transformShift(value any, shift int64) any {
 	switch v := value.(type) {
 	case uint8:
 		if shift > 0 {
-			value = v << int8(shift)
+			value = v >> int8(shift)
 		} else {
-			value = v >> int8(-shift)
+			value = v << int8(-shift)
 		}
 	case uint16:
 		if shift > 0 {
-			value = v << int16(shift)
+			value = v >> int16(shift)
 		} else {
-			value = v >> int16(-shift)
+			value = v << int16(-shift)
 		}
 	case uint32:
 		if shift > 0 {
-			value = v << int32(shift)
+			value = v >> int32(shift)
 		} else {
-			value = v >> int32(-shift)
+			value = v << int32(-shift)
 		}
 	case uint64:
 		if shift > 0 {
-			value = v << shift
+			value = v >> shift
 		} else {
-			value = v >> (-shift)
+			value = v << (-shift)
+		}
+	case int8:
+		if shift > 0 {
+			value = v >> int8(shift)
+		} else {
+			value = v << int8(-shift)
+		}
+	case int16:
+		if shift > 0 {
+			value = v >> int16(shift)
+		} else {
+			value = v << int16(-shift)
+		}
+	case int32:
+		if shift > 0 {
+			value = v >> int32(shift)
+		} else {
+			value = v << int32(-shift)
+		}
+	case int64:
+		if shift > 0 {
+			value = v >> shift
+		} else {
+			value = v << (-shift)
 		}
 	}
 
-	return value, nil
+	return value
 }
 
 func commandValueForTransform(cv *sdkModels.CommandValue) (interface{}, errors.EdgeX) {


Test patch:
diff --git a/internal/transformer/transformresult_test.go b/internal/transformer/transformresult_test.go
index 4f1accd1..8a3530e3 100644
--- a/internal/transformer/transformresult_test.go
+++ b/internal/transformer/transformresult_test.go
@@ -142,58 +142,58 @@ func Test_transformReadOffset(t *testing.T) {
 	}
 }
 
-func Test_transformReadMask(t *testing.T) {
+func Test_transformMask(t *testing.T) {
 	tests := []struct {
-		name        string
-		value       interface{}
-		mask        uint64
-		expected    interface{}
-		expectedErr bool
+		name     string
+		value    interface{}
+		mask     uint64
+		expected interface{}
 	}{
-		{"valid - uint8 mask transformation", uint8(math.MaxUint8), 15, uint8(15), false},
-		{"valid - uint16 mask transformation", uint16(math.MaxUint16), 256, uint16(256), false},
-		{"valid - uint32 mask transformation", uint32(math.MaxUint32), 256, uint32(256), false},
-		{"valid - uint64 mask transformation", uint64(math.MaxUint64), 256, uint64(256), false},
+		{"valid - uint8 mask transformation", uint8(math.MaxUint8), 15, uint8(15)},
+		{"valid - uint16 mask transformation", uint16(math.MaxUint16), 256, uint16(256)},
+		{"valid - uint32 mask transformation", uint32(math.MaxUint32), 256, uint32(256)},
+		{"valid - uint64 mask transformation", uint64(math.MaxUint64), 256, uint64(256)},
+		{"valid - int8 mask transformation", int8(math.MaxInt8), 15, int8(15)},
+		{"valid - int16 mask transformation", int16(math.MaxInt16), 127, int16(127)},
+		{"valid - int32 mask transformation", int32(math.MaxInt32), 32767, int32(32767)},
+		{"valid - int64 mask transformation", int64(math.MaxInt64), 2147483647, int64(2147483647)},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			res, err := transformReadMask(tt.value, tt.mask)
-			if !tt.expectedErr {
-				require.NoError(t, err)
-				assert.Equal(t, tt.expected, res)
-			} else {
-				require.Error(t, err)
-			}
+			res := transformMask(tt.value, tt.mask)
+			assert.Equal(t, tt.expected, res)
 		})
 	}
 }
 
-func Test_transformReadShift(t *testing.T) {
+func Test_transformShift(t *testing.T) {
 	tests := []struct {
-		name        string
-		value       interface{}
-		shift       int64
-		expected    interface{}
-		expectedErr bool
+		name     string
+		value    interface{}
+		shift    int64
+		expected interface{}
 	}{
-		{"valid - uint8 shift transformation with positive shift value", uint8(0b1), 4, uint8(0b10000), false},
-		{"valid - uint8 shift transformation with negative shift value", uint8(0b11111111), -4, uint8(0b00001111), false},
-		{"valid - uint16 shift transformation with positive shift value", uint16(0b1), 8, uint16(0b100000000), false},
-		{"valid - uint16 shift transformation with negative shift value", uint16(0b1111111100000000), -8, uint16(0b0000000011111111), false},
-		{"valid - uint32 shift transformation with positive shift value", uint32(0b1), 16, uint32(0b10000000000000000), false},
-		{"valid - uint32 shift transformation with negative shift value", uint32(0b11111111111111110000000000000000), -16, uint32(0b00000000000000001111111111111111), false},
-		{"valid - uint64 shift transformation with positive shift value", uint64(0b1), 32, uint64(0b100000000000000000000000000000000), false},
-		{"valid - uint64 shift transformation with negative shift value", uint64(0b1111111111111111111111111111111100000000000000000000000000000000), -32, uint64(0b0000000000000000000000000000000011111111111111111111111111111111), false},
+		{"valid - uint8 shift transformation with negative shift value", uint8(0b1), -4, uint8(0b10000)},
+		{"valid - uint8 shift transformation with positive shift value", uint8(0b11111111), 4, uint8(0b00001111)},
+		{"valid - uint16 shift transformation with negative shift value", uint16(0b1), -8, uint16(0b100000000)},
+		{"valid - uint16 shift transformation with positive shift value", uint16(0b1111111100000000), 8, uint16(0b0000000011111111)},
+		{"valid - uint32 shift transformation with negative shift value", uint32(0b1), -16, uint32(0b10000000000000000)},
+		{"valid - uint32 shift transformation with positive shift value", uint32(0b11111111111111110000000000000000), 16, uint32(0b00000000000000001111111111111111)},
+		{"valid - uint64 shift transformation with negative shift value", uint64(0b1), -32, uint64(0b100000000000000000000000000000000)},
+		{"valid - uint64 shift transformation with positive shift value", uint64(0b1111111111111111111111111111111100000000000000000000000000000000), 32, uint64(0b0000000000000000000000000000000011111111111111111111111111111111)},
+		{"valid - int8 shift transformation with negative shift value", int8(0b1), -4, int8(0b10000)},
+		{"valid - int8 shift transformation with positive shift value", int8(0b1111111), 4, int8(0b0000111)},
+		{"valid - int16 shift transformation with negative shift value", int16(0b1), -8, int16(0b100000000)},
+		{"valid - int16 shift transformation with positive shift value", int16(0b111111100000000), 8, int16(0b000000001111111)},
+		{"valid - int32 shift transformation with negative shift value", int32(0b1), -16, int32(0b10000000000000000)},
+		{"valid - int32 shift transformation with positive shift value", int32(0b1111111111111110000000000000000), 16, int32(0b0000000000000000111111111111111)},
+		{"valid - int64 shift transformation with negative shift value", int64(0b1), -32, int64(0b100000000000000000000000000000000)},
+		{"valid - int64 shift transformation with positive shift value", int64(0b111111111111111111111111111111100000000000000000000000000000000), 32, int64(0b000000000000000000000000000000001111111111111111111111111111111)},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			res, err := transformReadShift(tt.value, tt.shift)
-			if !tt.expectedErr {
-				require.NoError(t, err)
-				assert.Equal(t, tt.expected, res)
-			} else {
-				require.Error(t, err)
-			}
+			res := transformShift(tt.value, tt.shift)
+			assert.Equal(t, tt.expected, res)
 		})
 	}
 }