diff --git a/Makefile b/Makefile index 2a7f6bd6..6056eb6e 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,8 @@ CERT_TOOL_SIGN_ALG ?= ECDSA-SHA256 # supported values: P256, P384, P521 CERT_TOOL_ELLIPTIC_CURVE ?= P256 DEVSIM_IMAGE ?= ghcr.io/iotivity/iotivity-lite/cloud-server-discovery-resource-observable-debug:vnext -HUB_TEST_DEVICE_IMAGE = ghcr.io/plgd-dev/hub/test-cloud-server:main +# HUB_TEST_DEVICE_IMAGE = ghcr.io/plgd-dev/hub/test-cloud-server:main +HUB_TEST_DEVICE_IMAGE = ghcr.io/plgd-dev/hub/test-cloud-server:vnext-pr1274 default: build @@ -123,7 +124,34 @@ test: env build-testcontainer -v $(TMP_PATH):/tmp \ $(SERVICE_NAME):$(VERSION_TAG) -test.parallel 1 -test.v -test.coverprofile=/tmp/coverage.txt -test-bridge: +test-bridge/clean: + pkill -KILL bridge-device || : + rm -rf $(TMP_PATH)/bridge || : + +define SET-BRIDGE-DEVICE-CONFIG + yq -i '.apis.coap.externalAddresses=["127.0.0.1:15683","[::1]:15683"]' $(1) + yq -i '.cloud.enabled=true' $(1) + yq -i '.cloud.cloudID="$(CLOUD_SID)"' $(1) + yq -i '.cloud.tls.caPoolPath="$(2)/data/certs/root_ca.crt"' $(1) + yq -i '.cloud.tls.keyPath="$(2)/data/certs/external/coap-gateway.key"' $(1) + yq -i '.cloud.tls.certPath="$(2)/data/certs/external/coap-gateway.crt"' $(1) + yq -i '.numGeneratedBridgedDevices=3' $(1) + yq -i '.numResourcesPerDevice=0' $(1) + yq -i '.thingDescription.enabled=true' $(1) + yq -i '.thingDescription.file="$(2)/bridge/bridge-device.jsonld"' $(1) +endef + +# config-docker.yaml -> copy of configuration with paths valid inside docker container +# config-test.yaml -> copy of configuration with paths valid on host machine +test-bridge/env: test-bridge/clean + mkdir -p $(TMP_PATH)/bridge + cp ./cmd/bridge-device/bridge-device.jsonld $(TMP_PATH)/bridge + cp ./cmd/bridge-device/config.yaml $(TMP_PATH)/bridge/config-docker.yaml + $(call SET-BRIDGE-DEVICE-CONFIG,$(TMP_PATH)/bridge/config-docker.yaml,) + cp $(TMP_PATH)/bridge/config-docker.yaml $(TMP_PATH)/bridge/config-test.yaml + $(call SET-BRIDGE-DEVICE-CONFIG,$(TMP_PATH)/bridge/config-test.yaml,$(TMP_PATH)) + +test-bridge: test-bridge/env sudo rm -rf $(TMP_PATH)/data || : mkdir -p $(TMP_PATH)/data # pull image @@ -141,15 +169,10 @@ test-bridge: $(HUB_TEST_DEVICE_IMAGE) # start device - rm -rf $(TMP_PATH)/bridge || : - mkdir -p $(TMP_PATH)/bridge - go build -C ./test/bridge-device -cover -o ./bridge-device + go build -C ./cmd/bridge-device -cover -o ./bridge-device pkill -KILL bridge-device || : - CLOUD_SID=$(CLOUD_SID) CA_POOL=$(TMP_PATH)/data/certs/root_ca.crt \ - CERT_FILE=$(TMP_PATH)/data/certs/external/coap-gateway.crt \ - KEY_FILE=$(TMP_PATH)/data/certs/external/coap-gateway.key \ GOCOVERDIR=$(TMP_PATH)/bridge \ - ./test/bridge-device/bridge-device & + ./cmd/bridge-device/bridge-device -config $(TMP_PATH)/bridge/config-test.yaml & # run tests docker run \ @@ -161,9 +184,11 @@ test-bridge: --env COAP_GATEWAY_CLOUD_ID="$(CLOUD_SID)" \ --env TEST_DEVICE_NAME="bridged-device-0" \ --env TEST_DEVICE_TYPE="bridged" \ + --env TEST_BRIDGE_DEVICE_CONFIG="/bridge/config-docker.yaml" \ --env GRPC_GATEWAY_TEST_DISABLED=1 \ --env IOTIVITY_LITE_TEST_RUN="(TestOffboard|TestOffboardWithoutSignIn|TestOffboardWithRepeat|TestRepublishAfterRefresh)$$" \ -v $(TMP_PATH):/tmp \ + -v $(TMP_PATH)/bridge:/bridge \ -v $(TMP_PATH)/data:/data \ $(HUB_TEST_DEVICE_IMAGE) @@ -175,11 +200,10 @@ test-bridge: done go tool covdata textfmt -i=$(TMP_PATH)/bridge -o $(TMP_PATH)/bridge.coverage.txt -clean: +clean: test-bridge/clean docker rm -f devsim-net-host || : docker rm -f hub-device-tests-environment || : docker rm -f hub-device-tests || : - pkill -KILL bridge-device || : sudo rm -rf .tmp/* .PHONY: build-testcontainer build certificates clean env test unit-test diff --git a/bridge/device/thingDescription/manager.go b/bridge/device/thingDescription/manager.go index 12a5973c..1ad59ff4 100644 --- a/bridge/device/thingDescription/manager.go +++ b/bridge/device/thingDescription/manager.go @@ -1,35 +1,19 @@ package thingDescription import ( - "net/url" "reflect" "sync/atomic" - "github.com/fredbi/uri" "github.com/google/uuid" "github.com/plgd-dev/device/v2/bridge/net" "github.com/plgd-dev/device/v2/bridge/resources" "github.com/plgd-dev/device/v2/pkg/eventloop" "github.com/plgd-dev/device/v2/schema" - "github.com/plgd-dev/go-coap/v3/message" "github.com/plgd-dev/go-coap/v3/message/pool" "github.com/plgd-dev/go-coap/v3/pkg/sync" "github.com/web-of-things-open-source/thingdescription-go/thingDescription" ) -var ( - SecurityNoSec = "nosec_sc" - SecurityDefinitions = map[string]thingDescription.SecurityScheme{ - SecurityNoSec: { - Scheme: "nosec", - }, - } - HTTPSWWWW3Org2022WotTdV11 = thingDescription.HTTPSWWWW3Org2022WotTdV11 - Context = thingDescription.ThingContext{ - Enum: &HTTPSWWWW3Org2022WotTdV11, - } -) - // Resource to avoid import cycle also it is same as in Device package to avoid wrapping it type Resource = interface { Close() @@ -117,115 +101,3 @@ func (t *Manager) NotifySubscriptions(td thingDescription.ThingDescription) { default: } } - -func supportedOperationToTDOperation(ops resources.SupportedOperation) []string { - tdOps := make([]string, 0, 3) - if ops.HasOperation(resources.SupportedOperationRead) { - tdOps = append(tdOps, string(thingDescription.Readproperty)) - } - if ops.HasOperation(resources.SupportedOperationWrite) { - tdOps = append(tdOps, string(thingDescription.Writeproperty)) - } - if ops.HasOperation(resources.SupportedOperationObserve) { - tdOps = append(tdOps, string(thingDescription.Observeproperty), string(thingDescription.Unobserveproperty)) - } - if len(tdOps) == 0 { - return nil - } - return tdOps -} - -func boolToPtr(v bool) *bool { - if !v { - return nil - } - return &v -} - -func stringToPtr(v string) *string { - if v == "" { - return nil - } - return &v -} - -func createForms(deviceID uuid.UUID, href string, supportedOperations resources.SupportedOperation, setForm bool) []thingDescription.FormElementProperty { - if !setForm { - return nil - } - ops := supportedOperationToTDOperation(supportedOperations) - if len(ops) > 0 { - hrefStr := href - if deviceID != uuid.Nil { - hrefStr += "?di=" + deviceID.String() - } - href, err := url.Parse(hrefStr) - if err == nil { - return []thingDescription.FormElementProperty{ - { - ContentType: stringToPtr(message.AppCBOR.String()), - Op: &thingDescription.FormElementPropertyOp{ - StringArray: ops, - }, - Href: *href, - }, - } - } - } - return nil -} - -func PatchPropertyElement(prop thingDescription.PropertyElement, deviceID uuid.UUID, resource Resource, setForm bool) thingDescription.PropertyElement { - ops := resource.SupportsOperations() - observable := ops.HasOperation(resources.SupportedOperationObserve) - isReadOnly := ops.HasOperation(resources.SupportedOperationRead) && !ops.HasOperation(resources.SupportedOperationWrite) - isWriteOnly := ops.HasOperation(resources.SupportedOperationWrite) && !ops.HasOperation(resources.SupportedOperationRead) - resourceTypes := resource.GetResourceTypes() - - prop.Type = &thingDescription.TypeDeclaration{ - StringArray: resourceTypes, - } - prop.Observable = boolToPtr(observable) - prop.ReadOnly = boolToPtr(isReadOnly) - prop.WriteOnly = boolToPtr(isWriteOnly) - prop.Observable = boolToPtr(observable) - prop.Forms = createForms(deviceID, resource.GetHref(), ops, setForm) - return prop -} - -func PatchThingDescription(td thingDescription.ThingDescription, device Device, endpoint string, getPropertyElement func(resourceHref string, resource Resource) (thingDescription.PropertyElement, bool)) thingDescription.ThingDescription { - if td.Context == nil { - td.Context = &Context - } - id, err := uri.Parse("urn:uuid:" + device.GetID().String()) - if err == nil { - td.ID = id - } - td.Title = device.GetName() - if endpoint != "" { - // base - u, err := url.Parse(endpoint) - if err == nil { - td.Base = *u - } - // security - td.Security = &thingDescription.TypeDeclaration{ - String: &SecurityNoSec, - } - // securityDefinitions - td.SecurityDefinitions = SecurityDefinitions - } - - device.Range(func(resourceHref string, resource Resource) bool { - pe, ok := getPropertyElement(resourceHref, resource) - if !ok { - return true - } - if td.Properties == nil { - td.Properties = make(map[string]thingDescription.PropertyElement) - } - td.Properties[resourceHref] = pe - return true - }) - return td -} diff --git a/bridge/device/thingDescription/thingDescription.go b/bridge/device/thingDescription/thingDescription.go new file mode 100644 index 00000000..30353948 --- /dev/null +++ b/bridge/device/thingDescription/thingDescription.go @@ -0,0 +1,181 @@ +package thingDescription + +import ( + "net/url" + + "github.com/fredbi/uri" + "github.com/google/uuid" + "github.com/plgd-dev/device/v2/bridge/resources" + "github.com/web-of-things-open-source/thingdescription-go/thingDescription" +) + +var ( + SecurityNoSec = "nosec_sc" + SecurityDefinitions = map[string]thingDescription.SecurityScheme{ + SecurityNoSec: { + Scheme: "nosec", + }, + } + HTTPSWWWW3Org2022WotTdV11 = thingDescription.HTTPSWWWW3Org2022WotTdV11 + Context = thingDescription.ThingContext{ + Enum: &HTTPSWWWW3Org2022WotTdV11, + } +) + +func SupportedOperationToTDOperations(ops resources.SupportedOperation) []string { + tdOps := make([]string, 0, 3) + type translationItem struct { + resourceOp resources.SupportedOperation + tdOps []string + } + translationTable := []translationItem{ + {resources.SupportedOperationRead, []string{string(thingDescription.Readproperty)}}, + {resources.SupportedOperationWrite, []string{string(thingDescription.Writeproperty)}}, + {resources.SupportedOperationObserve, []string{string(thingDescription.Observeproperty), string(thingDescription.Unobserveproperty)}}, + } + for _, t := range translationTable { + if ops.HasOperation(t.resourceOp) { + tdOps = append(tdOps, t.tdOps...) + } + } + if len(tdOps) == 0 { + return nil + } + return tdOps +} + +func BoolToPtr(v bool) *bool { + if !v { + return nil + } + return &v +} + +func StringToPtr(v string) *string { + if v == "" { + return nil + } + return &v +} + +type PropertyElementOperations struct { + ReadOnly bool + WriteOnly bool + Observable bool +} + +func toPropertyElementOperations(ops resources.SupportedOperation) PropertyElementOperations { + return PropertyElementOperations{ + Observable: ops.HasOperation(resources.SupportedOperationObserve), + ReadOnly: ops.HasOperation(resources.SupportedOperationRead) && !ops.HasOperation(resources.SupportedOperationWrite), + WriteOnly: ops.HasOperation(resources.SupportedOperationWrite) && !ops.HasOperation(resources.SupportedOperationRead), + } +} + +func GetPropertyElementOperations(pe thingDescription.PropertyElement) PropertyElementOperations { + isNotNilAndTrue := func(val *bool) bool { + return val != nil && *val + } + return PropertyElementOperations{ + ReadOnly: isNotNilAndTrue(pe.ReadOnly), + WriteOnly: isNotNilAndTrue(pe.WriteOnly), + Observable: isNotNilAndTrue(pe.Observable), + } +} + +func (p PropertyElementOperations) ToSupportedOperations() resources.SupportedOperation { + var ops resources.SupportedOperation + if p.Observable { + ops |= resources.SupportedOperationObserve + } + if p.ReadOnly { + return ops | resources.SupportedOperationRead + } + if p.WriteOnly { + return ops | resources.SupportedOperationWrite + } + return ops | resources.SupportedOperationRead | resources.SupportedOperationWrite +} + +func PatchPropertyElement(prop thingDescription.PropertyElement, types []string, setForm bool, deviceID uuid.UUID, href string, ops resources.SupportedOperation, contentType string) (thingDescription.PropertyElement, error) { + if len(types) > 0 { + prop.Type = &thingDescription.TypeDeclaration{ + StringArray: types, + } + } + propOps := toPropertyElementOperations(ops) + prop.Observable = BoolToPtr(propOps.Observable) + prop.ReadOnly = BoolToPtr(propOps.ReadOnly) + prop.WriteOnly = BoolToPtr(propOps.WriteOnly) + if !setForm { + return prop, nil + } + opsStrs := SupportedOperationToTDOperations(ops) + if len(opsStrs) == 0 { + return prop, nil + } + var hrefUri *url.URL + if len(href) > 0 { + hrefStr := href + if deviceID != uuid.Nil { + hrefStr += "?di=" + deviceID.String() + } + var err error + hrefUri, err = url.Parse(hrefStr) + if err != nil { + return thingDescription.PropertyElement{}, err + } + } + form := thingDescription.FormElementProperty{ + ContentType: StringToPtr(contentType), + Op: &thingDescription.FormElementPropertyOp{ + StringArray: opsStrs, + }, + } + if hrefUri != nil { + form.Href = *hrefUri + } + prop.Forms = []thingDescription.FormElementProperty{form} + return prop, nil +} + +func GetThingDescriptionID(deviceID string) (uri.URI, error) { + return uri.Parse("urn:uuid:" + deviceID) +} + +func PatchThingDescription(td thingDescription.ThingDescription, device Device, endpoint string, getPropertyElement func(resourceHref string, resource Resource) (thingDescription.PropertyElement, bool)) thingDescription.ThingDescription { + if td.Context == nil { + td.Context = &Context + } + id, err := GetThingDescriptionID(device.GetID().String()) + if err == nil { + td.ID = id + } + td.Title = device.GetName() + if endpoint != "" { + // base + u, err := url.Parse(endpoint) + if err == nil { + td.Base = *u + } + // security + td.Security = &thingDescription.TypeDeclaration{ + String: &SecurityNoSec, + } + // securityDefinitions + td.SecurityDefinitions = SecurityDefinitions + } + + device.Range(func(resourceHref string, resource Resource) bool { + pe, ok := getPropertyElement(resourceHref, resource) + if !ok { + return true + } + if td.Properties == nil { + td.Properties = make(map[string]thingDescription.PropertyElement) + } + td.Properties[resourceHref] = pe + return true + }) + return td +} diff --git a/bridge/resources/thingDescription/ocfResources.go b/bridge/resources/thingDescription/ocfResources.go index 63008024..cdd1817e 100644 --- a/bridge/resources/thingDescription/ocfResources.go +++ b/bridge/resources/thingDescription/ocfResources.go @@ -3,6 +3,12 @@ package thingDescription import ( _ "embed" + "github.com/google/uuid" + bridgeTD "github.com/plgd-dev/device/v2/bridge/device/thingDescription" + schemaCloud "github.com/plgd-dev/device/v2/schema/cloud" + schemaCredential "github.com/plgd-dev/device/v2/schema/credential" + schemaDevice "github.com/plgd-dev/device/v2/schema/device" + schemaMaintenance "github.com/plgd-dev/device/v2/schema/maintenance" "github.com/web-of-things-open-source/thingdescription-go/thingDescription" ) @@ -32,3 +38,28 @@ func GetOCFResourcePropertyElement(resourceHref string) (thingDescription.Proper } return prop, true } + +func patchResourcePropertyElement(pe thingDescription.PropertyElement, deviceID uuid.UUID, resourceTypes []string, resourceHref, contentType string) (thingDescription.PropertyElement, error) { + propOps := bridgeTD.GetPropertyElementOperations(pe) + return bridgeTD.PatchPropertyElement(pe, resourceTypes, true, deviceID, resourceHref, propOps.ToSupportedOperations(), contentType) +} + +func PatchDeviceResourcePropertyElement(pe thingDescription.PropertyElement, deviceID uuid.UUID, baseURL, contentType string, deviceType string) (thingDescription.PropertyElement, error) { + var types []string + if deviceType != "" { + types = []string{schemaDevice.ResourceType, deviceType} + } + return patchResourcePropertyElement(pe, deviceID, types, baseURL+schemaDevice.ResourceURI, contentType) +} + +func PatchMaintenanceResourcePropertyElement(pe thingDescription.PropertyElement, deviceID uuid.UUID, baseURL, contentType string) (thingDescription.PropertyElement, error) { + return patchResourcePropertyElement(pe, deviceID, []string{schemaMaintenance.ResourceType}, baseURL+schemaMaintenance.ResourceURI, contentType) +} + +func PatchCloudResourcePropertyElement(pe thingDescription.PropertyElement, deviceID uuid.UUID, baseURL, contentType string) (thingDescription.PropertyElement, error) { + return patchResourcePropertyElement(pe, deviceID, []string{schemaCloud.ResourceType}, baseURL+schemaCloud.ResourceURI, contentType) +} + +func PatchCredentialResourcePropertyElement(pe thingDescription.PropertyElement, deviceID uuid.UUID, baseURL, contentType string) (thingDescription.PropertyElement, error) { + return patchResourcePropertyElement(pe, deviceID, []string{schemaCredential.ResourceType}, baseURL+schemaCredential.ResourceURI, contentType) +} diff --git a/bridge/resources/thingDescription/ocfResources.jsonld b/bridge/resources/thingDescription/ocfResources.jsonld index 673dba8e..c50409cc 100644 --- a/bridge/resources/thingDescription/ocfResources.jsonld +++ b/bridge/resources/thingDescription/ocfResources.jsonld @@ -7,6 +7,7 @@ "properties": { "/oic/d": { "title": "Device Information", + "readOnly": true, "type": "object", "properties": { "piid": { @@ -26,7 +27,10 @@ "readOnly": true, "format": "uuid" } - } + }, + "@type": [ + "oic.wk.d" + ] }, "/oic/mnt": { "title": "Maintenance", @@ -36,7 +40,10 @@ "title": "Factory Reset", "type": "boolean" } - } + }, + "@type": [ + "oic.wk.mnt" + ] }, "/CoapCloudConfResURI": { "title": "CoapCloudConfResURI", @@ -76,7 +83,177 @@ "title": "Last error code", "type": "integer" } - } + }, + "@type": [ + "oic.r.coapcloudconf" + ] + }, + "/oic/sec/cred": { + "title": "Credentials", + "type": "object", + "properties": { + "creds": { + "title": "Credentials", + "type": "array", + "items": { + "type": "object", + "properties": { + "credid": { + "title": "Credential ID", + "type": "integer", + "minimum": 0, + "maximum": 65535 + }, + "subjectuuid": { + "title": "Subject UUID", + "type": "string", + "format": "uuid" + }, + "roleid": { + "title": "Role ID", + "type": "object", + "properties": { + "role": { + "title": "Role", + "type": "string", + "readOnly": true + }, + "authority": { + "title": "Authority", + "type": "string", + "readOnly": true + } + }, + "required": [ + "role" + ] + }, + "credtype": { + "title": "Credential Type", + "type": "integer", + "enum": [ + "0", + "1", + "2", + "4", + "8", + "16", + "32", + "64", + "128", + "256" + ] + }, + "credusage": { + "title": "Credential Usage", + "type": "string", + "enum": [ + "oic.sec.cred.trustca", + "oic.sec.cred.cert", + "oic.sec.cred.rolecert", + "oic.sec.cred.mfgtrustca", + "oic.sec.cred.mfgtrustanchor" + ] + }, + "publicdata": { + "title": "Public Data", + "type": "object", + "properties": { + "data": { + "title": "Data", + "type": "string" + }, + "encoding": { + "title": "Encoding format", + "type": "string", + "enum": [ + "oic.sec.encoding.jwt", + "oic.sec.encoding.cwt", + "oic.sec.encoding.base64", + "oic.sec.encoding.uri", + "oic.sec.encoding.pem", + "oic.sec.encoding.der", + "oic.sec.encoding.raw" + ] + } + } + }, + "privatedata": { + "title": "Private Data", + "type": "object", + "properties": { + "data": { + "title": "Data", + "type": "string", + "writeOnly": true + }, + "encoding": { + "title": "Encoding format", + "type": "string", + "enum": [ + "oic.sec.encoding.jwt", + "oic.sec.encoding.cwt", + "oic.sec.encoding.base64", + "oic.sec.encoding.uri", + "oic.sec.encoding.handle", + "oic.sec.encoding.raw" + ] + }, + "handle": { + "title": "Handle", + "type": "integer", + "minimum": 0, + "maximum": 65535 + } + }, + "required": [ + "encoding" + ] + }, + "oscore": { + "title": "OSCORE Configuration", + "type": "object", + "properties": { + "senderid": { + "title": "Sender ID", + "type": "string" + }, + "recipientid": { + "title": "Recipient ID", + "type": "string" + }, + "ssn": { + "title": "Sender Sequence Number", + "type": "integer", + "readOnly": true + }, + "desc": { + "title": "Security Context Description", + "type": "string" + } + } + } + }, + "required": [ + "credid", + "subjectuuid", + "credtype" + ] + } + }, + "rowneruuid": { + "title": "Resource Owner ID", + "type": "string", + "format": "uuid" + } + }, + "required": [ + "creds", + "rowneruuid" + ], + "@type": [ + "oic.r.cred" + ] } } } \ No newline at end of file diff --git a/bridge/resources/thingDescription/resource_test.go b/bridge/resources/thingDescription/resource_test.go index 9764247c..eb770820 100644 --- a/bridge/resources/thingDescription/resource_test.go +++ b/bridge/resources/thingDescription/resource_test.go @@ -25,9 +25,8 @@ import ( "testing" "time" - "github.com/fredbi/uri" "github.com/google/uuid" - "github.com/plgd-dev/device/v2/bridge/device/thingDescription" + bridgeDeviceTD "github.com/plgd-dev/device/v2/bridge/device/thingDescription" thingDescriptionResource "github.com/plgd-dev/device/v2/bridge/resources/thingDescription" "github.com/plgd-dev/device/v2/bridge/service" bridgeTest "github.com/plgd-dev/device/v2/bridge/test" @@ -98,8 +97,8 @@ func getEndpoint(t *testing.T, c *client.Client, deviceID string) string { } func getPatchedTD(td wotTD.ThingDescription, d service.Device, epURI string) wotTD.ThingDescription { - return thingDescription.PatchThingDescription(td, d, epURI, func(resourceHref string, resource thingDescription.Resource) (wotTD.PropertyElement, bool) { - return bridgeTest.GetPropertyElement(td, d, epURI, resourceHref, resource) + return bridgeDeviceTD.PatchThingDescription(td, d, epURI, func(resourceHref string, resource bridgeDeviceTD.Resource) (wotTD.PropertyElement, bool) { + return bridgeTest.GetPropertyElement(td, d, epURI, resourceHref, resource, message.AppCBOR.String()) }) } @@ -108,8 +107,8 @@ func TestGetThingDescription(t *testing.T) { t.Cleanup(func() { _ = s.Shutdown() }) - deviceID := uuid.New().String() - d := bridgeTest.NewBridgedDevice(t, s, deviceID, true, true, true) + deviceID := uuid.New() + d := bridgeTest.NewBridgedDevice(t, s, deviceID.String(), true, true, true) defer func() { s.DeleteAndCloseDevice(d.GetID()) }() @@ -127,7 +126,7 @@ func TestGetThingDescription(t *testing.T) { require.NoError(t, errC) }() - td, err := bridgeTest.ThingDescription(true, true) + td, err := bridgeTest.ThingDescription(deviceID, "", true, true) require.NoError(t, err) epURI := getEndpoint(t, c, d.GetID().String()) td = getPatchedTD(td, d, epURI) @@ -201,10 +200,10 @@ func TestObserveThingDescription(t *testing.T) { _ = s.Shutdown() }) - deviceID := uuid.New().String() - td, err := bridgeTest.ThingDescription(true, true) + deviceID := uuid.New() + td, err := bridgeTest.ThingDescription(deviceID, "", true, true) require.NoError(t, err) - d := bridgeTest.NewBridgedDeviceWithThingDescription(t, s, deviceID, true, true, &td) + d := bridgeTest.NewBridgedDeviceWithThingDescription(t, s, deviceID.String(), true, true, &td) defer func() { s.DeleteAndCloseDevice(d.GetID()) }() @@ -244,7 +243,7 @@ func TestObserveThingDescription(t *testing.T) { base, err := url.Parse("http://localhost:8080") require.NoError(t, err) - id, err := uri.Parse("urn:uuid:" + deviceID) + id, err := bridgeDeviceTD.GetThingDescriptionID(deviceID.String()) require.NoError(t, err) td2 := wotTD.ThingDescription{ Base: *base, diff --git a/bridge/test/test.go b/bridge/test/test.go index 8b9ea736..341942fc 100644 --- a/bridge/test/test.go +++ b/bridge/test/test.go @@ -22,19 +22,24 @@ import ( "context" "crypto/tls" "crypto/x509" - "encoding/json" + "errors" + "net/url" "testing" "github.com/google/uuid" "github.com/plgd-dev/device/v2/bridge/device" "github.com/plgd-dev/device/v2/bridge/device/cloud" - "github.com/plgd-dev/device/v2/bridge/device/thingDescription" + bridgeDeviceTD "github.com/plgd-dev/device/v2/bridge/device/thingDescription" + thingDescriptionResource "github.com/plgd-dev/device/v2/bridge/resources/thingDescription" "github.com/plgd-dev/device/v2/bridge/service" "github.com/plgd-dev/device/v2/pkg/log" "github.com/plgd-dev/device/v2/schema" schemaCloud "github.com/plgd-dev/device/v2/schema/cloud" "github.com/plgd-dev/device/v2/schema/credential" + schemaDevice "github.com/plgd-dev/device/v2/schema/device" + schemaMaintenance "github.com/plgd-dev/device/v2/schema/maintenance" "github.com/plgd-dev/device/v2/test" + "github.com/plgd-dev/go-coap/v3/message" "github.com/stretchr/testify/require" wotTD "github.com/web-of-things-open-source/thingdescription-go/thingDescription" ) @@ -110,21 +115,22 @@ func makeDeviceConfig(id uuid.UUID, cloudEnabled bool, credentialEnabled bool) d return cfg } -func GetPropertyElement(td wotTD.ThingDescription, device thingDescription.Device, endpoint string, resourceHref string, resource thingDescription.Resource) (wotTD.PropertyElement, bool) { +func GetPropertyElement(td wotTD.ThingDescription, device bridgeDeviceTD.Device, endpoint string, resourceHref string, resource bridgeDeviceTD.Resource, contentType string) (wotTD.PropertyElement, bool) { propElement, ok := td.Properties[resourceHref] if !ok { return wotTD.PropertyElement{}, false } - propElement = thingDescription.PatchPropertyElement(propElement, device.GetID(), resource, endpoint != "") - return propElement, true + propElement, err := bridgeDeviceTD.PatchPropertyElement(propElement, resource.GetResourceTypes(), endpoint != "", device.GetID(), resource.GetHref(), + resource.SupportsOperations(), contentType) + return propElement, err == nil } func NewBridgedDevice(t *testing.T, s *service.Service, id string, cloudEnabled, credentialEnabled, thingDescriptionEnabled bool, opts ...device.Option) service.Device { - u, err := uuid.Parse(id) + deviceID, err := uuid.Parse(id) require.NoError(t, err) - cfg := makeDeviceConfig(u, cloudEnabled, credentialEnabled) + cfg := makeDeviceConfig(deviceID, cloudEnabled, credentialEnabled) if thingDescriptionEnabled { - td, err := ThingDescription(cloudEnabled, credentialEnabled) + td, err := ThingDescription(deviceID, "", cloudEnabled, credentialEnabled) require.NoError(t, err) return NewBridgedDeviceWithThingDescription(t, s, id, cloudEnabled, credentialEnabled, &td, opts...) } @@ -141,9 +147,9 @@ func NewBridgedDeviceWithThingDescription(t *testing.T, s *service.Service, id s if len(endpoints) > 0 { endpoint = endpoints[0].URI } - newTD := thingDescription.PatchThingDescription(*td, device, endpoint, - func(resourceHref string, resource thingDescription.Resource) (wotTD.PropertyElement, bool) { - return GetPropertyElement(*td, device, endpoint, resourceHref, resource) + newTD := bridgeDeviceTD.PatchThingDescription(*td, device, endpoint, + func(resourceHref string, resource bridgeDeviceTD.Resource) (wotTD.PropertyElement, bool) { + return GetPropertyElement(*td, device, endpoint, resourceHref, resource, message.AppCBOR.String()) }) return &newTD })) @@ -151,204 +157,76 @@ func NewBridgedDeviceWithThingDescription(t *testing.T, s *service.Service, id s return NewBridgedDeviceWithConfig(t, s, cfg, opts...) } -func ThingDescription(cloudEnabled, credentialEnabled bool) (wotTD.ThingDescription, error) { - type ThingDescription struct { - Context string `json:"@context"` - Type []string `json:"@type"` - ID string `json:"id"` - Properties map[string]interface{} `json:"properties"` +func getOCFResourcesProperties(deviceID uuid.UUID, baseURL string, cloudEnabled, credentialEnabled bool) (map[string]wotTD.PropertyElement, error) { + properties := make(map[string]wotTD.PropertyElement) + deviceResource, ok := thingDescriptionResource.GetOCFResourcePropertyElement(schemaDevice.ResourceURI) + if !ok { + return nil, errors.New("device resource not found") + } + deviceResource, err := thingDescriptionResource.PatchDeviceResourcePropertyElement(deviceResource, deviceID, baseURL, message.AppCBOR.String(), "") + if err != nil { + return nil, err } + properties[schemaDevice.ResourceURI] = deviceResource - td := ThingDescription{ - Context: "https://www.w3.org/2019/wot/td/v1", - Type: []string{"Thing"}, - ID: "urn:uuid:bridge", - Properties: map[string]interface{}{ - "/oic/d": map[string]interface{}{ - "title": "Device Information", - "type": "object", - "properties": map[string]interface{}{ - "piid": map[string]interface{}{ - "title": "Protocol Interface ID", - "type": "string", - "readOnly": true, - "format": "uuid", - }, - "n": map[string]interface{}{ - "title": "Device Name", - "type": "string", - "readOnly": true, - }, - "di": map[string]interface{}{ - "title": "Device ID", - "type": "string", - "readOnly": true, - "format": "uuid", - }, - }, - }, - "/oic/mnt": map[string]interface{}{ - "title": "Maintenance", - "type": "object", - "properties": map[string]interface{}{ - "fr": map[string]interface{}{ - "title": "Factory Reset", - "type": "boolean", - }, - }, - }, - }, + maintenanceResource, ok := thingDescriptionResource.GetOCFResourcePropertyElement(schemaMaintenance.ResourceURI) + if !ok { + return nil, errors.New("maintenance resource not found") + } + properties[schemaMaintenance.ResourceURI] = maintenanceResource + maintenanceResource, err = thingDescriptionResource.PatchMaintenanceResourcePropertyElement(maintenanceResource, deviceID, baseURL, message.AppCBOR.String()) + if err != nil { + return nil, err } + properties[schemaMaintenance.ResourceURI] = maintenanceResource if cloudEnabled { - td.Properties[schemaCloud.ResourceURI] = map[string]interface{}{ - "title": "CoapCloudConfResURI", - "type": "object", - "properties": map[string]interface{}{ - "apn": map[string]interface{}{ - "title": "Authorization provider name", - "type": "string", - }, - "cis": map[string]interface{}{ - "title": "Cloud interface server", - "type": "string", - "format": "uri", - }, - "sid": map[string]interface{}{ - "title": "Cloud ID", - "type": "string", - "format": "uuid", - }, - "at": map[string]interface{}{ - "title": "Access token", - "type": "string", - }, - "cps": map[string]interface{}{ - "title": "Provisioning status", - "type": "string", - "enum": []schemaCloud.ProvisioningStatus{ - schemaCloud.ProvisioningStatus_UNINITIALIZED, - schemaCloud.ProvisioningStatus_READY_TO_REGISTER, - schemaCloud.ProvisioningStatus_REGISTERING, - schemaCloud.ProvisioningStatus_REGISTERED, - schemaCloud.ProvisioningStatus_FAILED, - }, - }, - "clec": map[string]interface{}{ - "title": "Last error code", - "type": "integer", - }, - }, + cloudResource, ok := thingDescriptionResource.GetOCFResourcePropertyElement(schemaCloud.ResourceURI) + if !ok { + return nil, errors.New("cloud resource not found") + } + cloudResource, err = thingDescriptionResource.PatchCloudResourcePropertyElement(cloudResource, deviceID, baseURL, message.AppCBOR.String()) + if err != nil { + return nil, err } + properties[schemaCloud.ResourceURI] = cloudResource } if credentialEnabled { - td.Properties[credential.ResourceURI] = map[string]interface{}{ - "title": "Credential", - "type": "object", - "properties": map[string]interface{}{ - "credid": map[string]interface{}{ - "title": "Credential ID", - "type": "integer", - }, - "credtype": map[string]interface{}{ - "title": "Credential Type", - "type": "integer", - "enum": []int{ - int(credential.CredentialType_EMPTY), - int(credential.CredentialType_SYMMETRIC_PAIR_WISE), - int(credential.CredentialType_SYMMETRIC_GROUP), - int(credential.CredentialType_ASYMMETRIC_SIGNING), - int(credential.CredentialType_ASYMMETRIC_SIGNING_WITH_CERTIFICATE), - int(credential.CredentialType_PIN_OR_PASSWORD), - int(credential.CredentialType_ASYMMETRIC_ENCRYPTION_KEY), - }, - }, - "subjectuuid": map[string]interface{}{ - "title": "Subject UUID", - "type": "string", - }, - "credusage": map[string]interface{}{ - "title": "Credential Usage", - "type": "string", - "enum": []credential.CredentialUsage{ - credential.CredentialUsage_TRUST_CA, - credential.CredentialUsage_CERT, - credential.CredentialUsage_ROLE_CERT, - credential.CredentialUsage_MFG_TRUST_CA, - credential.CredentialUsage_MFG_CERT, - }, - }, - "privatedata": map[string]interface{}{ - "title": "Private Data", - "type": "object", - "properties": map[string]interface{}{ - "data": map[string]interface{}{ - "title": "Data", - "type": "string", - }, - "encoding": map[string]interface{}{ - "title": "Encoding", - "type": "string", - "enum": []credential.CredentialPrivateDataEncoding{ - credential.CredentialPrivateDataEncoding_JWT, - credential.CredentialPrivateDataEncoding_CWT, - credential.CredentialPrivateDataEncoding_BASE64, - credential.CredentialPrivateDataEncoding_URI, - credential.CredentialPrivateDataEncoding_HANDLE, - credential.CredentialPrivateDataEncoding_RAW, - }, - }, - }, - }, - "publicdata": map[string]interface{}{ - "title": "Public Data", - "type": "object", - "properties": map[string]interface{}{ - "data": map[string]interface{}{ - "title": "Data", - "type": "string", - }, - "encoding": map[string]interface{}{ - "title": "Encoding", - "type": "string", - "enum": []credential.CredentialPublicDataEncoding{ - credential.CredentialPublicDataEncoding_JWT, - credential.CredentialPublicDataEncoding_CWT, - credential.CredentialPublicDataEncoding_BASE64, - credential.CredentialPublicDataEncoding_URI, - credential.CredentialPublicDataEncoding_PEM, - credential.CredentialPublicDataEncoding_DER, - credential.CredentialPublicDataEncoding_RAW, - }, - }, - }, - }, - "roleid": map[string]interface{}{ - "title": "Role ID", - "type": "object", - "properties": map[string]interface{}{ - "authority": map[string]interface{}{ - "title": "Authority", - "type": "string", - }, - "role": map[string]interface{}{ - "title": "Role", - "type": "string", - }, - }, - }, - "tag": map[string]interface{}{ - "title": "Tag", - "type": "string", - }, - }, + credentialResource, ok := thingDescriptionResource.GetOCFResourcePropertyElement(credential.ResourceURI) + if !ok { + return nil, errors.New("credential resource not found") + } + credentialResource, err = thingDescriptionResource.PatchCredentialResourcePropertyElement(credentialResource, deviceID, baseURL, message.AppCBOR.String()) + if err != nil { + return nil, err + } + properties[credential.ResourceURI] = credentialResource + } + return properties, nil +} + +func ThingDescription(deviceID uuid.UUID, baseURL string, cloudEnabled, credentialEnabled bool) (wotTD.ThingDescription, error) { + td := wotTD.ThingDescription{} + td.Context = &bridgeDeviceTD.Context + td.Type = &wotTD.TypeDeclaration{StringArray: []string{"Thing"}} + id, err := bridgeDeviceTD.GetThingDescriptionID(deviceID.String()) + if err != nil { + return wotTD.ThingDescription{}, err + } + td.ID = id + if baseURL != "" { + base, errP := url.Parse(baseURL) + if errP != nil { + return wotTD.ThingDescription{}, errP } + td.Base = *base } - tdJson, err := json.Marshal(td) + properties, err := getOCFResourcesProperties(deviceID, baseURL, cloudEnabled, credentialEnabled) if err != nil { return wotTD.ThingDescription{}, err } - return wotTD.UnmarshalThingDescription(tdJson) + td.Properties = properties + return td, nil } diff --git a/cmd/bridge-device/bridge-device.jsonld b/cmd/bridge-device/bridge-device.jsonld index 245902d7..547b123c 100644 --- a/cmd/bridge-device/bridge-device.jsonld +++ b/cmd/bridge-device/bridge-device.jsonld @@ -4,36 +4,5 @@ "Thing" ], "id": "urn:uuid:bridge", - "properties": { - "/test/0": { - "title": "Test Resource", - "type": "object", - "properties": { - "Name": { - "title": "Name", - "type": "string" - } - } - }, - "/test/1": { - "title": "Test Resource", - "type": "object", - "properties": { - "Name": { - "title": "Name", - "type": "string" - } - } - }, - "/test/2": { - "title": "Test Resource", - "type": "object", - "properties": { - "Name": { - "title": "Name", - "type": "string" - } - } - } - } + "properties": {} } \ No newline at end of file diff --git a/cmd/bridge-device/device.go b/cmd/bridge-device/device.go new file mode 100644 index 00000000..9f2a6308 --- /dev/null +++ b/cmd/bridge-device/device.go @@ -0,0 +1,120 @@ +package main + +import ( + "bytes" + "fmt" + "sync" + "sync/atomic" + "time" + + "github.com/plgd-dev/device/v2/bridge/net" + "github.com/plgd-dev/device/v2/bridge/resources" + "github.com/plgd-dev/device/v2/bridge/service" + bridgeDevice "github.com/plgd-dev/device/v2/cmd/bridge-device/device" + "github.com/plgd-dev/device/v2/pkg/codec/cbor" + codecOcf "github.com/plgd-dev/device/v2/pkg/codec/ocf" + "github.com/plgd-dev/device/v2/schema/interfaces" + "github.com/plgd-dev/go-coap/v3/message" + "github.com/plgd-dev/go-coap/v3/message/codes" + "github.com/plgd-dev/go-coap/v3/message/pool" + coapSync "github.com/plgd-dev/go-coap/v3/pkg/sync" +) + +type resourceData struct { + Name string `json:"name,omitempty"` +} + +type resourceDataSync struct { + resourceData + lock sync.Mutex +} + +func (r *resourceDataSync) setName(name string) { + r.lock.Lock() + defer r.lock.Unlock() + r.Name = name +} + +func (r *resourceDataSync) copy() resourceData { + r.lock.Lock() + defer r.lock.Unlock() + return resourceData{ + Name: r.Name, + } +} + +func addResources(d service.Device, numResources int) { + if numResources <= 0 { + return + } + obsWatcher := coapSync.NewMap[uint64, func()]() + for i := 0; i < numResources; i++ { + addResource(d, i, obsWatcher) + } + go func() { + // notify observers every 500ms + for { + time.Sleep(time.Millisecond * 500) + obsWatcher.Range(func(_ uint64, h func()) bool { + h() + return true + }) + } + }() +} + +func addResource(d service.Device, idx int, obsWatcher *coapSync.Map[uint64, func()]) { + rds := resourceDataSync{ + resourceData: resourceData{ + Name: fmt.Sprintf("test-%v", idx), + }, + } + + resHandler := func(req *net.Request) (*pool.Message, error) { + resp := pool.NewMessage(req.Context()) + switch req.Code() { + case codes.GET: + resp.SetCode(codes.Content) + case codes.POST: + resp.SetCode(codes.Changed) + default: + return nil, fmt.Errorf("invalid method %v", req.Code()) + } + resp.SetContentFormat(message.AppOcfCbor) + data, err := cbor.Encode(rds.copy()) + if err != nil { + return nil, err + } + resp.SetBody(bytes.NewReader(data)) + return resp, nil + } + + resPostHandler := func(req *net.Request) (*pool.Message, error) { + codec := codecOcf.VNDOCFCBORCodec{} + var newData resourceData + err := codec.Decode(req.Message, &newData) + if err != nil { + return nil, err + } + rds.setName(newData.Name) + return resHandler(req) + } + + var subID atomic.Uint64 + res := resources.NewResource(bridgeDevice.GetTestResourceHref(idx), resHandler, resPostHandler, []string{bridgeDevice.TestResourceType}, []string{interfaces.OC_IF_BASELINE, interfaces.OC_IF_RW}) + res.SetObserveHandler(d.GetLoop(), func(req *net.Request, handler func(msg *pool.Message, err error)) (cancel func(), err error) { + sub := subID.Add(1) + obsWatcher.Store(sub, func() { + resp, err := resHandler(req) + if err != nil { + handler(nil, err) + return + } + handler(resp, nil) + }) + return func() { + obsWatcher.Delete(sub) + }, nil + }) + d.AddResources(res) +} diff --git a/cmd/bridge-device/config.go b/cmd/bridge-device/device/config.go similarity index 77% rename from cmd/bridge-device/config.go rename to cmd/bridge-device/device/config.go index c1162365..4cec9f97 100644 --- a/cmd/bridge-device/config.go +++ b/cmd/bridge-device/device/config.go @@ -1,10 +1,14 @@ -package main +package device import ( "errors" + "fmt" + "os" + "path/filepath" "github.com/plgd-dev/device/v2/bridge/service" "github.com/plgd-dev/device/v2/pkg/log" + "gopkg.in/yaml.v3" ) type TLSConfig struct { @@ -30,6 +34,7 @@ type LogConfig struct { type CloudConfig struct { Enabled bool `yaml:"enabled" json:"enabled" description:"enable cloud connection"` + CloudID string `yaml:"cloudID" json:"cloudID" description:"cloud id"` TLS TLSConfig `yaml:"tls" json:"tls"` } @@ -68,3 +73,24 @@ func (c *Config) Validate() error { } return nil } + +func LoadConfig(configFile string) (Config, error) { + // Sanitize the configFile variable to ensure it only contains a valid file path + configFile = filepath.Clean(configFile) + f, err := os.Open(configFile) + if err != nil { + return Config{}, fmt.Errorf("failed to load config from %s: %w", configFile, err) + } + defer func() { + _ = f.Close() + }() + var cfg Config + err = yaml.NewDecoder(f).Decode(&cfg) + if err != nil { + return Config{}, fmt.Errorf("failed to decode config: %w", err) + } + if err = cfg.Validate(); err != nil { + return Config{}, err + } + return cfg, nil +} diff --git a/cmd/bridge-device/device/device.go b/cmd/bridge-device/device/device.go new file mode 100644 index 00000000..3d5c879f --- /dev/null +++ b/cmd/bridge-device/device/device.go @@ -0,0 +1,83 @@ +package device + +import ( + "os" + "path/filepath" + "strconv" + + "github.com/google/uuid" + bridgeTD "github.com/plgd-dev/device/v2/bridge/device/thingDescription" + "github.com/web-of-things-open-source/thingdescription-go/thingDescription" +) + +const ( + DeviceResourceType = "oic.d.virtual" + TestResourcePropertyKey = "my-property" + TestResourceType = "x.plgd.test" +) + +func GetTestResourceHref(id int) string { + return "/test/" + strconv.Itoa(id) +} + +func GetPropertyDescriptionForTestResource() thingDescription.PropertyElement { + objectType := thingDescription.Object + stringType := thingDescription.String + return thingDescription.PropertyElement{ + Type: &thingDescription.TypeDeclaration{ + StringArray: []string{TestResourceType}, + }, + Title: bridgeTD.StringToPtr("Test Property"), + PropertyElementType: &objectType, + Properties: &thingDescription.Properties{ + DataSchemaMap: map[string]thingDescription.DataSchema{ + "Name": { + Title: bridgeTD.StringToPtr("Name"), + DataSchemaType: &stringType, + }, + }, + }, + Observable: bridgeTD.BoolToPtr(true), + } +} + +func PatchTestResourcePropertyElement(pe thingDescription.PropertyElement, deviceID uuid.UUID, href, contentType string) (thingDescription.PropertyElement, error) { + propOps := bridgeTD.GetPropertyElementOperations(pe) + return bridgeTD.PatchPropertyElement(pe, []string{TestResourceType}, true, deviceID, href, + propOps.ToSupportedOperations(), contentType) +} + +func GetAdditionalProperties() map[string]interface{} { + return map[string]interface{}{ + TestResourcePropertyKey: "my-value", + } +} + +func GetDataSchemaForAdditionalProperties() map[string]thingDescription.DataSchema { + dsm := map[string]thingDescription.DataSchema{} + stringType := thingDescription.String + readOnly := true + dsm[TestResourcePropertyKey] = thingDescription.DataSchema{ + DataSchemaType: &stringType, + ReadOnly: &readOnly, + } + return dsm +} + +func GetThingDescription(path string, numResources int) (thingDescription.ThingDescription, error) { + tdJson, err := os.ReadFile(filepath.Clean(path)) + if err != nil { + return thingDescription.ThingDescription{}, err + } + td, err := thingDescription.UnmarshalThingDescription(tdJson) + if err != nil { + return thingDescription.ThingDescription{}, err + } + if td.Properties == nil { + td.Properties = make(map[string]thingDescription.PropertyElement) + } + for i := 0; i < numResources; i++ { + td.Properties[GetTestResourceHref(i)] = GetPropertyDescriptionForTestResource() + } + return td, nil +} diff --git a/cmd/bridge-device/main.go b/cmd/bridge-device/main.go index 62ebb795..fa2115d3 100644 --- a/cmd/bridge-device/main.go +++ b/cmd/bridge-device/main.go @@ -1,7 +1,6 @@ package main import ( - "bytes" "context" "crypto/tls" "crypto/x509" @@ -10,156 +9,24 @@ import ( "fmt" "os" "os/signal" - "path/filepath" - "sync" - "sync/atomic" "syscall" - "time" "github.com/google/uuid" "github.com/plgd-dev/device/v2/bridge/device" "github.com/plgd-dev/device/v2/bridge/device/cloud" "github.com/plgd-dev/device/v2/bridge/device/thingDescription" - "github.com/plgd-dev/device/v2/bridge/net" - "github.com/plgd-dev/device/v2/bridge/resources" thingDescriptionResource "github.com/plgd-dev/device/v2/bridge/resources/thingDescription" "github.com/plgd-dev/device/v2/bridge/service" - "github.com/plgd-dev/device/v2/pkg/codec/cbor" - codecOcf "github.com/plgd-dev/device/v2/pkg/codec/ocf" + bridgeDevice "github.com/plgd-dev/device/v2/cmd/bridge-device/device" "github.com/plgd-dev/device/v2/pkg/log" pkgX509 "github.com/plgd-dev/device/v2/pkg/security/x509" "github.com/plgd-dev/device/v2/schema" deviceResource "github.com/plgd-dev/device/v2/schema/device" - "github.com/plgd-dev/device/v2/schema/interfaces" "github.com/plgd-dev/go-coap/v3/message" - "github.com/plgd-dev/go-coap/v3/message/codes" - "github.com/plgd-dev/go-coap/v3/message/pool" - coapSync "github.com/plgd-dev/go-coap/v3/pkg/sync" wotTD "github.com/web-of-things-open-source/thingdescription-go/thingDescription" - "gopkg.in/yaml.v3" ) -const myPropertyKey = "my-property" - -func loadConfig(configFile string) (Config, error) { - // Sanitize the configFile variable to ensure it only contains a valid file path - configFile = filepath.Clean(configFile) - f, err := os.Open(configFile) - if err != nil { - return Config{}, err - } - defer func() { - _ = f.Close() - }() - var cfg Config - err = yaml.NewDecoder(f).Decode(&cfg) - if err != nil { - return Config{}, err - } - if err = cfg.Validate(); err != nil { - return Config{}, err - } - return cfg, nil -} - -type resourceData struct { - Name string `json:"name,omitempty"` -} - -type resourceDataSync struct { - resourceData - lock sync.Mutex -} - -func (r *resourceDataSync) setName(name string) { - r.lock.Lock() - defer r.lock.Unlock() - r.Name = name -} - -func (r *resourceDataSync) copy() resourceData { - r.lock.Lock() - defer r.lock.Unlock() - return resourceData{ - Name: r.Name, - } -} - -func addResources(d service.Device, numResources int) { - if numResources <= 0 { - return - } - obsWatcher := coapSync.NewMap[uint64, func()]() - for i := 0; i < numResources; i++ { - addResource(d, i, obsWatcher) - } - go func() { - // notify observers every 500ms - for { - time.Sleep(time.Millisecond * 500) - obsWatcher.Range(func(_ uint64, h func()) bool { - h() - return true - }) - } - }() -} - -func addResource(d service.Device, idx int, obsWatcher *coapSync.Map[uint64, func()]) { - rds := resourceDataSync{ - resourceData: resourceData{ - Name: fmt.Sprintf("test-%v", idx), - }, - } - - resHandler := func(req *net.Request) (*pool.Message, error) { - resp := pool.NewMessage(req.Context()) - switch req.Code() { - case codes.GET: - resp.SetCode(codes.Content) - case codes.POST: - resp.SetCode(codes.Changed) - default: - return nil, fmt.Errorf("invalid method %v", req.Code()) - } - resp.SetContentFormat(message.AppOcfCbor) - data, err := cbor.Encode(rds.copy()) - if err != nil { - return nil, err - } - resp.SetBody(bytes.NewReader(data)) - return resp, nil - } - - var subID atomic.Uint64 - res := resources.NewResource(fmt.Sprintf("/test/%d", idx), resHandler, func(req *net.Request) (*pool.Message, error) { - codec := codecOcf.VNDOCFCBORCodec{} - var newData resourceData - err := codec.Decode(req.Message, &newData) - if err != nil { - return nil, err - } - rds.setName(newData.Name) - return resHandler(req) - }, []string{"x.plgd.test"}, []string{interfaces.OC_IF_BASELINE, interfaces.OC_IF_RW}) - res.SetObserveHandler(d.GetLoop(), func(req *net.Request, handler func(msg *pool.Message, err error)) (cancel func(), err error) { - sub := subID.Add(1) - obsWatcher.Store(sub, func() { - resp, err := resHandler(req) - if err != nil { - handler(nil, err) - return - } - handler(resp, nil) - }) - return func() { - obsWatcher.Delete(sub) - }, nil - }) - d.AddResources(res) -} - -func getCloudTLS(cfg CloudConfig, credentialEnabled bool) (cloud.CAPool, *tls.Certificate, error) { +func getCloudTLS(cfg bridgeDevice.CloudConfig, credentialEnabled bool) (cloud.CAPool, *tls.Certificate, error) { var ca []*x509.Certificate var err error if cfg.TLS.CAPoolPath == "" && !credentialEnabled { @@ -202,7 +69,7 @@ func handleSignals(s *service.Service) { } } -func getCloudOpts(cfg Config) ([]device.Option, error) { +func getCloudOpts(cfg bridgeDevice.Config) ([]device.Option, error) { caPool, cert, err := getCloudTLS(cfg.Cloud, cfg.Credential.Enabled) if err != nil { return nil, err @@ -216,12 +83,26 @@ func getCloudOpts(cfg Config) ([]device.Option, error) { return opts, nil } -func getTDOpts(cfg Config) ([]device.Option, error) { - tdJson, err := os.ReadFile(cfg.ThingDescription.File) - if err != nil { - return nil, err +func patchPropertyElement(td wotTD.ThingDescription, dev *device.Device, endpoint string, resourceHref string, resource thingDescription.Resource) (wotTD.PropertyElement, bool) { + propElement, ok := td.Properties[resourceHref] + if !ok { + propElement, ok = thingDescriptionResource.GetOCFResourcePropertyElement(resourceHref) + if ok && resourceHref == deviceResource.ResourceURI && propElement.Properties != nil && propElement.Properties.DataSchemaMap != nil { + addProps := bridgeDevice.GetDataSchemaForAdditionalProperties() + for key, prop := range addProps { + propElement.Properties.DataSchemaMap[key] = prop + } + } } - td, err := wotTD.UnmarshalThingDescription(tdJson) + if !ok { + return wotTD.PropertyElement{}, false + } + propElement, err := thingDescription.PatchPropertyElement(propElement, resource.GetResourceTypes(), endpoint != "", dev.GetID(), resource.GetHref(), resource.SupportsOperations(), message.AppCBOR.String()) + return propElement, err == nil +} + +func getTDOpts(cfg bridgeDevice.Config) ([]device.Option, error) { + td, err := bridgeDevice.GetThingDescription(cfg.ThingDescription.File, cfg.NumResourcesPerDevice) if err != nil { return nil, err } @@ -231,35 +112,15 @@ func getTDOpts(cfg Config) ([]device.Option, error) { endpoint = endpoints[0].URI } newTD := thingDescription.PatchThingDescription(td, dev, endpoint, func(resourceHref string, resource thingDescription.Resource) (wotTD.PropertyElement, bool) { - propElement, ok := td.Properties[resourceHref] - if !ok { - propElement, ok = thingDescriptionResource.GetOCFResourcePropertyElement(resourceHref) - if ok && resourceHref == deviceResource.ResourceURI && propElement.Properties != nil && propElement.Properties.DataSchemaMap != nil { - stringType := wotTD.String - readOnly := true - propElement.Properties.DataSchemaMap[myPropertyKey] = wotTD.DataSchema{ - DataSchemaType: &stringType, - ReadOnly: &readOnly, - } - } - } - if !ok { - return wotTD.PropertyElement{}, false - } - propElement = thingDescription.PatchPropertyElement(propElement, dev.GetID(), resource, endpoint != "") - return propElement, true + return patchPropertyElement(td, dev, endpoint, resourceHref, resource) }) return &newTD })}, nil } -func getOpts(cfg Config) ([]device.Option, error) { +func getOpts(cfg bridgeDevice.Config) ([]device.Option, error) { opts := []device.Option{ - device.WithGetAdditionalPropertiesForResponse(func() map[string]interface{} { - return map[string]interface{}{ - myPropertyKey: "my-value", - } - }), + device.WithGetAdditionalPropertiesForResponse(bridgeDevice.GetAdditionalProperties), } if cfg.Cloud.Enabled { cloudOpts, err := getCloudOpts(cfg) @@ -281,7 +142,7 @@ func getOpts(cfg Config) ([]device.Option, error) { func main() { configFile := flag.String("config", "config.yaml", "path to config file") flag.Parse() - cfg, err := loadConfig(*configFile) + cfg, err := bridgeDevice.LoadConfig(*configFile) if err != nil { panic(err) } @@ -299,12 +160,15 @@ func main() { newDevice := func(id uuid.UUID, piid uuid.UUID) (service.Device, error) { return device.New(device.Config{ Name: fmt.Sprintf("bridged-device-%d", i), - ResourceTypes: []string{"oic.d.virtual"}, + ResourceTypes: []string{bridgeDevice.DeviceResourceType}, ID: id, ProtocolIndependentID: piid, MaxMessageSize: cfg.Config.API.CoAP.MaxMessageSize, Cloud: device.CloudConfig{ Enabled: cfg.Cloud.Enabled, + Config: cloud.Config{ + CloudID: cfg.Cloud.CloudID, + }, }, Credential: device.CredentialConfig{ Enabled: cfg.Credential.Enabled, diff --git a/test/bridge-device/main.go b/test/bridge-device/main.go deleted file mode 100644 index 327f5e6f..00000000 --- a/test/bridge-device/main.go +++ /dev/null @@ -1,129 +0,0 @@ -package main - -import ( - "crypto/tls" - "crypto/x509" - "fmt" - "os" - "os/signal" - "syscall" - - "github.com/google/uuid" - "github.com/plgd-dev/device/v2/bridge/device" - "github.com/plgd-dev/device/v2/bridge/device/cloud" - "github.com/plgd-dev/device/v2/bridge/net" - "github.com/plgd-dev/device/v2/bridge/service" - "github.com/plgd-dev/device/v2/pkg/log" - pkgX509 "github.com/plgd-dev/device/v2/pkg/security/x509" -) - -const ( - numGeneratedBridgedDevices = 3 -) - -func testConfig() service.Config { - return service.Config{ - API: service.APIConfig{ - CoAP: service.CoAPConfig{ - ID: uuid.New().String(), - Config: net.Config{ - ExternalAddresses: []string{"127.0.0.1:15683", "[::1]:15683"}, - MaxMessageSize: 2097152, - }, - }, - }, - } -} - -func getCloudTLS() (cloud.CAPool, *tls.Certificate, error) { - caPath := os.Getenv("CA_POOL") - fmt.Printf("Loading CA(%s)\n", caPath) - ca, err := pkgX509.ReadPemCertificates(caPath) - if err != nil { - return cloud.CAPool{}, nil, fmt.Errorf("cannot load ca: %w", err) - } - caPool := cloud.MakeCAPool(func() []*x509.Certificate { - return ca - }, false) - - certPath := os.Getenv("CERT_FILE") - keyPath := os.Getenv("KEY_FILE") - if keyPath != "" && certPath != "" { - fmt.Printf("Loading certificate(%s) and key(%s)\n", certPath, keyPath) - cert, err := tls.LoadX509KeyPair(certPath, keyPath) - if err != nil { - return cloud.CAPool{}, nil, fmt.Errorf("cannot load cert: %w", err) - } - return caPool, &cert, nil - } - return caPool, nil, nil -} - -func handleSignals(s *service.Service) { - sigCh := make(chan os.Signal, 1) - signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) - - for sig := range sigCh { - switch sig { - case syscall.SIGINT: - os.Exit(0) - return - case syscall.SIGTERM: - _ = s.Shutdown() - return - } - } -} - -func main() { - cfg := testConfig() - if err := cfg.Validate(); err != nil { - panic(err) - } - s, err := service.New(cfg, service.WithLogger(log.NewStdLogger(log.LevelDebug))) - if err != nil { - panic(err) - } - - opts := []device.Option{} - caPool, cert, errC := getCloudTLS() - if errC != nil { - panic(errC) - } - opts = append(opts, device.WithCAPool(caPool)) - if cert != nil { - opts = append(opts, device.WithGetCertificates(func(string) []tls.Certificate { - return []tls.Certificate{*cert} - })) - } - - for i := 0; i < numGeneratedBridgedDevices; i++ { - newDevice := func(id uuid.UUID, piid uuid.UUID) (service.Device, error) { - return device.New(device.Config{ - Name: fmt.Sprintf("bridged-device-%d", i), - ResourceTypes: []string{"oic.d.virtual"}, - ID: id, - ProtocolIndependentID: piid, - MaxMessageSize: cfg.API.CoAP.MaxMessageSize, - Cloud: device.CloudConfig{ - Enabled: true, - Config: cloud.Config{ - CloudID: os.Getenv("CLOUD_SID"), - }, - }, - }, append(opts, device.WithLogger(device.NewLogger(id, log.LevelDebug)))...) - } - d, errC := s.CreateDevice(uuid.New(), newDevice) - if errC == nil { - d.Init() - } - } - - go func() { - handleSignals(s) - }() - - if err = s.Serve(); err != nil { - panic(err) - } -}