Repository: edgexfoundry/device-sdk-go
Pull request #1379 (2023-03-14 11:30:13)

feat: enable regex for executing GET command
SDK now default treat resource name as regex pattern and will return
readings whose resource name matches against the regex pattern.

For protocol whose resource name contains special character, specify
'ds-regexcmd=false' in query parameter to make it as normal resource name.

closes edgexfoundry/edgex-go#4279

<!-- 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?)
- [x] I have opened a PR for the related docs change (if not, why?)
  https://github.com/edgexfoundry/edgex-docs/pull/991

## Testing Instructions
<!-- How can the reviewers test your change? -->
1. Run `device-simple` from this branch
2. Run core-command with URL-escaped regex command name: `.rotation` or `[A-Z]+rotation` and query parameter `ds-regexcmd=true`
```shell
$ curl http://localhost:59882/api/v2/device/name/Simple-Device01/%2Erotation
{"apiVersion":"v2","statusCode":200,"event":{"apiVersion":"v2","id":"183d4439-fe9c-4706-9f79-829dce9e2d39","deviceName":"Simple-Device01","profileName":"Simple-Device","sourceName":"[A-Z]+rotation","origin":1678793090008476000,"readings":[{"id":"a527b677-069b-41d0-a4d5-231a7b69b423","origin":1678793090008476000,"deviceName":"Simple-Device01","resourceName":"Xrotation","profileName":"Simple-Device","valueType":"Int32","units":"rpm","value":"0"},{"id":"afb88931-82f0-422f-b5d6-45985db08566","origin":1678793090008476000,"deviceName":"Simple-Device01","resourceName":"Yrotation","profileName":"Simple-Device","valueType":"Int32","units":"rpm","value":"0"},{"id":"b0453fe5-9c1d-4019-8b7a-6555f5fcf361","origin":1678793090008476000,"deviceName":"Simple-Device01","resourceName":"Zrotation","profileName":"Simple-Device","valueType":"Int32","units":"rpm","value":"0"}]}}%
```
3. Check `device-simple` logs has something similiar:
```shell
level=DEBUG ts=2023-03-22T04:29:03.044589908Z app=device-simple source=command.go:64 msg="GET Device Command successfully. Device: Simple-Device01, Source: .rotation, X-Correlation-ID: 15b4c7ed-53f9-4aa4-bb10-6a2b6bf70493"
```

## 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 -->

