diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..48b8006 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,40 @@ + + +**FEATURE REQUEST** + +1. Is there an open issue addressing this request? If it does, please add a "+1" reaction to the + existing issue, otherwise proceed to step 2. + +2. Describe the feature you are requesting, as well as the possible use case(s) for it. + +3. Indicate the importance of this feature to you (must-have, should-have, nice-to-have). + +**BUG REPORT** + +1. What were you trying to achieve? + +2. What are the expected results? + +3. What are the received results? + +4. What are the steps to reproduce the issue? + +5. In what environment did you encounter the issue? + +6. Additional information you deem important: + +**ENHANCEMENT** +1. Describe the enhancement you are requesting. Enhancements include: + - tests + - code refactoring + - documentation + - research + - tooling + +2. Indicate the importance of this enhancement to you (must-have, should-have, nice-to-have). \ No newline at end of file diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..bc7ef53 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,15 @@ +Pull request title should be `MF-XXX - description` or `NOISSUE - description` where XXX is ID of issue that this PR relate to. +Please review the [CONTRIBUTING.md](https://github.com/mainflux/mainflux/blob/master/CONTRIBUTING.md) file for detailed contributing guidelines. + +### What does this do? + +### Which issue(s) does this PR fix/relate to? +Put here `Resolves #XXX` to auto-close the issue that your PR fixes (if such) + +### List any changes that modify/break current functionality + +### Have you included tests for your changes? + +### Did you document any new/modified functionality? + +### Notes \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..e4e2dae --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,29 @@ +name: CI +on: + push: + branches: [main] + pull_request: + branches: [main] +jobs: + test: + strategy: + matrix: + go-version: [1.21.x] + os: [ubuntu-latest] + runs-on: ${{ matrix.os }} + steps: + - name: Install Go + uses: actions/setup-go@v3 + with: + go-version: ${{ matrix.go-version }} + - name: Checkout code + uses: actions/checkout@v3 + - name: Build + run: go build -v ./... + - name: Lint + uses: golangci/golangci-lint-action@v3 + with: + version: latest + args: --no-config --disable-all --enable gosimple --enable errcheck --enable govet --enable unused --enable goconst --enable godot --enable unused --enable deadcode --timeout 3m + - name: Run tests + run: go test -mod=vendor -v --race -covermode=atomic -coverprofile cover.out ./... diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3865add --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +# Copyright (c) Mainflux +# SPDX-License-Identifier: Apache-2.0 + +# Set your private global .gitignore: +# https://digitalfortress.tech/tricks/creating-a-global-gitignore/ + +build + diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..9406f5b --- /dev/null +++ b/Makefile @@ -0,0 +1,113 @@ +# Copyright (c) Mainflux +# SPDX-License-Identifier: Apache-2.0 + +MF_DOCKER_IMAGE_NAME_PREFIX ?= mainflux +BUILD_DIR = build +SERVICES = modbus +DOCKERS = $(addprefix docker_,$(SERVICES)) +DOCKERS_DEV = $(addprefix docker_dev_,$(SERVICES)) +CGO_ENABLED ?= 0 +GOARCH ?= amd64 +VERSION ?= $(shell git describe --abbrev=0 --tags) +COMMIT ?= $(shell git rev-parse HEAD) +TIME ?= $(shell date +%F_%T) + +ifneq ($(MF_BROKER_TYPE),) + MF_BROKER_TYPE := $(MF_BROKER_TYPE) +else + MF_BROKER_TYPE=nats +endif + +define compile_service + CGO_ENABLED=$(CGO_ENABLED) GOOS=$(GOOS) GOARCH=$(GOARCH) GOARM=$(GOARM) \ + go build -tags $(MF_BROKER_TYPE) -ldflags "-s -w \ + -X 'github.com/mainflux/mainflux.BuildTime=$(TIME)' \ + -X 'github.com/mainflux/mainflux.Version=$(VERSION)' \ + -X 'github.com/mainflux/mainflux.Commit=$(COMMIT)'" \ + -o ${BUILD_DIR}/mainflux-$(1) cmd/$(1)/main.go +endef + +define make_docker + $(eval svc=$(subst docker_,,$(1))) + + docker build \ + --no-cache \ + --build-arg SVC=$(svc) \ + --build-arg GOARCH=$(GOARCH) \ + --build-arg GOARM=$(GOARM) \ + --build-arg VERSION=$(VERSION) \ + --build-arg COMMIT=$(COMMIT) \ + --build-arg TIME=$(TIME) \ + --tag=$(MF_DOCKER_IMAGE_NAME_PREFIX)/$(svc) \ + -f docker/Dockerfile . +endef + +define make_docker_dev + $(eval svc=$(subst docker_dev_,,$(1))) + + docker build \ + --no-cache \ + --build-arg SVC=$(svc) \ + --tag=$(MF_DOCKER_IMAGE_NAME_PREFIX)/$(svc) \ + -f docker/Dockerfile.dev ./build +endef + +all: $(SERVICES) + +.PHONY: all $(SERVICES) dockers dockers_dev latest release + +clean: + rm -rf ${BUILD_DIR} + +cleandocker: + # Stops containers and removes containers, networks, volumes, and images created by up + docker-compose -f docker/docker-compose.yml down --rmi all -v --remove-orphans + +ifdef pv + # Remove unused volumes + docker volume ls -f name=$(MF_DOCKER_IMAGE_NAME_PREFIX) -f dangling=true -q | xargs -r docker volume rm +endif + +install: + cp ${BUILD_DIR}/* $(GOBIN) + +test: + go test -v -race -count 1 -tags test $(shell go list ./... | grep -v 'vendor\|cmd') + +$(SERVICES): + $(call compile_service,$(@)) + +$(DOCKERS): + $(call make_docker,$(@),$(GOARCH)) + +$(DOCKERS_DEV): + $(call make_docker_dev,$(@)) + +dockers: $(DOCKERS) +dockers_dev: $(DOCKERS_DEV) + +define docker_push + for svc in $(SERVICES); do \ + docker push $(MF_DOCKER_IMAGE_NAME_PREFIX)/$$svc:$(1); \ + done +endef + +changelog: + git log $(shell git describe --tags --abbrev=0)..HEAD --pretty=format:"- %s" + +latest: dockers + $(call docker_push,latest) + +release: + $(eval version = $(shell git describe --abbrev=0 --tags)) + git checkout $(version) + $(MAKE) dockers + for svc in $(SERVICES); do \ + docker tag $(MF_DOCKER_IMAGE_NAME_PREFIX)/$$svc $(MF_DOCKER_IMAGE_NAME_PREFIX)/$$svc:$(version); \ + done + $(call docker_push,$(version)) + +run: + sed -i "s,file: brokers/.*.yml,file: brokers/${MF_BROKER_TYPE}.yml," docker/docker-compose.yml + sed -i "s,MF_BROKER_URL=.*,MF_BROKER_URL=$$\{MF_$(shell echo ${MF_BROKER_TYPE} | tr 'a-z' 'A-Z')_URL\}," docker/.env + docker compose -f docker/docker-compose.yml up diff --git a/cmd/modbus/main.go b/cmd/modbus/main.go new file mode 100644 index 0000000..67cd49d --- /dev/null +++ b/cmd/modbus/main.go @@ -0,0 +1,77 @@ +// Copyright (c) Mainflux +// SPDX-License-Identifier: Apache-2.0 + +// Package main contains modbus-adapter main function to start the modbus-adapter service. +package main + +import ( + "context" + "fmt" + "log" + "os" + + "github.com/caarlos0/env/v7" + "github.com/mainflux/edge/modbus" + "github.com/mainflux/edge/modbus/api" + mflog "github.com/mainflux/mainflux/logger" + "github.com/mainflux/mainflux/pkg/errors" + "golang.org/x/sync/errgroup" +) + +const svcName = "modbus" + +type config struct { + LogLevel string `env:"MF_MODBUS_ADAPTER_LOG_LEVEL" envDefault:"info"` + RPCPort int `env:"MF_MODBUS_ADAPTER_RPC_PORT" envDefault:"8855"` + RPCHost string `env:"MF_MODBUS_ADAPTER_RPC_HOST" envDefault:"localhost"` +} + +func main() { + ctx, cancel := context.WithCancel(context.Background()) + g, ctx := errgroup.WithContext(ctx) + + cfg := config{} + if err := env.Parse(&cfg); err != nil { + log.Fatalf("failed to load %s configuration : %s", svcName, err) + } + + logger, err := mflog.New(os.Stdout, cfg.LogLevel) + if err != nil { + log.Fatalf("failed to init logger: %s", err) + } + + var exitCode int + defer mflog.ExitWithError(&exitCode) + + svc := modbus.New() + + server, err := api.NewServer(svc, fmt.Sprintf("%s:%d", cfg.RPCHost, cfg.RPCPort)) + if err != nil { + logger.Error(err.Error()) + return + } + + g.Go(func() error { + return server.Start(ctx) + }) + + logger.Info(fmt.Sprintf("modbus service listening on rpc %s:%d", cfg.RPCHost, cfg.RPCPort)) + + defer func() { + if err := server.Stop(); err != nil { + logger.Error(err.Error()) + } + }() + + g.Go(func() error { + if sig := errors.SignalHandler(ctx); sig != nil { + cancel() + logger.Info(fmt.Sprintf("modbus shutdown by signal: %s", sig)) + } + return nil + }) + + if err := g.Wait(); err != nil { + logger.Error(fmt.Sprintf("modbus service terminated: %s", err)) + } +} diff --git a/docker/.env b/docker/.env new file mode 100644 index 0000000..8f41cbe --- /dev/null +++ b/docker/.env @@ -0,0 +1,5 @@ +### Modbus +MF_MODBUS_ADAPTER_LOG_LEVEL=info +MF_MODBUS_ADAPTER_INSTANCE_ID= +MF_MODBUS_ADAPTER_RPC_HOST="http://localhost" +MF_MODBUS_ADAPTER_RPC_PORT=8855 diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000..35f541c --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,8 @@ +# Copyright (c) Mainflux +# SPDX-License-Identifier: Apache-2.0 + +version: "3.7" + +networks: + mainflux-edge-base-net: + driver: bridge diff --git a/docker/modbus/docker-compose.yml b/docker/modbus/docker-compose.yml new file mode 100644 index 0000000..2cb1830 --- /dev/null +++ b/docker/modbus/docker-compose.yml @@ -0,0 +1,31 @@ +# Copyright (c) Mainflux +# SPDX-License-Identifier: Apache-2.0 + +# This docker-compose file contains modbus service. Since it's optional, this file is +# dependent of docker-compose file from /docker. In order to run this services, execute command: +# docker-compose -f docker/docker-compose.yml -f docker/modbus/docker-compose.yml up +# from project root. + +version: "3.7" + +networks: + docker_mainflux-edge-base-net: + external: true + +services: + modbus: + image: mainflux/modbus:${MF_RELEASE_TAG} + container_name: mainflux-modbus + depends_on: + - broker + restart: on-failure + environment: + MF_MODBUS_ADAPTER_LOG_LEVEL: ${MF_MODBUS_ADAPTER_LOG_LEVEL} + MF_MODBUS_ADAPTER_RPC_HOST: ${MF_MODBUS_ADAPTER_RPC_HOST} + MF_HTTP_ADAPTER_RPC_PORT: ${MF_HTTP_ADAPTER_RPC_PORT} + MF_JAEGER_URL: ${MF_JAEGER_URL} + MF_MODBUS_ADAPTER_INSTANCE_ID: ${MF_MODBUS_ADAPTER_INSTANCE_ID} + ports: + - ${MF_HTTP_ADAPTER_RPC_PORT}:${MF_HTTP_ADAPTER_RPC_PORT} + networks: + - docker_mainflux-edge-base-net diff --git a/examples/modbus/main.go b/examples/modbus/main.go new file mode 100644 index 0000000..51fc200 --- /dev/null +++ b/examples/modbus/main.go @@ -0,0 +1,72 @@ +package main + +import ( + "encoding/hex" + "fmt" + "log" + "net/rpc" + + "github.com/mainflux/edge/modbus" +) + +func main() { + client, err := rpc.Dial("tcp", "localhost:8855") + if err != nil { + log.Fatal(err) + } + defer client.Close() + + var id int + // configure + config := modbus.TCPHandlerOptions{ + Address: "localhost:1502", + } + + if err = client.Call("Adapter.ConfigureTCP", config, &id); err != nil { + fmt.Println("Configure Error:", err) + } else { + fmt.Println("Configure Response:", id) + } + + configRead := modbus.RWOptions{ + Address: 100, + Quantity: 1, + DataPoint: modbus.HoldingRegister, + ID: id, + } + + data := make([]byte, 1) + + // Read + if err = client.Call("Adapter.Read", configRead, &data); err != nil { + fmt.Println("Read Error:", err) + } else { + fmt.Println("Read Data:", hex.EncodeToString(data)) + } + + configWrite := modbus.RWOptions{ + Address: 100, + Quantity: 1, + Value: modbus.ValueWrapper{Data: uint16(1)}, + DataPoint: modbus.Register, + ID: id, + } + + // Write + if err = client.Call("Adapter.Write", configWrite, &data); err != nil { + fmt.Println("Write Error:", err) + } else { + fmt.Println("Write Response:", hex.EncodeToString(data)) + } + + // Close + + var closed bool + err = client.Call("Adapter.Close", id, &closed) + if err != nil { + fmt.Println("Close Error:", err) + } else { + fmt.Println("Close Response:", closed) + } + +} diff --git a/examples/publish/main.go b/examples/publish/main.go new file mode 100644 index 0000000..75cfe5b --- /dev/null +++ b/examples/publish/main.go @@ -0,0 +1,64 @@ +package main + +import ( + "context" + "flag" + "log" + "os" + + mflog "github.com/mainflux/mainflux/logger" + "github.com/mainflux/mainflux/pkg/messaging" + "github.com/mainflux/mainflux/pkg/messaging/brokers" + "github.com/nats-io/nats.go" +) + +func main() { + var urls = flag.String("s", nats.DefaultURL, "The nats server URLs (separated by comma)") + var showHelp = flag.Bool("h", false, "Show help message") + + log.SetFlags(0) + flag.Usage = usage + flag.Parse() + + if *showHelp { + showUsageAndExit(0) + } + + args := flag.Args() + if len(args) != 2 { + showUsageAndExit(1) + } + + subj, msg := args[0], []byte(args[1]) + + logger, err := mflog.New(os.Stdout, "info") + if err != nil { + log.Fatalf("failed to init logger: %s", err) + } + + ps, err := brokers.NewPublisher(*urls) + if err != nil { + logger.Error(err.Error()) + return + } + defer ps.Close() + + if err := ps.Publish(context.Background(), subj, &messaging.Message{ + Channel: subj, + Payload: msg, + }); err != nil { + logger.Error(err.Error()) + return + } + logger.Info("Message published") +} + +func usage() { + log.Printf("Usage: publish [-s server] \n") + flag.PrintDefaults() +} + +func showUsageAndExit(exitcode int) { + usage() + os.Exit(exitcode) +} diff --git a/examples/subscribe/main.go b/examples/subscribe/main.go new file mode 100644 index 0000000..7824979 --- /dev/null +++ b/examples/subscribe/main.go @@ -0,0 +1,105 @@ +package main + +import ( + "context" + "encoding/hex" + "flag" + "fmt" + "log" + "os" + + "github.com/mainflux/mainflux/pkg/errors" + + mflog "github.com/mainflux/mainflux/logger" + "github.com/mainflux/mainflux/pkg/messaging" + "github.com/mainflux/mainflux/pkg/messaging/brokers" + "github.com/nats-io/nats.go" + "golang.org/x/sync/errgroup" +) + +func main() { + var urls = flag.String("s", nats.DefaultURL, "The nats server URLs (separated by comma)") + var showHelp = flag.Bool("h", false, "Show help message") + + log.SetFlags(0) + flag.Usage = usage + flag.Parse() + + if *showHelp { + showUsageAndExit(0) + } + + args := flag.Args() + if len(args) != 2 { + showUsageAndExit(1) + } + + subj, format := args[0], args[1] + + logger, err := mflog.New(os.Stdout, "info") + if err != nil { + log.Fatalf("failed to init logger: %s", err) + } + + ps, err := brokers.NewPubSub(*urls, "", logger) + if err != nil { + logger.Error(err.Error()) + return + } + defer ps.Close() + + handler := handler{logger: logger, format: format} + + if err := ps.Subscribe(context.Background(), "edge", fmt.Sprintf("channels.%s", subj), &handler); err != nil { + logger.Error(err.Error()) + return + } + + ctx, cancel := context.WithCancel(context.Background()) + g, ctx := errgroup.WithContext(ctx) + + g.Go(func() error { + if sig := errors.SignalHandler(ctx); sig != nil { + cancel() + logger.Info(fmt.Sprintf("subscriber shutdown by signal: %s", sig)) + } + return nil + }) + + if err := g.Wait(); err != nil { + logger.Error(fmt.Sprintf("subscriber terminated: %s", err)) + } + +} + +func usage() { + log.Printf("Usage: subscribe [-s server] \n") + flag.PrintDefaults() +} + +func showUsageAndExit(exitcode int) { + usage() + os.Exit(exitcode) +} + +// TraceHandler is used to trace the message handling operation. +type handler struct { + logger mflog.Logger + format string +} + +func (h *handler) Handle(msg *messaging.Message) error { + switch h.format { + case "string": + h.logger.Info(string(msg.Payload)) + case "hex": + h.logger.Info(hex.EncodeToString(msg.Payload)) + default: + return errors.New("invalid format") + } + return nil +} + +func (h *handler) Cancel() error { + return nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..88c1773 --- /dev/null +++ b/go.mod @@ -0,0 +1,64 @@ +module github.com/mainflux/edge + +go 1.21.0 + +require ( + github.com/caarlos0/env/v7 v7.1.0 + github.com/goburrow/modbus v0.1.0 + github.com/goburrow/serial v0.1.0 + github.com/mainflux/mainflux v0.0.0-20230920101217-28f4965d2638 + github.com/ory/dockertest/v3 v3.10.0 + github.com/stretchr/testify v1.8.4 + golang.org/x/sync v0.3.0 +) + +require ( + github.com/google/go-cmp v0.5.9 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/lib/pq v1.10.7 // indirect + github.com/minio/highwayhash v1.0.2 // indirect + github.com/nats-io/nats-server/v2 v2.5.0 // indirect + github.com/rogpeppe/go-internal v1.10.0 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect +) + +require ( + github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect + github.com/Microsoft/go-winio v0.6.1 // indirect + github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect + github.com/cenkalti/backoff/v4 v4.2.1 // indirect + github.com/containerd/continuity v0.4.1 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/docker/cli v24.0.2+incompatible // indirect + github.com/docker/docker v24.0.2+incompatible // indirect + github.com/docker/go-connections v0.4.0 // indirect + github.com/docker/go-units v0.5.0 // indirect + github.com/go-kit/log v0.2.1 // indirect + github.com/go-logfmt/logfmt v0.6.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect + github.com/imdario/mergo v0.3.13 // indirect + github.com/klauspost/compress v1.16.7 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/moby/term v0.5.0 // indirect + github.com/nats-io/nats.go v1.27.1 + github.com/nats-io/nkeys v0.4.4 // indirect + github.com/nats-io/nuid v1.0.1 // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.0.2 // indirect + github.com/opencontainers/runc v1.1.7 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rabbitmq/amqp091-go v1.8.1 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + github.com/xeipuuv/gojsonschema v1.2.0 // indirect + golang.org/x/crypto v0.11.0 // indirect + golang.org/x/mod v0.12.0 // indirect + golang.org/x/sys v0.10.0 // indirect + golang.org/x/tools v0.11.0 // indirect + google.golang.org/protobuf v1.31.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..c12287e --- /dev/null +++ b/go.sum @@ -0,0 +1,205 @@ +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= +github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= +github.com/caarlos0/env/v7 v7.1.0 h1:9lzTF5amyQeWHZzuZeKlCb5FWSUxpG1js43mhbY8ozg= +github.com/caarlos0/env/v7 v7.1.0/go.mod h1:LPPWniDUq4JaO6Q41vtlyikhMknqymCLBw0eX4dcH1E= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/containerd/continuity v0.4.1 h1:wQnVrjIyQ8vhU2sgOiL5T07jo+ouqc2bnKsv5/EqGhU= +github.com/containerd/continuity v0.4.1/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/docker/cli v24.0.2+incompatible h1:QdqR7znue1mtkXIJ+ruQMGQhpw2JzMJLRXp6zpzF6tM= +github.com/docker/cli v24.0.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/docker v24.0.2+incompatible h1:eATx+oLz9WdNVkQrr0qjQ8HvRJ4bOOxfzEo8R+dA3cg= +github.com/docker/docker v24.0.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= +github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= +github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= +github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/goburrow/modbus v0.1.0 h1:DejRZY73nEM6+bt5JSP6IsFolJ9dVcqxsYbpLbeW/ro= +github.com/goburrow/modbus v0.1.0/go.mod h1:Kx552D5rLIS8E7TyUwQ/UdHEqvX5T8tyiGBTlzMcZBg= +github.com/goburrow/serial v0.1.0 h1:v2T1SQa/dlUqQiYIT8+Cu7YolfqAi3K96UmhwYyuSrA= +github.com/goburrow/serial v0.1.0/go.mod h1:sAiqG0nRVswsm1C97xsttiYCzSLBmUZ/VSlVLZJ8haA= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= +github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= +github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I= +github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw= +github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mainflux/mainflux v0.0.0-20230920101217-28f4965d2638 h1:eVU2SKNGRdEMxm/eQHhoFWxleTyKRm1/ihJWod7943E= +github.com/mainflux/mainflux v0.0.0-20230920101217-28f4965d2638/go.mod h1:cHi+VUm+VST3OaROF0W34pqtr7DHOhrjJ3PDNBTI5W4= +github.com/minio/highwayhash v1.0.1/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY= +github.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA/g= +github.com/minio/highwayhash v1.0.2/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/nats-io/jwt v1.2.2 h1:w3GMTO969dFg+UOKTmmyuu7IGdusK+7Ytlt//OYH/uU= +github.com/nats-io/jwt v1.2.2/go.mod h1:/xX356yQA6LuXI9xWW7mZNpxgF2mBmGecH+Fj34sP5Q= +github.com/nats-io/jwt/v2 v2.0.3 h1:i/O6cmIsjpcQyWDYNcq2JyZ3/VTF8SJ4JWluI5OhpvI= +github.com/nats-io/jwt/v2 v2.0.3/go.mod h1:VRP+deawSXyhNjXmxPCHskrR6Mq50BqpEI5SEcNiGlY= +github.com/nats-io/nats-server/v2 v2.5.0 h1:wsnVaaXH9VRSg+A2MVg5Q727/CqxnmPLGFQ3YZYKTQg= +github.com/nats-io/nats-server/v2 v2.5.0/go.mod h1:Kj86UtrXAL6LwYRA6H4RqzkHhK0Vcv2ZnKD5WbQ1t3g= +github.com/nats-io/nats.go v1.12.1/go.mod h1:BPko4oXsySz4aSWeFgOHLZs3G4Jq4ZAyE6/zMCxRT6w= +github.com/nats-io/nats.go v1.27.1 h1:OuYnal9aKVSnOzLQIzf7554OXMCG7KbaTkCSBHRcSoo= +github.com/nats-io/nats.go v1.27.1/go.mod h1:XpbWUlOElGwTYbMR7imivs7jJj9GtK7ypv321Wp6pjc= +github.com/nats-io/nkeys v0.2.0/go.mod h1:XdZpAbhgyyODYqjTawOnIOI7VlbKSarI9Gfy1tqEu/s= +github.com/nats-io/nkeys v0.3.0/go.mod h1:gvUNGjVcM2IPr5rCsRsC6Wb3Hr2CQAm08dsxtV6A5y4= +github.com/nats-io/nkeys v0.4.4 h1:xvBJ8d69TznjcQl9t6//Q5xXuVhyYiSos6RPtvQNTwA= +github.com/nats-io/nkeys v0.4.4/go.mod h1:XUkxdLPTufzlihbamfzQ7mw/VGx6ObUs+0bN5sNvt64= +github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= +github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/runc v1.1.7 h1:y2EZDS8sNng4Ksf0GUYNhKbTShZJPJg1FiXJNH/uoCk= +github.com/opencontainers/runc v1.1.7/go.mod h1:CbUumNnWCuTGFukNXahoo/RFBZvDAgRh/smNYNOhA50= +github.com/ory/dockertest/v3 v3.10.0 h1:4K3z2VMe8Woe++invjaTB7VRyQXQy5UY+loujO4aNE4= +github.com/ory/dockertest/v3 v3.10.0/go.mod h1:nr57ZbRWMqfsdGdFNLHz5jjNdDb7VVFnzAeW1n5N1Lg= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rabbitmq/amqp091-go v1.8.1 h1:RejT1SBUim5doqcL6s7iN6SBmsQqyTgXb1xMlH0h1hA= +github.com/rabbitmq/amqp091-go v1.8.1/go.mod h1:+jPrT9iY2eLjRaMSRHUhc3z14E/l85kv/f+6luSD3pc= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= +go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= +golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= +golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sys v0.0.0-20190130150945-aca44879d564/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.11.0 h1:EMCa6U9S2LtZXLAMoWiR/R8dAQFRqbAitmbJ2UKhoi8= +golang.org/x/tools v0.11.0/go.mod h1:anzJrxPjNtfgiYQYirP2CPGzGLxrH2u2QBhn6Bf3qY8= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.3.0 h1:MfDY1b1/0xN1CyMlQDac0ziEy9zJQd9CXBRRDHw2jJo= +gotest.tools/v3 v3.3.0/go.mod h1:Mcr9QNxkg0uMvy/YElmo4SpXgJKWgQvYrT7Kw5RzJ1A= diff --git a/modbus/README.md b/modbus/README.md new file mode 100644 index 0000000..9ab57a0 --- /dev/null +++ b/modbus/README.md @@ -0,0 +1,159 @@ +# Mainflux Modbus Adapter + +The Mainflux Modbus Adapter service is responsible for reading and writing data to Modbus sensors using various protocols such as TCP and RTU/ASCII. It serves as an interface between Mainflux and Modbus devices, allowing you to easily integrate Modbus devices into your IoT ecosystem. + +## Configuration + +The service is configured using the environment variables presented in the +following table. Note that any unset variables will be replaced with their +default values. + +| Variable | Description | Default | +| ----------------------------- | -------------------------- | ------------------------------ | +| MF_MODBUS_ADAPTER_LOG_LEVEL | Service log level | info | +| MF_MODBUS_ADAPTER_RPC_HOST | Modbus service HTTP host | | +| MF_MODBUS_ADAPTER_RPC_PORT | Modbus service HTTP port | 8855 | + +## Deployment + +Check the [`modbus-adapter`](https://github.com/mainflux/edge/blob/master/docker/modbus/docker-compose.yml#L6) service section in +docker-compose to see how service is deployed. + +Running this service outside of container requires working instance of the message broker service. +To start the service outside of the container, execute the following shell script: + +```bash +# download the latest version of the service +git clone https://github.com/mainflux/edge + +cd edge + +# compile the binary +make modbus + +# copy binary to bin +make install + +# set the environment variables and run the service +MF_MODBUS_ADAPTER_LOG_LEVEL=[Service log level] \ +MF_MODBUS_ADAPTER_RPC_HOST=[Message broker instance URL] \ +MF_MODBUS_ADAPTER_RPC_PORT=[Message broker instance URL] \ +$GOBIN/mainflux-modbus +``` + +## Usage + +The Mainflux Modbus Adapter service interacts with Modbus sensors through an RPC interface to perform read and write operations. + +## Configuration +Before using the service an RPC call needs to be made to the Configure method passing either RTU or TCP configuration as shown in the example. + +```json +{ + "address": "/dev/ttyS0", + "baud_rate": 9600, + "config": {}, + "data_bits": 8, + "idle_timeout": "5m", + "parity": "even", + "rs485": {}, + "slave_id": 1, + "stop_bits": 1, + "timeout": "10s", + "sampling_frequency": "1s" +} +``` + +```json +{ + "address": "localhost:1502", + "idle_time": "15m", + "slave_id": 2, + "timeout": "5s", + "sampling_frequency": "30s" +} +``` + +```go +client, err := rpc.Dial("tcp", "localhost:8855") +if err != nil { + log.Fatal(err) +} +defer client.Close() + +var id int +// configure +config := modbus.TCPHandlerOptions{ + Address: "localhost:1502", +} + +err = client.Call("Adapter.ConfigureTCP", config, &id) +if err != nil { + fmt.Println("Configure Error:", err) +} else { + fmt.Println("Configure Response:", id) +} +``` + +### Reading Values + +To start reading values, you need to perform an RPC call to the read method. + +```go +configRead := modbus.RWOptions{ + Address: 100, + Quantity: 1, + DataPoint: modbus.HoldingRegister, + ID: id, +} + +data := make([]byte, 1) + +// Read +err = client.Call("Adapter.Read", configRead, &data) +if err != nil { + fmt.Println("Read Error:", err) +} else { + fmt.Println("Read Data:", hex.EncodeToString(data)) +} +``` + + +The supported data points include: + +- coil +- h_register +- i_register +- register +- discrete +- fifo + + +### Writing Values + +To start writing values, you need to perform an RPC call to the write method. + +```go +configWrite := modbus.RWOptions{ + Address: 100, + Quantity: 1, + Value: modbus.ValueWrapper{Data: uint16(1)}, + DataPoint: modbus.Register, + ID: id, +} + +// Write +err = client.Call("Adapter.Write", configWrite, &data) +if err != nil { + fmt.Println("Write Error:", err) +} else { + fmt.Println("Write Response:", hex.EncodeToString(data)) +} +``` + +The value field can be either `uint16` or `[]byte`. + +### Notes +More examples in the `example` dir. +Some simulators are available to get you started testing: +- https://github.com/TechplexEngineer/modbus-sim diff --git a/modbus/adapter.go b/modbus/adapter.go new file mode 100644 index 0000000..cb4131d --- /dev/null +++ b/modbus/adapter.go @@ -0,0 +1,95 @@ +package modbus + +import ( + "errors" + "sync" +) + +var errDeviceNotConfigured = errors.New("modbus device is not configured") + +type Service interface { + // Read subscribes to the Subscriber and + // reads modbus sensor values while publishing them to publisher. + Read(config RWOptions, res *[]byte) error + // Write subscribes to the Subscriber and + // writes to modbus sensor. + Write(config RWOptions, res *[]byte) error + // ConfigureTCP sets the configuration for a TCP device and returns the index for the connection. + ConfigureTCP(config TCPHandlerOptions, id *int) error + // ConfigureRTU sets the configuration for a RTU/Serial device and returns the index for the connection. + ConfigureRTU(config RTUHandlerOptions, id *int) error + // Close closes the modbus connection. + Close(id int, res *bool) error +} + +type Adapter struct { + mutex sync.Mutex + servers map[int]ModbusService +} + +func New() *Adapter { + return &Adapter{ + servers: make(map[int]ModbusService), + } +} + +func (s *Adapter) Read(config RWOptions, res *[]byte) error { + server, ok := s.servers[config.ID] + if !ok { + return errDeviceNotConfigured + } + dat, err := server.Read(config.Address, config.Quantity, config.DataPoint) + *res = dat + return err +} + +func (s *Adapter) Write(config RWOptions, res *[]byte) error { + server, ok := s.servers[config.ID] + if !ok { + return errDeviceNotConfigured + } + dat, err := server.Write(config.Address, config.Quantity, config.Value.Data, config.DataPoint) + *res = dat + return err +} + +func (s *Adapter) ConfigureTCP(config TCPHandlerOptions, id *int) error { + svc, err := NewTCPClient(config) + if err != nil { + return err + } + s.mutex.Lock() + newID := len(s.servers) + 1 + s.servers[newID] = svc + s.mutex.Unlock() + *id = newID + return nil +} + +func (s *Adapter) ConfigureRTU(config RTUHandlerOptions, id *int) error { + svc, err := NewRTUClient(config) + if err != nil { + return err + } + s.mutex.Lock() + newID := len(s.servers) + 1 + s.servers[newID] = svc + s.mutex.Unlock() + *id = newID + return nil +} + +func (s *Adapter) Close(id int, res *bool) error { + server, ok := s.servers[id] + if !ok { + return errDeviceNotConfigured + } + if err := server.Close(); err != nil { + return err + } + s.mutex.Lock() + delete(s.servers, id) + s.mutex.Unlock() + *res = true + return nil +} diff --git a/modbus/api/rpc.go b/modbus/api/rpc.go new file mode 100644 index 0000000..72c0858 --- /dev/null +++ b/modbus/api/rpc.go @@ -0,0 +1,43 @@ +package api + +import ( + "context" + "net" + "net/rpc" + + "github.com/mainflux/edge/modbus" +) + +type Server interface { + Start(ctx context.Context) error + Stop() error +} + +type server struct { + inbound *net.TCPListener +} + +func NewServer(svc *modbus.Adapter, address string) (Server, error) { + addr, err := net.ResolveTCPAddr("tcp", address) + if err != nil { + return nil, err + } + inbound, err := net.ListenTCP("tcp", addr) + if err != nil { + return nil, err + } + + if err = rpc.Register(svc); err != nil { + return nil, err + } + return &server{inbound: inbound}, nil +} + +func (s server) Start(ctx context.Context) error { + rpc.Accept(s.inbound) + return nil +} + +func (s server) Stop() error { + return s.inbound.Close() +} diff --git a/modbus/modbus.go b/modbus/modbus.go new file mode 100644 index 0000000..608f8ba --- /dev/null +++ b/modbus/modbus.go @@ -0,0 +1,260 @@ +package modbus + +import ( + "encoding/json" + "fmt" + "reflect" + "time" + + "github.com/mainflux/mainflux/pkg/errors" + + "github.com/goburrow/modbus" + "github.com/goburrow/serial" +) + +type DataPoint string + +const ( + Coil DataPoint = "coil" + HoldingRegister DataPoint = "h_register" + InputRegister DataPoint = "i_register" + Register DataPoint = "register" + Discrete DataPoint = "discrete" + FIFO DataPoint = "fifo" +) + +var ( + errInvalidInput = errors.New("invalid input type") + errUnsupportedRead = errors.New("invalid iotype for Write method: register") +) + +type ModbusService interface { + // Read gets data from modbus. + Read(address, quantity uint16, iotype DataPoint) ([]byte, error) + // Write writes a value/s on Modbus. + Write(address, quantity uint16, value interface{}, iotype DataPoint) ([]byte, error) + // Close closes the modbus connection. + Close() error +} + +var _ ModbusService = (*service)(nil) + +// adapterService provides methods for reading and writing data on Modbus. +type service struct { + Client modbus.Client + handler modbus.ClientHandler +} + +// TCPHandlerOptions defines optional handler values. +type TCPHandlerOptions struct { + Address string `json:"address"` + IdleTimeout customDuration `json:"idle_time"` + SlaveId byte `json:"slave_id,omitempty"` + Timeout customDuration `json:"timeout,omitempty"` + SamplingFrequency customDuration `json:"sampling_frequency,omitempty"` +} + +// NewRTUClient initializes a new modbus.Client on TCP protocol from the address +// and handler options provided. +func NewTCPClient(config TCPHandlerOptions) (ModbusService, error) { + handler := modbus.NewTCPClientHandler(config.Address) + if !isZeroValue(config.IdleTimeout) { + handler.IdleTimeout = config.IdleTimeout.Duration + } + if !isZeroValue(config.SlaveId) { + handler.SlaveId = config.SlaveId + } + if !isZeroValue(config.Timeout) { + handler.Timeout = config.Timeout.Duration + } + + if err := handler.Connect(); err != nil { + return nil, err + } + + return &service{ + Client: modbus.NewClient(handler), + handler: handler, + }, nil +} + +// RTUHandlerOptions defines optional handler values. +type RTUHandlerOptions struct { + Address string `json:"address,omitempty"` + BaudRate int `json:"baud_rate,omitempty"` + Config serial.Config `json:"config,omitempty"` + DataBits int `json:"data_bits,omitempty"` + IdleTimeout customDuration `json:"idle_timeout,omitempty"` + Parity string `json:"parity,omitempty"` + RS485 serial.RS485Config `json:"rs485,omitempty"` + SlaveId byte `json:"slave_id,omitempty"` + StopBits int `json:"stop_bits,omitempty"` + Timeout customDuration `json:"timeout,omitempty"` + SamplingFrequency customDuration `json:"sampling_frequency,omitempty"` +} + +// NewRTUClient initializes a new modbus.Client on RTU/ASCII protocol from the address +// and handler options provided. +func NewRTUClient(config RTUHandlerOptions) (ModbusService, error) { + handler := modbus.NewRTUClientHandler(config.Address) + if !isZeroValue(config.BaudRate) { + handler.BaudRate = config.BaudRate + } + if !isZeroValue(config.Config) { + handler.Config = config.Config + } + if !isZeroValue(config.DataBits) { + handler.DataBits = config.DataBits + } + if !isZeroValue(config.IdleTimeout) { + handler.IdleTimeout = config.IdleTimeout.Duration + } + if !isZeroValue(config.Parity) { + handler.Parity = config.Parity + } + if !isZeroValue(config.RS485) { + handler.RS485 = config.RS485 + } + if !isZeroValue(config.SlaveId) { + handler.SlaveId = config.SlaveId + } + if !isZeroValue(config.StopBits) { + handler.StopBits = config.StopBits + } + if !isZeroValue(config.Timeout) { + handler.Timeout = config.Timeout.Duration + } + + if err := handler.Connect(); err != nil { + return nil, err + } + return &service{ + Client: modbus.NewClient(handler), + }, nil +} + +func isZeroValue(val interface{}) bool { + v := reflect.ValueOf(val) + switch v.Kind() { + case reflect.Func, reflect.Map, reflect.Slice: + return v.IsNil() + case reflect.Bool: + return !v.Bool() + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return v.Int() == 0 + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return v.Uint() == 0 + case reflect.Float32, reflect.Float64: + return v.Float() == 0 + case reflect.Interface, reflect.Ptr: + return v.IsNil() + default: + return reflect.DeepEqual(v.Interface(), reflect.Zero(v.Type()).Interface()) + } +} + +// Write writes a value/s on Modbus. +func (s *service) Write(address, quantity uint16, value interface{}, iotype DataPoint) ([]byte, error) { + switch iotype { + case Coil: + switch val := value.(type) { + case uint16: + return s.Client.WriteSingleCoil(address, val) + case []byte: + return s.Client.WriteMultipleCoils(address, quantity, val) + default: + return nil, errInvalidInput + } + case Register: + switch val := value.(type) { + case uint16: + return s.Client.WriteSingleRegister(address, val) + case []byte: + return s.Client.WriteMultipleRegisters(address, quantity, val) + default: + return nil, errInvalidInput + } + case HoldingRegister, InputRegister, Discrete, FIFO: + return nil, fmt.Errorf("invalid iotype for Write method: %s", iotype) + default: + return nil, errInvalidInput + } +} + +// Read gets data from modbus. +func (s *service) Read(address uint16, quantity uint16, iotype DataPoint) ([]byte, error) { + switch iotype { + case Coil: + return s.Client.ReadCoils(address, quantity) + case Discrete: + return s.Client.ReadDiscreteInputs(address, quantity) + case FIFO: + return s.Client.ReadFIFOQueue(address) + case HoldingRegister: + return s.Client.ReadHoldingRegisters(address, quantity) + case InputRegister: + return s.Client.ReadInputRegisters(address, quantity) + case Register: + return nil, errUnsupportedRead + default: + return nil, errInvalidInput + } +} + +func (s *service) Close() error { + switch h := s.handler.(type) { + case *modbus.RTUClientHandler: + return h.Close() + case *modbus.TCPClientHandler: + return h.Close() + default: + return nil + } +} + +type RWOptions struct { + ID int `json:"id"` + DataPoint DataPoint `json:"data_type"` + Address uint16 `json:"address"` + Quantity uint16 `json:"quantity"` + Value ValueWrapper `json:"value,omitempty"` +} + +type ValueWrapper struct { + Data interface{} +} + +func (vw *ValueWrapper) UnmarshalJSON(data []byte) error { + var num uint16 + if err := json.Unmarshal(data, &num); err == nil { + vw.Data = num + return nil + } + + var byteArray []byte + if err := json.Unmarshal(data, &byteArray); err == nil { + vw.Data = byteArray + return nil + } + + return fmt.Errorf("unable to unmarshal Value") +} + +type customDuration struct { + time.Duration +} + +func (cd *customDuration) UnmarshalJSON(data []byte) error { + var durationStr string + if err := json.Unmarshal(data, &durationStr); err != nil { + return err + } + + duration, err := time.ParseDuration(durationStr) + if err != nil { + return err + } + + cd.Duration = duration + return nil +} diff --git a/modbus/modbus_test.go b/modbus/modbus_test.go new file mode 100644 index 0000000..c469b72 --- /dev/null +++ b/modbus/modbus_test.go @@ -0,0 +1,131 @@ +package modbus + +import ( + "encoding/hex" + "testing" + + "github.com/goburrow/modbus" + "github.com/stretchr/testify/assert" +) + +func TestRead(t *testing.T) { + modbusService, err := NewTCPClient(TCPHandlerOptions{ + Address: address, + }) + if err != nil { + t.Fatalf("Failed to create ModbusService: %v", err) + } + defer modbusService.Close() + + tests := []struct { + name string + readOpts RWOptions + result string + err error + exceptionCode byte + dataPointOpt DataPoint + }{ + { + name: "Test Read Holding Register", + readOpts: RWOptions{ + Address: 100, + Quantity: 1, + }, + result: "ff00", + err: nil, + dataPointOpt: HoldingRegister, + }, + { + name: "Test Read Holding Register with error", + readOpts: RWOptions{ + Address: 201, + Quantity: 1, + }, + result: "", + exceptionCode: 0x1, // illegal action. + dataPointOpt: HoldingRegister, + }, + { + name: "Test invalid input", + readOpts: RWOptions{ + Address: 101, + Quantity: 1, + }, + result: "", + err: errUnsupportedRead, + dataPointOpt: Register, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + readData, err := modbusService.Read(test.readOpts.Address, test.readOpts.Quantity, test.dataPointOpt) + switch er := err.(type) { + case *modbus.ModbusError: + assert.Equal(t, test.exceptionCode, er.ExceptionCode) + default: + assert.Equal(t, test.err, err) + } + assert.Equal(t, test.result, hex.EncodeToString(readData)) + }) + } +} + +func TestWrite(t *testing.T) { + modbusService, err := NewTCPClient(TCPHandlerOptions{ + Address: address, + }) + if err != nil { + t.Fatalf("Failed to create ModbusService: %v", err) + } + defer modbusService.Close() + + tests := []struct { + name string + writeOpts RWOptions + result string + err error + exceptionCode byte + dataPointOpt DataPoint + }{ + { + name: "Test Write Single Register", + writeOpts: RWOptions{ + Address: 100, + Quantity: 1, + Value: ValueWrapper{ + Data: uint16(1), + }, + }, + dataPointOpt: Register, + err: nil, + result: "0001", + }, + { + name: "Test Write Single Register with invalid input", + writeOpts: RWOptions{ + Address: 100, + Quantity: 1, + Value: ValueWrapper{ + Data: 1, + }, + }, + dataPointOpt: Register, + err: errInvalidInput, + result: "", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + res, err := modbusService.Write(test.writeOpts.Address, test.writeOpts.Quantity, test.writeOpts.Value.Data, test.dataPointOpt) + switch er := err.(type) { + case *modbus.ModbusError: + assert.Equal(t, test.exceptionCode, er.ExceptionCode) + default: + assert.Equal(t, test.err, err) + } + assert.Equal(t, test.result, hex.EncodeToString(res)) + }) + } +} diff --git a/modbus/setup_test.go b/modbus/setup_test.go new file mode 100644 index 0000000..489d4f6 --- /dev/null +++ b/modbus/setup_test.go @@ -0,0 +1,57 @@ +// Copyright (c) Mainflux +// SPDX-License-Identifier: Apache-2.0 + +package modbus + +import ( + "fmt" + "log" + "os" + "os/signal" + "syscall" + "testing" + + dockertest "github.com/ory/dockertest/v3" +) + +var address string + +func TestMain(m *testing.M) { + pool, err := dockertest.NewPool("") + if err != nil { + log.Fatalf("Could not connect to docker: %s", err) + } + + container, err := pool.Run("techplex/modbus-sim", "latest", []string{}) + if err != nil { + log.Fatalf("Could not start container: %s", err) + } + handleInterrupt(pool, container) + + address = fmt.Sprintf("%s:%s", "localhost", container.GetPort("1502/tcp")) + if err := pool.Retry(func() error { + return nil + }); err != nil { + log.Fatalf("Could not connect to docker: %s", err) + } + + code := m.Run() + if err := pool.Purge(container); err != nil { + log.Fatalf("Could not purge container: %s", err) + } + + os.Exit(code) +} + +func handleInterrupt(pool *dockertest.Pool, container *dockertest.Resource) { + c := make(chan os.Signal, 2) + signal.Notify(c, os.Interrupt, syscall.SIGTERM) + + go func() { + <-c + if err := pool.Purge(container); err != nil { + log.Fatalf("Could not purge container: %s", err) + } + os.Exit(0) + }() +}