codecov-commenter: ## [Codecov](https://codecov.io/gh/edgexfoundry/device-sdk-go/pull/1379?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=edgexfoundry) Report
> Merging [#1379](https://codecov.io/gh/edgexfoundry/device-sdk-go/pull/1379?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=edgexfoundry) (95b100d) into [main](https://codecov.io/gh/edgexfoundry/device-sdk-go/commit/c61610bbbff1187c8ac68f5c7e0a10add7b75ddb?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=edgexfoundry) (c61610b) will **increase** coverage by `0.20%`.
> The diff coverage is `52.17%`.

:mega: This organization is not using Codecov’s [GitHub App Integration](https://github.com/apps/codecov). We recommend you install it so Codecov can continue to function properly for your repositories. [Learn more](https://about.codecov.io/blog/codecov-is-updating-its-github-integration/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=edgexfoundry)

```diff
@@            Coverage Diff             @@
##             main    #1379      +/-   ##
==========================================
+ Coverage   47.74%   47.94%   +0.20%     
==========================================
  Files          27       27              
  Lines        2593     2607      +14     
==========================================
+ Hits         1238     1250      +12     
- Misses       1253     1255       +2     
  Partials      102      102              
```


| [Impacted Files](https://codecov.io/gh/edgexfoundry/device-sdk-go/pull/1379?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=edgexfoundry) | Coverage Δ | |
|---|---|---|
| [internal/autoevent/executor.go](https://codecov.io/gh/edgexfoundry/device-sdk-go/pull/1379?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=edgexfoundry#diff-aW50ZXJuYWwvYXV0b2V2ZW50L2V4ZWN1dG9yLmdv) | `47.50% <ø> (ø)` | |
| [internal/controller/messaging/command.go](https://codecov.io/gh/edgexfoundry/device-sdk-go/pull/1379?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=edgexfoundry#diff-aW50ZXJuYWwvY29udHJvbGxlci9tZXNzYWdpbmcvY29tbWFuZC5nbw==) | `0.00% <0.00%> (ø)` | |
| [internal/cache/profiles.go](https://codecov.io/gh/edgexfoundry/device-sdk-go/pull/1379?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=edgexfoundry#diff-aW50ZXJuYWwvY2FjaGUvcHJvZmlsZXMuZ28=) | `93.22% <100.00%> (+0.76%)` | :arrow_up: |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=edgexfoundry). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=edgexfoundry)

cloudxxx8: @hahattan please help rebase this PR

Patch:
diff --git a/example/driver/simpledriver.go b/example/driver/simpledriver.go
index 17c2492c..683454a2 100644
--- a/example/driver/simpledriver.go
+++ b/example/driver/simpledriver.go
@@ -165,21 +165,19 @@ func (s *SimpleDriver) ProcessCustomConfigChanges(rawWritableConfig interface{})
 func (s *SimpleDriver) HandleReadCommands(deviceName string, protocols map[string]models.ProtocolProperties, reqs []sdkModels.CommandRequest) (res []*sdkModels.CommandValue, err error) {
 	s.lc.Debugf("SimpleDriver.HandleReadCommands: protocols: %v resource: %v attributes: %v", protocols, reqs[0].DeviceResourceName, reqs[0].Attributes)
 
-	if len(reqs) == 1 {
-		res = make([]*sdkModels.CommandValue, 1)
-		if reqs[0].DeviceResourceName == "SwitchButton" {
-			cv, _ := sdkModels.NewCommandValue(reqs[0].DeviceResourceName, common.ValueTypeBool, s.switchButton)
-			res[0] = cv
-		} else if reqs[0].DeviceResourceName == "Xrotation" {
-			cv, _ := sdkModels.NewCommandValue(reqs[0].DeviceResourceName, common.ValueTypeInt32, s.xRotation)
-			res[0] = cv
-		} else if reqs[0].DeviceResourceName == "Yrotation" {
-			cv, _ := sdkModels.NewCommandValue(reqs[0].DeviceResourceName, common.ValueTypeInt32, s.yRotation)
-			res[0] = cv
-		} else if reqs[0].DeviceResourceName == "Zrotation" {
-			cv, _ := sdkModels.NewCommandValue(reqs[0].DeviceResourceName, common.ValueTypeInt32, s.zRotation)
-			res[0] = cv
-		} else if reqs[0].DeviceResourceName == "Image" {
+	res = make([]*sdkModels.CommandValue, 0)
+	for _, req := range reqs {
+		var cv *sdkModels.CommandValue
+		switch req.DeviceResourceName {
+		case "SwitchButton":
+			cv, _ = sdkModels.NewCommandValue(req.DeviceResourceName, common.ValueTypeBool, s.switchButton)
+		case "Xrotation":
+			cv, _ = sdkModels.NewCommandValue(req.DeviceResourceName, common.ValueTypeInt32, s.xRotation)
+		case "Yrotation":
+			cv, _ = sdkModels.NewCommandValue(req.DeviceResourceName, common.ValueTypeInt32, s.yRotation)
+		case "Zrotation":
+			cv, _ = sdkModels.NewCommandValue(req.DeviceResourceName, common.ValueTypeInt32, s.zRotation)
+		case "Image":
 			// Show a binary/image representation of the switch's on/off value
 			buf := new(bytes.Buffer)
 			if s.switchButton {
@@ -187,32 +185,16 @@ func (s *SimpleDriver) HandleReadCommands(deviceName string, protocols map[strin
 			} else {
 				err = getImageBytes(s.serviceConfig.SimpleCustom.OffImageLocation, buf)
 			}
-			cvb, _ := sdkModels.NewCommandValue(reqs[0].DeviceResourceName, common.ValueTypeBinary, buf.Bytes())
-			res[0] = cvb
-		} else if reqs[0].DeviceResourceName == "StringArray" {
-			cv, _ := sdkModels.NewCommandValue(reqs[0].DeviceResourceName, common.ValueTypeStringArray, s.stringArray)
-			res[0] = cv
-		} else if reqs[0].DeviceResourceName == "Uint8Array" {
-			cv, _ := sdkModels.NewCommandValue(reqs[0].DeviceResourceName, common.ValueTypeUint8Array, []uint8{0, 1, 2})
-			res[0] = cv
-		} else if reqs[0].DeviceResourceName == "Counter" {
-			cv, _ := sdkModels.NewCommandValue(reqs[0].DeviceResourceName, common.ValueTypeObject, s.counter)
-			res[0] = cv
-		}
-	} else if len(reqs) == 3 {
-		res = make([]*sdkModels.CommandValue, 3)
-		for i, r := range reqs {
-			var cv *sdkModels.CommandValue
-			switch r.DeviceResourceName {
-			case "Xrotation":
-				cv, _ = sdkModels.NewCommandValue(r.DeviceResourceName, common.ValueTypeInt32, s.xRotation)
-			case "Yrotation":
-				cv, _ = sdkModels.NewCommandValue(r.DeviceResourceName, common.ValueTypeInt32, s.yRotation)
-			case "Zrotation":
-				cv, _ = sdkModels.NewCommandValue(r.DeviceResourceName, common.ValueTypeInt32, s.zRotation)
-			}
-			res[i] = cv
+			cv, _ = sdkModels.NewCommandValue(req.DeviceResourceName, common.ValueTypeBinary, buf.Bytes())
+		case "StringArray":
+			cv, _ = sdkModels.NewCommandValue(req.DeviceResourceName, common.ValueTypeStringArray, s.stringArray)
+		case "Uint8Array":
+			cv, _ = sdkModels.NewCommandValue(req.DeviceResourceName, common.ValueTypeUint8Array, []uint8{0, 1, 2})
+		case "Counter":
+			cv, _ = sdkModels.NewCommandValue(req.DeviceResourceName, common.ValueTypeObject, s.counter)
 		}
+
+		res = append(res, cv)
 	}
 
 	s.readCommandsExecuted.Inc(1)
diff --git a/internal/application/command.go b/internal/application/command.go
index 193b5fa2..4df77810 100644
--- a/internal/application/command.go
+++ b/internal/application/command.go
@@ -33,7 +33,7 @@ import (
 	"github.com/edgexfoundry/go-mod-core-contracts/v3/models"
 )
 
-func GetCommand(ctx context.Context, deviceName string, commandName string, queryParams string, dic *di.Container) (*dtos.Event, errors.EdgeX) {
+func GetCommand(ctx context.Context, deviceName string, commandName string, queryParams string, regexCmd bool, dic *di.Container) (*dtos.Event, errors.EdgeX) {
 	if deviceName == "" {
 		return nil, errors.NewCommonEdgeX(errors.KindContractInvalid, "device name is empty", nil)
 	}
@@ -50,6 +50,8 @@ func GetCommand(ctx context.Context, deviceName string, commandName string, quer
 	_, cmdExist := cache.Profiles().DeviceCommand(device.ProfileName, commandName)
 	if cmdExist {
 		res, err = readDeviceCommand(device, commandName, queryParams, dic)
+	} else if regexCmd {
+		res, err = readDeviceResourcesRegex(device, commandName, queryParams, dic)
 	} else {
 		res, err = readDeviceResource(device, commandName, queryParams, dic)
 	}
@@ -96,12 +98,12 @@ func SetCommand(ctx context.Context, deviceName string, commandName string, quer
 func readDeviceResource(device models.Device, resourceName string, attributes string, dic *di.Container) (res *dtos.Event, edgexErr errors.EdgeX) {
 	dr, ok := cache.Profiles().DeviceResource(device.ProfileName, resourceName)
 	if !ok {
-		errMsg := fmt.Sprintf("deviceResource %s not found", resourceName)
+		errMsg := fmt.Sprintf("DeviceResource %s not found", resourceName)
 		return res, errors.NewCommonEdgeX(errors.KindEntityDoesNotExist, errMsg, nil)
 	}
 	// check deviceResource is not write-only
 	if dr.Properties.ReadWrite == common.ReadWrite_W {
-		errMsg := fmt.Sprintf("deviceResource %s is marked as write-only", dr.Name)
+		errMsg := fmt.Sprintf("DeviceResource %s is marked as write-only", dr.Name)
 		return res, errors.NewCommonEdgeX(errors.KindNotAllowed, errMsg, nil)
 	}
 
@@ -138,15 +140,69 @@ func readDeviceResource(device models.Device, resourceName string, attributes st
 	return res, nil
 }
 
+func readDeviceResourcesRegex(device models.Device, regexResourceName string, attributes string, dic *di.Container) (res *dtos.Event, edgexErr errors.EdgeX) {
+	deviceResources, ok := cache.Profiles().DeviceResourcesByRegex(device.ProfileName, regexResourceName)
+	if !ok || len(deviceResources) == 0 {
+		errMsg := fmt.Sprintf("Regex DeviceResource %s not found", regexResourceName)
+		return res, errors.NewCommonEdgeX(errors.KindEntityDoesNotExist, errMsg, nil)
+	}
+
+	lc := bootstrapContainer.LoggingClientFrom(dic.Get)
+	reqs := make([]sdkModels.CommandRequest, 0)
+	for _, dr := range deviceResources {
+		// check deviceResource is not write-only
+		if dr.Properties.ReadWrite == common.ReadWrite_W {
+			lc.Debugf("DeviceResource %s is marked as write-only, skipping adding to RegEx Read list", dr.Name)
+			continue
+		}
+
+		// prepare CommandRequest
+		var req sdkModels.CommandRequest
+		req.DeviceResourceName = dr.Name
+		req.Attributes = dr.Attributes
+		if attributes != "" {
+			if len(req.Attributes) <= 0 {
+				req.Attributes = make(map[string]any)
+			}
+			req.Attributes[sdkCommon.URLRawQuery] = attributes
+		}
+		req.Type = dr.Properties.ValueType
+
+		reqs = append(reqs, req)
+	}
+
+	if len(reqs) == 0 {
+		errMsg := fmt.Sprintf("no readable resources matched with %s", regexResourceName)
+		return res, errors.NewCommonEdgeX(errors.KindNotAllowed, errMsg, nil)
+	}
+
+	// execute protocol-specific read operation
+	driver := container.ProtocolDriverFrom(dic.Get)
+	results, err := driver.HandleReadCommands(device.Name, device.Protocols, reqs)
+	if err != nil {
+		errMsg := fmt.Sprintf("error reading Regex DeviceResource(s) %s for %s", regexResourceName, device.Name)
+		return res, errors.NewCommonEdgeX(errors.KindServerError, errMsg, err)
+	}
+
+	// convert CommandValue to Event
+	configuration := container.ConfigurationFrom(dic.Get)
+	res, edgexErr = transformer.CommandValuesToEventDTO(results, device.Name, regexResourceName, configuration.Device.DataTransform, dic)
+	if edgexErr != nil {
+		return res, errors.NewCommonEdgeX(errors.KindServerError, "failed to convert CommandValue to Event", err)
+	}
+
+	return res, nil
+}
+
 func readDeviceCommand(device models.Device, commandName string, attributes string, dic *di.Container) (res *dtos.Event, edgexErr errors.EdgeX) {
 	dc, ok := cache.Profiles().DeviceCommand(device.ProfileName, commandName)
 	if !ok {
-		errMsg := fmt.Sprintf("deviceCommand %s not found", commandName)
+		errMsg := fmt.Sprintf("DeviceCommand %s not found", commandName)
 		return res, errors.NewCommonEdgeX(errors.KindEntityDoesNotExist, errMsg, nil)
 	}
 	// check deviceCommand is not write-only
 	if dc.ReadWrite == common.ReadWrite_W {
-		errMsg := fmt.Sprintf("deviceCommand %s is marked as write-only", dc.Name)
+		errMsg := fmt.Sprintf("DeviceCommand %s is marked as write-only", dc.Name)
 		return res, errors.NewCommonEdgeX(errors.KindNotAllowed, errMsg, nil)
 	}
 	// check ResourceOperation count does not exceed MaxCmdOps defined in configuration
@@ -163,7 +219,7 @@ func readDeviceCommand(device models.Device, commandName string, attributes stri
 		// check the deviceResource in ResourceOperation actually exist
 		dr, ok := cache.Profiles().DeviceResource(device.ProfileName, drName)
 		if !ok {
-			errMsg := fmt.Sprintf("deviceResource %s in GET commnd %s for %s not defined", drName, dc.Name, device.Name)
+			errMsg := fmt.Sprintf("DeviceResource %s in GET commnd %s for %s not defined", drName, dc.Name, device.Name)
 			return res, errors.NewCommonEdgeX(errors.KindServerError, errMsg, nil)
 		}
 
@@ -198,12 +254,12 @@ func readDeviceCommand(device models.Device, commandName string, attributes stri
 func writeDeviceResource(device models.Device, resourceName string, attributes string, requests map[string]any, dic *di.Container) (*dtos.Event, errors.EdgeX) {
 	dr, ok := cache.Profiles().DeviceResource(device.ProfileName, resourceName)
 	if !ok {
-		errMsg := fmt.Sprintf("deviceResource %s not found", resourceName)
+		errMsg := fmt.Sprintf("DeviceResource %s not found", resourceName)
 		return nil, errors.NewCommonEdgeX(errors.KindEntityDoesNotExist, errMsg, nil)
 	}
 	// check deviceResource is not read-only
 	if dr.Properties.ReadWrite == common.ReadWrite_R {
-		errMsg := fmt.Sprintf("deviceResource %s is marked as read-only", dr.Name)
+		errMsg := fmt.Sprintf("DeviceResource %s is marked as read-only", dr.Name)
 		return nil, errors.NewCommonEdgeX(errors.KindNotAllowed, errMsg, nil)
 	}
 
@@ -213,7 +269,7 @@ func writeDeviceResource(device models.Device, resourceName string, attributes s
 		if dr.Properties.DefaultValue != "" {
 			v = dr.Properties.DefaultValue
 		} else {
-			errMsg := fmt.Sprintf("deviceResource %s not found in request body and no default value defined", dr.Name)
+			errMsg := fmt.Sprintf("DeviceResource %s not found in request body and no default value defined", dr.Name)
 			return nil, errors.NewCommonEdgeX(errors.KindServerError, errMsg, nil)
 		}
 	}
@@ -264,12 +320,12 @@ func writeDeviceResource(device models.Device, resourceName string, attributes s
 func writeDeviceCommand(device models.Device, commandName string, attributes string, requests map[string]any, dic *di.Container) (*dtos.Event, errors.EdgeX) {
 	dc, ok := cache.Profiles().DeviceCommand(device.ProfileName, commandName)
 	if !ok {
-		errMsg := fmt.Sprintf("deviceCommand %s not found", commandName)
+		errMsg := fmt.Sprintf("DeviceCommand %s not found", commandName)
 		return nil, errors.NewCommonEdgeX(errors.KindEntityDoesNotExist, errMsg, nil)
 	}
 	// check deviceCommand is not read-only
 	if dc.ReadWrite == common.ReadWrite_R {
-		errMsg := fmt.Sprintf("deviceCommand %s is marked as read-only", dc.Name)
+		errMsg := fmt.Sprintf("DeviceCommand %s is marked as read-only", dc.Name)
 		return nil, errors.NewCommonEdgeX(errors.KindNotAllowed, errMsg, nil)
 	}
 	// check ResourceOperation count does not exceed MaxCmdOps defined in configuration
@@ -286,7 +342,7 @@ func writeDeviceCommand(device models.Device, commandName string, attributes str
 		// check the deviceResource in ResourceOperation actually exist
 		dr, ok := cache.Profiles().DeviceResource(device.ProfileName, drName)
 		if !ok {
-			errMsg := fmt.Sprintf("deviceResource %s in SET commnd %s for %s not defined", drName, dc.Name, device.Name)
+			errMsg := fmt.Sprintf("DeviceResource %s in SET commnd %s for %s not defined", drName, dc.Name, device.Name)
 			return nil, errors.NewCommonEdgeX(errors.KindServerError, errMsg, nil)
 		}
 
@@ -298,7 +354,7 @@ func writeDeviceCommand(device models.Device, commandName string, attributes str
 			} else if dr.Properties.DefaultValue != "" {
 				value = dr.Properties.DefaultValue
 			} else {
-				errMsg := fmt.Sprintf("deviceResource %s not found in request body and no default value defined", dr.Name)
+				errMsg := fmt.Sprintf("DeviceResource %s not found in request body and no default value defined", dr.Name)
 				return nil, errors.NewCommonEdgeX(errors.KindServerError, errMsg, nil)
 			}
 		}
diff --git a/internal/autoevent/executor.go b/internal/autoevent/executor.go
index d218d0a1..6d53a23e 100644
--- a/internal/autoevent/executor.go
+++ b/internal/autoevent/executor.go
@@ -1,6 +1,6 @@
 // -*- Mode: Go; indent-tabs-mode: t -*-
 //
-// Copyright (C) 2019-2022 IOTech Ltd
+// Copyright (C) 2019-2023 IOTech Ltd
 //
 // SPDX-License-Identifier: Apache-2.0
 
@@ -86,7 +86,7 @@ func readResource(e *Executor, dic *di.Container) (event *dtos.Event, err errors
 	vars[common.Name] = e.deviceName
 	vars[common.Command] = e.sourceName
 
-	res, err := application.GetCommand(context.Background(), e.deviceName, e.sourceName, "", dic)
+	res, err := application.GetCommand(context.Background(), e.deviceName, e.sourceName, "", true, dic)
 	if err != nil {
 		return event, err
 	}
diff --git a/internal/cache/profiles.go b/internal/cache/profiles.go
index 7c425a13..30749e2f 100644
--- a/internal/cache/profiles.go
+++ b/internal/cache/profiles.go
@@ -1,6 +1,6 @@
 // -*- Mode: Go; indent-tabs-mode: t -*-
 //
-// Copyright (C) 2020-2021 IOTech Ltd
+// Copyright (C) 2020-2023 IOTech Ltd
 //
 // SPDX-License-Identifier: Apache-2.0
 
@@ -8,6 +8,7 @@ package cache
 
 import (
 	"fmt"
+	"regexp"
 	"sync"
 
 	"github.com/edgexfoundry/go-mod-core-contracts/v3/errors"
@@ -25,6 +26,7 @@ type ProfileCache interface {
 	Update(profile models.DeviceProfile) errors.EdgeX
 	RemoveByName(name string) errors.EdgeX
 	DeviceResource(profileName string, resourceName string) (models.DeviceResource, bool)
+	DeviceResourcesByRegex(profileName string, regex string) ([]models.DeviceResource, bool)
 	DeviceCommand(profileName string, commandName string) (models.DeviceCommand, bool)
 	ResourceOperation(profileName string, deviceResource string) (models.ResourceOperation, errors.EdgeX)
 }
@@ -167,6 +169,27 @@ func (p *profileCache) DeviceResource(profileName string, resourceName string) (
 	return dr, ok
 }
 
+// DeviceResourcesByRegex returns matched DeviceResource list with given profileName and regex pattern
+func (p *profileCache) DeviceResourcesByRegex(profileName string, regex string) ([]models.DeviceResource, bool) {
+	p.mutex.RLock()
+	defer p.mutex.RUnlock()
+
+	drs, ok := p.deviceResourceMap[profileName]
+	if !ok {
+		return nil, false
+	}
+
+	var res []models.DeviceResource
+	for _, dr := range drs {
+		match, _ := regexp.MatchString(regex, dr.Name)
+		if match {
+			res = append(res, dr)
+		}
+	}
+
+	return res, true
+}
+
 // DeviceCommand returns the DeviceCommand with given profileName and commandName
 func (p *profileCache) DeviceCommand(profileName string, commandName string) (models.DeviceCommand, bool) {
 	p.mutex.RLock()
diff --git a/internal/common/utils.go b/internal/common/utils.go
index 35f8bcb0..0e817618 100644
--- a/internal/common/utils.go
+++ b/internal/common/utils.go
@@ -9,7 +9,6 @@ package common
 
 import (
 	"context"
-	"net/url"
 
 	"github.com/edgexfoundry/device-sdk-go/v3/internal/cache"
 	"github.com/edgexfoundry/device-sdk-go/v3/internal/container"
@@ -72,7 +71,7 @@ func SendEvent(event *dtos.Event, correlationID string, dic *di.Container) {
 	ctx = context.WithValue(ctx, common.ContentType, encoding) // nolint: staticcheck
 	envelope := types.NewMessageEnvelope(bytes, ctx)
 	serviceName := container.DeviceServiceFrom(dic.Get).Name
-	publishTopic := common.BuildTopic(configuration.MessageBus.GetBaseTopicPrefix(), common.EventsPublishTopic, DeviceServiceEventPrefix, serviceName, event.ProfileName, event.DeviceName, url.QueryEscape(event.SourceName))
+	publishTopic := common.BuildTopic(configuration.MessageBus.GetBaseTopicPrefix(), common.EventsPublishTopic, DeviceServiceEventPrefix, serviceName, event.ProfileName, event.DeviceName, common.URLEncode(event.SourceName))
 	err = mc.Publish(envelope, publishTopic)
 	if err != nil {
 		lc.Errorf("Failed to publish event to MessageBus: %s", err)
diff --git a/internal/controller/http/command.go b/internal/controller/http/command.go
index 2aa5d724..f2f5ac31 100644
--- a/internal/controller/http/command.go
+++ b/internal/controller/http/command.go
@@ -39,19 +39,24 @@ func (c *RestController) GetCommand(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	event, err := application.GetCommand(ctx, deviceName, commandName, queryParams, c.dic)
+	regexCmd := true
+	if useRegex := reserved.Get(common.RegexCommand); useRegex == common.ValueFalse {
+		regexCmd = false
+	}
+
+	event, err := application.GetCommand(ctx, deviceName, commandName, queryParams, regexCmd, c.dic)
 	if err != nil {
 		c.sendEdgexError(w, r, err, common.ApiDeviceNameCommandNameRoute)
 		return
 	}
 
-	// push event to CoreData if specified (default no)
-	if ok, exist := reserved[common.PushEvent]; exist && ok[0] == common.ValueTrue {
+	// push event to CoreData if specified (default false)
+	if pushEvent := reserved.Get(common.PushEvent); pushEvent == common.ValueTrue {
 		go sdkCommon.SendEvent(event, correlationId, c.dic)
 	}
 
-	// return event in http response if specified (default yes)
-	if ok, exist := reserved[common.ReturnEvent]; !exist || ok[0] == common.ValueTrue {
+	// return event in http response if specified (default true)
+	if returnEvent := reserved.Get(common.ReturnEvent); returnEvent == "" || returnEvent == common.ValueTrue {
 		res := responses.NewEventResponse("", "", http.StatusOK, *event)
 		c.sendEventResponse(w, r, res, http.StatusOK)
 		return
diff --git a/internal/controller/messaging/command.go b/internal/controller/messaging/command.go
index 4336b407..35f2ee65 100644
--- a/internal/controller/messaging/command.go
+++ b/internal/controller/messaging/command.go
@@ -104,11 +104,11 @@ func getCommand(ctx context.Context, msgEnvelope types.MessageEnvelope, response
 
 	lc := bootstrapContainer.LoggingClientFrom(dic.Get)
 	messageBus := bootstrapContainer.MessagingClientFrom(dic.Get)
-	rawQuery, pushEvent, returnEvent := filterQueryParams(msgEnvelope.QueryParams)
+	rawQuery, reserved := filterQueryParams(msgEnvelope.QueryParams)
 
 	// TODO: fix properly in EdgeX 3.0
 	ctx = context.WithValue(ctx, common.CorrelationHeader, msgEnvelope.CorrelationID) // nolint: staticcheck
-	event, edgexErr := application.GetCommand(ctx, deviceName, commandName, rawQuery, dic)
+	event, edgexErr := application.GetCommand(ctx, deviceName, commandName, rawQuery, reserved[common.RegexCommand], dic)
 	if edgexErr != nil {
 		lc.Errorf("Failed to process get device command %s for device %s: %s", commandName, deviceName, edgexErr.Error())
 		responseEnvelope = types.NewMessageEnvelopeWithError(msgEnvelope.RequestID, edgexErr.Error())
@@ -122,7 +122,7 @@ func getCommand(ctx context.Context, msgEnvelope types.MessageEnvelope, response
 	var err error
 	var encoding string
 	var eventResponseBytes []byte
-	if returnEvent {
+	if reserved[common.ReturnEvent] {
 		eventResponse := responses.NewEventResponse(msgEnvelope.RequestID, "", http.StatusOK, *event)
 		eventResponseBytes, encoding, err = eventResponse.Encode()
 		if err != nil {
@@ -156,7 +156,7 @@ func getCommand(ctx context.Context, msgEnvelope types.MessageEnvelope, response
 		return
 	}
 
-	if pushEvent {
+	if reserved[common.PushEvent] {
 		go sdkCommon.SendEvent(event, msgEnvelope.CorrelationID, dic)
 	}
 
@@ -167,7 +167,7 @@ func setCommand(ctx context.Context, msgEnvelope types.MessageEnvelope, response
 
 	lc := bootstrapContainer.LoggingClientFrom(dic.Get)
 	messageBus := bootstrapContainer.MessagingClientFrom(dic.Get)
-	rawQuery, _, _ := filterQueryParams(msgEnvelope.QueryParams)
+	rawQuery, _ := filterQueryParams(msgEnvelope.QueryParams)
 
 	requestPayload := make(map[string]any)
 	err := json.Unmarshal(msgEnvelope.Payload, &requestPayload)
@@ -216,23 +216,30 @@ func setCommand(ctx context.Context, msgEnvelope types.MessageEnvelope, response
 	}
 }
 
-func filterQueryParams(queries map[string]string) (string, bool, bool) {
+func filterQueryParams(queries map[string]string) (string, map[string]bool) {
 	var rawQuery []string
-	pushEvent, returnEvent := false, true
+	reserved := make(map[string]bool)
+	reserved[common.PushEvent] = false
+	reserved[common.ReturnEvent] = true
+	reserved[common.RegexCommand] = true
+
 	for k, v := range queries {
 		if k == common.PushEvent && v == common.ValueTrue {
-			pushEvent = true
+			reserved[common.PushEvent] = true
 			continue
 		}
 		if k == common.ReturnEvent && v == common.ValueFalse {
-			returnEvent = false
+			reserved[common.ReturnEvent] = false
 			continue
 		}
+		if k == common.RegexCommand && v == common.ValueFalse {
+			reserved[common.RegexCommand] = false
+		}
 		if strings.HasPrefix(k, sdkCommon.SDKReservedPrefix) {
 			continue
 		}
 		rawQuery = append(rawQuery, fmt.Sprintf("%s=%s", k, v))
 	}
 
-	return strings.Join(rawQuery, "&"), pushEvent, returnEvent
+	return strings.Join(rawQuery, "&"), reserved
 }
diff --git a/openapi/v3/device-sdk.yaml b/openapi/v3/device-sdk.yaml
index 7bee3029..d6e036d7 100644
--- a/openapi/v3/device-sdk.yaml
+++ b/openapi/v3/device-sdk.yaml
@@ -387,6 +387,16 @@ paths:
             default: true
           example: false
           description: "If set to false, there will be no Event returned in the http response"
+        - in: query
+          name: ds-regexcmd
+          schema:
+            type: string
+            enum:
+              - true
+              - false
+            default: true
+          example: false
+          description: "If set to false, the command name will be treated as normal string instead of regex syntax"
       responses:
         '200':
           description: String as returned by the device/sensor through the device service.


Test patch:
diff --git a/internal/cache/profiles_test.go b/internal/cache/profiles_test.go
index 3421f312..a26057b9 100644
--- a/internal/cache/profiles_test.go
+++ b/internal/cache/profiles_test.go
@@ -1,5 +1,5 @@
 //
-// Copyright (C) 2021 IOTech Ltd
+// Copyright (C) 2021-2023 IOTech Ltd
 //
 // SPDX-License-Identifier: Apache-2.0
 
@@ -191,3 +191,26 @@ func Test_profileCache_ResourceOperation(t *testing.T) {
 		})
 	}
 }
+
+func Test_profileCache_DeviceResourcesByRegex(t *testing.T) {
+	newProfileCache([]models.DeviceProfile{testProfile})
+
+	tests := []struct {
+		name              string
+		profileName       string
+		regexResourceName string
+		deviceResources   []models.DeviceResource
+		expected          bool
+	}{
+		{"valid - resources found by regex pattern", TestProfile, ".+Resource", testProfile.DeviceResources, true},
+		{"Valid -  resources not found by regex pattern", TestProfile, "nil", nil, true},
+		{"invalid - profile not found", "nil", "%2E%2B-resource", nil, false},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			res, ok := pc.DeviceResourcesByRegex(tt.profileName, tt.regexResourceName)
+			assert.Equal(t, res, tt.deviceResources, "DeviceResource returns wrong deviceResource")
+			assert.Equal(t, ok, tt.expected, "DeviceResource returns opposite result")
+		})
+	}
+}
diff --git a/internal/controller/http/command_test.go b/internal/controller/http/command_test.go
index a3ef14fb..9e72fbb9 100644
--- a/internal/controller/http/command_test.go
+++ b/internal/controller/http/command_test.go
@@ -56,6 +56,8 @@ const (
 	readOnlyResource  = "ro-resource"
 	writeOnlyResource = "wo-resource"
 	objectResource    = "object-resource"
+
+	testRegexResource = "^t.+-resource"
 )
 
 func mockDic() *di.Container {
@@ -218,6 +220,7 @@ func TestRestController_GetCommand(t *testing.T) {
 	}{
 		{"valid - read device resource", testDevice, testResource, http.StatusOK},
 		{"valid - read device command", testDevice, testCommand, http.StatusOK},
+		{"valid - read regex device resource", testDevice, testRegexResource, http.StatusOK},
 		{"invalid - device name parameter is empty", "", testResource, http.StatusBadRequest},
 		{"invalid - command is empty", testDevice, "", http.StatusBadRequest},
 		{"invalid - device name not found", "notFound", testCommand, http.StatusNotFound},