diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000..2f64bea --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,8 @@ +version: 2 +jobs: + build: + docker: + - image: circleci/golang:1.13-stretch-node + steps: + - checkout + - run: make diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fe46866 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.idea +.gobincache +build/files diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..933e120 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Einride AB + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..68036df --- /dev/null +++ b/Makefile @@ -0,0 +1,67 @@ +all: \ + go-stringer \ + go-mock-gen \ + testdata \ + go-lint \ + go-test \ + go-mod-tidy \ + git-verify-nodiff + +include build/rules.mk + +.PHONY: clean +clean: + rm -rf $(FILES_DIR) + +.PHONY: go-lint +go-lint: $(GOLANGCI_LINT) + # funlen: too strict + # dupl: allow duplication in tests + # interfacer: deprecated + # godox: allow TODOs + # lll: long go:generate directives + $(GOLANGCI_LINT) run --enable-all --disable funlen,dupl,interfacer,godox,lll + +.PHONY: go-mock-gen +go-mock-gen: \ + internal/mocks/mockcanrunner/mocks.go \ + internal/mocks/mockclock/mocks.go \ + internal/mocks/mocksocketcan/mocks.go + +internal/mocks/mockcanrunner/mocks.go: pkg/canrunner/run.go $(GOBIN) + $(GOBIN) -m -run github.com/golang/mock/mockgen -destination $@ \ + -package mockcanrunner go.einride.tech/can/pkg/canrunner \ + Node,TransmittedMessage,ReceivedMessage,FrameTransmitter,FrameReceiver + +internal/mocks/mockclock/mocks.go: internal/clock/clock.go $(GOBIN) + $(GOBIN) -m -run github.com/golang/mock/mockgen -destination $@ \ + -package mockclock go.einride.tech/can/internal/clock \ + Clock,Ticker + +internal/mocks/mocksocketcan/mocks.go: pkg/socketcan/fileconn.go $(GOBIN) + $(GOBIN) -m -run github.com/golang/mock/mockgen -destination $@ \ + -package mocksocketcan -source $< + +.PHONY: go-stringer +go-stringer: \ + pkg/descriptor/sendtype_string.go \ + pkg/socketcan/errorclass_string.go \ + pkg/socketcan/protocolviolationerrorlocation_string.go \ + pkg/socketcan/protocolviolationerror_string.go \ + pkg/socketcan/controllererror_string.go \ + pkg/socketcan/transceivererror_string.go + +%_string.go: %.go + go generate $< + +.PHONY: testdata +testdata: + go run cmd/cantool/main.go generate testdata/dbc testdata/gen/go + +.PHONY: go-test +go-test: + go test -race -cover ./... + +.PHONY: go-mod-tidy +go-mod-tidy: + go mod tidy -v diff --git a/README.md b/README.md new file mode 100644 index 0000000..9fb9e5b --- /dev/null +++ b/README.md @@ -0,0 +1,6 @@ +# :electric_plug: CAN Go [![GoDoc][doc-img]][doc] + +CAN toolkit for Go programmers. + +[doc-img]: https://godoc.org/go.einride.tech/can?status.svg +[doc]: https://godoc.org/go.einride.tech/can diff --git a/build/git.mk b/build/git.mk new file mode 100644 index 0000000..1c21a56 --- /dev/null +++ b/build/git.mk @@ -0,0 +1,3 @@ +.PHONY: git-verify-nodiff +git-verify-nodiff: + @$(BUILD_DIR)/scripts/git-verify-nodiff.sh diff --git a/build/go.mk b/build/go.mk new file mode 100644 index 0000000..2d6d35e --- /dev/null +++ b/build/go.mk @@ -0,0 +1,21 @@ +GOLANGCI_LINT_VERSION := 1.19.1 +GOLANGCI_LINT := $(FILES_DIR)/golangci-lint/$(GOLANGCI_LINT_VERSION)/golangci-lint + +GOBIN_VERSION := 0.0.13 +GOBIN := $(FILES_DIR)/gobin/$(GOBIN_VERSION)/gobin +export PATH := $(dir $(GOBIN)):$(PATH) + +$(GOLANGCI_LINT): + mkdir -p $(dir $@) + curl -s -L -o $(dir $(GOLANGCI_LINT))/archive.tar.gz \ + https://github.com/golangci/golangci-lint/releases/download/v$(GOLANGCI_LINT_VERSION)/golangci-lint-$(GOLANGCI_LINT_VERSION)-$(UNAME)-amd64.tar.gz + tar xzf $(dir $(GOLANGCI_LINT))/archive.tar.gz -C $(dir $(GOLANGCI_LINT)) --strip 1 + chmod +x $@ + touch $@ + +$(GOBIN): + mkdir -p $(dir $@) + curl -s -L -o $@ \ + https://github.com/myitcv/gobin/releases/download/v$(GOBIN_VERSION)/$(UNAME)-amd64 + chmod +x $@ + touch $@ diff --git a/build/rules.mk b/build/rules.mk new file mode 100644 index 0000000..a34aa70 --- /dev/null +++ b/build/rules.mk @@ -0,0 +1,28 @@ +SHELL := /bin/bash + +BUILD_DIR := $(abspath $(dir $(lastword $(MAKEFILE_LIST)))) +FILES_DIR := $(BUILD_DIR)/files + +UNAME := $(shell uname -s) +UNAME_LOWERCASE := $(shell uname -s | tr '[:upper:]' '[:lower:]') + +ifeq ($(UNAME),Linux) +else ifeq ($(UNAME),Darwin) +else +$(error This Makefile only supports Linux and OSX build agents.) +endif + +ifneq ($(shell uname -m),x86_64) +$(error This Makefile only supports x86_64 build agents.) +endif + +ifneq ($(shell which curl >/dev/null; echo $$?),0) +$(error cURL not installed. This Makefile requires cURL.) +endif + +ifneq ($(shell which realpath >/dev/null; echo $$?),0) +$(error Coreutils not installed. OSX users run: brew install coreutils) +endif + +include $(BUILD_DIR)/git.mk +include $(BUILD_DIR)/go.mk diff --git a/build/scripts/git-verify-nodiff.sh b/build/scripts/git-verify-nodiff.sh new file mode 100755 index 0000000..de36518 --- /dev/null +++ b/build/scripts/git-verify-nodiff.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +set -euo pipefail + +if [[ ! -z $(git status --porcelain) ]]; then + echo "Staging area is dirty, please add all files created by the build to .gitignore" + git status -s + exit 1 +fi diff --git a/can.go b/can.go new file mode 100644 index 0000000..0a4993c --- /dev/null +++ b/can.go @@ -0,0 +1,4 @@ +// Package can provides primitives for working with CAN. +// +// See: https://en.wikipedia.org/wiki/CAN_bus +package can // import "go.einride.tech/can" diff --git a/cmd/cantool/main.go b/cmd/cantool/main.go new file mode 100644 index 0000000..373e0ce --- /dev/null +++ b/cmd/cantool/main.go @@ -0,0 +1,213 @@ +package main + +import ( + "errors" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" + "text/scanner" + + "github.com/fatih/color" + "go.einride.tech/can/internal/generate" + "go.einride.tech/can/pkg/dbc" + "go.einride.tech/can/pkg/dbc/analysis" + "go.einride.tech/can/pkg/dbc/analysis/passes/definitiontypeorder" + "go.einride.tech/can/pkg/dbc/analysis/passes/intervals" + "go.einride.tech/can/pkg/dbc/analysis/passes/lineendings" + "go.einride.tech/can/pkg/dbc/analysis/passes/messagenames" + "go.einride.tech/can/pkg/dbc/analysis/passes/multiplexedsignals" + "go.einride.tech/can/pkg/dbc/analysis/passes/newsymbols" + "go.einride.tech/can/pkg/dbc/analysis/passes/nodereferences" + "go.einride.tech/can/pkg/dbc/analysis/passes/noreservedsignals" + "go.einride.tech/can/pkg/dbc/analysis/passes/requireddefinitions" + "go.einride.tech/can/pkg/dbc/analysis/passes/signalbounds" + "go.einride.tech/can/pkg/dbc/analysis/passes/signalnames" + "go.einride.tech/can/pkg/dbc/analysis/passes/singletondefinitions" + "go.einride.tech/can/pkg/dbc/analysis/passes/siunits" + "go.einride.tech/can/pkg/dbc/analysis/passes/uniquenodenames" + "go.einride.tech/can/pkg/dbc/analysis/passes/uniquesignalnames" + "go.einride.tech/can/pkg/dbc/analysis/passes/unitsuffixes" + "go.einride.tech/can/pkg/dbc/analysis/passes/valuedescriptions" + "go.einride.tech/can/pkg/dbc/analysis/passes/version" + "gopkg.in/alecthomas/kingpin.v2" +) + +func main() { + app := kingpin.New("cantool", "CAN tool for Go programmers") + generateCommand(app) + lintCommand(app) + kingpin.MustParse(app.Parse(os.Args[1:])) +} + +func generateCommand(app *kingpin.Application) { + command := app.Command("generate", "generate CAN messages") + inputDir := command. + Arg("input-dir", "input directory"). + Required(). + ExistingDir() + outputDir := command. + Arg("output-dir", "output directory"). + Required(). + String() + command.Action(func(c *kingpin.ParseContext) error { + return filepath.Walk(*inputDir, func(p string, i os.FileInfo, err error) error { + if err != nil { + return err + } + if i.IsDir() || filepath.Ext(p) != ".dbc" { + return nil + } + relPath, err := filepath.Rel(*inputDir, p) + if err != nil { + return err + } + outputFile := relPath + ".go" + outputPath := filepath.Join(*outputDir, outputFile) + if err := genGo(p, outputPath); err != nil { + return err + } + return nil + }) + }) +} + +func lintCommand(app *kingpin.Application) { + command := app.Command("lint", "lint DBC files") + fileOrDir := command. + Arg("file-or-dir", "DBC file or directory"). + Required(). + ExistingFileOrDir() + command.Action(func(context *kingpin.ParseContext) error { + filesToLint, err := resolveFileOrDirectory(*fileOrDir) + if err != nil { + return err + } + var hasFailed bool + for _, lintFile := range filesToLint { + f, err := os.Open(lintFile) + if err != nil { + return err + } + source, err := ioutil.ReadAll(f) + if err != nil { + return err + } + p := dbc.NewParser(f.Name(), source) + if err := p.Parse(); err != nil { + printError(source, err.Position(), err.Reason(), "parse") + continue + } + for _, a := range analyzers() { + pass := &analysis.Pass{ + Analyzer: a, + File: p.File(), + } + if err := a.Run(pass); err != nil { + return err + } + hasFailed = hasFailed || len(pass.Diagnostics) > 0 + for _, d := range pass.Diagnostics { + printError(source, d.Pos, d.Message, a.Name) + } + } + } + if hasFailed { + return errors.New("one or more lint errors") + } + return nil + }) +} + +func analyzers() []*analysis.Analyzer { + return []*analysis.Analyzer{ + // TODO: Re-evaluate if we want boolprefix.Analyzer(), since it creates a lot of churn in vendor schemas + definitiontypeorder.Analyzer(), + intervals.Analyzer(), + lineendings.Analyzer(), + messagenames.Analyzer(), + multiplexedsignals.Analyzer(), + newsymbols.Analyzer(), + nodereferences.Analyzer(), + noreservedsignals.Analyzer(), + requireddefinitions.Analyzer(), + signalbounds.Analyzer(), + signalnames.Analyzer(), + singletondefinitions.Analyzer(), + siunits.Analyzer(), + uniquenodenames.Analyzer(), + uniquesignalnames.Analyzer(), + unitsuffixes.Analyzer(), + valuedescriptions.Analyzer(), + version.Analyzer(), + } +} + +func genGo(inputFile string, outputFile string) error { + if err := os.MkdirAll(filepath.Dir(outputFile), 0755); err != nil { + return err + } + input, err := ioutil.ReadFile(inputFile) + if err != nil { + return err + } + result, err := generate.Compile(inputFile, input) + if err != nil { + return err + } + for _, warning := range result.Warnings { + return warning + } + output, err := generate.Database(result.Database) + if err != nil { + return err + } + if err := ioutil.WriteFile(outputFile, output, 0644); err != nil { + return err + } + fmt.Println("wrote:", outputFile) + return nil +} + +func resolveFileOrDirectory(fileOrDirectory string) ([]string, error) { + fileInfo, err := os.Stat(fileOrDirectory) + if err != nil { + return nil, err + } + if !fileInfo.IsDir() { + return []string{fileOrDirectory}, nil + } + var files []string + if err := filepath.Walk(fileOrDirectory, func(path string, info os.FileInfo, err error) error { + if !info.IsDir() && filepath.Ext(path) == ".dbc" { + files = append(files, path) + } + return nil + }); err != nil { + return nil, err + } + return files, nil +} + +func printError(source []byte, pos scanner.Position, msg string, name string) { + fmt.Printf("\n%s: %s (%s)\n", pos, color.RedString("%s", msg), name) + fmt.Printf("%s\n", getSourceLine(source, pos)) + fmt.Printf("%s\n", caretAtPosition(pos)) +} + +func getSourceLine(source []byte, pos scanner.Position) []byte { + lineStart := pos.Offset + for lineStart > 0 && source[lineStart-1] != '\n' { + lineStart-- + } + lineEnd := pos.Offset + for lineEnd < len(source) && source[lineEnd] != '\n' { + lineEnd++ + } + return source[lineStart:lineEnd] +} + +func caretAtPosition(pos scanner.Position) string { + return strings.Repeat(" ", pos.Column-1) + color.YellowString("^") +} diff --git a/data.go b/data.go new file mode 100644 index 0000000..d1b5d2f --- /dev/null +++ b/data.go @@ -0,0 +1,309 @@ +package can + +import ( + "fmt" + + "go.einride.tech/can/internal/reinterpret" +) + +const MaxDataLength = 8 + +// Data holds the data in a CAN frame. +// +// Layout +// +// Individual bits in the data are numbered according to the following scheme: +// +// BIT +// NUMBER +// +------+------+------+------+------+------+------+------+ +// | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | +// BYTE +------+------+------+------+------+------+------+------+ +// NUMBER +// +-----+ +------+------+------+------+------+------+------+------+ +// | 0 | | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | +// +-----+ +------+------+------+------+------+------+------+------+ +// | 1 | | 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | +// +-----+ +------+------+------+------+------+------+------+------+ +// | 2 | | 23 | 22 | 21 | 20 | 19 | 18 | 17 | 16 | +// +-----+ +------+------+------+------+------+------+------+------+ +// | 3 | | 31 | 30 | 29 | 28 | 27 | 26 | 25 | 24 | +// +-----+ +------+------+------+------+------+------+------+------+ +// | 4 | | 39 | 38 | 37 | 36 | 35 | 34 | 33 | 32 | +// +-----+ +------+------+------+------+------+------+------+------+ +// | 5 | | 47 | 46 | 45 | 44 | 43 | 42 | 41 | 40 | +// +-----+ +------+------+------+------+------+------+------+------+ +// | 6 | | 55 | 54 | 53 | 52 | 51 | 50 | 49 | 48 | +// +-----+ +------+------+------+------+------+------+------+------+ +// | 7 | | 63 | 62 | 61 | 60 | 59 | 58 | 57 | 56 | +// +-----+ +------+------+------+------+------+------+------+------+ +// +// Bit ranges can be manipulated using little-endian and big-endian bit ordering. +// +// Little-endian bit ranges +// +// Example range of length 32 starting at bit 29: +// +// BIT +// NUMBER +// +------+------+------+------+------+------+------+------+ +// | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | +// BYTE +------+------+------+------+------+------+------+------+ +// NUMBER +// +-----+ +------+------+------+------+------+------+------+------+ +// | 0 | | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | +// +-----+ +------+------+------+------+------+------+------+------+ +// | 1 | | 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | +// +-----+ +------+------+------+------+------+------+------+------+ +// | 2 | | 23 | 22 | 21 | 20 | 19 | 18 | 17 | 16 | +// +-----+ +------+------+------+------+------+------+------+------+ +// | 3 | | <-------------LSb | 28 | 27 | 26 | 25 | 24 | +// +-----+ +------+------+------+------+------+------+------+------+ +// | 4 | | <-------------------------------------------------- | +// +-----+ +------+------+------+------+------+------+------+------+ +// | 5 | | <-------------------------------------------------- | +// +-----+ +------+------+------+------+------+------+------+------+ +// | 6 | | <-------------------------------------------------- | +// +-----+ +------+------+------+------+------+------+------+------+ +// | 7 | | 63 | 62 | 61 | <-MSb--------------------------- | +// +-----+ +------+------+------+------+------+------+------+------+ +// +// Big-endian bit ranges +// +// Example range of length 32 starting at bit 29: +// +// BIT +// NUMBER +// +------+------+------+------+------+------+------+------+ +// | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | +// BYTE +------+------+------+------+------+------+------+------+ +// NUMBER +// +-----+ +------+------+------+------+------+------+------+------+ +// | 0 | | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 | +// +-----+ +------+------+------+------+------+------+------+------+ +// | 1 | | 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | +// +-----+ +------+------+------+------+------+------+------+------+ +// | 2 | | 23 | 22 | 21 | 20 | 19 | 18 | 17 | 16 | +// +-----+ +------+------+------+------+------+------+------+------+ +// | 3 | | 31 | 30 | <-MSb--------------------------------- | +// +-----+ +------+------+------+------+------+------+------+------+ +// | 4 | | <-------------------------------------------------- | +// +-----+ +------+------+------+------+------+------+------+------+ +// | 5 | | <-------------------------------------------------- | +// +-----+ +------+------+------+------+------+------+------+------+ +// | 6 | | <-------------------------------------------------- | +// +-----+ +------+------+------+------+------+------+------+------+ +// | 7 | | <------LSb | 61 | 60 | 59 | 58 | 57 | 56 | +// +-----+ +------+------+------+------+------+------+------+------+ +type Data [MaxDataLength]byte + +// UnsignedBitsLittleEndian returns the little-endian bit range [start, start+length) as an unsigned value. +func (d *Data) UnsignedBitsLittleEndian(start, length uint8) uint64 { + // pack bits into one continuous value + packed := d.PackLittleEndian() + // lsb index in the packed value is the start bit + lsbIndex := start + // shift away lower bits + shifted := packed >> lsbIndex + // mask away higher bits + masked := shifted & ((1 << length) - 1) + // done + return masked +} + +// UnsignedBitsBigEndian returns the big-endian bit range [start, start+length) as an unsigned value. +func (d *Data) UnsignedBitsBigEndian(start, length uint8) uint64 { + // pack bits into one continuous value + packed := d.PackBigEndian() + // calculate msb index in the packed value + msbIndex := invertEndian(start) + // calculate lsb index in the packed value + lsbIndex := msbIndex - length + 1 + // shift away lower bits + shifted := packed >> lsbIndex + // mask away higher bits + masked := shifted & ((1 << length) - 1) + // done + return masked +} + +// SignedBitsLittleEndian returns little-endian bit range [start, start+length) as a signed value. +func (d *Data) SignedBitsLittleEndian(start, length uint8) int64 { + unsigned := d.UnsignedBitsLittleEndian(start, length) + return reinterpret.AsSigned(unsigned, length) +} + +// SignedBitsBigEndian returns little-endian bit range [start, start+length) as a signed value. +func (d *Data) SignedBitsBigEndian(start, length uint8) int64 { + unsigned := d.UnsignedBitsBigEndian(start, length) + return reinterpret.AsSigned(unsigned, length) +} + +// SetUnsignedBitsBigEndian sets the little-endian bit range [start, start+length) to the provided unsigned value. +func (d *Data) SetUnsignedBitsLittleEndian(start, length uint8, value uint64) { + // pack bits into one continuous value + packed := d.PackLittleEndian() + // lsb index in the packed value is the start bit + lsbIndex := start + // calculate bit mask for zeroing the bit range to set + unsetMask := ^uint64(((1 << length) - 1) << lsbIndex) + // calculate bit mask for setting the new value + setMask := value << lsbIndex + // calculate the new packed value + newPacked := packed&unsetMask | setMask + // unpack the new packed value into the data + d.UnpackLittleEndian(newPacked) +} + +// SetUnsignedBitsBigEndian sets the big-endian bit range [start, start+length) to the provided unsigned value. +func (d *Data) SetUnsignedBitsBigEndian(start, length uint8, value uint64) { + // pack bits into one continuous value + packed := d.PackBigEndian() + // calculate msb index in the packed value + msbIndex := invertEndian(start) + // calculate lsb index in the packed value + lsbIndex := msbIndex - length + 1 + // calculate bit mask for zeroing the bit range to set + unsetMask := ^uint64(((1 << length) - 1) << lsbIndex) + // calculate bit mask for setting the new value + setMask := value << lsbIndex + // calculate the new packed value + newPacked := packed&unsetMask | setMask + // unpack the new packed value into the data + d.UnpackBigEndian(newPacked) +} + +// SetSignedBitsLittleEndian sets the little-endian bit range [start, start+length) to the provided signed value. +func (d *Data) SetSignedBitsLittleEndian(start, length uint8, value int64) { + d.SetUnsignedBitsLittleEndian(start, length, reinterpret.AsUnsigned(value, length)) +} + +// SetSignedBitsBigEndian sets the big-endian bit range [start, start+length) to the provided signed value. +func (d *Data) SetSignedBitsBigEndian(start, length uint8, value int64) { + d.SetUnsignedBitsBigEndian(start, length, reinterpret.AsUnsigned(value, length)) +} + +// Bit returns the value of the i:th bit in the data as a bool. +func (d *Data) Bit(i uint8) bool { + if i > 63 { + return false + } + // calculate which byte the bit belongs to + byteIndex := i / 8 + // calculate bit mask for extracting the bit + bitMask := uint8(1 << (i % 8)) + // mocks the bit + bit := d[byteIndex]&bitMask > 0 + // done + return bit +} + +// SetBit sets the value of the i:th bit in the data. +func (d *Data) SetBit(i uint8, value bool) { + if i > 63 { + return + } + byteIndex := i / 8 + bitIndex := i % 8 + if value { + d[byteIndex] |= uint8(1 << bitIndex) + } else { + d[byteIndex] &= ^uint8(1 << bitIndex) + } +} + +// PackLittleEndian packs the data into a contiguous uint64 value for little-endian signals. +func (d *Data) PackLittleEndian() uint64 { + var packed uint64 + packed |= uint64(d[0]) << (0 * 8) + packed |= uint64(d[1]) << (1 * 8) + packed |= uint64(d[2]) << (2 * 8) + packed |= uint64(d[3]) << (3 * 8) + packed |= uint64(d[4]) << (4 * 8) + packed |= uint64(d[5]) << (5 * 8) + packed |= uint64(d[6]) << (6 * 8) + packed |= uint64(d[7]) << (7 * 8) + return packed +} + +// PackBigEndian packs the data into a contiguous uint64 value for big-endian signals. +func (d *Data) PackBigEndian() uint64 { + var packed uint64 + packed |= uint64(d[0]) << (7 * 8) + packed |= uint64(d[1]) << (6 * 8) + packed |= uint64(d[2]) << (5 * 8) + packed |= uint64(d[3]) << (4 * 8) + packed |= uint64(d[4]) << (3 * 8) + packed |= uint64(d[5]) << (2 * 8) + packed |= uint64(d[6]) << (1 * 8) + packed |= uint64(d[7]) << (0 * 8) + return packed +} + +// UnpackLittleEndian sets the value of d.Bytes by unpacking the provided value as sequential little-endian bits. +func (d *Data) UnpackLittleEndian(packed uint64) { + d[0] = uint8(packed >> (0 * 8)) + d[1] = uint8(packed >> (1 * 8)) + d[2] = uint8(packed >> (2 * 8)) + d[3] = uint8(packed >> (3 * 8)) + d[4] = uint8(packed >> (4 * 8)) + d[5] = uint8(packed >> (5 * 8)) + d[6] = uint8(packed >> (6 * 8)) + d[7] = uint8(packed >> (7 * 8)) +} + +// UnpackBigEndian sets the value of d.Bytes by unpacking the provided value as sequential big-endian bits. +func (d *Data) UnpackBigEndian(packed uint64) { + d[0] = uint8(packed >> (7 * 8)) + d[1] = uint8(packed >> (6 * 8)) + d[2] = uint8(packed >> (5 * 8)) + d[3] = uint8(packed >> (4 * 8)) + d[4] = uint8(packed >> (3 * 8)) + d[5] = uint8(packed >> (2 * 8)) + d[6] = uint8(packed >> (1 * 8)) + d[7] = uint8(packed >> (0 * 8)) +} + +// invertEndian converts from big-endian to little-endian bit indexing and vice versa. +func invertEndian(i uint8) uint8 { + row := i / 8 + col := i % 8 + oppositeRow := 7 - row + bitIndex := (oppositeRow * 8) + col + return bitIndex +} + +// CheckBitRangeLittleEndian checks that a little-endian bit range fits in the data. +func CheckBitRangeLittleEndian(frameLength, rangeStart, rangeLength uint8) error { + lsbIndex := rangeStart + msbIndex := rangeStart + rangeLength - 1 + upperBound := frameLength * 8 + if msbIndex >= upperBound { + return fmt.Errorf("bit range out of bounds [0, %v): [%v, %v]", upperBound, lsbIndex, msbIndex) + } + return nil +} + +// CheckBitRangeBigEndian checks that a big-endian bit range fits in the data. +func CheckBitRangeBigEndian(frameLength, rangeStart, rangeLength uint8) error { + upperBound := frameLength * 8 + if rangeStart >= upperBound { + return fmt.Errorf("bit range starts out of bounds [0, %v): %v", upperBound, rangeStart) + } + msbIndex := invertEndian(rangeStart) + lsbIndex := msbIndex - rangeLength + 1 + end := invertEndian(lsbIndex) + if end >= upperBound { + return fmt.Errorf("bit range ends out of bounds [0, %v): %v", upperBound, end) + } + return nil +} + +// CheckValue checks that a value fits in a number of bits. +func CheckValue(value uint64, bits uint8) error { + upperBound := uint64(1 << bits) + if value >= upperBound { + return fmt.Errorf("value out of bounds [0, %v): %v", upperBound, value) + } + return nil +} diff --git a/data_test.go b/data_test.go new file mode 100644 index 0000000..44acc9c --- /dev/null +++ b/data_test.go @@ -0,0 +1,333 @@ +package can + +import ( + "fmt" + "testing" + "testing/quick" + + "github.com/stretchr/testify/require" +) + +func TestData_Bit(t *testing.T) { + for i, tt := range []struct { + data Data + bits []struct { + i uint8 + bit bool + } + }{ + { + data: Data{0x01, 0x23}, + bits: []struct { + i uint8 + bit bool + }{ + // nibble 1: 0x1 + {bit: true, i: 0}, + {bit: false, i: 1}, + {bit: false, i: 2}, + {bit: false, i: 3}, + // nibble 2: 0x0 + {bit: false, i: 4}, + {bit: false, i: 5}, + {bit: false, i: 6}, + {bit: false, i: 7}, + // nibble 3: 0x3 + {bit: true, i: 8}, + {bit: true, i: 9}, + {bit: false, i: 10}, + {bit: false, i: 11}, + // nibble 4: 0x2 + {bit: false, i: 12}, + {bit: true, i: 13}, + {bit: false, i: 14}, + {bit: false, i: 15}, + }, + }, + } { + i, tt := i, tt + t.Run("Get", func(t *testing.T) { + i, tt := i, tt + for j, ttBit := range tt.bits { + j, ttBit := j, ttBit + t.Run(fmt.Sprintf("tt=%v,bit=%v", i, j), func(t *testing.T) { + bit := tt.data.Bit(ttBit.i) + require.Equal(t, ttBit.bit, bit) + }) + } + }) + t.Run("Set", func(t *testing.T) { + i, tt := i, tt + t.Run(fmt.Sprintf("data=%v", i), func(t *testing.T) { + var data Data + for _, tBit := range tt.bits { + data.SetBit(tBit.i, tBit.bit) + } + require.Equal(t, tt.data, data) + }) + }) + } +} + +func TestData_Property_SetGetBit(t *testing.T) { + f := func(_ Data, _ uint8, bit bool) bool { + return bit + } + g := func(data Data, i uint8, bit bool) bool { + i %= 64 + data.SetBit(i, bit) + return data.Bit(i) + } + require.NoError(t, quick.CheckEqual(f, g, nil)) +} + +func TestData_LittleEndian(t *testing.T) { + for i, tt := range []struct { + data Data + signals []struct { + start uint8 + length uint8 + unsigned uint64 + signed int64 + } + }{ + { + data: Data{0x80, 0x01}, + signals: []struct { + start uint8 + length uint8 + unsigned uint64 + signed int64 + }{ + {start: 7, length: 2, unsigned: 0x3, signed: -1}, + }, + }, + { + data: Data{0x01, 0x02, 0x03}, + signals: []struct { + start uint8 + length uint8 + unsigned uint64 + signed int64 + }{ + {start: 0, length: 24, unsigned: 0x030201, signed: 197121}, + }, + }, + { + data: Data{0x40, 0x23, 0x01, 0x12}, + signals: []struct { + start uint8 + length uint8 + unsigned uint64 + signed int64 + }{ + {start: 24, length: 8, unsigned: 0x12, signed: 18}, + {start: 8, length: 8, unsigned: 0x23, signed: 35}, + {start: 4, length: 16, unsigned: 0x1234, signed: 4660}, + }, + }, + } { + i, tt := i, tt + t.Run(fmt.Sprintf("UnsignedBits:%v", i), func(t *testing.T) { + for j, signal := range tt.signals { + j, signal := j, signal + t.Run(fmt.Sprintf("signal:%v", j), func(t *testing.T) { + actual := tt.data.UnsignedBitsLittleEndian(signal.start, signal.length) + require.Equal(t, signal.unsigned, actual) + }) + } + }) + t.Run(fmt.Sprintf("SignedBits:%v", i), func(t *testing.T) { + for j, signal := range tt.signals { + j, signal := j, signal + t.Run(fmt.Sprintf("signal:%v", j), func(t *testing.T) { + actual := tt.data.SignedBitsLittleEndian(signal.start, signal.length) + require.Equal(t, signal.signed, actual) + }) + } + }) + t.Run(fmt.Sprintf("SetUnsignedBits:%v", i), func(t *testing.T) { + var data Data + for j, signal := range tt.signals { + j, signal := j, signal + t.Run(fmt.Sprintf("data:%v", j), func(t *testing.T) { + data.SetUnsignedBitsLittleEndian(signal.start, signal.length, signal.unsigned) + }) + } + require.Equal(t, tt.data, data) + }) + t.Run(fmt.Sprintf("SetSignedBits:%v", i), func(t *testing.T) { + var data Data + for j, signal := range tt.signals { + j, signal := j, signal + t.Run(fmt.Sprintf("data:%v", j), func(t *testing.T) { + data.SetSignedBitsLittleEndian(signal.start, signal.length, signal.signed) + }) + } + require.Equal(t, tt.data, data) + }) + } +} + +func TestData_BigEndian(t *testing.T) { + for i, tt := range []struct { + data Data + signals []struct { + start uint8 + length uint8 + unsigned uint64 + signed int64 + } + }{ + { + data: Data{0x3f, 0xf7, 0x0d, 0xc4, 0x0c, 0x93, 0xff, 0xff}, + signals: []struct { + start uint8 + length uint8 + unsigned uint64 + signed int64 + }{ + {start: 7, length: 3, unsigned: 0x1, signed: 1}, + {start: 4, length: 1, unsigned: 0x1, signed: -1}, + {start: 55, length: 16, unsigned: 0xffff, signed: -1}, + {start: 39, length: 16, unsigned: 0xc93, signed: 3219}, + {start: 23, length: 16, unsigned: 0xdc4, signed: 3524}, + {start: 3, length: 12, unsigned: 0xff7, signed: -9}, + }, + }, + { + data: Data{0x3f, 0xe4, 0x0e, 0xb6, 0x0c, 0xba, 0x00, 0x05}, + signals: []struct { + start uint8 + length uint8 + unsigned uint64 + signed int64 + }{ + {start: 7, length: 3, unsigned: 0x1, signed: 1}, + {start: 4, length: 1, unsigned: 0x1, signed: -1}, + {start: 55, length: 16, unsigned: 0x5, signed: 5}, + {start: 39, length: 16, unsigned: 0xcba, signed: 3258}, + {start: 23, length: 16, unsigned: 0xeb6, signed: 3766}, + {start: 3, length: 12, unsigned: 0xfe4, signed: -28}, + }, + }, + { + data: Data{0x30, 0x53, 0x23, 0xe5, 0x0e, 0x11, 0xff, 0xff}, + signals: []struct { + start uint8 + length uint8 + unsigned uint64 + signed int64 + }{ + {start: 7, length: 3, unsigned: 0x1, signed: 1}, + {start: 4, length: 1, unsigned: 0x1, signed: -1}, + {start: 55, length: 16, unsigned: 0xffff, signed: -1}, + {start: 39, length: 16, unsigned: 0xe11, signed: 3601}, + {start: 23, length: 16, unsigned: 0x23e5, signed: 9189}, + {start: 3, length: 12, unsigned: 0x53, signed: 83}, + }, + }, + } { + i, tt := i, tt + t.Run(fmt.Sprintf("UnsignedBits:%v", i), func(t *testing.T) { + for j, signal := range tt.signals { + j, signal := j, signal + t.Run(fmt.Sprintf("signal:%v", j), func(t *testing.T) { + actual := tt.data.UnsignedBitsBigEndian(signal.start, signal.length) + require.Equal(t, signal.unsigned, actual) + }) + } + }) + t.Run(fmt.Sprintf("SignedBits:%v", i), func(t *testing.T) { + for j, signal := range tt.signals { + j, signal := j, signal + t.Run(fmt.Sprintf("signal:%v", j), func(t *testing.T) { + actual := tt.data.SignedBitsBigEndian(signal.start, signal.length) + require.Equal(t, signal.signed, actual) + }) + } + }) + t.Run(fmt.Sprintf("SetUnsignedBits:%v", i), func(t *testing.T) { + var data Data + for j, signal := range tt.signals { + j, signal := j, signal + t.Run(fmt.Sprintf("data:%v", j), func(t *testing.T) { + data.SetUnsignedBitsBigEndian(signal.start, signal.length, signal.unsigned) + }) + } + require.Equal(t, tt.data, data) + }) + t.Run(fmt.Sprintf("SetSignedBits:%v", i), func(t *testing.T) { + var data Data + for j, signal := range tt.signals { + j, signal := j, signal + t.Run(fmt.Sprintf("data:%v", j), func(t *testing.T) { + data.SetSignedBitsBigEndian(signal.start, signal.length, signal.signed) + }) + } + require.Equal(t, tt.data, data) + }) + } +} + +func TestInvertEndian_Property_Idempotent(t *testing.T) { + for i := uint8(0); i < 64; i++ { + require.Equal(t, i, invertEndian(invertEndian(i))) + } +} + +func TestPackUnpackBigEndian(t *testing.T) { + f := func(data Data) Data { + return data + } + g := func(data Data) Data { + data.UnpackBigEndian(data.PackBigEndian()) + return data + } + require.NoError(t, quick.CheckEqual(f, g, nil)) +} + +func TestPackUnpackLittleEndian(t *testing.T) { + f := func(data Data) Data { + return data + } + g := func(data Data) Data { + data.UnpackLittleEndian(data.PackLittleEndian()) + return data + } + require.NoError(t, quick.CheckEqual(f, g, nil)) +} + +func TestData_CheckBitRange(t *testing.T) { + // example case that big-endian signals and little-endian signals use different indexing + require.NoError(t, CheckBitRangeBigEndian(8, 55, 16)) + require.Error(t, CheckBitRangeLittleEndian(8, 55, 16)) +} + +func BenchmarkData_UnpackLittleEndian(b *testing.B) { + var data Data + for i := 0; i < b.N; i++ { + data.UnpackLittleEndian(0) + } +} + +func BenchmarkData_UnpackBigEndian(b *testing.B) { + var data Data + for i := 0; i < b.N; i++ { + data.UnpackBigEndian(0) + } +} + +func BenchmarkData_PackBigEndian(b *testing.B) { + var data Data + for i := 0; i < b.N; i++ { + _ = data.PackBigEndian() + } +} + +func BenchmarkData_PackLittleEndian(b *testing.B) { + var data Data + for i := 0; i < b.N; i++ { + _ = data.PackLittleEndian() + } +} diff --git a/frame.go b/frame.go new file mode 100644 index 0000000..4fa0af7 --- /dev/null +++ b/frame.go @@ -0,0 +1,131 @@ +package can + +import ( + "encoding/hex" + "fmt" + "strconv" + "strings" +) + +const ( + idBits = 11 + extendedIDBits = 29 +) + +// CAN format constants. +const ( + MaxID = 0x7ff + MaxExtendedID = 0x1fffffff +) + +// Frame represents a CAN frame. +// +// A Frame is intentionally designed to fit into 16 bytes on common architectures +// and is therefore amenable to pass-by-value and judicious copying. +type Frame struct { + // ID is the CAN ID + ID uint32 + // Length is the number of bytes of data in the frame. + Length uint8 + // Data is the frame data. + Data Data + // IsRemote is true for remote frames. + IsRemote bool + // IsExtended is true for extended frames, i.e. frames with 29-bit IDs. + IsExtended bool +} + +// Validate returns an error if the Frame is not a valid CAN frame. +func (f *Frame) Validate() error { + // Validate: ID + if f.IsExtended && f.ID > MaxExtendedID { + return fmt.Errorf( + "invalid extended CAN id: %v does not fit in %v bits", + f.ID, extendedIDBits) + } else if !f.IsExtended && f.ID > MaxID { + return fmt.Errorf( + "invalid standard CAN id: %v does not fit in %v bits", + f.ID, idBits) + } + // Validate: Data + if f.Length > MaxDataLength { + return fmt.Errorf("invalid data length: %v", f.Length) + } + return nil +} + +// String returns an ASCII representation the CAN frame. +// +// Format: +// +// ([0-9A-F]{3}|[0-9A-F]{3})#(R[0-8]?|[0-9A-F]{0,16}) +// +// The format is compatible with the candump(1) log file format. +func (f Frame) String() string { + var id string + if f.IsExtended { + id = fmt.Sprintf("%08X", f.ID) + } else { + id = fmt.Sprintf("%03X", f.ID) + } + if f.IsRemote && f.Length == 0 { + return id + "#R" + } else if f.IsRemote { + return id + "#R" + strconv.Itoa(int(f.Length)) + } + return id + "#" + strings.ToUpper(hex.EncodeToString(f.Data[:f.Length])) +} + +// UnmarshalString sets *f using the provided ASCII representation of a Frame. +func (f *Frame) UnmarshalString(s string) error { + // Split split into parts + parts := strings.Split(s, "#") + if len(parts) != 2 { + return fmt.Errorf("invalid frame format: %v", s) + } + idPart, dataPart := parts[0], parts[1] + var frame Frame + // Parse: IsExtended + if len(idPart) != 3 && len(idPart) != 8 { + return fmt.Errorf("invalid ID length: %v", s) + } + frame.IsExtended = len(idPart) == 8 + // Parse: ID + id, err := strconv.ParseUint(idPart, 16, 32) + if err != nil { + return fmt.Errorf("invalid frame ID: %v", s) + } + frame.ID = uint32(id) + if len(dataPart) == 0 { + *f = frame + return nil + } + // Parse: IsRemote + if dataPart[0] == 'R' { + frame.IsRemote = true + if len(dataPart) > 2 { + return fmt.Errorf("invalid remote length: %v", s) + } else if len(dataPart) == 2 { + dataLength, err := strconv.Atoi(dataPart[1:2]) + if err != nil { + return fmt.Errorf("invalid remote length: %v: %w", s, err) + } + frame.Length = uint8(dataLength) + } + *f = frame + return nil + } + // Parse: Length + if len(dataPart) > 16 || len(dataPart)%2 != 0 { + return fmt.Errorf("invalid data length: %v", s) + } + frame.Length = uint8(len(dataPart) / 2) + // Parse: Data + decodedData, err := hex.DecodeString(dataPart) + if err != nil { + return fmt.Errorf("invalid data: %v: %w", s, err) + } + copy(frame.Data[:], decodedData) + *f = frame + return nil +} diff --git a/frame_json.go b/frame_json.go new file mode 100644 index 0000000..a96efca --- /dev/null +++ b/frame_json.go @@ -0,0 +1,95 @@ +package can + +import ( + "encoding/hex" + "encoding/json" + "fmt" + "strconv" +) + +type jsonFrame struct { + ID uint32 `json:"id"` + Data *string `json:"data"` + Length *uint8 `json:"length"` + Extended *bool `json:"extended"` + Remote *bool `json:"remote"` +} + +// JSON returns the JSON-encoding of f, using hex-encoding for the data. +// +// Examples: +// +// {"id":32,"data":"0102030405060708"} +// {"id":32,"extended":true,"remote":true,"length":4} +func (f Frame) JSON() string { + switch { + case f.IsRemote && f.IsExtended: + return `{"id":` + strconv.Itoa(int(f.ID)) + + `,"extended":true,"remote":true,"length":` + + strconv.Itoa(int(f.Length)) + `}` + case f.IsRemote: + return `{"id":` + strconv.Itoa(int(f.ID)) + + `,"remote":true,"length":` + + strconv.Itoa(int(f.Length)) + `}` + case f.IsExtended && f.Length == 0: + return `{"id":` + strconv.Itoa(int(f.ID)) + `,"extended":true}` + case f.IsExtended: + return `{"id":` + strconv.Itoa(int(f.ID)) + + `,"data":"` + hex.EncodeToString(f.Data[:f.Length]) + `"` + + `,"extended":true}` + case f.Length == 0: + return `{"id":` + strconv.Itoa(int(f.ID)) + `}` + default: + return `{"id":` + strconv.Itoa(int(f.ID)) + + `,"data":"` + hex.EncodeToString(f.Data[:f.Length]) + `"}` + } +} + +// MarshalJSON returns the JSON-encoding of f, using hex-encoding for the data. +// +// See JSON for an example of the JSON schema. +func (f Frame) MarshalJSON() ([]byte, error) { + return []byte(f.JSON()), nil +} + +// UnmarshalJSON sets *f using the provided JSON-encoded values. +// +// See MarshalJSON for an example of the expected JSON schema. +// +// The result should be checked with Validate to guard against invalid JSON data. +func (f *Frame) UnmarshalJSON(jsonData []byte) error { + jf := jsonFrame{} + if err := json.Unmarshal(jsonData, &jf); err != nil { + return err + } + if jf.Data != nil { + data, err := hex.DecodeString(*jf.Data) + if err != nil { + return fmt.Errorf("failed to hex-decode CAN data: %v: %w", string(jsonData), err) + } + f.Data = Data{} + copy(f.Data[:], data) + f.Length = uint8(len(data)) + } else { + f.Data = Data{} + f.Length = 0 + } + f.ID = jf.ID + if jf.Remote != nil { + f.IsRemote = *jf.Remote + } else { + f.IsRemote = false + } + if f.IsRemote { + if jf.Length == nil { + return fmt.Errorf("missing length field for remote JSON frame: %v", string(jsonData)) + } + f.Length = *jf.Length + } + if jf.Extended != nil { + f.IsExtended = *jf.Extended + } else { + f.IsExtended = false + } + return nil +} diff --git a/frame_json_test.go b/frame_json_test.go new file mode 100644 index 0000000..1d9e802 --- /dev/null +++ b/frame_json_test.go @@ -0,0 +1,125 @@ +package can + +import ( + "encoding/json" + "fmt" + "math/rand" + "reflect" + "testing" + "testing/quick" + + "github.com/stretchr/testify/assert" +) + +func TestFrame_JSON(t *testing.T) { + for _, tt := range []struct { + jsonFrame string + frame Frame + }{ + { + // Standard frame + jsonFrame: `{"id":42,"data":"00010203"}`, + frame: Frame{ + ID: 42, + Length: 4, + Data: Data{0x00, 0x01, 0x02, 0x03}, + }, + }, + { + // Standard frame, no data + jsonFrame: `{"id":42}`, + frame: Frame{ID: 42}, + }, + { + // Standard remote frame + jsonFrame: `{"id":42,"remote":true,"length":4}`, + frame: Frame{ + ID: 42, + IsRemote: true, + Length: 4, + }, + }, + { + // Extended frame + jsonFrame: `{"id":42,"data":"0001020304050607","extended":true}`, + frame: Frame{ + ID: 42, + IsExtended: true, + Length: 8, + Data: Data{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07}, + }, + }, + { + // Extended frame, no data + jsonFrame: `{"id":42,"extended":true}`, + frame: Frame{ID: 42, IsExtended: true}, + }, + { + // Extended remote frame + jsonFrame: `{"id":42,"extended":true,"remote":true,"length":8}`, + frame: Frame{ + ID: 42, + IsExtended: true, + IsRemote: true, + Length: 8, + }, + }, + } { + tt := tt + t.Run(fmt.Sprintf("JSON|frame=%v", tt.frame), func(t *testing.T) { + assert.Equal(t, tt.jsonFrame, tt.frame.JSON()) + }) + t.Run(fmt.Sprintf("UnmarshalJSON|frame=%v", tt.frame), func(t *testing.T) { + var frame Frame + if err := json.Unmarshal([]byte(tt.jsonFrame), &frame); err != nil { + t.Fatal(err) + } + assert.Equal(t, tt.frame, frame) + }) + } +} + +func TestFrame_UnmarshalJSON_Invalid(t *testing.T) { + var f Frame + t.Run("invalid JSON", func(t *testing.T) { + data := `foobar` + assert.NotNil(t, f.UnmarshalJSON([]uint8(data))) + }) + t.Run("invalid payload", func(t *testing.T) { + data := `{"id":1,"data":"foobar","extended":false,"remote":false}` + assert.NotNil(t, f.UnmarshalJSON([]uint8(data))) + }) +} + +func (Frame) Generate(rand *rand.Rand, size int) reflect.Value { + f := Frame{ + IsExtended: rand.Intn(2) == 0, + IsRemote: rand.Intn(2) == 0, + } + if f.IsExtended { + f.ID = rand.Uint32() & MaxExtendedID + } else { + f.ID = rand.Uint32() & MaxID + } + f.Length = uint8(rand.Intn(9)) + if !f.IsRemote { + _, _ = rand.Read(f.Data[:f.Length]) + } + return reflect.ValueOf(f) +} + +func TestPropertyFrame_MarshalUnmarshalJSON(t *testing.T) { + f := func(f Frame) Frame { + return f + } + g := func(f Frame) Frame { + f2 := Frame{} + if err := json.Unmarshal([]uint8(f.JSON()), &f2); err != nil { + t.Fatal(err) + } + return f2 + } + if err := quick.CheckEqual(f, g, nil); err != nil { + t.Fatal(err) + } +} diff --git a/frame_string_test.go b/frame_string_test.go new file mode 100644 index 0000000..e6bd6e5 --- /dev/null +++ b/frame_string_test.go @@ -0,0 +1,85 @@ +package can + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestFrame_String(t *testing.T) { + for _, tt := range []struct { + frame Frame + str string + }{ + { + frame: Frame{ + ID: 0x62e, + Length: 2, + Data: Data{0x10, 0x44}, + }, + str: "62E#1044", + }, + { + frame: Frame{ + ID: 0x410, + IsRemote: true, + Length: 3, + }, + str: "410#R3", + }, + { + frame: Frame{ + ID: 0xd2, + Length: 2, + Data: Data{0xf0, 0x31}, + }, + str: "0D2#F031", + }, + { + frame: Frame{ID: 0xee}, + str: "0EE#", + }, + { + frame: Frame{ID: 0}, + str: "000#", + }, + { + frame: Frame{ID: 0, IsExtended: true}, + str: "00000000#", + }, + { + frame: Frame{ID: 0x1234abcd, IsExtended: true}, + str: "1234ABCD#", + }, + } { + tt := tt + t.Run(fmt.Sprintf("String|frame=%v,str=%v", tt.frame, tt.str), func(t *testing.T) { + assert.Equal(t, tt.str, tt.frame.String()) + }) + t.Run(fmt.Sprintf("UnmarshalString|frame=%v,str=%v", tt.frame, tt.str), func(t *testing.T) { + var actual Frame + if err := actual.UnmarshalString(tt.str); err != nil { + t.Fatal(err) + } + assert.Equal(t, actual, tt.frame) + }) + } +} + +func TestParseFrame_Errors(t *testing.T) { + for _, tt := range []string{ + "foo", // invalid + "foo#", // invalid ID + "0D23#F031", // invalid ID length + "62E#104400000000000000", // invalid data length + } { + tt := tt + t.Run(fmt.Sprintf("str=%v", tt), func(t *testing.T) { + var frame Frame + err := frame.UnmarshalString(tt) + assert.Error(t, err) + assert.Equal(t, Frame{}, frame) + }) + } +} diff --git a/frame_test.go b/frame_test.go new file mode 100644 index 0000000..0263820 --- /dev/null +++ b/frame_test.go @@ -0,0 +1,28 @@ +package can + +import ( + "fmt" + "testing" + "unsafe" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// If this mocks ever starts failing, the documentation needs to be updated +// to prefer pass-by-pointer over pass-by-value. +func TestFrame_Size(t *testing.T) { + require.True(t, unsafe.Sizeof(Frame{}) <= 16, "Frame size is <= 16 bytes") +} + +func TestFrame_Validate_Error(t *testing.T) { + for _, tt := range []Frame{ + {ID: MaxID + 1}, + {ID: MaxExtendedID + 1, IsExtended: true}, + } { + tt := tt + t.Run(fmt.Sprintf("%v", tt), func(t *testing.T) { + assert.NotNil(t, tt.Validate(), "should return validation error") + }) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..4e41ce6 --- /dev/null +++ b/go.mod @@ -0,0 +1,24 @@ +module go.einride.tech/can + +go 1.13 + +require ( + github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect + github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d // indirect + github.com/davecgh/go-spew v1.1.1 + github.com/fatih/color v1.7.0 + github.com/golang/mock v1.3.1 + github.com/golang/protobuf v1.3.2 + github.com/mattn/go-colorable v0.1.4 // indirect + github.com/mattn/go-isatty v0.0.9 // indirect + github.com/shurcooL/go v0.0.0-20190704215121-7189cc372560 // indirect + github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041 + github.com/stretchr/testify v1.4.0 + go.uber.org/goleak v0.10.0 + golang.org/x/net v0.0.0-20190930134127-c5a3c61f89f3 + golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e + golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24 + golang.org/x/tools v0.0.0-20190425150028-36563e24a262 + golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 + gopkg.in/alecthomas/kingpin.v2 v2.2.6 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..342224a --- /dev/null +++ b/go.sum @@ -0,0 +1,52 @@ +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d h1:UQZhZ2O0vMHr2cI+DC1Mbh0TJxzA3RcLoMsFw+aXw7E= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +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/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/golang/mock v1.3.1 h1:qGJ6qTW+x6xX/my+8YUVl4WNpX9B7+/l2tRsHGZ7f2s= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg= +github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= +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/shurcooL/go v0.0.0-20190704215121-7189cc372560 h1:SpaoQDTgpo2YZkvmr2mtgloFFfPTjtLMlZkQtNAPQik= +github.com/shurcooL/go v0.0.0-20190704215121-7189cc372560/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= +github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041 h1:llrF3Fs4018ePo4+G/HV/uQUqEI1HMDjCeOf2V6puPc= +github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +go.uber.org/goleak v0.10.0 h1:G3eWbSNIskeRqtsN/1uI5B+eP73y3JUuBsv9AZjehb4= +go.uber.org/goleak v0.10.0/go.mod h1:VCZuO8V8mFPlL0F5J5GK1rtHV3DrFcQ1R8ryq7FK0aI= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190930134127-c5a3c61f89f3 h1:6KET3Sqa7fkVfD63QnAM81ZeYg5n4HwApOJkufONnHA= +golang.org/x/net v0.0.0-20190930134127-c5a3c61f89f3/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24 h1:R8bzl0244nw47n1xKs1MUMAaTNgjavKcN/aX2Ss3+Fo= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262 h1:qsl9y/CJx34tuA7QCPNp86JNJe4spst6Ff8MjvPUdPg= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/internal/clock/clock.go b/internal/clock/clock.go new file mode 100644 index 0000000..54b3647 --- /dev/null +++ b/internal/clock/clock.go @@ -0,0 +1,32 @@ +// Package clock provides primitives for mocking time. +package clock + +import ( + "time" + + "github.com/golang/protobuf/ptypes/timestamp" +) + +// Clock provides capabilities from the time standard library package. +type Clock interface { + // After waits for the duration to elapse and then sends the current time on the returned channel. + After(duration time.Duration) <-chan time.Time + + // NewTicker returns a new Ticker. + NewTicker(d time.Duration) Ticker + + // Now returns the current local time. + Now() time.Time + + // NowProto returns a new Protobuf timestamp representing the current local time. + NowProto() *timestamp.Timestamp +} + +// Ticker wraps the time.Ticker class. +type Ticker interface { + // C returns the channel on which the ticks are delivered. + C() <-chan time.Time + + // Stop the Ticker. + Stop() +} diff --git a/internal/clock/system.go b/internal/clock/system.go new file mode 100644 index 0000000..2d33888 --- /dev/null +++ b/internal/clock/system.go @@ -0,0 +1,41 @@ +package clock + +import ( + "time" + + "github.com/golang/protobuf/ptypes" + "github.com/golang/protobuf/ptypes/timestamp" +) + +// System returns a Clock implementation that delegate to the time package. +func System() Clock { + return &systemClock{} +} + +type systemClock struct{} + +var _ Clock = &systemClock{} + +func (c systemClock) After(d time.Duration) <-chan time.Time { + return time.After(d) +} + +func (c systemClock) NowProto() *timestamp.Timestamp { + return ptypes.TimestampNow() +} + +func (c systemClock) NewTicker(d time.Duration) Ticker { + return &systemTicker{Ticker: *time.NewTicker(d)} +} + +func (c systemClock) Now() time.Time { + return time.Now() +} + +type systemTicker struct { + time.Ticker +} + +func (t systemTicker) C() <-chan time.Time { + return t.Ticker.C +} diff --git a/internal/generate/compile.go b/internal/generate/compile.go new file mode 100644 index 0000000..6f87209 --- /dev/null +++ b/internal/generate/compile.go @@ -0,0 +1,207 @@ +package generate + +import ( + "fmt" + "sort" + "time" + + "go.einride.tech/can/pkg/dbc" + "go.einride.tech/can/pkg/descriptor" +) + +type CompileResult struct { + Database *descriptor.Database + Warnings []error +} + +func Compile(sourceFile string, data []byte) (result *CompileResult, err error) { + p := dbc.NewParser(sourceFile, data) + if err := p.Parse(); err != nil { + return nil, fmt.Errorf("failed to parse DBC source file: %w", err) + } + defs := p.Defs() + c := &compiler{ + db: &descriptor.Database{SourceFile: sourceFile}, + defs: defs, + } + c.collectDescriptors() + c.addMetadata() + c.sortDescriptors() + return &CompileResult{Database: c.db, Warnings: c.warnings}, nil +} + +type compileError struct { + def dbc.Def + reason string +} + +func (e *compileError) Error() string { + return fmt.Sprintf("failed to compile: %v (%v)", e.reason, e.def) +} + +type compiler struct { + db *descriptor.Database + defs []dbc.Def + warnings []error +} + +func (c *compiler) addWarning(warning error) { + c.warnings = append(c.warnings, warning) +} + +func (c *compiler) collectDescriptors() { + for _, def := range c.defs { + switch def := def.(type) { + case *dbc.VersionDef: + c.db.Version = def.Version + case *dbc.MessageDef: + if def.MessageID == dbc.IndependentSignalsMessageID { + continue // don't compile + } + message := &descriptor.Message{ + Name: string(def.Name), + ID: def.MessageID.ToCAN(), + IsExtended: def.MessageID.IsExtended(), + Length: uint8(def.Size), + SenderNode: string(def.Transmitter), + } + for _, signalDef := range def.Signals { + signal := &descriptor.Signal{ + Name: string(signalDef.Name), + IsBigEndian: signalDef.IsBigEndian, + IsSigned: signalDef.IsSigned, + IsMultiplexer: signalDef.IsMultiplexerSwitch, + IsMultiplexed: signalDef.IsMultiplexed, + MultiplexerValue: uint(signalDef.MultiplexerSwitch), + Start: uint8(signalDef.StartBit), + Length: uint8(signalDef.Size), + Scale: signalDef.Factor, + Offset: signalDef.Offset, + Min: signalDef.Minimum, + Max: signalDef.Maximum, + Unit: signalDef.Unit, + } + for _, receiver := range signalDef.Receivers { + signal.ReceiverNodes = append(signal.ReceiverNodes, string(receiver)) + } + message.Signals = append(message.Signals, signal) + } + c.db.Messages = append(c.db.Messages, message) + case *dbc.NodesDef: + for _, node := range def.NodeNames { + c.db.Nodes = append(c.db.Nodes, &descriptor.Node{Name: string(node)}) + } + } + } +} + +func (c *compiler) addMetadata() { + for _, def := range c.defs { + switch def := def.(type) { + case *dbc.CommentDef: + switch def.ObjectType { + case dbc.ObjectTypeMessage: + if def.MessageID == dbc.IndependentSignalsMessageID { + continue // don't compile + } + message, ok := c.db.Message(def.MessageID.ToCAN()) + if !ok { + c.addWarning(&compileError{def: def, reason: "no declared message"}) + continue + } + message.Description = def.Comment + case dbc.ObjectTypeSignal: + if def.MessageID == dbc.IndependentSignalsMessageID { + continue // don't compile + } + signal, ok := c.db.Signal(def.MessageID.ToCAN(), string(def.SignalName)) + if !ok { + c.addWarning(&compileError{def: def, reason: "no declared signal"}) + continue + } + signal.Description = def.Comment + case dbc.ObjectTypeNetworkNode: + node, ok := c.db.Node(string(def.NodeName)) + if !ok { + c.addWarning(&compileError{def: def, reason: "no declared node"}) + continue + } + node.Description = def.Comment + } + case *dbc.ValueDescriptionsDef: + if def.MessageID == dbc.IndependentSignalsMessageID { + continue // don't compile + } + if def.ObjectType != dbc.ObjectTypeSignal { + continue // don't compile + } + signal, ok := c.db.Signal(def.MessageID.ToCAN(), string(def.SignalName)) + if !ok { + c.addWarning(&compileError{def: def, reason: "no declared signal"}) + continue + } + for _, valueDescription := range def.ValueDescriptions { + signal.ValueDescriptions = append(signal.ValueDescriptions, &descriptor.ValueDescription{ + Description: valueDescription.Description, + Value: int(valueDescription.Value), + }) + } + case *dbc.AttributeValueForObjectDef: + switch def.ObjectType { + case dbc.ObjectTypeMessage: + msg, ok := c.db.Message(def.MessageID.ToCAN()) + if !ok { + c.addWarning(&compileError{def: def, reason: "no declared message"}) + continue + } + switch def.AttributeName { + case "GenMsgSendType": + if err := msg.SendType.UnmarshalString(def.StringValue); err != nil { + c.addWarning(&compileError{def: def, reason: err.Error()}) + continue + } + case "GenMsgCycleTime": + msg.CycleTime = time.Duration(def.IntValue) * time.Millisecond + case "GenMsgDelayTime": + msg.DelayTime = time.Duration(def.IntValue) * time.Millisecond + } + case dbc.ObjectTypeSignal: + sig, ok := c.db.Signal(def.MessageID.ToCAN(), string(def.SignalName)) + if !ok { + c.addWarning(&compileError{def: def, reason: "no declared signal"}) + } + if def.AttributeName == "GenSigStartValue" { + sig.DefaultValue = int(def.IntValue) + } + } + } + } +} + +func (c *compiler) sortDescriptors() { + // Sort nodes by name + sort.Slice(c.db.Nodes, func(i, j int) bool { + return c.db.Nodes[i].Name < c.db.Nodes[j].Name + }) + // Sort messages by ID + sort.Slice(c.db.Messages, func(i, j int) bool { + return c.db.Messages[i].ID < c.db.Messages[j].ID + }) + for _, m := range c.db.Messages { + m := m + // Sort signals by start (and multiplexer value) + sort.Slice(m.Signals, func(j, k int) bool { + if m.Signals[j].MultiplexerValue < m.Signals[k].MultiplexerValue { + return true + } + return m.Signals[j].Start < m.Signals[k].Start + }) + // Sort value descriptions by value + for _, s := range m.Signals { + s := s + sort.Slice(s.ValueDescriptions, func(k, l int) bool { + return s.ValueDescriptions[k].Value < s.ValueDescriptions[l].Value + }) + } + } +} diff --git a/internal/generate/compile_test.go b/internal/generate/compile_test.go new file mode 100644 index 0000000..433e29c --- /dev/null +++ b/internal/generate/compile_test.go @@ -0,0 +1,307 @@ +package generate + +import ( + "io/ioutil" + "testing" + "time" + + "github.com/stretchr/testify/require" + "go.einride.tech/can/pkg/descriptor" + examplecan "go.einride.tech/can/testdata/gen/go/example" +) + +func TestCompile_ExampleDBC(t *testing.T) { + finish := runTestInDir(t, "../..") + defer finish() + const exampleDBCFile = "testdata/dbc/example/example.dbc" + exampleDatabase := &descriptor.Database{ + SourceFile: exampleDBCFile, + Version: "", + Nodes: []*descriptor.Node{ + { + Name: "DBG", + }, + { + Name: "DRIVER", + Description: "The driver controller driving the car", + }, + { + Name: "IO", + }, + { + Name: "MOTOR", + Description: "The motor controller of the car", + }, + { + Name: "SENSOR", + Description: "The sensor controller of the car", + }, + }, + + Messages: []*descriptor.Message{ + { + ID: 1, + Name: "EmptyMessage", + SenderNode: "DBG", + }, + + { + ID: 100, + Name: "DriverHeartbeat", + Length: 1, + SenderNode: "DRIVER", + Description: "Sync message used to synchronize the controllers", + SendType: descriptor.SendTypeCyclic, + CycleTime: time.Second, + Signals: []*descriptor.Signal{ + { + Name: "Command", + Start: 0, + Length: 8, + Scale: 1, + ReceiverNodes: []string{"SENSOR", "MOTOR"}, + ValueDescriptions: []*descriptor.ValueDescription{ + {Value: 0, Description: "None"}, + {Value: 1, Description: "Sync"}, + {Value: 2, Description: "Reboot"}, + }, + }, + }, + }, + + { + ID: 101, + Name: "MotorCommand", + Length: 1, + SenderNode: "DRIVER", + SendType: descriptor.SendTypeCyclic, + CycleTime: 100 * time.Millisecond, + Signals: []*descriptor.Signal{ + { + Name: "Steer", + Start: 0, + Length: 4, + IsSigned: true, + Scale: 1, + Offset: -5, + Min: -5, + Max: 5, + ReceiverNodes: []string{"MOTOR"}, + }, + { + Name: "Drive", + Start: 4, + Length: 4, + Scale: 1, + Max: 9, + ReceiverNodes: []string{"MOTOR"}, + }, + }, + }, + + { + ID: 200, + Name: "SensorSonars", + Length: 8, + SenderNode: "SENSOR", + SendType: descriptor.SendTypeCyclic, + CycleTime: 100 * time.Millisecond, + Signals: []*descriptor.Signal{ + { + Name: "Mux", + IsMultiplexer: true, + Start: 0, + Length: 4, + Scale: 1, + ReceiverNodes: []string{"DRIVER", "IO"}, + }, + { + Name: "ErrCount", + Start: 4, + Length: 12, + Scale: 1, + ReceiverNodes: []string{"DRIVER", "IO"}, + }, + { + Name: "Left", + IsMultiplexed: true, + MultiplexerValue: 0, + Start: 16, + Length: 12, + Scale: 0.1, + ReceiverNodes: []string{"DRIVER", "IO"}, + }, + { + Name: "NoFiltLeft", + IsMultiplexed: true, + MultiplexerValue: 1, + Start: 16, + Length: 12, + Scale: 0.1, + ReceiverNodes: []string{"DBG"}, + }, + { + Name: "Middle", + IsMultiplexed: true, + MultiplexerValue: 0, + Start: 28, + Length: 12, + Scale: 0.1, + ReceiverNodes: []string{"DRIVER", "IO"}, + }, + { + Name: "NoFiltMiddle", + IsMultiplexed: true, + MultiplexerValue: 1, + Start: 28, + Length: 12, + Scale: 0.1, + ReceiverNodes: []string{"DBG"}, + }, + { + Name: "Right", + IsMultiplexed: true, + MultiplexerValue: 0, + Start: 40, + Length: 12, + Scale: 0.1, + ReceiverNodes: []string{"DRIVER", "IO"}, + }, + { + Name: "NoFiltRight", + IsMultiplexed: true, + MultiplexerValue: 1, + Start: 40, + Length: 12, + Scale: 0.1, + ReceiverNodes: []string{"DBG"}, + }, + { + Name: "Rear", + IsMultiplexed: true, + MultiplexerValue: 0, + Start: 52, + Length: 12, + Scale: 0.1, + ReceiverNodes: []string{"DRIVER", "IO"}, + }, + { + Name: "NoFiltRear", + IsMultiplexed: true, + MultiplexerValue: 1, + Start: 52, + Length: 12, + Scale: 0.1, + ReceiverNodes: []string{"DBG"}, + }, + }, + }, + + { + ID: 400, + Name: "MotorStatus", + Length: 3, + SenderNode: "MOTOR", + SendType: descriptor.SendTypeCyclic, + CycleTime: 100 * time.Millisecond, + Signals: []*descriptor.Signal{ + { + Name: "WheelError", + Start: 0, + Length: 1, + Scale: 1, + ReceiverNodes: []string{"DRIVER", "IO"}, + }, + { + Name: "SpeedKph", + Start: 8, + Length: 16, + Scale: 0.001, + Unit: "km/h", + ReceiverNodes: []string{"DRIVER", "IO"}, + }, + }, + }, + + { + ID: 500, + Name: "IODebug", + Length: 6, + SenderNode: "IO", + SendType: descriptor.SendTypeEvent, + Signals: []*descriptor.Signal{ + { + Name: "TestUnsigned", + Start: 0, + Length: 8, + Scale: 1, + ReceiverNodes: []string{"DBG"}, + }, + { + Name: "TestEnum", + Start: 8, + Length: 6, + Scale: 1, + ReceiverNodes: []string{"DBG"}, + DefaultValue: int(examplecan.IODebug_TestEnum_Two), + ValueDescriptions: []*descriptor.ValueDescription{ + {Value: 1, Description: "One"}, + {Value: 2, Description: "Two"}, + }, + }, + { + Name: "TestSigned", + Start: 16, + Length: 8, + IsSigned: true, + Scale: 1, + ReceiverNodes: []string{"DBG"}, + }, + { + Name: "TestFloat", + Start: 24, + Length: 8, + Scale: 0.5, + ReceiverNodes: []string{"DBG"}, + }, + { + Name: "TestBoolEnum", + Start: 32, + Length: 1, + Scale: 1, + ReceiverNodes: []string{"DBG"}, + ValueDescriptions: []*descriptor.ValueDescription{ + {Value: 0, Description: "Zero"}, + {Value: 1, Description: "One"}, + }, + }, + { + Name: "TestScaledEnum", + Start: 40, + Length: 2, + Scale: 2, + Min: 0, + Max: 6, + ReceiverNodes: []string{"DBG"}, + ValueDescriptions: []*descriptor.ValueDescription{ + {Value: 0, Description: "Zero"}, + {Value: 1, Description: "Two"}, + {Value: 2, Description: "Four"}, + {Value: 3, Description: "Six"}, + }, + }, + }, + }, + }, + } + input, err := ioutil.ReadFile(exampleDBCFile) + require.NoError(t, err) + result, err := Compile(exampleDBCFile, input) + if err != nil { + t.Fatal(err) + } + if len(result.Warnings) > 0 { + t.Fatal(result.Warnings) + } + require.Equal(t, exampleDatabase, result.Database) +} diff --git a/internal/generate/example_test.go b/internal/generate/example_test.go new file mode 100644 index 0000000..49540c2 --- /dev/null +++ b/internal/generate/example_test.go @@ -0,0 +1,338 @@ +package generate + +import ( + "context" + "fmt" + "net" + "reflect" + "testing" + "time" + + "github.com/stretchr/testify/require" + "go.einride.tech/can" + "go.einride.tech/can/pkg/generated" + "go.einride.tech/can/pkg/socketcan" + examplecan "go.einride.tech/can/testdata/gen/go/example" + "golang.org/x/sync/errgroup" +) + +func TestExampleDatabase_MarshalUnmarshal(t *testing.T) { + for _, tt := range []struct { + name string + m can.Message + f can.Frame + }{ + { + name: "IODebug", + m: examplecan.NewIODebug(). + SetTestUnsigned(5). + SetTestEnum(examplecan.IODebug_TestEnum_Two). + SetTestSigned(-42). + SetTestFloat(61.5). + SetTestBoolEnum(examplecan.IODebug_TestBoolEnum_One). + SetRawTestScaledEnum(examplecan.IODebug_TestScaledEnum_Four), + f: can.Frame{ + ID: 500, + Length: 6, + Data: can.Data{5, 2, 214, 123, 1, 2}, + }, + }, + + { + name: "MotorStatus1", + m: examplecan.NewMotorStatus(). + SetSpeedKph(0.423). + SetWheelError(true), + f: can.Frame{ + ID: 400, + Length: 3, + Data: can.Data{0x1, 0xa7, 0x1}, + }, + }, + + { + name: "MotorStatus2", + m: examplecan.NewMotorStatus(). + SetSpeedKph(12), + f: can.Frame{ + ID: 400, + Length: 3, + Data: can.Data{0x00, 0xe0, 0x2e}, + }, + }, + } { + tt := tt + t.Run(tt.name, func(t *testing.T) { + f, err := tt.m.MarshalFrame() + require.NoError(t, err) + require.Equal(t, tt.f, f) + // allocate new message of same type as tt.m + msg := reflect.New(reflect.ValueOf(tt.m).Elem().Type()).Interface().(generated.Message) + require.NoError(t, msg.UnmarshalFrame(f)) + require.Equal(t, tt.m, msg) + }) + } +} + +func TestExampleDatabase_UnmarshalFrame_Error(t *testing.T) { + for _, tt := range []struct { + name string + f can.Frame + m generated.Message + err string + }{ + { + name: "wrong ID", + f: can.Frame{ID: 11, Length: 8}, + m: examplecan.NewSensorSonars(), + err: "unmarshal SensorSonars: expects ID 200 (got 00B#0000000000000000 with ID 11)", + }, + { + name: "wrong length", + f: can.Frame{ID: 200, Length: 4}, + m: examplecan.NewSensorSonars(), + err: "unmarshal SensorSonars: expects length 8 (got 0C8#00000000 with length 4)", + }, + { + name: "remote frame", + f: can.Frame{ID: 200, Length: 8, IsRemote: true}, + m: examplecan.NewSensorSonars(), + err: "unmarshal SensorSonars: expects non-remote frame (got remote frame 0C8#R8)", + }, + { + name: "extended ID", + f: can.Frame{ID: 200, Length: 8, IsExtended: true}, + m: examplecan.NewSensorSonars(), + err: "unmarshal SensorSonars: expects standard ID (got 000000C8#0000000000000000 with extended ID)", + }, + } { + tt := tt + t.Run(tt.name, func(t *testing.T) { + require.Equal(t, tt.err, tt.m.UnmarshalFrame(tt.f).Error()) + }) + } +} + +func TestExampleDatabase_TestEnum_String(t *testing.T) { + require.Equal(t, "One", examplecan.IODebug_TestEnum_One.String()) + require.Equal(t, "Two", examplecan.IODebug_TestEnum_Two.String()) + require.Equal(t, "IODebug_TestEnum(3)", examplecan.IODebug_TestEnum(3).String()) +} + +func TestExampleDatabase_Message_String(t *testing.T) { + const expected = "{WheelError: true, SpeedKph: 42km/h}" + msg := examplecan.NewMotorStatus(). + SetSpeedKph(42). + SetWheelError(true) + require.Equal(t, expected, msg.String()) + require.Equal(t, expected, fmt.Sprintf("%v", msg)) +} + +func TestExampleDatabase_OutOfBoundsValue(t *testing.T) { + const expected = examplecan.IODebug_TestEnum(63) + actual := examplecan.NewIODebug().SetTestEnum(255).TestEnum() + require.Equal(t, expected, actual) +} + +func TestExampleDatabase_MultiplexedSignals(t *testing.T) { + // Given a message with multiplexed signals + msg := examplecan.NewSensorSonars(). + SetErrCount(1). + SetMux(1). + SetLeft(20). + SetMiddle(30). + SetRight(40). + SetRear(50). + SetNoFiltLeft(60). + SetNoFiltMiddle(70). + SetNoFiltRight(80). + SetNoFiltRear(90) + for _, tt := range []struct { + expectedMux uint8 + expectedErrCount uint16 + expectedLeft float64 + expectedMiddle float64 + expectedRight float64 + expectedRear float64 + expectedNoFiltLeft float64 + expectedNoFiltMiddle float64 + expectedNoFiltRight float64 + expectedNoFiltRear float64 + }{ + { + expectedMux: 0, + expectedErrCount: 1, + expectedLeft: 20, + expectedMiddle: 30, + expectedRight: 40, + expectedRear: 50, + expectedNoFiltLeft: 0, + expectedNoFiltMiddle: 0, + expectedNoFiltRight: 0, + expectedNoFiltRear: 0, + }, + { + expectedMux: 1, + expectedErrCount: 1, + expectedLeft: 0, + expectedMiddle: 0, + expectedRight: 0, + expectedRear: 0, + expectedNoFiltLeft: 60, + expectedNoFiltMiddle: 70, + expectedNoFiltRight: 80, + expectedNoFiltRear: 90, + }, + } { + tt := tt + t.Run(fmt.Sprintf("mux=%v", tt.expectedMux), func(t *testing.T) { + unmarshal1 := examplecan.NewSensorSonars() + // When the multiplexer signal is 0 and we marshal the message + // to a CAN frame + msg.SetMux(tt.expectedMux) + f1, err := msg.MarshalFrame() + require.NoError(t, err) + // When we unmarshal the CAN frame back to a message + require.NoError(t, unmarshal1.UnmarshalFrame(f1)) + // Then only the multiplexed signals with multiplexer value 0 + // should be unmarshaled + require.Equal(t, tt.expectedMux, unmarshal1.Mux(), "Mux") + require.Equal(t, tt.expectedErrCount, unmarshal1.ErrCount(), "ErrCount") + require.Equal(t, tt.expectedLeft, unmarshal1.Left(), "Left") + require.Equal(t, tt.expectedMiddle, unmarshal1.Middle(), "Middle") + require.Equal(t, tt.expectedRight, unmarshal1.Right(), "Right") + require.Equal(t, tt.expectedRear, unmarshal1.Rear(), "Rear") + require.Equal(t, tt.expectedNoFiltLeft, unmarshal1.NoFiltLeft(), "NoFiltLeft") + require.Equal(t, tt.expectedNoFiltMiddle, unmarshal1.NoFiltMiddle(), "NoFiltMiddle") + require.Equal(t, tt.expectedNoFiltRight, unmarshal1.NoFiltRight(), "NoFiltRight") + require.Equal(t, tt.expectedNoFiltRear, unmarshal1.NoFiltRear(), "NoFiltRear") + }) + } +} + +func TestExampleDatabase_CopyFrom(t *testing.T) { + // Given: an original message + from := examplecan.NewIODebug(). + SetRawTestScaledEnum(examplecan.IODebug_TestScaledEnum_Four). + SetTestBoolEnum(true). + SetTestFloat(0.1). + SetTestSigned(-10). + SetTestUnsigned(10) + // When: another message copies from the original message + to := examplecan.NewIODebug().CopyFrom(from) + // Then: + // all fields should be equal... + require.Equal(t, from.String(), to.String()) + require.Equal(t, from.TestScaledEnum(), to.TestScaledEnum()) + require.Equal(t, from.TestBoolEnum(), to.TestBoolEnum()) + require.Equal(t, from.TestFloat(), to.TestFloat()) + require.Equal(t, from.TestSigned(), to.TestSigned()) + require.Equal(t, from.TestUnsigned(), to.TestUnsigned()) + // ...and changes to the original should not affect the new message + from.SetTestUnsigned(100) + require.Equal(t, uint8(10), to.TestUnsigned()) +} + +func TestExample_Nodes(t *testing.T) { + const testTimeout = 2 * time.Second + requireVCAN0(t) + // given a DRIVER node and a MOTOR node + motor := examplecan.NewMOTOR("can", "vcan0") + driver := examplecan.NewDRIVER("can", "vcan0") + // when starting them + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + g, ctx := errgroup.WithContext(ctx) + g.Go(func() error { + return motor.Run(ctx) + }) + g.Go(func() error { + return driver.Run(ctx) + }) + // and the MOTOR node is configured to send a speed report + const expectedSpeedKph = 42 + motor.Lock() + motor.Tx().MotorStatus().SetSpeedKph(expectedSpeedKph) + motor.Tx().MotorStatus().SetCyclicTransmissionEnabled(true) + motor.Unlock() + // and the DRIVER node is configured to send a steering command + const expectedSteer = -4 + driver.Lock() + driver.Tx().MotorCommand().SetSteer(expectedSteer) + driver.Tx().MotorCommand().SetCyclicTransmissionEnabled(true) + driver.Unlock() + // and the MOTOR node is listening for the steering command + expectedSteerReceivedChan := make(chan struct{}) + motor.Lock() + motor.Rx().MotorCommand().SetAfterReceiveHook(func(context.Context) error { + motor.Lock() + if motor.Rx().MotorCommand().Steer() == expectedSteer { + close(expectedSteerReceivedChan) + motor.Rx().MotorCommand().SetAfterReceiveHook(func(context.Context) error { return nil }) + } + motor.Unlock() + return nil + }) + motor.Unlock() + // and the DRIVER node is listening for the speed report + expectedSpeedReceivedChan := make(chan struct{}) + driver.Lock() + driver.Rx().MotorStatus().SetAfterReceiveHook(func(context.Context) error { + driver.Lock() + if driver.Rx().MotorStatus().SpeedKph() == expectedSpeedKph { + close(expectedSpeedReceivedChan) + driver.Rx().MotorStatus().SetAfterReceiveHook(func(context.Context) error { return nil }) + } + driver.Unlock() + return nil + }) + driver.Unlock() + // then the steer command transmitted by DRIVER should be received by MOTOR + select { + case <-expectedSteerReceivedChan: + case <-ctx.Done(): + t.Fatalf("expected steer not received: %v", expectedSteer) + } + // and the speed report transmitted by MOTOR should be received by DRIVER + select { + case <-expectedSpeedReceivedChan: + case <-ctx.Done(): + t.Fatalf("expected speed not received: %v", expectedSpeedKph) + } + cancel() + require.NoError(t, g.Wait()) +} + +func TestExample_Node_NoEmptyMessages(t *testing.T) { + const testTimeout = 2 * time.Second + requireVCAN0(t) + // given a DRIVER node and a MOTOR node + motor := examplecan.NewMOTOR("can", "vcan0") + // when starting them + ctx, cancel := context.WithTimeout(context.Background(), testTimeout) + handler := func(ctx context.Context) error { + motor.Lock() + motor.Tx().MotorStatus().SetSpeedKph(100).SetWheelError(true) + motor.Unlock() + return nil + } + motor.Tx().MotorStatus().SetBeforeTransmitHook(handler) + motor.Tx().MotorStatus().SetCyclicTransmissionEnabled(true) + c, err := socketcan.Dial("can", "vcan0") + r := socketcan.NewReceiver(c) + require.NoError(t, err) + g := errgroup.Group{} + g.Go(func() error { + return motor.Run(ctx) + }) + require.True(t, r.Receive()) + require.Equal(t, examplecan.NewMotorStatus().SetSpeedKph(100).SetWheelError(true).Frame(), r.Frame()) + cancel() + require.NoError(t, g.Wait()) +} + +func requireVCAN0(t *testing.T) { + t.Helper() + if _, err := net.InterfaceByName("vcan0"); err != nil { + t.Skip("interface vcan0 does not exist") + } +} diff --git a/internal/generate/file.go b/internal/generate/file.go new file mode 100644 index 0000000..69c3192 --- /dev/null +++ b/internal/generate/file.go @@ -0,0 +1,915 @@ +package generate + +import ( + "bytes" + "fmt" + "go/format" + "go/types" + "path" + "strings" + + "github.com/shurcooL/go-goon" + "go.einride.tech/can/pkg/descriptor" +) + +type File struct { + buf bytes.Buffer + err error +} + +func NewFile() *File { + f := &File{} + f.buf.Grow(1e5) // 100K + return f +} + +func (f *File) Write(p []byte) (int, error) { + if f.err != nil { + return 0, f.err + } + n, err := f.buf.Write(p) + f.err = err + return n, err +} + +func (f *File) P(v ...interface{}) { + for _, x := range v { + _, _ = fmt.Fprint(f, x) + } + _, _ = fmt.Fprintln(f) +} + +func (f *File) Dump(v interface{}) { + _, _ = goon.Fdump(f, v) +} + +func (f *File) Content() ([]byte, error) { + if f.err != nil { + return nil, fmt.Errorf("file content: %w", f.err) + } + formatted, err := format.Source(f.buf.Bytes()) + if err != nil { + return nil, fmt.Errorf("file content: %s: %w", f.buf.String(), err) + } + return formatted, nil +} + +func Database(d *descriptor.Database) ([]byte, error) { + f := NewFile() + Package(f, d) + Imports(f) + for _, m := range d.Messages { + MessageType(f, m) + for _, s := range m.Signals { + if hasCustomType(s) { + SignalCustomType(f, m, s) + } + } + MarshalFrame(f, m) + UnmarshalFrame(f, m) + } + if hasSendType(d) { // only code-generate nodes for schemas with send types specified + for _, n := range d.Nodes { + Node(f, d, n) + } + } + Descriptors(f, d) + return f.Content() +} + +func Package(f *File, d *descriptor.Database) { + packageName := strings.TrimSuffix(path.Base(d.SourceFile), path.Ext(d.SourceFile)) + "can" + f.P("// Package ", packageName, " provides primitives for encoding and decoding ", d.Name(), " CAN messages.") + f.P("//") + f.P("// Source: ", d.SourceFile) + f.P("package ", packageName) + f.P() +} + +func Imports(f *File) { + f.P("import (") + f.P(`"context"`) + f.P(`"fmt"`) + f.P(`"net"`) + f.P(`"net/http"`) + f.P(`"sync"`) + f.P(`"time"`) + f.P() + f.P(`"go.einride.tech/can"`) + f.P(`"go.einride.tech/can/pkg/socketcan"`) + f.P(`"go.einride.tech/can/pkg/candebug"`) + f.P(`"go.einride.tech/can/pkg/canrunner"`) + f.P(`"go.einride.tech/can/pkg/descriptor"`) + f.P(`"go.einride.tech/can/pkg/generated"`) + f.P(`"go.einride.tech/can/pkg/cantext"`) + f.P(")") + f.P() + // we could use goimports for this, but it significantly slows down code generation + f.P("// prevent unused imports") + f.P("var (") + f.P("_ = context.Background") + f.P("_ = fmt.Print") + f.P("_ = net.Dial") + f.P("_ = http.Error") + f.P("_ = sync.Mutex{}") + f.P("_ = time.Now") + f.P("_ = socketcan.Dial") + f.P("_ = candebug.ServeMessagesHTTP") + f.P("_ = canrunner.Run") + f.P(")") + f.P() + f.P("// Generated code. DO NOT EDIT.") +} + +func SignalCustomType(f *File, m *descriptor.Message, s *descriptor.Signal) { + f.P("// ", signalType(m, s), " models the ", s.Name, " signal of the ", m.Name, " message.") + f.P("type ", signalType(m, s), " ", signalPrimitiveType(s)) + f.P() + f.P("// Value descriptions for the ", s.Name, " signal of the ", m.Name, " message.") + f.P("const (") + for _, vd := range s.ValueDescriptions { + switch { + case s.Length == 1 && vd.Value == 1: + f.P(signalType(m, s), "_", vd.Description, " ", signalType(m, s), " = true") + case s.Length == 1 && vd.Value == 0: + f.P(signalType(m, s), "_", vd.Description, " ", signalType(m, s), " = false") + default: + f.P(signalType(m, s), "_", vd.Description, " ", signalType(m, s), " = ", vd.Value) + } + } + f.P(")") + f.P() + f.P("func (v ", signalType(m, s), ") String() string {") + if s.Length == 1 { + f.P("switch bool(v) {") + for _, vd := range s.ValueDescriptions { + if vd.Value == 1 { + f.P("case true:") + } else { + f.P("case false:") + } + f.P(`return "`, vd.Description, `"`) + } + f.P("}") + f.P(`return fmt.Sprintf("`, signalType(m, s), `(%t)", v)`) + } else { + f.P("switch v {") + for _, vd := range s.ValueDescriptions { + f.P("case ", vd.Value, ":") + f.P(`return "`, vd.Description, `"`) + } + f.P("default:") + f.P(`return fmt.Sprintf("`, signalType(m, s), `(%d)", v)`) + f.P("}") + } + f.P("}") +} + +func MessageType(f *File, m *descriptor.Message) { + f.P("// ", messageReaderInterface(m), " provides read access to a ", m.Name, " message.") + f.P("type ", messageReaderInterface(m), " interface {") + for _, s := range m.Signals { + if hasPhysicalRepresentation(s) { + f.P("// ", s.Name, " returns the physical value of the ", s.Name, " signal.") + f.P(s.Name, "() float64") + if len(s.ValueDescriptions) > 0 { + f.P() + f.P("// ", s.Name, " returns the raw (encoded) value of the ", s.Name, " signal.") + f.P("Raw", s.Name, "() ", signalType(m, s)) + } + } else { + f.P("// ", s.Name, " returns the value of the ", s.Name, " signal.") + f.P(s.Name, "()", signalType(m, s)) + } + } + f.P("}") + f.P() + f.P("// ", messageWriterInterface(m), " provides write access to a ", m.Name, " message.") + f.P("type ", messageWriterInterface(m), " interface {") + f.P("// CopyFrom copies all values from ", messageReaderInterface(m), ".") + f.P("CopyFrom(", messageReaderInterface(m), ") *", messageStruct(m)) + for _, s := range m.Signals { + if hasPhysicalRepresentation(s) { + f.P("// Set", s.Name, " sets the physical value of the ", s.Name, " signal.") + f.P("Set", s.Name, "(float64) *", messageStruct(m)) + if len(s.ValueDescriptions) > 0 { + f.P() + f.P("// SetRaw", s.Name, " sets the raw (encoded) value of the ", s.Name, " signal.") + f.P("SetRaw", s.Name, "(", signalType(m, s), ") *", messageStruct(m)) + } + } else { + f.P("// Set", s.Name, " sets the value of the ", s.Name, " signal.") + f.P("Set", s.Name, "(", signalType(m, s), ") *", messageStruct(m)) + } + } + f.P("}") + f.P() + f.P("type ", messageStruct(m), " struct {") + for _, s := range m.Signals { + f.P(signalField(s), " ", signalType(m, s)) + } + f.P("}") + f.P() + f.P("func New", messageStruct(m), "() *", messageStruct(m), " {") + f.P("m := &", messageStruct(m), "{}") + f.P("m.Reset()") + f.P("return m") + f.P("}") + f.P() + f.P("func (m *", messageStruct(m), ") Reset() {") + for _, s := range m.Signals { + switch { + case s.Length == 1 && s.DefaultValue == 1: + f.P("m.", signalField(s), " = true") + case s.Length == 1: + f.P("m.", signalField(s), " = false") + default: + f.P("m.", signalField(s), " = ", s.DefaultValue) + } + } + f.P("}") + f.P() + f.P("func (m *", messageStruct(m), ") CopyFrom(o ", messageReaderInterface(m), ") *", messageStruct(m), "{") + for _, s := range m.Signals { + if hasPhysicalRepresentation(s) { + f.P("m.Set", s.Name, "(o.", s.Name, "())") + } else { + f.P("m.", signalField(s), " = o.", s.Name, "()") + } + } + f.P("return m") + f.P("}") + f.P() + f.P("// Descriptor returns the ", m.Name, " descriptor.") + f.P("func (m *", messageStruct(m), ") Descriptor() *descriptor.Message {") + f.P("return ", messageDescriptor(m), ".Message") + f.P("}") + f.P() + f.P("// String returns a compact string representation of the message.") + f.P("func(m *", messageStruct(m), ") String() string {") + f.P("return cantext.MessageString(m)") + f.P("}") + f.P() + for _, s := range m.Signals { + if !hasPhysicalRepresentation(s) { + f.P("func (m *", messageStruct(m), ") ", s.Name, "() ", signalType(m, s), " {") + f.P("return m.", signalField(s)) + f.P("}") + f.P() + f.P("func (m *", messageStruct(m), ") Set", s.Name, "(v ", signalType(m, s), ") *", messageStruct(m), " {") + if s.Length == 1 { + f.P("m.", signalField(s), " = v") + } else { + f.P( + "m.", signalField(s), " = ", signalType(m, s), "(", + signalDescriptor(m, s), ".SaturatedCast", signalSuperType(s), "(", + signalPrimitiveSuperType(s), "(v)))", + ) + } + f.P("return m") + f.P("}") + f.P() + continue + } + f.P("func (m *", messageStruct(m), ") ", s.Name, "() float64 {") + f.P("return ", signalDescriptor(m, s), ".ToPhysical(float64(m.", signalField(s), "))") + f.P("}") + f.P() + f.P("func (m *", messageStruct(m), ") Set", s.Name, "(v float64) *", messageStruct(m), " {") + f.P("m.", signalField(s), " = ", signalType(m, s), "(", signalDescriptor(m, s), ".FromPhysical(v))") + f.P("return m") + f.P("}") + f.P() + if len(s.ValueDescriptions) > 0 { + f.P("func (m *", messageStruct(m), ") Raw", s.Name, "() ", signalType(m, s), " {") + f.P("return m.", signalField(s)) + f.P("}") + f.P() + f.P("func (m *", messageStruct(m), ") SetRaw", s.Name, "(v ", signalType(m, s), ") *", messageStruct(m), "{") + f.P( + "m.", signalField(s), " = ", signalType(m, s), "(", + signalDescriptor(m, s), ".SaturatedCast", signalSuperType(s), "(", + signalPrimitiveSuperType(s), "(v)))", + ) + f.P("return m") + f.P("}") + f.P() + } + } +} + +func Descriptors(f *File, d *descriptor.Database) { + f.P("// Nodes returns the ", d.Name(), " node descriptors.") + f.P("func Nodes() *NodesDescriptor {") + f.P("return nd") + f.P("}") + f.P() + f.P("// NodesDescriptor contains all ", d.Name(), " node descriptors.") + f.P("type NodesDescriptor struct{") + for _, n := range d.Nodes { + f.P(n.Name, " *descriptor.Node") + } + f.P("}") + f.P() + f.P("// Messages returns the ", d.Name(), " message descriptors.") + f.P("func Messages() *MessagesDescriptor {") + f.P("return md") + f.P("}") + f.P() + f.P("// MessagesDescriptor contains all ", d.Name(), " message descriptors.") + f.P("type MessagesDescriptor struct{") + for _, m := range d.Messages { + f.P(m.Name, " *", m.Name, "Descriptor") + } + f.P("}") + f.P() + f.P("// UnmarshalFrame unmarshals the provided ", d.Name(), " CAN frame.") + f.P("func (md *MessagesDescriptor) UnmarshalFrame(f can.Frame) (generated.Message, error) {") + f.P("switch f.ID {") + for _, m := range d.Messages { + f.P("case md.", m.Name, ".ID:") + f.P("var msg ", messageStruct(m)) + f.P("if err := msg.UnmarshalFrame(f); err != nil {") + f.P(`return nil, fmt.Errorf("unmarshal `, d.Name(), ` frame: %w", err)`) + f.P("}") + f.P("return &msg, nil") + } + f.P("default:") + f.P(`return nil, fmt.Errorf("unmarshal `, d.Name(), ` frame: ID not in database: %d", f.ID)`) + f.P("}") + f.P("}") + f.P() + for _, m := range d.Messages { + f.P("type ", m.Name, "Descriptor struct{") + f.P("*descriptor.Message") + for _, s := range m.Signals { + f.P(s.Name, " *descriptor.Signal") + } + f.P("}") + f.P() + } + f.P("// Database returns the ", d.Name(), " database descriptor.") + f.P("func (md *MessagesDescriptor) Database() *descriptor.Database {") + f.P("return d") + f.P("}") + f.P() + f.P("var nd = &NodesDescriptor{") + for ni, n := range d.Nodes { + f.P(n.Name, ": d.Nodes[", ni, "],") + } + f.P("}") + f.P() + f.P("var md = &MessagesDescriptor{") + for mi, m := range d.Messages { + f.P(m.Name, ": &", m.Name, "Descriptor{") + f.P("Message: d.Messages[", mi, "],") + for si, s := range m.Signals { + f.P(s.Name, ": d.Messages[", mi, "].Signals[", si, "],") + } + f.P("},") + } + f.P("}") + f.P() + f.P("var d = ") + f.Dump(d) + f.P() +} + +func MarshalFrame(f *File, m *descriptor.Message) { + f.P("// Frame returns a CAN frame representing the message.") + f.P("func (m *", messageStruct(m), ") Frame() can.Frame {") + f.P("md := ", messageDescriptor(m)) + f.P("f := can.Frame{ID: md.ID, IsExtended: md.IsExtended, Length: md.Length}") + for _, s := range m.Signals { + if s.IsMultiplexed { + continue + } + f.P( + "md.", s.Name, ".Marshal", signalSuperType(s), + "(&f.Data, ", signalPrimitiveSuperType(s), "(m.", signalField(s), "))", + ) + } + if mux, ok := m.MultiplexerSignal(); ok { + for _, s := range m.Signals { + if !s.IsMultiplexed { + continue + } + f.P("if m.", signalField(mux), " == ", s.MultiplexerValue, " {") + f.P( + "md.", s.Name, ".Marshal", signalSuperType(s), "(&f.Data, ", signalPrimitiveSuperType(s), + "(m.", signalField(s), "))", + ) + f.P("}") + } + } + f.P("return f") + f.P("}") + f.P() + f.P("// MarshalFrame encodes the message as a CAN frame.") + f.P("func (m *", messageStruct(m), ") MarshalFrame() (can.Frame, error) {") + f.P("return m.Frame(), nil") + f.P("}") + f.P() +} + +func UnmarshalFrame(f *File, m *descriptor.Message) { + f.P("// UnmarshalFrame decodes the message from a CAN frame.") + f.P("func (m *", messageStruct(m), ") UnmarshalFrame(f can.Frame) error {") + f.P("md := ", messageDescriptor(m)) + // generate frame checks + id := func(isExtended bool) string { + if isExtended { + return "extended ID" + } + return "standard ID" + } + f.P("switch {") + f.P("case f.ID != md.ID:") + f.P(`return fmt.Errorf(`) + f.P(`"unmarshal `, m.Name, `: expects ID `, m.ID, ` (got %s with ID %d)", f.String(), f.ID,`) + f.P(`)`) + f.P("case f.Length != md.Length:") + f.P(`return fmt.Errorf(`) + f.P(`"unmarshal `, m.Name, `: expects length `, m.Length, ` (got %s with length %d)", f.String(), f.Length,`) + f.P(`)`) + f.P("case f.IsRemote:") + f.P(`return fmt.Errorf(`) + f.P(`"unmarshal `, m.Name, `: expects non-remote frame (got remote frame %s)", f.String(),`) + f.P(`)`) + f.P("case f.IsExtended != md.IsExtended:") + f.P(`return fmt.Errorf(`) + f.P(`"unmarshal `, m.Name, `: expects `, id(m.IsExtended), ` (got %s with `, id(!m.IsExtended), `)", f.String(),`) + f.P(`)`) + f.P("}") + if len(m.Signals) == 0 { + f.P("return nil") + f.P("}") + return + } + // generate non-multiplexed signal unmarshaling + for _, s := range m.Signals { + if s.IsMultiplexed { + continue + } + f.P("m.", signalField(s), " = ", signalType(m, s), "(md.", s.Name, ".Unmarshal", signalSuperType(s), "(f.Data))") + } + // generate multiplexed signal unmarshaling + if mux, ok := m.MultiplexerSignal(); ok { + for _, s := range m.Signals { + if !s.IsMultiplexed { + continue + } + f.P("if m.", signalField(mux), " == ", s.MultiplexerValue, " {") + f.P("m.", signalField(s), " = ", signalType(m, s), "(md.", s.Name, ".Unmarshal", signalSuperType(s), "(f.Data))") + f.P("}") + } + } + f.P("return nil") + f.P("}") + f.P() +} + +func Node(f *File, d *descriptor.Database, n *descriptor.Node) { + rxMessages := collectRxMessages(d, n) + txMessages := collectTxMessages(d, n) + f.P("type ", nodeInterface(n), " interface {") + f.P("sync.Locker") + f.P("Tx() ", txGroupInterface(n)) + f.P("Rx() ", rxGroupInterface(n)) + f.P("Run(ctx context.Context) error") + f.P("}") + f.P() + f.P("type ", rxGroupInterface(n), " interface {") + f.P("http.Handler // for debugging") + for _, m := range rxMessages { + f.P(m.Name, "() ", rxMessageInterface(n, m)) + } + f.P("}") + f.P() + f.P("type ", txGroupInterface(n), " interface {") + f.P("http.Handler // for debugging") + for _, m := range txMessages { + f.P(m.Name, "() ", txMessageInterface(n, m)) + } + f.P("}") + f.P() + for _, m := range rxMessages { + f.P("type ", rxMessageInterface(n, m), " interface {") + f.P(messageReaderInterface(m)) + f.P("ReceiveTime() time.Time") + f.P("SetAfterReceiveHook(h func(context.Context) error)") + f.P("}") + f.P() + } + for _, m := range txMessages { + f.P("type ", txMessageInterface(n, m), " interface {") + f.P(messageReaderInterface(m)) + f.P(messageWriterInterface(m)) + f.P("TransmitTime() time.Time") + f.P("Transmit(ctx context.Context) error") + f.P("SetBeforeTransmitHook(h func(context.Context) error)") + if m.SendType == descriptor.SendTypeCyclic { + f.P("// SetCyclicTransmissionEnabled enables/disables cyclic transmission.") + f.P("SetCyclicTransmissionEnabled(bool)") + f.P("// IsCyclicTransmissionEnabled returns whether cyclic transmission is enabled/disabled.") + f.P("IsCyclicTransmissionEnabled() bool") + } + f.P("}") + f.P() + } + f.P("type ", nodeStruct(n), " struct {") + f.P("sync.Mutex // protects all node state") + f.P("network string") + f.P("address string") + f.P("rx ", rxGroupStruct(n)) + f.P("tx ", txGroupStruct(n)) + f.P("}") + f.P() + f.P("var _ ", nodeInterface(n), " = &", nodeStruct(n), "{}") + f.P("var _ canrunner.Node = &", nodeStruct(n), "{}") + f.P() + f.P("func New", nodeInterface(n), "(network, address string) ", nodeInterface(n), " {") + f.P("n := &", nodeStruct(n), "{network: network, address: address}") + f.P("n.rx.parentMutex = &n.Mutex") + f.P("n.tx.parentMutex = &n.Mutex") + for _, m := range rxMessages { + f.P("n.rx.", messageField(m), ".init()") + f.P("n.rx.", messageField(m), ".Reset()") + } + for _, m := range txMessages { + f.P("n.tx.", messageField(m), ".init()") + f.P("n.tx.", messageField(m), ".Reset()") + } + f.P("return n") + f.P("}") + f.P() + f.P("func (n *", nodeStruct(n), ") Run(ctx context.Context) error {") + f.P("return canrunner.Run(ctx, n)") + f.P("}") + f.P() + f.P("func (n *", nodeStruct(n), ") Rx() ", rxGroupInterface(n), " {") + f.P("return &n.rx") + f.P("}") + f.P() + f.P("func (n *", nodeStruct(n), ") Tx() ", txGroupInterface(n), " {") + f.P("return &n.tx") + f.P("}") + f.P() + f.P("type ", rxGroupStruct(n), " struct {") + f.P("parentMutex *sync.Mutex") + for _, m := range rxMessages { + f.P(messageField(m), " ", rxMessageStruct(n, m)) + } + f.P("}") + f.P() + f.P("var _ ", rxGroupInterface(n), " = &", rxGroupStruct(n), "{}") + f.P() + f.P("func (rx *", rxGroupStruct(n), ") ServeHTTP(w http.ResponseWriter, r *http.Request) {") + f.P("rx.parentMutex.Lock()") + f.P("defer rx.parentMutex.Unlock()") + f.P("candebug.ServeMessagesHTTP(w, r, []generated.Message{") + for _, m := range rxMessages { + f.P("&rx.", messageField(m), ",") + } + f.P("})") + f.P("}") + f.P() + for _, m := range rxMessages { + f.P("func (rx *", rxGroupStruct(n), ") ", m.Name, "() ", rxMessageInterface(n, m), " {") + f.P("return &rx.", messageField(m)) + f.P("}") + f.P() + } + f.P() + f.P("type ", txGroupStruct(n), " struct {") + f.P("parentMutex *sync.Mutex") + for _, m := range txMessages { + f.P(messageField(m), " ", txMessageStruct(n, m)) + } + f.P("}") + f.P() + f.P("var _ ", txGroupInterface(n), " = &", txGroupStruct(n), "{}") + f.P() + f.P("func (tx *", txGroupStruct(n), ") ServeHTTP(w http.ResponseWriter, r *http.Request) {") + f.P("tx.parentMutex.Lock()") + f.P("defer tx.parentMutex.Unlock()") + f.P("candebug.ServeMessagesHTTP(w, r, []generated.Message{") + for _, m := range txMessages { + f.P("&tx.", messageField(m), ",") + } + f.P("})") + f.P("}") + f.P() + for _, m := range txMessages { + f.P("func (tx *", txGroupStruct(n), ") ", m.Name, "() ", txMessageInterface(n, m), " {") + f.P("return &tx.", messageField(m)) + f.P("}") + f.P() + } + f.P() + f.P("func (n *", nodeStruct(n), ") Descriptor() *descriptor.Node {") + f.P("return ", nodeDescriptor(n)) + f.P("}") + f.P() + f.P("func (n *", nodeStruct(n), ") Connect() (net.Conn, error) {") + f.P("return socketcan.Dial(n.network, n.address)") + f.P("}") + f.P() + f.P("func (n *", nodeStruct(n), ") ReceivedMessage(id uint32) (canrunner.ReceivedMessage, bool) {") + f.P("switch id {") + for _, m := range rxMessages { + f.P("case ", m.ID, ":") + f.P("return &n.rx.", messageField(m), ", true") + } + f.P("default:") + f.P("return nil, false") + f.P("}") + f.P("}") + f.P() + f.P("func (n *", nodeStruct(n), ") TransmittedMessages() []canrunner.TransmittedMessage {") + f.P("return []canrunner.TransmittedMessage{") + for _, m := range txMessages { + f.P("&n.tx.", messageField(m), ",") + } + f.P("}") + f.P("}") + f.P() + for _, m := range rxMessages { + f.P("type ", rxMessageStruct(n, m), " struct {") + f.P(messageStruct(m)) + f.P("receiveTime time.Time") + f.P("afterReceiveHook func(context.Context) error") + f.P("}") + f.P() + f.P("func (m *", rxMessageStruct(n, m), ") init() {") + f.P("m.afterReceiveHook = func(context.Context) error { return nil }") + f.P("}") + f.P() + f.P("func (m *", rxMessageStruct(n, m), ") SetAfterReceiveHook(h func(context.Context) error) {") + f.P("m.afterReceiveHook = h") + f.P("}") + f.P() + f.P("func (m *", rxMessageStruct(n, m), ") AfterReceiveHook() func(context.Context) error {") + f.P("return m.afterReceiveHook") + f.P("}") + f.P() + f.P("func (m *", rxMessageStruct(n, m), ") ReceiveTime() time.Time {") + f.P("return m.receiveTime") + f.P("}") + f.P() + f.P("func (m *", rxMessageStruct(n, m), ") SetReceiveTime(t time.Time) {") + f.P("m.receiveTime = t") + f.P("}") + f.P() + f.P("var _ canrunner.ReceivedMessage = &", rxMessageStruct(n, m), "{}") + f.P() + } + for _, m := range txMessages { + f.P("type ", txMessageStruct(n, m), " struct {") + f.P(messageStruct(m)) + f.P("transmitTime time.Time") + f.P("beforeTransmitHook func(context.Context) error") + f.P("isCyclicEnabled bool") + f.P("wakeUpChan chan struct{}") + f.P("transmitEventChan chan struct{}") + f.P("}") + f.P() + f.P("var _ ", txMessageInterface(n, m), " = &", txMessageStruct(n, m), "{}") + f.P("var _ canrunner.TransmittedMessage = &", txMessageStruct(n, m), "{}") + f.P() + f.P("func (m *", txMessageStruct(n, m), ") init() {") + f.P("m.beforeTransmitHook = func(context.Context) error { return nil }") + f.P("m.wakeUpChan = make(chan struct{}, 1)") + f.P("m.transmitEventChan = make(chan struct{})") + f.P("}") + f.P() + f.P("func (m *", txMessageStruct(n, m), ") SetBeforeTransmitHook(h func(context.Context) error) {") + f.P("m.beforeTransmitHook = h") + f.P("}") + f.P() + f.P("func (m *", txMessageStruct(n, m), ") BeforeTransmitHook() func(context.Context) error {") + f.P("return m.beforeTransmitHook") + f.P("}") + f.P() + f.P("func (m *", txMessageStruct(n, m), ") TransmitTime() time.Time {") + f.P("return m.transmitTime") + f.P("}") + f.P() + f.P("func (m *", txMessageStruct(n, m), ") SetTransmitTime(t time.Time) {") + f.P("m.transmitTime = t") + f.P("}") + f.P() + f.P("func (m *", txMessageStruct(n, m), ") IsCyclicTransmissionEnabled() bool {") + f.P("return m.isCyclicEnabled") + f.P("}") + f.P() + f.P("func (m *", txMessageStruct(n, m), ") SetCyclicTransmissionEnabled(b bool) {") + f.P("m.isCyclicEnabled = b") + f.P("select {") + f.P("case m.wakeUpChan <-struct{}{}:") + f.P("default:") + f.P("}") + f.P("}") + f.P() + f.P("func (m *", txMessageStruct(n, m), ") WakeUpChan() <-chan struct{} {") + f.P("return m.wakeUpChan") + f.P("}") + f.P() + f.P("func (m *", txMessageStruct(n, m), ") Transmit(ctx context.Context) error {") + f.P("select {") + f.P("case m.transmitEventChan <- struct{}{}:") + f.P("return nil") + f.P("case <-ctx.Done():") + f.P(`return fmt.Errorf("event-triggered transmit of `, m.Name, `: %w", ctx.Err())`) + f.P("}") + f.P("}") + f.P() + f.P("func (m *", txMessageStruct(n, m), ") TransmitEventChan() <-chan struct{} {") + f.P("return m.transmitEventChan") + f.P("}") + f.P() + f.P("var _ canrunner.TransmittedMessage = &", txMessageStruct(n, m), "{}") + f.P() + } +} + +func txGroupInterface(n *descriptor.Node) string { + return n.Name + "_Tx" +} + +func txGroupStruct(n *descriptor.Node) string { + return "xxx_" + n.Name + "_Tx" +} + +func rxGroupInterface(n *descriptor.Node) string { + return n.Name + "_Rx" +} + +func rxGroupStruct(n *descriptor.Node) string { + return "xxx_" + n.Name + "_Rx" +} + +func rxMessageInterface(n *descriptor.Node, m *descriptor.Message) string { + return n.Name + "_Rx_" + m.Name +} + +func rxMessageStruct(n *descriptor.Node, m *descriptor.Message) string { + return "xxx_" + n.Name + "_Rx_" + m.Name +} + +func txMessageInterface(n *descriptor.Node, m *descriptor.Message) string { + return n.Name + "_Tx_" + m.Name +} + +func txMessageStruct(n *descriptor.Node, m *descriptor.Message) string { + return "xxx_" + n.Name + "_Tx_" + m.Name +} + +func collectTxMessages(d *descriptor.Database, n *descriptor.Node) []*descriptor.Message { + tx := make([]*descriptor.Message, 0, len(d.Messages)) + for _, m := range d.Messages { + if m.SenderNode == n.Name && m.SendType != descriptor.SendTypeNone { + tx = append(tx, m) + } + } + return tx +} + +func collectRxMessages(d *descriptor.Database, n *descriptor.Node) []*descriptor.Message { + rx := make([]*descriptor.Message, 0, len(d.Messages)) +Loop: + for _, m := range d.Messages { + for _, s := range m.Signals { + for _, node := range s.ReceiverNodes { + if node != n.Name { + continue + } + rx = append(rx, m) + continue Loop + } + } + } + return rx +} + +func hasPhysicalRepresentation(s *descriptor.Signal) bool { + hasScale := s.Scale != 0 && s.Scale != 1 + hasOffset := s.Offset != 0 + hasRange := s.Min != 0 || s.Max != 0 + var hasConstrainedRange bool + if s.IsSigned { + hasConstrainedRange = s.Min > float64(s.MinSigned()) || s.Max < float64(s.MaxSigned()) + } else { + hasConstrainedRange = s.Min > 0 || s.Max < float64(s.MaxUnsigned()) + } + return hasScale || hasOffset || hasRange && hasConstrainedRange +} + +func hasCustomType(s *descriptor.Signal) bool { + return len(s.ValueDescriptions) > 0 +} + +func hasSendType(d *descriptor.Database) bool { + for _, m := range d.Messages { + if m.SendType != descriptor.SendTypeNone { + return true + } + } + return false +} + +func signalType(m *descriptor.Message, s *descriptor.Signal) string { + if hasCustomType(s) { + return m.Name + "_" + s.Name + } + return signalPrimitiveType(s).String() +} + +func signalPrimitiveType(s *descriptor.Signal) types.Type { + var t types.BasicKind + switch { + case s.Length == 1: + t = types.Bool + case s.Length <= 8 && s.IsSigned: + t = types.Int8 + case s.Length <= 8: + t = types.Uint8 + case s.Length <= 16 && s.IsSigned: + t = types.Int16 + case s.Length <= 16: + t = types.Uint16 + case s.Length <= 32 && s.IsSigned: + t = types.Int32 + case s.Length <= 32: + t = types.Uint32 + case s.Length <= 64 && s.IsSigned: + t = types.Int64 + default: + t = types.Uint64 + } + return types.Typ[t] +} + +func signalPrimitiveSuperType(s *descriptor.Signal) types.Type { + var t types.BasicKind + switch { + case s.Length == 1: + t = types.Bool + case s.IsSigned: + t = types.Int64 + default: + t = types.Uint64 + } + return types.Typ[t] +} + +func signalSuperType(s *descriptor.Signal) string { + switch { + case s.Length == 1: + return "Bool" + case s.IsSigned: + return "Signed" + default: + return "Unsigned" + } +} + +func nodeInterface(n *descriptor.Node) string { + return n.Name +} + +func nodeStruct(n *descriptor.Node) string { + return "xxx_" + n.Name +} + +func messageStruct(m *descriptor.Message) string { + return m.Name +} + +func messageReaderInterface(m *descriptor.Message) string { + return m.Name + "Reader" +} + +func messageWriterInterface(m *descriptor.Message) string { + return m.Name + "Writer" +} + +func messageField(m *descriptor.Message) string { + return "xxx_" + m.Name +} + +func signalField(s *descriptor.Signal) string { + return "xxx_" + s.Name +} + +func nodeDescriptor(n *descriptor.Node) string { + return "Nodes()." + n.Name +} + +func messageDescriptor(m *descriptor.Message) string { + return "Messages()." + m.Name +} + +func signalDescriptor(m *descriptor.Message, s *descriptor.Signal) string { + return messageDescriptor(m) + "." + s.Name +} diff --git a/internal/generate/file_test.go b/internal/generate/file_test.go new file mode 100644 index 0000000..6335ddd --- /dev/null +++ b/internal/generate/file_test.go @@ -0,0 +1,18 @@ +package generate + +import ( + "os" + "testing" + + "github.com/stretchr/testify/require" +) + +func runTestInDir(t *testing.T, dir string) func() { + // change working directory to project root + wd, err := os.Getwd() + require.NoError(t, err) + require.NoError(t, os.Chdir(dir)) + return func() { + require.NoError(t, os.Chdir(wd)) + } +} diff --git a/internal/identifiers/case.go b/internal/identifiers/case.go new file mode 100644 index 0000000..73ec047 --- /dev/null +++ b/internal/identifiers/case.go @@ -0,0 +1,12 @@ +package identifiers + +import "unicode" + +func IsCamelCase(s string) bool { + for i, r := range s { + if i == 0 && !unicode.IsUpper(r) || !IsAlphaChar(r) && !IsNumChar(r) { + return false + } + } + return true +} diff --git a/internal/identifiers/case_test.go b/internal/identifiers/case_test.go new file mode 100644 index 0000000..ffd6bc4 --- /dev/null +++ b/internal/identifiers/case_test.go @@ -0,0 +1,16 @@ +package identifiers + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestIsCamelCase(t *testing.T) { + require.True(t, IsCamelCase("SOC")) + require.True(t, IsCamelCase("Camel")) + require.True(t, IsCamelCase("CamelCase")) + require.False(t, IsCamelCase("camelCase")) + require.False(t, IsCamelCase("snake_case")) + require.False(t, IsCamelCase("kebab-case")) +} diff --git a/internal/identifiers/char.go b/internal/identifiers/char.go new file mode 100644 index 0000000..f99d800 --- /dev/null +++ b/internal/identifiers/char.go @@ -0,0 +1,9 @@ +package identifiers + +func IsAlphaChar(r rune) bool { + return ('A' <= r && r <= 'Z') || ('a' <= r && r <= 'z') +} + +func IsNumChar(r rune) bool { + return '0' <= r && r <= '9' +} diff --git a/internal/identifiers/char_test.go b/internal/identifiers/char_test.go new file mode 100644 index 0000000..9450922 --- /dev/null +++ b/internal/identifiers/char_test.go @@ -0,0 +1,23 @@ +package identifiers + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestIsAlphaChar(t *testing.T) { + require.True(t, IsAlphaChar('b')) + require.True(t, IsAlphaChar('C')) + require.False(t, IsAlphaChar('Ö')) + require.False(t, IsAlphaChar('_')) +} + +func TestIsNumChar(t *testing.T) { + require.True(t, IsNumChar('0')) + require.True(t, IsNumChar('1')) + require.True(t, IsNumChar('2')) + require.True(t, IsNumChar('9')) + require.False(t, IsNumChar('/')) + require.False(t, IsNumChar('a')) +} diff --git a/internal/mocks/mockcanrunner/mocks.go b/internal/mocks/mockcanrunner/mocks.go new file mode 100644 index 0000000..4549bfe --- /dev/null +++ b/internal/mocks/mockcanrunner/mocks.go @@ -0,0 +1,529 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: go.einride.tech/can/pkg/canrunner (interfaces: Node,TransmittedMessage,ReceivedMessage,FrameTransmitter,FrameReceiver) + +// Package mockcanrunner is a generated GoMock package. +package mockcanrunner + +import ( + context "context" + gomock "github.com/golang/mock/gomock" + can "go.einride.tech/can" + canrunner "go.einride.tech/can/pkg/canrunner" + descriptor "go.einride.tech/can/pkg/descriptor" + net "net" + reflect "reflect" + time "time" +) + +// MockNode is a mock of Node interface +type MockNode struct { + ctrl *gomock.Controller + recorder *MockNodeMockRecorder +} + +// MockNodeMockRecorder is the mock recorder for MockNode +type MockNodeMockRecorder struct { + mock *MockNode +} + +// NewMockNode creates a new mock instance +func NewMockNode(ctrl *gomock.Controller) *MockNode { + mock := &MockNode{ctrl: ctrl} + mock.recorder = &MockNodeMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockNode) EXPECT() *MockNodeMockRecorder { + return m.recorder +} + +// Connect mocks base method +func (m *MockNode) Connect() (net.Conn, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Connect") + ret0, _ := ret[0].(net.Conn) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Connect indicates an expected call of Connect +func (mr *MockNodeMockRecorder) Connect() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Connect", reflect.TypeOf((*MockNode)(nil).Connect)) +} + +// Descriptor mocks base method +func (m *MockNode) Descriptor() *descriptor.Node { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Descriptor") + ret0, _ := ret[0].(*descriptor.Node) + return ret0 +} + +// Descriptor indicates an expected call of Descriptor +func (mr *MockNodeMockRecorder) Descriptor() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Descriptor", reflect.TypeOf((*MockNode)(nil).Descriptor)) +} + +// Lock mocks base method +func (m *MockNode) Lock() { + m.ctrl.T.Helper() + m.ctrl.Call(m, "Lock") +} + +// Lock indicates an expected call of Lock +func (mr *MockNodeMockRecorder) Lock() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Lock", reflect.TypeOf((*MockNode)(nil).Lock)) +} + +// ReceivedMessage mocks base method +func (m *MockNode) ReceivedMessage(arg0 uint32) (canrunner.ReceivedMessage, bool) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ReceivedMessage", arg0) + ret0, _ := ret[0].(canrunner.ReceivedMessage) + ret1, _ := ret[1].(bool) + return ret0, ret1 +} + +// ReceivedMessage indicates an expected call of ReceivedMessage +func (mr *MockNodeMockRecorder) ReceivedMessage(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReceivedMessage", reflect.TypeOf((*MockNode)(nil).ReceivedMessage), arg0) +} + +// TransmittedMessages mocks base method +func (m *MockNode) TransmittedMessages() []canrunner.TransmittedMessage { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "TransmittedMessages") + ret0, _ := ret[0].([]canrunner.TransmittedMessage) + return ret0 +} + +// TransmittedMessages indicates an expected call of TransmittedMessages +func (mr *MockNodeMockRecorder) TransmittedMessages() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TransmittedMessages", reflect.TypeOf((*MockNode)(nil).TransmittedMessages)) +} + +// Unlock mocks base method +func (m *MockNode) Unlock() { + m.ctrl.T.Helper() + m.ctrl.Call(m, "Unlock") +} + +// Unlock indicates an expected call of Unlock +func (mr *MockNodeMockRecorder) Unlock() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Unlock", reflect.TypeOf((*MockNode)(nil).Unlock)) +} + +// MockTransmittedMessage is a mock of TransmittedMessage interface +type MockTransmittedMessage struct { + ctrl *gomock.Controller + recorder *MockTransmittedMessageMockRecorder +} + +// MockTransmittedMessageMockRecorder is the mock recorder for MockTransmittedMessage +type MockTransmittedMessageMockRecorder struct { + mock *MockTransmittedMessage +} + +// NewMockTransmittedMessage creates a new mock instance +func NewMockTransmittedMessage(ctrl *gomock.Controller) *MockTransmittedMessage { + mock := &MockTransmittedMessage{ctrl: ctrl} + mock.recorder = &MockTransmittedMessageMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockTransmittedMessage) EXPECT() *MockTransmittedMessageMockRecorder { + return m.recorder +} + +// BeforeTransmitHook mocks base method +func (m *MockTransmittedMessage) BeforeTransmitHook() func(context.Context) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "BeforeTransmitHook") + ret0, _ := ret[0].(func(context.Context) error) + return ret0 +} + +// BeforeTransmitHook indicates an expected call of BeforeTransmitHook +func (mr *MockTransmittedMessageMockRecorder) BeforeTransmitHook() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BeforeTransmitHook", reflect.TypeOf((*MockTransmittedMessage)(nil).BeforeTransmitHook)) +} + +// Descriptor mocks base method +func (m *MockTransmittedMessage) Descriptor() *descriptor.Message { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Descriptor") + ret0, _ := ret[0].(*descriptor.Message) + return ret0 +} + +// Descriptor indicates an expected call of Descriptor +func (mr *MockTransmittedMessageMockRecorder) Descriptor() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Descriptor", reflect.TypeOf((*MockTransmittedMessage)(nil).Descriptor)) +} + +// Frame mocks base method +func (m *MockTransmittedMessage) Frame() can.Frame { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Frame") + ret0, _ := ret[0].(can.Frame) + return ret0 +} + +// Frame indicates an expected call of Frame +func (mr *MockTransmittedMessageMockRecorder) Frame() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Frame", reflect.TypeOf((*MockTransmittedMessage)(nil).Frame)) +} + +// IsCyclicTransmissionEnabled mocks base method +func (m *MockTransmittedMessage) IsCyclicTransmissionEnabled() bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "IsCyclicTransmissionEnabled") + ret0, _ := ret[0].(bool) + return ret0 +} + +// IsCyclicTransmissionEnabled indicates an expected call of IsCyclicTransmissionEnabled +func (mr *MockTransmittedMessageMockRecorder) IsCyclicTransmissionEnabled() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsCyclicTransmissionEnabled", reflect.TypeOf((*MockTransmittedMessage)(nil).IsCyclicTransmissionEnabled)) +} + +// MarshalFrame mocks base method +func (m *MockTransmittedMessage) MarshalFrame() (can.Frame, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "MarshalFrame") + ret0, _ := ret[0].(can.Frame) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// MarshalFrame indicates an expected call of MarshalFrame +func (mr *MockTransmittedMessageMockRecorder) MarshalFrame() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MarshalFrame", reflect.TypeOf((*MockTransmittedMessage)(nil).MarshalFrame)) +} + +// Reset mocks base method +func (m *MockTransmittedMessage) Reset() { + m.ctrl.T.Helper() + m.ctrl.Call(m, "Reset") +} + +// Reset indicates an expected call of Reset +func (mr *MockTransmittedMessageMockRecorder) Reset() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Reset", reflect.TypeOf((*MockTransmittedMessage)(nil).Reset)) +} + +// SetTransmitTime mocks base method +func (m *MockTransmittedMessage) SetTransmitTime(arg0 time.Time) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetTransmitTime", arg0) +} + +// SetTransmitTime indicates an expected call of SetTransmitTime +func (mr *MockTransmittedMessageMockRecorder) SetTransmitTime(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetTransmitTime", reflect.TypeOf((*MockTransmittedMessage)(nil).SetTransmitTime), arg0) +} + +// String mocks base method +func (m *MockTransmittedMessage) String() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "String") + ret0, _ := ret[0].(string) + return ret0 +} + +// String indicates an expected call of String +func (mr *MockTransmittedMessageMockRecorder) String() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "String", reflect.TypeOf((*MockTransmittedMessage)(nil).String)) +} + +// TransmitEventChan mocks base method +func (m *MockTransmittedMessage) TransmitEventChan() <-chan struct{} { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "TransmitEventChan") + ret0, _ := ret[0].(<-chan struct{}) + return ret0 +} + +// TransmitEventChan indicates an expected call of TransmitEventChan +func (mr *MockTransmittedMessageMockRecorder) TransmitEventChan() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TransmitEventChan", reflect.TypeOf((*MockTransmittedMessage)(nil).TransmitEventChan)) +} + +// UnmarshalFrame mocks base method +func (m *MockTransmittedMessage) UnmarshalFrame(arg0 can.Frame) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UnmarshalFrame", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// UnmarshalFrame indicates an expected call of UnmarshalFrame +func (mr *MockTransmittedMessageMockRecorder) UnmarshalFrame(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UnmarshalFrame", reflect.TypeOf((*MockTransmittedMessage)(nil).UnmarshalFrame), arg0) +} + +// WakeUpChan mocks base method +func (m *MockTransmittedMessage) WakeUpChan() <-chan struct{} { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "WakeUpChan") + ret0, _ := ret[0].(<-chan struct{}) + return ret0 +} + +// WakeUpChan indicates an expected call of WakeUpChan +func (mr *MockTransmittedMessageMockRecorder) WakeUpChan() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WakeUpChan", reflect.TypeOf((*MockTransmittedMessage)(nil).WakeUpChan)) +} + +// MockReceivedMessage is a mock of ReceivedMessage interface +type MockReceivedMessage struct { + ctrl *gomock.Controller + recorder *MockReceivedMessageMockRecorder +} + +// MockReceivedMessageMockRecorder is the mock recorder for MockReceivedMessage +type MockReceivedMessageMockRecorder struct { + mock *MockReceivedMessage +} + +// NewMockReceivedMessage creates a new mock instance +func NewMockReceivedMessage(ctrl *gomock.Controller) *MockReceivedMessage { + mock := &MockReceivedMessage{ctrl: ctrl} + mock.recorder = &MockReceivedMessageMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockReceivedMessage) EXPECT() *MockReceivedMessageMockRecorder { + return m.recorder +} + +// AfterReceiveHook mocks base method +func (m *MockReceivedMessage) AfterReceiveHook() func(context.Context) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AfterReceiveHook") + ret0, _ := ret[0].(func(context.Context) error) + return ret0 +} + +// AfterReceiveHook indicates an expected call of AfterReceiveHook +func (mr *MockReceivedMessageMockRecorder) AfterReceiveHook() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AfterReceiveHook", reflect.TypeOf((*MockReceivedMessage)(nil).AfterReceiveHook)) +} + +// Descriptor mocks base method +func (m *MockReceivedMessage) Descriptor() *descriptor.Message { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Descriptor") + ret0, _ := ret[0].(*descriptor.Message) + return ret0 +} + +// Descriptor indicates an expected call of Descriptor +func (mr *MockReceivedMessageMockRecorder) Descriptor() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Descriptor", reflect.TypeOf((*MockReceivedMessage)(nil).Descriptor)) +} + +// Frame mocks base method +func (m *MockReceivedMessage) Frame() can.Frame { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Frame") + ret0, _ := ret[0].(can.Frame) + return ret0 +} + +// Frame indicates an expected call of Frame +func (mr *MockReceivedMessageMockRecorder) Frame() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Frame", reflect.TypeOf((*MockReceivedMessage)(nil).Frame)) +} + +// MarshalFrame mocks base method +func (m *MockReceivedMessage) MarshalFrame() (can.Frame, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "MarshalFrame") + ret0, _ := ret[0].(can.Frame) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// MarshalFrame indicates an expected call of MarshalFrame +func (mr *MockReceivedMessageMockRecorder) MarshalFrame() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MarshalFrame", reflect.TypeOf((*MockReceivedMessage)(nil).MarshalFrame)) +} + +// Reset mocks base method +func (m *MockReceivedMessage) Reset() { + m.ctrl.T.Helper() + m.ctrl.Call(m, "Reset") +} + +// Reset indicates an expected call of Reset +func (mr *MockReceivedMessageMockRecorder) Reset() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Reset", reflect.TypeOf((*MockReceivedMessage)(nil).Reset)) +} + +// SetReceiveTime mocks base method +func (m *MockReceivedMessage) SetReceiveTime(arg0 time.Time) { + m.ctrl.T.Helper() + m.ctrl.Call(m, "SetReceiveTime", arg0) +} + +// SetReceiveTime indicates an expected call of SetReceiveTime +func (mr *MockReceivedMessageMockRecorder) SetReceiveTime(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetReceiveTime", reflect.TypeOf((*MockReceivedMessage)(nil).SetReceiveTime), arg0) +} + +// String mocks base method +func (m *MockReceivedMessage) String() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "String") + ret0, _ := ret[0].(string) + return ret0 +} + +// String indicates an expected call of String +func (mr *MockReceivedMessageMockRecorder) String() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "String", reflect.TypeOf((*MockReceivedMessage)(nil).String)) +} + +// UnmarshalFrame mocks base method +func (m *MockReceivedMessage) UnmarshalFrame(arg0 can.Frame) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UnmarshalFrame", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// UnmarshalFrame indicates an expected call of UnmarshalFrame +func (mr *MockReceivedMessageMockRecorder) UnmarshalFrame(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UnmarshalFrame", reflect.TypeOf((*MockReceivedMessage)(nil).UnmarshalFrame), arg0) +} + +// MockFrameTransmitter is a mock of FrameTransmitter interface +type MockFrameTransmitter struct { + ctrl *gomock.Controller + recorder *MockFrameTransmitterMockRecorder +} + +// MockFrameTransmitterMockRecorder is the mock recorder for MockFrameTransmitter +type MockFrameTransmitterMockRecorder struct { + mock *MockFrameTransmitter +} + +// NewMockFrameTransmitter creates a new mock instance +func NewMockFrameTransmitter(ctrl *gomock.Controller) *MockFrameTransmitter { + mock := &MockFrameTransmitter{ctrl: ctrl} + mock.recorder = &MockFrameTransmitterMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockFrameTransmitter) EXPECT() *MockFrameTransmitterMockRecorder { + return m.recorder +} + +// TransmitFrame mocks base method +func (m *MockFrameTransmitter) TransmitFrame(arg0 context.Context, arg1 can.Frame) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "TransmitFrame", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// TransmitFrame indicates an expected call of TransmitFrame +func (mr *MockFrameTransmitterMockRecorder) TransmitFrame(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TransmitFrame", reflect.TypeOf((*MockFrameTransmitter)(nil).TransmitFrame), arg0, arg1) +} + +// MockFrameReceiver is a mock of FrameReceiver interface +type MockFrameReceiver struct { + ctrl *gomock.Controller + recorder *MockFrameReceiverMockRecorder +} + +// MockFrameReceiverMockRecorder is the mock recorder for MockFrameReceiver +type MockFrameReceiverMockRecorder struct { + mock *MockFrameReceiver +} + +// NewMockFrameReceiver creates a new mock instance +func NewMockFrameReceiver(ctrl *gomock.Controller) *MockFrameReceiver { + mock := &MockFrameReceiver{ctrl: ctrl} + mock.recorder = &MockFrameReceiverMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockFrameReceiver) EXPECT() *MockFrameReceiverMockRecorder { + return m.recorder +} + +// Err mocks base method +func (m *MockFrameReceiver) Err() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Err") + ret0, _ := ret[0].(error) + return ret0 +} + +// Err indicates an expected call of Err +func (mr *MockFrameReceiverMockRecorder) Err() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Err", reflect.TypeOf((*MockFrameReceiver)(nil).Err)) +} + +// Frame mocks base method +func (m *MockFrameReceiver) Frame() can.Frame { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Frame") + ret0, _ := ret[0].(can.Frame) + return ret0 +} + +// Frame indicates an expected call of Frame +func (mr *MockFrameReceiverMockRecorder) Frame() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Frame", reflect.TypeOf((*MockFrameReceiver)(nil).Frame)) +} + +// Receive mocks base method +func (m *MockFrameReceiver) Receive() bool { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Receive") + ret0, _ := ret[0].(bool) + return ret0 +} + +// Receive indicates an expected call of Receive +func (mr *MockFrameReceiverMockRecorder) Receive() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Receive", reflect.TypeOf((*MockFrameReceiver)(nil).Receive)) +} diff --git a/internal/mocks/mockclock/mocks.go b/internal/mocks/mockclock/mocks.go new file mode 100644 index 0000000..94d4efb --- /dev/null +++ b/internal/mocks/mockclock/mocks.go @@ -0,0 +1,141 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: go.einride.tech/can/internal/clock (interfaces: Clock,Ticker) + +// Package mockclock is a generated GoMock package. +package mockclock + +import ( + gomock "github.com/golang/mock/gomock" + timestamp "github.com/golang/protobuf/ptypes/timestamp" + clock "go.einride.tech/can/internal/clock" + reflect "reflect" + time "time" +) + +// MockClock is a mock of Clock interface +type MockClock struct { + ctrl *gomock.Controller + recorder *MockClockMockRecorder +} + +// MockClockMockRecorder is the mock recorder for MockClock +type MockClockMockRecorder struct { + mock *MockClock +} + +// NewMockClock creates a new mock instance +func NewMockClock(ctrl *gomock.Controller) *MockClock { + mock := &MockClock{ctrl: ctrl} + mock.recorder = &MockClockMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockClock) EXPECT() *MockClockMockRecorder { + return m.recorder +} + +// After mocks base method +func (m *MockClock) After(arg0 time.Duration) <-chan time.Time { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "After", arg0) + ret0, _ := ret[0].(<-chan time.Time) + return ret0 +} + +// After indicates an expected call of After +func (mr *MockClockMockRecorder) After(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "After", reflect.TypeOf((*MockClock)(nil).After), arg0) +} + +// NewTicker mocks base method +func (m *MockClock) NewTicker(arg0 time.Duration) clock.Ticker { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewTicker", arg0) + ret0, _ := ret[0].(clock.Ticker) + return ret0 +} + +// NewTicker indicates an expected call of NewTicker +func (mr *MockClockMockRecorder) NewTicker(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewTicker", reflect.TypeOf((*MockClock)(nil).NewTicker), arg0) +} + +// Now mocks base method +func (m *MockClock) Now() time.Time { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Now") + ret0, _ := ret[0].(time.Time) + return ret0 +} + +// Now indicates an expected call of Now +func (mr *MockClockMockRecorder) Now() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Now", reflect.TypeOf((*MockClock)(nil).Now)) +} + +// NowProto mocks base method +func (m *MockClock) NowProto() *timestamp.Timestamp { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NowProto") + ret0, _ := ret[0].(*timestamp.Timestamp) + return ret0 +} + +// NowProto indicates an expected call of NowProto +func (mr *MockClockMockRecorder) NowProto() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NowProto", reflect.TypeOf((*MockClock)(nil).NowProto)) +} + +// MockTicker is a mock of Ticker interface +type MockTicker struct { + ctrl *gomock.Controller + recorder *MockTickerMockRecorder +} + +// MockTickerMockRecorder is the mock recorder for MockTicker +type MockTickerMockRecorder struct { + mock *MockTicker +} + +// NewMockTicker creates a new mock instance +func NewMockTicker(ctrl *gomock.Controller) *MockTicker { + mock := &MockTicker{ctrl: ctrl} + mock.recorder = &MockTickerMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockTicker) EXPECT() *MockTickerMockRecorder { + return m.recorder +} + +// C mocks base method +func (m *MockTicker) C() <-chan time.Time { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "C") + ret0, _ := ret[0].(<-chan time.Time) + return ret0 +} + +// C indicates an expected call of C +func (mr *MockTickerMockRecorder) C() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "C", reflect.TypeOf((*MockTicker)(nil).C)) +} + +// Stop mocks base method +func (m *MockTicker) Stop() { + m.ctrl.T.Helper() + m.ctrl.Call(m, "Stop") +} + +// Stop indicates an expected call of Stop +func (mr *MockTickerMockRecorder) Stop() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Stop", reflect.TypeOf((*MockTicker)(nil).Stop)) +} diff --git a/internal/mocks/mocksocketcan/mocks.go b/internal/mocks/mocksocketcan/mocks.go new file mode 100644 index 0000000..d7e5341 --- /dev/null +++ b/internal/mocks/mocksocketcan/mocks.go @@ -0,0 +1,120 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: pkg/socketcan/fileconn.go + +// Package mocksocketcan is a generated GoMock package. +package mocksocketcan + +import ( + gomock "github.com/golang/mock/gomock" + reflect "reflect" + time "time" +) + +// Mockfile is a mock of file interface +type Mockfile struct { + ctrl *gomock.Controller + recorder *MockfileMockRecorder +} + +// MockfileMockRecorder is the mock recorder for Mockfile +type MockfileMockRecorder struct { + mock *Mockfile +} + +// NewMockfile creates a new mock instance +func NewMockfile(ctrl *gomock.Controller) *Mockfile { + mock := &Mockfile{ctrl: ctrl} + mock.recorder = &MockfileMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *Mockfile) EXPECT() *MockfileMockRecorder { + return m.recorder +} + +// Read mocks base method +func (m *Mockfile) Read(arg0 []byte) (int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Read", arg0) + ret0, _ := ret[0].(int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Read indicates an expected call of Read +func (mr *MockfileMockRecorder) Read(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*Mockfile)(nil).Read), arg0) +} + +// Write mocks base method +func (m *Mockfile) Write(arg0 []byte) (int, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Write", arg0) + ret0, _ := ret[0].(int) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Write indicates an expected call of Write +func (mr *MockfileMockRecorder) Write(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Write", reflect.TypeOf((*Mockfile)(nil).Write), arg0) +} + +// SetDeadline mocks base method +func (m *Mockfile) SetDeadline(arg0 time.Time) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetDeadline", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetDeadline indicates an expected call of SetDeadline +func (mr *MockfileMockRecorder) SetDeadline(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetDeadline", reflect.TypeOf((*Mockfile)(nil).SetDeadline), arg0) +} + +// SetReadDeadline mocks base method +func (m *Mockfile) SetReadDeadline(arg0 time.Time) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetReadDeadline", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetReadDeadline indicates an expected call of SetReadDeadline +func (mr *MockfileMockRecorder) SetReadDeadline(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetReadDeadline", reflect.TypeOf((*Mockfile)(nil).SetReadDeadline), arg0) +} + +// SetWriteDeadline mocks base method +func (m *Mockfile) SetWriteDeadline(arg0 time.Time) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SetWriteDeadline", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// SetWriteDeadline indicates an expected call of SetWriteDeadline +func (mr *MockfileMockRecorder) SetWriteDeadline(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetWriteDeadline", reflect.TypeOf((*Mockfile)(nil).SetWriteDeadline), arg0) +} + +// Close mocks base method +func (m *Mockfile) Close() error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Close") + ret0, _ := ret[0].(error) + return ret0 +} + +// Close indicates an expected call of Close +func (mr *MockfileMockRecorder) Close() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*Mockfile)(nil).Close)) +} diff --git a/internal/reinterpret/reinterpret.go b/internal/reinterpret/reinterpret.go new file mode 100644 index 0000000..2a4e46a --- /dev/null +++ b/internal/reinterpret/reinterpret.go @@ -0,0 +1,50 @@ +// Package reinterpret provides primitives for reinterpreting arbitrary-length values as signed or unsigned. +package reinterpret + +// AsSigned reinterprets the provided unsigned value as a signed value. +func AsSigned(unsigned uint64, bits uint8) int64 { + switch bits { + case 8: + return int64(int8(uint8(unsigned))) + case 16: + return int64(int16(uint16(unsigned))) + case 32: + return int64(int32(uint32(unsigned))) + case 64: + return int64(unsigned) + default: + // calculate bit mask for sign bit + signBitMask := uint64(1 << (bits - 1)) + // check if sign bit is set + isNegative := unsigned&signBitMask > 0 + if !isNegative { + // sign bit not set means we can reinterpret the value as-is + return int64(unsigned) + } + // calculate bit mask for extracting value bits (all bits except the sign bit) + valueBitMask := signBitMask - 1 + // calculate two's complement of the value bits + value := ((^unsigned) & valueBitMask) + 1 + // result is the negative value of the two's complement + return -1 * int64(value) + } +} + +// AsUnsigned reinterprets the provided signed value as an unsigned value. +func AsUnsigned(signed int64, bits uint8) uint64 { + switch bits { + case 8: + return uint64(uint8(int8(signed))) + case 16: + return uint64(uint16(int16(signed))) + case 32: + return uint64(uint32(int32(signed))) + case 64: + return uint64(signed) + default: + // calculate bit mask for extracting relevant bits + valueBitMask := uint64(1< 0 + if !isCyclic || !hasCycleTime || cyclicTransmissionTicker != nil { + return + } + cyclicTransmissionTicker = time.NewTicker(m.Descriptor().CycleTime) + cyclicTransmissionTickChan = cyclicTransmissionTicker.C + } + disableCyclicTransmission := func() { + if cyclicTransmissionTicker == nil { + return + } + cyclicTransmissionTicker.Stop() + cyclicTransmissionTicker = nil + } + transmit := func() error { + l.Lock() + hook := m.BeforeTransmitHook() + m.SetTransmitTime(c.Now()) + l.Unlock() + if err := hook(ctx); err != nil { + return fmt.Errorf("%s transmitter: %w", m.Descriptor().Name, err) + } + l.Lock() + f := m.Frame() + l.Unlock() + ctx, cancel := context.WithTimeout(ctx, sendTimeout) + err := tx.TransmitFrame(ctx, f) + cancel() + if err != nil { + return fmt.Errorf("%s transmitter: %w", m.Descriptor().Name, err) + } + return nil + } + ctxDone := ctx.Done() + transmitEventChan := m.TransmitEventChan() + wakeUpChan := m.WakeUpChan() + for { + select { + case <-ctxDone: + return nil + case <-wakeUpChan: + l.Lock() + isCyclicTransmissionEnabled := m.IsCyclicTransmissionEnabled() + l.Unlock() + if isCyclicTransmissionEnabled { + enableCyclicTransmission() + } else { + disableCyclicTransmission() + } + case <-transmitEventChan: + if err := transmit(); err != nil { + return err + } + case <-cyclicTransmissionTickChan: + if err := transmit(); err != nil { + return err + } + } + } +} diff --git a/pkg/canrunner/run_test.go b/pkg/canrunner/run_test.go new file mode 100644 index 0000000..862210d --- /dev/null +++ b/pkg/canrunner/run_test.go @@ -0,0 +1,120 @@ +package canrunner_test + +import ( + "context" + "os" + "testing" + "time" + + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/require" + "go.einride.tech/can" + "go.einride.tech/can/internal/mocks/mockcanrunner" + "go.einride.tech/can/internal/mocks/mockclock" + "go.einride.tech/can/pkg/canrunner" + "go.einride.tech/can/pkg/descriptor" + "golang.org/x/sync/errgroup" + "golang.org/x/xerrors" +) + +func TestRunMessageReceiver_NoMessages(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + rx := mockcanrunner.NewMockFrameReceiver(ctrl) + node := mockcanrunner.NewMockNode(ctrl) + clock := mockclock.NewMockClock(ctrl) + ctx := context.Background() + // when the first receive fails + rx.EXPECT().Receive().Return(false) + rx.EXPECT().Err().Return(os.ErrClosed) + // then an error is returned + require.True(t, xerrors.Is(canrunner.RunMessageReceiver(ctx, rx, node, clock), os.ErrClosed)) +} + +func TestRunMessageReceiver_ReceiveMessage(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + rx := mockcanrunner.NewMockFrameReceiver(ctrl) + node := mockcanrunner.NewMockNode(ctrl) + clock := mockclock.NewMockClock(ctrl) + msg := mockcanrunner.NewMockReceivedMessage(ctrl) + ctx := context.Background() + // when the first receive succeeds + frame := can.Frame{ID: 42} + rx.EXPECT().Receive().Return(true) + rx.EXPECT().Frame().Return(frame) + // then the receiver should do a message lookup + node.EXPECT().ReceivedMessage(frame.ID).Return(msg, true) + // and the node should be locked + node.EXPECT().Lock() + // and the message should be queried for a hook with the same context + afterReceiveHook := func(c context.Context) error { + require.Equal(t, ctx, c) + return nil + } + msg.EXPECT().AfterReceiveHook().Return(afterReceiveHook) + // and the receive time should be set + now := time.Unix(0, 1) + clock.EXPECT().Now().Return(now) + msg.EXPECT().SetReceiveTime(now) + // and the message should be called to unmarshal the frame + msg.EXPECT().UnmarshalFrame(frame) + // and the node should be unlocked + node.EXPECT().Unlock() + // when the next receive fails + rx.EXPECT().Receive().Return(false) + rx.EXPECT().Err().Return(nil) + // then the receiver should return + require.NoError(t, canrunner.RunMessageReceiver(ctx, rx, node, clock)) +} + +func TestRunMessageTransmitter_TransmitEventMessage(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + tx := mockcanrunner.NewMockFrameTransmitter(ctrl) + node := mockcanrunner.NewMockNode(ctrl) + msg := mockcanrunner.NewMockTransmittedMessage(ctrl) + clock := mockclock.NewMockClock(ctrl) + desc := &descriptor.Message{ + Name: "TestMessage", + SendType: descriptor.SendTypeEvent, + } + transmitEventChan := make(chan struct{}) + wakeUpChan := make(chan struct{}) + ctx := context.Background() + msg.EXPECT().Descriptor().AnyTimes().Return(desc) + msg.EXPECT().TransmitEventChan().Return(transmitEventChan) + msg.EXPECT().WakeUpChan().Return(wakeUpChan) + // given a running transmitter + ctx, cancel := context.WithCancel(context.Background()) + var g errgroup.Group + g.Go(func() error { + return canrunner.RunMessageTransmitter(ctx, tx, node, msg, clock) + }) + // then the node should be locked + node.EXPECT().Lock() + // and the time should be queried + now := time.Unix(0, 1) + clock.EXPECT().Now().Return(now) + // and the transmit hook should be queried with the same context + hook := func(c context.Context) error { + require.Equal(t, ctx, c) + return nil + } + msg.EXPECT().BeforeTransmitHook().Return(hook) + // and the message should be marshaled to a CAN frame + frame := can.Frame{ID: 42} + // and the transmit time should be set + msg.EXPECT().SetTransmitTime(now) + // and the node should be unlocked + node.EXPECT().Unlock() + node.EXPECT().Lock() + msg.EXPECT().Frame().Return(frame) + node.EXPECT().Unlock() + // and the CAN frame should be transmitted + tx.EXPECT().TransmitFrame(gomock.Any(), frame) + // when the transmitter receives a transmit event + transmitEventChan <- struct{}{} + cancel() + require.NoError(t, g.Wait()) +} diff --git a/pkg/cantext/encode.go b/pkg/cantext/encode.go new file mode 100644 index 0000000..e7ba323 --- /dev/null +++ b/pkg/cantext/encode.go @@ -0,0 +1,127 @@ +package cantext + +import ( + "strconv" + + "go.einride.tech/can" + "go.einride.tech/can/pkg/descriptor" + "go.einride.tech/can/pkg/generated" +) + +// preAllocatedBytesPerSignal is an estimate of how many bytes each signal needs. +const preAllocatedBytesPerSignal = 40 + +func MessageString(m generated.Message) string { + return string(MarshalCompact(m)) +} + +func MarshalCompact(m generated.Message) []byte { + f := m.Frame() + buf := make([]byte, 0, len(m.Descriptor().Signals)*preAllocatedBytesPerSignal) + buf = append(buf, "{"...) + for i, s := range m.Descriptor().Signals { + buf = AppendSignalCompact(buf, s, f.Data) + if i != len(m.Descriptor().Signals)-1 { + buf = append(buf, ", "...) + } + } + buf = append(buf, "}"...) + return buf +} + +func Marshal(m generated.Message) []byte { + f := m.Frame() + // allocate space for one "extra" signal to account for message header + buf := make([]byte, 0, (len(m.Descriptor().Signals)+1)*preAllocatedBytesPerSignal) + buf = append(buf, m.Descriptor().Name...) + for _, s := range m.Descriptor().Signals { + buf = append(buf, "\n\t"...) + buf = AppendSignal(buf, s, f.Data) + } + return buf +} + +func AppendSignal(buf []byte, s *descriptor.Signal, d can.Data) []byte { + buf = append(buf, s.Name...) + buf = append(buf, ": "...) + switch { + case s.Length == 1: // bool + val := s.UnmarshalBool(d) + buf = strconv.AppendBool(buf, val) + case s.IsSigned: // signed + buf = strconv.AppendFloat(buf, s.UnmarshalPhysical(d), 'g', -1, 64) + buf = append(buf, s.Unit...) + buf = append(buf, " ("...) + buf = append(buf, "0x"...) + buf = strconv.AppendUint(buf, uint64(s.UnmarshalSigned(d)), 16) + buf = append(buf, ')') + default: // unsigned + buf = strconv.AppendFloat(buf, s.UnmarshalPhysical(d), 'g', -1, 64) + buf = append(buf, s.Unit...) + buf = append(buf, " ("...) + buf = append(buf, "0x"...) + buf = strconv.AppendUint(buf, s.UnmarshalUnsigned(d), 16) + buf = append(buf, ")"...) + } + if vd, ok := s.UnmarshalValueDescription(d); ok { + buf = append(buf, ' ') + buf = append(buf, vd...) + } + return buf +} + +func AppendSignalCompact(buf []byte, s *descriptor.Signal, d can.Data) []byte { + buf = append(buf, s.Name...) + buf = append(buf, ": "...) + valueDescription, hasValueDescription := s.UnmarshalValueDescription(d) + switch { + case hasValueDescription: + buf = append(buf, valueDescription...) + case s.Length == 1: // bool + val := s.UnmarshalBool(d) + buf = strconv.AppendBool(buf, val) + case s.IsSigned: // signed + buf = strconv.AppendFloat(buf, s.UnmarshalPhysical(d), 'g', -1, 64) + buf = append(buf, s.Unit...) + default: // unsigned + buf = strconv.AppendFloat(buf, s.UnmarshalPhysical(d), 'g', -1, 64) + buf = append(buf, s.Unit...) + } + return buf +} + +func AppendID(buf []byte, m *descriptor.Message) []byte { + buf = append(buf, "ID: "...) + buf = strconv.AppendUint(buf, uint64(m.ID), 10) + buf = append(buf, " (0x"...) + buf = strconv.AppendUint(buf, uint64(m.ID), 16) + buf = append(buf, ")"...) + return buf +} + +func AppendSender(buf []byte, m *descriptor.Message) []byte { + return appendAttributeString(buf, "Sender", m.SenderNode) +} + +func AppendSendType(buf []byte, m *descriptor.Message) []byte { + return appendAttributeString(buf, "SendType", m.SendType.String()) +} + +func AppendCycleTime(buf []byte, m *descriptor.Message) []byte { + return appendAttributeString(buf, "CycleTime", m.CycleTime.String()) +} + +func AppendDelayTime(buf []byte, m *descriptor.Message) []byte { + return appendAttributeString(buf, "DelayTime", m.DelayTime.String()) +} + +func AppendFrame(buf []byte, f can.Frame) []byte { + return appendAttributeString(buf, "Frame", f.String()) +} + +func appendAttributeString(buf []byte, name string, s string) []byte { + buf = append(buf, name...) + buf = append(buf, ": "...) + buf = append(buf, s...) + return buf +} diff --git a/pkg/cantext/encode_test.go b/pkg/cantext/encode_test.go new file mode 100644 index 0000000..f8ffd95 --- /dev/null +++ b/pkg/cantext/encode_test.go @@ -0,0 +1,234 @@ +package cantext + +import ( + "strings" + "testing" + "time" + + "github.com/stretchr/testify/require" + "go.einride.tech/can" + "go.einride.tech/can/pkg/descriptor" + "go.einride.tech/can/pkg/generated" +) + +func TestMarshal(t *testing.T) { + for _, tt := range []struct { + name string + msg generated.Message + expected string + expectedCompact string + }{ + { + name: "with enum", + msg: &testMessage{ + frame: can.Frame{ID: 100, Length: 1, Data: can.Data{2}}, + descriptor: newDriverHeartbeatDescriptor(), + }, + expected: ` +DriverHeartbeat + Command: 2 (0x2) Reboot +`, + expectedCompact: `{Command: Reboot}`, + }, + { + name: "with unit", + msg: &testMessage{ + frame: can.Frame{ID: 100, Length: 3, Data: can.Data{1, 0x7b}}, + descriptor: newMotorStatusDescriptor(), + }, + expected: ` +MotorStatus + WheelError: true + SpeedKph: 0.123km/h (0x7b) +`, + expectedCompact: `{WheelError: true, SpeedKph: 0.123km/h}`, + }, + } { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Run("standard", func(t *testing.T) { + txt := Marshal(tt.msg) + require.Equal(t, strings.TrimSpace(tt.expected), string(txt)) + }) + t.Run("compact", func(t *testing.T) { + txt := MarshalCompact(tt.msg) + require.Equal(t, strings.TrimSpace(tt.expectedCompact), string(txt)) + }) + }) + } +} + +func TestAppendID(t *testing.T) { + const expected = "ID: 100 (0x64)" + actual := string(AppendID([]byte{}, newDriverHeartbeatDescriptor())) + require.Equal(t, expected, actual) +} + +func TestAppendSender(t *testing.T) { + const expected = "Sender: DRIVER" + actual := string(AppendSender([]byte{}, newDriverHeartbeatDescriptor())) + require.Equal(t, expected, actual) +} + +func TestAppendSendType(t *testing.T) { + const expected = "SendType: Cyclic" + actual := string(AppendSendType([]byte{}, newDriverHeartbeatDescriptor())) + require.Equal(t, expected, actual) +} + +func TestAppendCycleTime(t *testing.T) { + const expected = "CycleTime: 100ms" + actual := string(AppendCycleTime([]byte{}, newDriverHeartbeatDescriptor())) + require.Equal(t, expected, actual) +} + +func TestAppendDelayTime(t *testing.T) { + const expected = "DelayTime: 2s" + actual := string(AppendDelayTime([]byte{}, newDriverHeartbeatDescriptor())) + require.Equal(t, expected, actual) +} + +func TestAppendFrame(t *testing.T) { + const expected = "Frame: 042#123456" + actual := string(AppendFrame([]byte{}, can.Frame{ID: 0x42, Length: 3, Data: can.Data{0x12, 0x34, 0x56}})) + require.Equal(t, expected, actual) +} + +func newDriverHeartbeatDescriptor() *descriptor.Message { + return &descriptor.Message{ + Name: (string)("DriverHeartbeat"), + ID: (uint32)(100), + IsExtended: (bool)(false), + Length: (uint8)(1), + Description: (string)("Sync message used to synchronize the controllers"), + SendType: descriptor.SendTypeCyclic, + CycleTime: 100 * time.Millisecond, + DelayTime: 2 * time.Second, + Signals: []*descriptor.Signal{ + { + Name: (string)("Command"), + Start: (uint8)(0), + Length: (uint8)(8), + IsBigEndian: (bool)(false), + IsSigned: (bool)(false), + IsMultiplexer: (bool)(false), + IsMultiplexed: (bool)(false), + MultiplexerValue: (uint)(0), + Offset: (float64)(0), + Scale: (float64)(1), + Min: (float64)(0), + Max: (float64)(0), + Unit: (string)(""), + Description: (string)(""), + ValueDescriptions: []*descriptor.ValueDescription{ + { + Value: (int)(0), + Description: (string)("None"), + }, + { + Value: (int)(1), + Description: (string)("Sync"), + }, + { + Value: (int)(2), + Description: (string)("Reboot"), + }, + }, + ReceiverNodes: []string{ + (string)("SENSOR"), + (string)("MOTOR"), + }, + DefaultValue: (int)(0), + }, + }, + SenderNode: (string)("DRIVER"), + } +} + +func newMotorStatusDescriptor() *descriptor.Message { + return &descriptor.Message{ + Name: (string)("MotorStatus"), + ID: (uint32)(400), + IsExtended: (bool)(false), + Length: (uint8)(3), + Description: (string)(""), + Signals: []*descriptor.Signal{ + { + Name: (string)("WheelError"), + Start: (uint8)(0), + Length: (uint8)(1), + IsBigEndian: (bool)(false), + IsSigned: (bool)(false), + IsMultiplexer: (bool)(false), + IsMultiplexed: (bool)(false), + MultiplexerValue: (uint)(0), + Offset: (float64)(0), + Scale: (float64)(1), + Min: (float64)(0), + Max: (float64)(0), + Unit: (string)(""), + Description: (string)(""), + ValueDescriptions: ([]*descriptor.ValueDescription)(nil), + ReceiverNodes: []string{ + (string)("DRIVER"), + (string)("IO"), + }, + DefaultValue: (int)(0), + }, + { + Name: (string)("SpeedKph"), + Start: (uint8)(8), + Length: (uint8)(16), + IsBigEndian: (bool)(false), + IsSigned: (bool)(false), + IsMultiplexer: (bool)(false), + IsMultiplexed: (bool)(false), + MultiplexerValue: (uint)(0), + Offset: (float64)(0), + Scale: (float64)(0.001), + Min: (float64)(0), + Max: (float64)(0), + Unit: (string)("km/h"), + Description: (string)(""), + ValueDescriptions: ([]*descriptor.ValueDescription)(nil), + ReceiverNodes: []string{ + (string)("DRIVER"), + (string)("IO"), + }, + DefaultValue: (int)(0), + }, + }, + SenderNode: (string)("MOTOR"), + CycleTime: (time.Duration)(100000000), + DelayTime: (time.Duration)(0), + } +} + +type testMessage struct { + frame can.Frame + descriptor *descriptor.Message +} + +func (m *testMessage) Frame() can.Frame { + return m.frame +} + +func (m *testMessage) Descriptor() *descriptor.Message { + return m.descriptor +} + +func (m *testMessage) MarshalFrame() (can.Frame, error) { + panic("should not be called") +} + +func (testMessage) Reset() { + panic("should not be called") +} + +func (testMessage) String() string { + panic("should not be called") +} + +func (testMessage) UnmarshalFrame(can.Frame) error { + panic("should not be called") +} diff --git a/pkg/dbc/accesstype.go b/pkg/dbc/accesstype.go new file mode 100644 index 0000000..b499d55 --- /dev/null +++ b/pkg/dbc/accesstype.go @@ -0,0 +1,26 @@ +package dbc + +import "fmt" + +// AccessType represents the access type of an environment variable. +type AccessType string + +const ( + AccessTypeUnrestricted AccessType = "DUMMY_NODE_VECTOR0" + AccessTypeRead AccessType = "DUMMY_NODE_VECTOR1" + AccessTypeWrite AccessType = "DUMMY_NODE_VECTOR2" + AccessTypeReadWrite AccessType = "DUMMY_NODE_VECTOR3" +) + +// Validate returns an error for invalid access types. +func (a AccessType) Validate() error { + switch a { + case AccessTypeUnrestricted: + case AccessTypeRead: + case AccessTypeWrite: + case AccessTypeReadWrite: + default: + return fmt.Errorf("invalid access type: %v", a) + } + return nil +} diff --git a/pkg/dbc/accesstype_test.go b/pkg/dbc/accesstype_test.go new file mode 100644 index 0000000..e3191e5 --- /dev/null +++ b/pkg/dbc/accesstype_test.go @@ -0,0 +1,22 @@ +package dbc + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestAccessType_Validate(t *testing.T) { + for _, tt := range []AccessType{ + AccessTypeUnrestricted, + AccessTypeRead, + AccessTypeWrite, + AccessTypeReadWrite, + } { + require.NoError(t, tt.Validate()) + } +} + +func TestAccessType_Validate_Error(t *testing.T) { + require.Error(t, AccessType("foo").Validate()) +} diff --git a/pkg/dbc/analysis/analysis.go b/pkg/dbc/analysis/analysis.go new file mode 100644 index 0000000..c16a6fa --- /dev/null +++ b/pkg/dbc/analysis/analysis.go @@ -0,0 +1,53 @@ +package analysis + +import ( + "fmt" + "strings" + "text/scanner" + + "go.einride.tech/can/pkg/dbc" +) + +// An Analyzer describes an analysis function and its options. +type Analyzer struct { + // Name of the analyzer. + Name string + + // Doc is the documentation for the analyzer. + Doc string + + // Run the analyzer. + Run func(*Pass) error +} + +// Title is the part before the first "\n\n" of the documentation. +func (a *Analyzer) Title() string { + return strings.SplitN(a.Doc, "\n\n", 2)[0] +} + +// Validate the analyzer metadata. +func (a *Analyzer) Validate() error { + if a.Doc == "" { + return fmt.Errorf("missing doc") + } + return nil +} + +// A Diagnostic is a message associated with a source location. +type Diagnostic struct { + Pos scanner.Position + Message string +} + +// Pass is the interface to the run function that analyzes DBC definitions. +type Pass struct { + Analyzer *Analyzer + File *dbc.File + Diagnostics []*Diagnostic +} + +// Reportf reports a diagnostic by building a message from the provided format and args. +func (pass *Pass) Reportf(pos scanner.Position, format string, args ...interface{}) { + msg := fmt.Sprintf(format, args...) + pass.Diagnostics = append(pass.Diagnostics, &Diagnostic{Pos: pos, Message: msg}) +} diff --git a/pkg/dbc/analysis/analysistest/analysistest.go b/pkg/dbc/analysis/analysistest/analysistest.go new file mode 100644 index 0000000..d3c8090 --- /dev/null +++ b/pkg/dbc/analysis/analysistest/analysistest.go @@ -0,0 +1,38 @@ +package analysistest + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/require" + "go.einride.tech/can/pkg/dbc" + "go.einride.tech/can/pkg/dbc/analysis" +) + +type Case struct { + Name string + Data string + Diagnostics []*analysis.Diagnostic +} + +func Run(t *testing.T, a *analysis.Analyzer, cs []*Case) { + t.Helper() + for _, c := range cs { + p := dbc.NewParser(c.Name, []byte(strings.TrimLeft(c.Data, "\n"))) + require.NoError(t, p.Parse()) + pass := &analysis.Pass{ + Analyzer: a, + File: p.File(), + } + require.NoError(t, a.Run(pass)) + // allow omitting byte offsets and file names + for _, d := range c.Diagnostics { + d.Pos.Offset = 0 + d.Pos.Filename = c.Name + } + for _, d := range pass.Diagnostics { + d.Pos.Offset = 0 + } + require.Equal(t, c.Diagnostics, pass.Diagnostics) + } +} diff --git a/pkg/dbc/analysis/passes/boolprefix/analyzer.go b/pkg/dbc/analysis/passes/boolprefix/analyzer.go new file mode 100644 index 0000000..44bc21f --- /dev/null +++ b/pkg/dbc/analysis/passes/boolprefix/analyzer.go @@ -0,0 +1,60 @@ +package boolprefix + +import ( + "strings" + + "go.einride.tech/can/pkg/dbc" + "go.einride.tech/can/pkg/dbc/analysis" +) + +func Analyzer() *analysis.Analyzer { + return &analysis.Analyzer{ + Name: "boolprefix", + Doc: "check that bools (1-bit signals) have a correct prefix", + Run: run, + } +} + +func allowedPrefixes() []string { + return []string{ + "Is", + "Has", + } +} + +func run(pass *analysis.Pass) error { + for _, d := range pass.File.Defs { + messageDef, ok := d.(*dbc.MessageDef) + if !ok { + continue + } + SignalLoop: + for _, signalDef := range messageDef.Signals { + if signalDef.Size != 1 { + continue // skip all non-bool signals + } + for _, allowedPrefix := range allowedPrefixes() { + if strings.HasPrefix(string(signalDef.Name), allowedPrefix) { + continue SignalLoop // has allowed prefix + } + } + // edge-case: allow non-prefixed 1-bit signals with value descriptions + for _, d := range pass.File.Defs { + valueDescriptionsDef, ok := d.(*dbc.ValueDescriptionsDef) + if !ok { + continue // not value descriptions + } + if valueDescriptionsDef.MessageID == messageDef.MessageID && + valueDescriptionsDef.SignalName == signalDef.Name { + continue SignalLoop // has value descriptions + } + } + pass.Reportf( + signalDef.Pos, + "bool signals (1-bit) must have prefix %s", + strings.Join(allowedPrefixes(), " or "), + ) + } + } + return nil +} diff --git a/pkg/dbc/analysis/passes/boolprefix/analyzer_test.go b/pkg/dbc/analysis/passes/boolprefix/analyzer_test.go new file mode 100644 index 0000000..ace20af --- /dev/null +++ b/pkg/dbc/analysis/passes/boolprefix/analyzer_test.go @@ -0,0 +1,53 @@ +package boolprefix + +import ( + "testing" + "text/scanner" + + "go.einride.tech/can/pkg/dbc/analysis" + "go.einride.tech/can/pkg/dbc/analysis/analysistest" +) + +func TestAnalyzer(t *testing.T) { + analysistest.Run(t, Analyzer(), []*analysistest.Case{ + { + Name: "prefix has", + Data: ` +BO_ 400 MOTOR_STATUS: 3 MOTOR + SG_ HasWheelError : 0|1@1+ (1,0) [0|0] "" DRIVER,IO +`, + }, + + { + Name: "prefix is", + Data: ` +BO_ 400 MOTOR_STATUS: 3 MOTOR + SG_ IsOverheated : 0|1@1+ (1,0) [0|0] "" DRIVER,IO +`, + }, + + { + Name: "missing prefix", + Data: ` +BO_ 400 MOTOR_STATUS: 3 MOTOR + SG_ WheelError : 0|1@1+ (1,0) [0|0] "" DRIVER,IO +`, + Diagnostics: []*analysis.Diagnostic{ + { + Pos: scanner.Position{Line: 2, Column: 2}, + Message: "bool signals (1-bit) must have prefix Is or Has", + }, + }, + }, + + { + Name: "missing prefix with value descriptions", + Data: ` +BO_ 400 MOTOR_STATUS: 3 MOTOR + SG_ Status : 0|1@1+ (1,0) [0|0] "" DRIVER,IO + +VAL_ 400 Status 1 "ValidDataPresent" 0 "NoData" ; +`, + }, + }) +} diff --git a/pkg/dbc/analysis/passes/definitiontypeorder/analyzer.go b/pkg/dbc/analysis/passes/definitiontypeorder/analyzer.go new file mode 100644 index 0000000..f7a69dc --- /dev/null +++ b/pkg/dbc/analysis/passes/definitiontypeorder/analyzer.go @@ -0,0 +1,56 @@ +package definitiontypeorder + +import ( + "math" + "reflect" + + "go.einride.tech/can/pkg/dbc" + "go.einride.tech/can/pkg/dbc/analysis" +) + +func Analyzer() *analysis.Analyzer { + return &analysis.Analyzer{ + Name: "definitiontypeorder", + Doc: "check that definitions are in the correct order", + Run: run, + } +} + +func orderOf(def dbc.Def) uint64 { + for i, orderDef := range []dbc.Def{ + &dbc.VersionDef{}, + &dbc.NewSymbolsDef{}, + &dbc.BitTimingDef{}, + &dbc.NodesDef{}, + &dbc.ValueTableDef{}, + &dbc.MessageDef{}, + &dbc.MessageTransmittersDef{}, + &dbc.EnvironmentVariableDef{}, + &dbc.EnvironmentVariableDataDef{}, + &dbc.CommentDef{}, + &dbc.AttributeDef{}, + &dbc.AttributeDefaultValueDef{}, + &dbc.AttributeValueForObjectDef{}, + &dbc.ValueDescriptionsDef{}, + } { + if reflect.TypeOf(def) == reflect.TypeOf(orderDef) { + return uint64(i) + } + } + return math.MaxUint64 +} + +func run(pass *analysis.Pass) error { + minOrder := uint64(math.MaxUint64) + for i := range pass.File.Defs { + // diagnostics make more sense when going backwards + def := pass.File.Defs[len(pass.File.Defs)-i-1] + currOrder := orderOf(def) + if currOrder > minOrder { + pass.Reportf(def.Position(), "definition out of order") + } else { + minOrder = currOrder + } + } + return nil +} diff --git a/pkg/dbc/analysis/passes/definitiontypeorder/analyzer_test.go b/pkg/dbc/analysis/passes/definitiontypeorder/analyzer_test.go new file mode 100644 index 0000000..aa49e91 --- /dev/null +++ b/pkg/dbc/analysis/passes/definitiontypeorder/analyzer_test.go @@ -0,0 +1,56 @@ +package definitiontypeorder + +import ( + "testing" + "text/scanner" + + "go.einride.tech/can/pkg/dbc/analysis" + "go.einride.tech/can/pkg/dbc/analysis/analysistest" +) + +func TestAnalyzer(t *testing.T) { + analysistest.Run(t, Analyzer(), []*analysistest.Case{ + { + Name: "correct order", + Data: ` +VERSION "foo" +NS_ : +BS_: +BU_: + `, + }, + + { + Name: "incorrect order", + Data: ` +VERSION "foo" +NS_ : +BU_: +BS_: + `, + Diagnostics: []*analysis.Diagnostic{ + { + Pos: scanner.Position{Line: 3, Column: 1}, + Message: "definition out of order", + }, + }, + }, + + { + Name: "unknown defs last", + Data: ` +VERSION "foo" +NS_ : +BS_: +FOO "bar" +BU_: + `, + Diagnostics: []*analysis.Diagnostic{ + { + Pos: scanner.Position{Line: 4, Column: 1}, + Message: "definition out of order", + }, + }, + }, + }) +} diff --git a/pkg/dbc/analysis/passes/intervals/analyzer.go b/pkg/dbc/analysis/passes/intervals/analyzer.go new file mode 100644 index 0000000..ba70139 --- /dev/null +++ b/pkg/dbc/analysis/passes/intervals/analyzer.go @@ -0,0 +1,40 @@ +package intervals + +import ( + "go.einride.tech/can/pkg/dbc" + "go.einride.tech/can/pkg/dbc/analysis" +) + +func Analyzer() *analysis.Analyzer { + return &analysis.Analyzer{ + Name: "intervals", + Doc: "check that all intervals are valid (min <= max)", + Run: run, + } +} + +func run(pass *analysis.Pass) error { + for _, def := range pass.File.Defs { + switch def := def.(type) { + case *dbc.EnvironmentVariableDef: + if def.Minimum > def.Maximum { + pass.Reportf(def.Pos, "invalid interval: [%f, %f]", def.Minimum, def.Maximum) + } + case *dbc.MessageDef: + for i := range def.Signals { + signal := &def.Signals[i] + if signal.Minimum > signal.Maximum { + pass.Reportf(def.Pos, "invalid interval: [%f, %f]", signal.Minimum, signal.Maximum) + } + } + case *dbc.AttributeDef: + if def.MinimumInt > def.MaximumInt || def.MinimumFloat > def.MaximumFloat { + pass.Reportf(def.Pos, "invalid interval: [%d, %d]", def.MinimumInt, def.MaximumInt) + } + if def.MinimumFloat > def.MaximumFloat { + pass.Reportf(def.Pos, "invalid interval: [%f, %f]", def.MinimumFloat, def.MaximumFloat) + } + } + } + return nil +} diff --git a/pkg/dbc/analysis/passes/intervals/analyzer_test.go b/pkg/dbc/analysis/passes/intervals/analyzer_test.go new file mode 100644 index 0000000..6f47060 --- /dev/null +++ b/pkg/dbc/analysis/passes/intervals/analyzer_test.go @@ -0,0 +1,29 @@ +package intervals + +import ( + "testing" + "text/scanner" + + "go.einride.tech/can/pkg/dbc/analysis" + "go.einride.tech/can/pkg/dbc/analysis/analysistest" +) + +func TestAnalyzer(t *testing.T) { + analysistest.Run(t, Analyzer(), []*analysistest.Case{ + { + Name: "attribute interval ok", + Data: `BA_DEF_ "AttributeName" INT 0 10;`, + }, + + { + Name: "attribute interval bad", + Data: `BA_DEF_ "AttributeName" INT 10 0;`, + Diagnostics: []*analysis.Diagnostic{ + { + Pos: scanner.Position{Line: 1, Column: 1}, + Message: "invalid interval: [10, 0]", + }, + }, + }, + }) +} diff --git a/pkg/dbc/analysis/passes/lineendings/analyzer.go b/pkg/dbc/analysis/passes/lineendings/analyzer.go new file mode 100644 index 0000000..15779a6 --- /dev/null +++ b/pkg/dbc/analysis/passes/lineendings/analyzer.go @@ -0,0 +1,26 @@ +package lineendings + +import ( + "bytes" + "text/scanner" + + "go.einride.tech/can/pkg/dbc/analysis" +) + +func Analyzer() *analysis.Analyzer { + return &analysis.Analyzer{ + Name: "lineendings", + Doc: `check that the file does not contain Windows line-endings (\r\n)`, + Run: run, + } +} + +func run(pass *analysis.Pass) error { + if bytes.Contains(pass.File.Data, []byte{'\r', '\n'}) { + pass.Reportf( + scanner.Position{Filename: pass.File.Name, Line: 1, Column: 1}, + `file must not contain Windows line-endings (\r\n)`, + ) + } + return nil +} diff --git a/pkg/dbc/analysis/passes/lineendings/analyzer_test.go b/pkg/dbc/analysis/passes/lineendings/analyzer_test.go new file mode 100644 index 0000000..5a54112 --- /dev/null +++ b/pkg/dbc/analysis/passes/lineendings/analyzer_test.go @@ -0,0 +1,29 @@ +package lineendings + +import ( + "testing" + "text/scanner" + + "go.einride.tech/can/pkg/dbc/analysis" + "go.einride.tech/can/pkg/dbc/analysis/analysistest" +) + +func TestAnalyzer(t *testing.T) { + analysistest.Run(t, Analyzer(), []*analysistest.Case{ + { + Name: "ok", + Data: `NS_ :`, + }, + + { + Name: "not ok", + Data: "NS_ :\r\n", + Diagnostics: []*analysis.Diagnostic{ + { + Pos: scanner.Position{Line: 1, Column: 1}, + Message: `file must not contain Windows line-endings (\r\n)`, + }, + }, + }, + }) +} diff --git a/pkg/dbc/analysis/passes/messagenames/analyzer.go b/pkg/dbc/analysis/passes/messagenames/analyzer.go new file mode 100644 index 0000000..64b0440 --- /dev/null +++ b/pkg/dbc/analysis/passes/messagenames/analyzer.go @@ -0,0 +1,28 @@ +package messagenames + +import ( + "go.einride.tech/can/internal/identifiers" + "go.einride.tech/can/pkg/dbc" + "go.einride.tech/can/pkg/dbc/analysis" +) + +func Analyzer() *analysis.Analyzer { + return &analysis.Analyzer{ + Name: "messagenames", + Doc: "check that message names are valid CamelCase identifiers", + Run: run, + } +} + +func run(pass *analysis.Pass) error { + for _, def := range pass.File.Defs { + messageDef, ok := def.(*dbc.MessageDef) + if !ok { + continue // not a message + } + if !identifiers.IsCamelCase(string(messageDef.Name)) { + pass.Reportf(messageDef.Pos, "message names must be CamelCase") + } + } + return nil +} diff --git a/pkg/dbc/analysis/passes/messagenames/analyzer_test.go b/pkg/dbc/analysis/passes/messagenames/analyzer_test.go new file mode 100644 index 0000000..144873f --- /dev/null +++ b/pkg/dbc/analysis/passes/messagenames/analyzer_test.go @@ -0,0 +1,29 @@ +package messagenames + +import ( + "testing" + "text/scanner" + + "go.einride.tech/can/pkg/dbc/analysis" + "go.einride.tech/can/pkg/dbc/analysis/analysistest" +) + +func TestAnalyzer(t *testing.T) { + analysistest.Run(t, Analyzer(), []*analysistest.Case{ + { + Name: "ok", + Data: `BO_ 100 DriverHeartbeat: 1 DRIVER`, + }, + + { + Name: "not ok", + Data: `BO_ 100 DRIVER_HEARTBEAT: 1 DRIVER`, + Diagnostics: []*analysis.Diagnostic{ + { + Pos: scanner.Position{Line: 1, Column: 1}, + Message: "message names must be CamelCase", + }, + }, + }, + }) +} diff --git a/pkg/dbc/analysis/passes/multiplexedsignals/analyzer.go b/pkg/dbc/analysis/passes/multiplexedsignals/analyzer.go new file mode 100644 index 0000000..7f26f72 --- /dev/null +++ b/pkg/dbc/analysis/passes/multiplexedsignals/analyzer.go @@ -0,0 +1,59 @@ +package multiplexedsignals + +import ( + "go.einride.tech/can/pkg/dbc" + "go.einride.tech/can/pkg/dbc/analysis" +) + +func Analyzer() *analysis.Analyzer { + return &analysis.Analyzer{ + Name: "multiplexedsignals", + Doc: "check that multiplexed signals are valid", + Run: run, + } +} + +func run(pass *analysis.Pass) error { + for _, def := range pass.File.Defs { + message, ok := def.(*dbc.MessageDef) + if !ok { + continue + } + // locate multiplexer switch + var multiplexerSwitch *dbc.SignalDef + for i := range message.Signals { + if !message.Signals[i].IsMultiplexerSwitch { + continue + } + if multiplexerSwitch != nil { + pass.Reportf(message.Signals[i].Pos, "more than one multiplexer switch") + continue + } + multiplexerSwitch = &message.Signals[i] + if multiplexerSwitch.IsSigned { + pass.Reportf(message.Signals[i].Pos, "signed multiplexer switch") + continue + } + if multiplexerSwitch.IsMultiplexed { + pass.Reportf(message.Signals[i].Pos, "can't be multiplexer and multiplexed") + continue + } + } + for i := range message.Signals { + signal := &message.Signals[i] + if !signal.IsMultiplexed { + continue + } + if multiplexerSwitch == nil { + pass.Reportf(message.Signals[i].Pos, "no multiplexer switch for multiplexed signal") + continue + } + multiplexerSwitchMaxValue := uint64((1 << multiplexerSwitch.Size) - 1) + if signal.MultiplexerSwitch > multiplexerSwitchMaxValue { + pass.Reportf(signal.Pos, "multiplexer switch exceeds max value: %v", multiplexerSwitchMaxValue) + continue + } + } + } + return nil +} diff --git a/pkg/dbc/analysis/passes/multiplexedsignals/analyzer_test.go b/pkg/dbc/analysis/passes/multiplexedsignals/analyzer_test.go new file mode 100644 index 0000000..b1c2700 --- /dev/null +++ b/pkg/dbc/analysis/passes/multiplexedsignals/analyzer_test.go @@ -0,0 +1,114 @@ +package multiplexedsignals + +import ( + "testing" + "text/scanner" + + "go.einride.tech/can/pkg/dbc/analysis" + "go.einride.tech/can/pkg/dbc/analysis/analysistest" +) + +func TestAnalyzer(t *testing.T) { + analysistest.Run(t, Analyzer(), []*analysistest.Case{ + { + Name: "valid", + Data: ` +BO_ 200 SENSOR_SONARS: 8 SENSOR + SG_ SENSOR_SONARS_mux M : 0|4@1+ (1,0) [0|0] "" DRIVER,IO + SG_ SENSOR_SONARS_err_count : 4|12@1+ (1,0) [0|0] "" DRIVER,IO + SG_ SENSOR_SONARS_left m0 : 16|12@1+ (0.1,0) [0|0] "" DRIVER,IO + SG_ SENSOR_SONARS_middle m0 : 28|12@1+ (0.1,0) [0|0] "" DRIVER,IO + SG_ SENSOR_SONARS_right m0 : 40|12@1+ (0.1,0) [0|0] "" DRIVER,IO + SG_ SENSOR_SONARS_rear m0 : 52|12@1+ (0.1,0) [0|0] "" DRIVER,IO + SG_ SENSOR_SONARS_no_filt_left m1 : 16|12@1+ (0.1,0) [0|0] "" DBG + SG_ SENSOR_SONARS_no_filt_middle m1 : 28|12@1+ (0.1,0) [0|0] "" DBG + SG_ SENSOR_SONARS_no_filt_right m1 : 40|12@1+ (0.1,0) [0|0] "" DBG + SG_ SENSOR_SONARS_no_filt_rear m1 : 52|12@1+ (0.1,0) [0|0] "" DBG + `, + }, + + { + Name: "multiple multiplexer switches", + Data: ` +BO_ 200 SENSOR_SONARS: 8 SENSOR + SG_ SENSOR_SONARS_mux M : 0|4@1+ (1,0) [0|0] "" DRIVER,IO + SG_ SENSOR_SONARS_err_count M : 4|12@1+ (1,0) [0|0] "" DRIVER,IO + SG_ SENSOR_SONARS_left m0 : 16|12@1+ (0.1,0) [0|0] "" DRIVER,IO + SG_ SENSOR_SONARS_middle m0 : 28|12@1+ (0.1,0) [0|0] "" DRIVER,IO + SG_ SENSOR_SONARS_right m0 : 40|12@1+ (0.1,0) [0|0] "" DRIVER,IO + SG_ SENSOR_SONARS_rear m0 : 52|12@1+ (0.1,0) [0|0] "" DRIVER,IO + SG_ SENSOR_SONARS_no_filt_left m1 : 16|12@1+ (0.1,0) [0|0] "" DBG + SG_ SENSOR_SONARS_no_filt_middle m1 : 28|12@1+ (0.1,0) [0|0] "" DBG + SG_ SENSOR_SONARS_no_filt_right m1 : 40|12@1+ (0.1,0) [0|0] "" DBG + SG_ SENSOR_SONARS_no_filt_rear m1 : 52|12@1+ (0.1,0) [0|0] "" DBG + `, + Diagnostics: []*analysis.Diagnostic{ + { + Pos: scanner.Position{Line: 3, Column: 2}, + Message: "more than one multiplexer switch", + }, + }, + }, + + { + Name: "signed multiplexer switch", + Data: ` +BO_ 200 SENSOR_SONARS: 8 SENSOR + SG_ SENSOR_SONARS_mux M : 0|4@1- (1,0) [0|0] "" DRIVER,IO + SG_ SENSOR_SONARS_err_count : 4|12@1+ (1,0) [0|0] "" DRIVER,IO + SG_ SENSOR_SONARS_left m0 : 16|12@1+ (0.1,0) [0|0] "" DRIVER,IO + SG_ SENSOR_SONARS_middle m0 : 28|12@1+ (0.1,0) [0|0] "" DRIVER,IO + SG_ SENSOR_SONARS_right m0 : 40|12@1+ (0.1,0) [0|0] "" DRIVER,IO + SG_ SENSOR_SONARS_rear m0 : 52|12@1+ (0.1,0) [0|0] "" DRIVER,IO + SG_ SENSOR_SONARS_no_filt_left m1 : 16|12@1+ (0.1,0) [0|0] "" DBG + SG_ SENSOR_SONARS_no_filt_middle m1 : 28|12@1+ (0.1,0) [0|0] "" DBG + SG_ SENSOR_SONARS_no_filt_right m1 : 40|12@1+ (0.1,0) [0|0] "" DBG + SG_ SENSOR_SONARS_no_filt_rear m1 : 52|12@1+ (0.1,0) [0|0] "" DBG + `, + Diagnostics: []*analysis.Diagnostic{ + { + Pos: scanner.Position{Line: 2, Column: 2}, + Message: "signed multiplexer switch", + }, + }, + }, + + { + Name: "no multiplexer switch", + Data: ` +BO_ 200 SENSOR_SONARS: 8 SENSOR + SG_ SENSOR_SONARS_err_count : 4|12@1+ (1,0) [0|0] "" DRIVER,IO + SG_ SENSOR_SONARS_left m0 : 16|12@1+ (0.1,0) [0|0] "" DRIVER,IO + `, + Diagnostics: []*analysis.Diagnostic{ + { + Pos: scanner.Position{Line: 3, Column: 2}, + Message: "no multiplexer switch for multiplexed signal", + }, + }, + }, + + { + Name: "too big multiplexer switch", + Data: ` +BO_ 200 SENSOR_SONARS: 8 SENSOR + SG_ SENSOR_SONARS_mux M : 0|4@1+ (1,0) [0|0] "" DRIVER,IO + SG_ SENSOR_SONARS_err_count : 4|12@1+ (1,0) [0|0] "" DRIVER,IO + SG_ SENSOR_SONARS_left m16 : 16|12@1+ (0.1,0) [0|0] "" DRIVER,IO + SG_ SENSOR_SONARS_middle m0 : 28|12@1+ (0.1,0) [0|0] "" DRIVER,IO + SG_ SENSOR_SONARS_right m0 : 40|12@1+ (0.1,0) [0|0] "" DRIVER,IO + SG_ SENSOR_SONARS_rear m0 : 52|12@1+ (0.1,0) [0|0] "" DRIVER,IO + SG_ SENSOR_SONARS_no_filt_left m1 : 16|12@1+ (0.1,0) [0|0] "" DBG + SG_ SENSOR_SONARS_no_filt_middle m1 : 28|12@1+ (0.1,0) [0|0] "" DBG + SG_ SENSOR_SONARS_no_filt_right m1 : 40|12@1+ (0.1,0) [0|0] "" DBG + SG_ SENSOR_SONARS_no_filt_rear m1 : 52|12@1+ (0.1,0) [0|0] "" DBG + `, + Diagnostics: []*analysis.Diagnostic{ + { + Pos: scanner.Position{Line: 4, Column: 2}, + Message: "multiplexer switch exceeds max value: 15", + }, + }, + }, + }) +} diff --git a/pkg/dbc/analysis/passes/newsymbols/analyzer.go b/pkg/dbc/analysis/passes/newsymbols/analyzer.go new file mode 100644 index 0000000..252a351 --- /dev/null +++ b/pkg/dbc/analysis/passes/newsymbols/analyzer.go @@ -0,0 +1,27 @@ +package newsymbols + +import ( + "go.einride.tech/can/pkg/dbc" + "go.einride.tech/can/pkg/dbc/analysis" +) + +func Analyzer() *analysis.Analyzer { + return &analysis.Analyzer{ + Name: "newsymbols", + Doc: "check that the new symbols definition is empty", + Run: run, + } +} + +func run(pass *analysis.Pass) error { + for _, def := range pass.File.Defs { + newSymbolsDef, ok := def.(*dbc.NewSymbolsDef) + if !ok { + continue // not a new symbols definition + } + if len(newSymbolsDef.Symbols) > 0 { + pass.Reportf(newSymbolsDef.Pos, "new symbols should be empty") + } + } + return nil +} diff --git a/pkg/dbc/analysis/passes/newsymbols/analyzer_test.go b/pkg/dbc/analysis/passes/newsymbols/analyzer_test.go new file mode 100644 index 0000000..412fc58 --- /dev/null +++ b/pkg/dbc/analysis/passes/newsymbols/analyzer_test.go @@ -0,0 +1,32 @@ +package newsymbols + +import ( + "testing" + "text/scanner" + + "go.einride.tech/can/pkg/dbc/analysis" + "go.einride.tech/can/pkg/dbc/analysis/analysistest" +) + +func TestAnalyzer(t *testing.T) { + analysistest.Run(t, Analyzer(), []*analysistest.Case{ + { + Name: "ok", + Data: `NS_ :`, + }, + + { + Name: "not ok", + Data: ` +NS_ : + BA_DEF_DEF_REL_ + BA_DEF_SGTYPE_`, + Diagnostics: []*analysis.Diagnostic{ + { + Pos: scanner.Position{Line: 1, Column: 1}, + Message: "new symbols should be empty", + }, + }, + }, + }) +} diff --git a/pkg/dbc/analysis/passes/nodereferences/analyzer.go b/pkg/dbc/analysis/passes/nodereferences/analyzer.go new file mode 100644 index 0000000..0453851 --- /dev/null +++ b/pkg/dbc/analysis/passes/nodereferences/analyzer.go @@ -0,0 +1,58 @@ +package nodereferences + +import ( + "go.einride.tech/can/pkg/dbc" + "go.einride.tech/can/pkg/dbc/analysis" +) + +func Analyzer() *analysis.Analyzer { + return &analysis.Analyzer{ + Name: "nodereferences", + Doc: "check that all node references refer to declared nodes", + Run: run, + } +} + +func run(pass *analysis.Pass) error { + declaredNodes := map[dbc.Identifier]struct{}{ + dbc.NodePlaceholder: {}, // placeholder is implicitly declared + } + // collect declared nodes + for _, def := range pass.File.Defs { + if def, ok := def.(*dbc.NodesDef); ok { + for _, nodeName := range def.NodeNames { + declaredNodes[nodeName] = struct{}{} + } + } + } + // verify node references + for _, def := range pass.File.Defs { + switch def := def.(type) { + case *dbc.MessageDef: + if _, ok := declaredNodes[def.Transmitter]; !ok { + pass.Reportf(def.Pos, "undeclared transmitter node: %v", def.Transmitter) + } + for i := range def.Signals { + signal := &def.Signals[i] + for _, receiver := range signal.Receivers { + if _, ok := declaredNodes[receiver]; !ok { + pass.Reportf(signal.Pos, "undeclared receiver node: %v", receiver) + } + } + } + case *dbc.EnvironmentVariableDef: + for _, accessNode := range def.AccessNodes { + if _, ok := declaredNodes[accessNode]; !ok { + pass.Reportf(def.Pos, "undeclared access node: %v", accessNode) + } + } + case *dbc.MessageTransmittersDef: + for _, transmitter := range def.Transmitters { + if _, ok := declaredNodes[transmitter]; !ok { + pass.Reportf(def.Pos, "undeclared transmitter node: %v", transmitter) + } + } + } + } + return nil +} diff --git a/pkg/dbc/analysis/passes/nodereferences/analyzer_test.go b/pkg/dbc/analysis/passes/nodereferences/analyzer_test.go new file mode 100644 index 0000000..ea7fd6f --- /dev/null +++ b/pkg/dbc/analysis/passes/nodereferences/analyzer_test.go @@ -0,0 +1,52 @@ +package nodereferences + +import ( + "testing" + "text/scanner" + + "go.einride.tech/can/pkg/dbc/analysis" + "go.einride.tech/can/pkg/dbc/analysis/analysistest" +) + +func TestAnalyzer(t *testing.T) { + analysistest.Run(t, Analyzer(), []*analysistest.Case{ + { + Name: "valid", + Data: ` +BU_: ECU1 ECU2 +BO_ 42 TestMessage: 8 ECU2 + SG_ CellTempLowest : 32|8@0+ (1,-40) [-40|215] "C" ECU1 + `, + }, + + { + Name: "undeclared transmitter", + Data: ` +BU_: ECU1 ECU2 +BO_ 42 TestMessage: 8 ECU3 + SG_ CellTempLowest : 32|8@0+ (1,-40) [-40|215] "C" ECU1 + `, + Diagnostics: []*analysis.Diagnostic{ + { + Pos: scanner.Position{Line: 2, Column: 1}, + Message: "undeclared transmitter node: ECU3", + }, + }, + }, + + { + Name: "undeclared receiver", + Data: ` +BU_: ECU1 ECU2 +BO_ 42 TestMessage: 8 ECU2 + SG_ CellTempLowest : 32|8@0+ (1,-40) [-40|215] "C" ECU2,ECU3 + `, + Diagnostics: []*analysis.Diagnostic{ + { + Pos: scanner.Position{Line: 3, Column: 2}, + Message: "undeclared receiver node: ECU3", + }, + }, + }, + }) +} diff --git a/pkg/dbc/analysis/passes/noreservedsignals/analyzer.go b/pkg/dbc/analysis/passes/noreservedsignals/analyzer.go new file mode 100644 index 0000000..fe65850 --- /dev/null +++ b/pkg/dbc/analysis/passes/noreservedsignals/analyzer.go @@ -0,0 +1,31 @@ +package noreservedsignals + +import ( + "strings" + + "go.einride.tech/can/pkg/dbc" + "go.einride.tech/can/pkg/dbc/analysis" +) + +func Analyzer() *analysis.Analyzer { + return &analysis.Analyzer{ + Name: "noreservedsignals", + Doc: `checks that no signals have the prefix "Reserved"`, + Run: run, + } +} + +func run(pass *analysis.Pass) error { + for _, d := range pass.File.Defs { + messageDef, ok := d.(*dbc.MessageDef) + if !ok { + continue + } + for _, signalDef := range messageDef.Signals { + if strings.HasPrefix(string(signalDef.Name), "Reserved") { + pass.Reportf(signalDef.Pos, "remove reserved signals") + } + } + } + return nil +} diff --git a/pkg/dbc/analysis/passes/noreservedsignals/analyzer_test.go b/pkg/dbc/analysis/passes/noreservedsignals/analyzer_test.go new file mode 100644 index 0000000..b31afac --- /dev/null +++ b/pkg/dbc/analysis/passes/noreservedsignals/analyzer_test.go @@ -0,0 +1,35 @@ +package noreservedsignals + +import ( + "testing" + "text/scanner" + + "go.einride.tech/can/pkg/dbc/analysis" + "go.einride.tech/can/pkg/dbc/analysis/analysistest" +) + +func TestAnalyzer(t *testing.T) { + analysistest.Run(t, Analyzer(), []*analysistest.Case{ + { + Name: "ok", + Data: ` +BO_ 400 MotorStatus: 3 MOTOR + SG_ HasWheelError : 0|1@1+ (1,0) [0|0] "" DRIVER,IO +`, + }, + + { + Name: "not ok", + Data: ` +BO_ 400 MotorStatus: 3 MOTOR + SG_ Reserved1 : 0|1@1+ (1,0) [0|0] "" DRIVER,IO +`, + Diagnostics: []*analysis.Diagnostic{ + { + Pos: scanner.Position{Line: 2, Column: 2}, + Message: "remove reserved signals", + }, + }, + }, + }) +} diff --git a/pkg/dbc/analysis/passes/requireddefinitions/analyzer.go b/pkg/dbc/analysis/passes/requireddefinitions/analyzer.go new file mode 100644 index 0000000..fd4c136 --- /dev/null +++ b/pkg/dbc/analysis/passes/requireddefinitions/analyzer.go @@ -0,0 +1,38 @@ +package requireddefinitions + +import ( + "reflect" + + "go.einride.tech/can/pkg/dbc" + "go.einride.tech/can/pkg/dbc/analysis" +) + +func Analyzer() *analysis.Analyzer { + return &analysis.Analyzer{ + Name: "requireddefinitions", + Doc: "check that the file contains exactly one of all required definitions", + Run: run, + } +} + +func requiredDefinitions() []dbc.Def { + return []dbc.Def{ + &dbc.BitTimingDef{}, + &dbc.NodesDef{}, + } +} + +func run(pass *analysis.Pass) error { + counts := make(map[reflect.Type]int) + for _, def := range pass.File.Defs { + counts[reflect.TypeOf(def)]++ + } + for _, requiredDef := range requiredDefinitions() { + if counts[reflect.TypeOf(requiredDef)] == 0 { + // we have no definition to return, so return the first + pass.Reportf(pass.File.Defs[0].Position(), "missing required definition(s)") + break + } + } + return nil +} diff --git a/pkg/dbc/analysis/passes/requireddefinitions/analyzer_test.go b/pkg/dbc/analysis/passes/requireddefinitions/analyzer_test.go new file mode 100644 index 0000000..34b911e --- /dev/null +++ b/pkg/dbc/analysis/passes/requireddefinitions/analyzer_test.go @@ -0,0 +1,47 @@ +package requireddefinitions + +import ( + "testing" + "text/scanner" + + "go.einride.tech/can/pkg/dbc/analysis" + "go.einride.tech/can/pkg/dbc/analysis/analysistest" +) + +func TestAnalyzer(t *testing.T) { + analysistest.Run(t, Analyzer(), []*analysistest.Case{ + { + Name: "ok", + Data: ` +BS_: +BU_: ECU1 + `, + }, + + { + Name: "missing bit timing", + Data: ` +BU_: ECU1 + `, + Diagnostics: []*analysis.Diagnostic{ + { + Pos: scanner.Position{Line: 1, Column: 1}, + Message: "missing required definition(s)", + }, + }, + }, + + { + Name: "missing nodes", + Data: ` +BS_: + `, + Diagnostics: []*analysis.Diagnostic{ + { + Pos: scanner.Position{Line: 1, Column: 1}, + Message: "missing required definition(s)", + }, + }, + }, + }) +} diff --git a/pkg/dbc/analysis/passes/signalbounds/analyzer.go b/pkg/dbc/analysis/passes/signalbounds/analyzer.go new file mode 100644 index 0000000..b971fcb --- /dev/null +++ b/pkg/dbc/analysis/passes/signalbounds/analyzer.go @@ -0,0 +1,31 @@ +package signalbounds + +import ( + "go.einride.tech/can/pkg/dbc" + "go.einride.tech/can/pkg/dbc/analysis" +) + +func Analyzer() *analysis.Analyzer { + return &analysis.Analyzer{ + Name: "signalbounds", + Doc: "check that signal start and end bits are within bounds of the message size", + Run: run, + } +} + +func run(pass *analysis.Pass) error { + for _, def := range pass.File.Defs { + message, ok := def.(*dbc.MessageDef) + if !ok || dbc.IsIndependentSignalsMessage(message) { + continue + } + for i := range message.Signals { + signal := &message.Signals[i] + if signal.StartBit >= 8*message.Size { + pass.Reportf(signal.Pos, "start bit out of bounds") + } + // TODO: Check end bit + } + } + return nil +} diff --git a/pkg/dbc/analysis/passes/signalbounds/analyzer_test.go b/pkg/dbc/analysis/passes/signalbounds/analyzer_test.go new file mode 100644 index 0000000..28e05ca --- /dev/null +++ b/pkg/dbc/analysis/passes/signalbounds/analyzer_test.go @@ -0,0 +1,41 @@ +package signalbounds + +import ( + "testing" + "text/scanner" + + "go.einride.tech/can/pkg/dbc/analysis" + "go.einride.tech/can/pkg/dbc/analysis/analysistest" +) + +func TestAnalyzer(t *testing.T) { + analysistest.Run(t, Analyzer(), []*analysistest.Case{ + { + Name: "ok", + Data: ` +BO_ 500 IO_DEBUG: 4 IO + SG_ IO_DEBUG_test_unsigned : 0|8@1+ (1,0) [0|0] "" DBG + SG_ IO_DEBUG_test_enum : 8|8@1+ (1,0) [0|0] "" DBG + SG_ IO_DEBUG_test_signed : 16|8@1- (1,0) [0|0] "" DBG + SG_ IO_DEBUG_test_float : 24|8@1+ (0.5,0) [0|0] "" DBG +`, + }, + + { + Name: "start bit out of bounds", + Data: ` +BO_ 500 IO_DEBUG: 4 IO + SG_ IO_DEBUG_test_unsigned : 0|8@1+ (1,0) [0|0] "" DBG + SG_ IO_DEBUG_test_enum : 8|8@1+ (1,0) [0|0] "" DBG + SG_ IO_DEBUG_test_signed : 16|8@1- (1,0) [0|0] "" DBG + SG_ IO_DEBUG_test_float : 32|8@1+ (0.5,0) [0|0] "" DBG +`, + Diagnostics: []*analysis.Diagnostic{ + { + Pos: scanner.Position{Line: 5, Column: 2}, + Message: "start bit out of bounds", + }, + }, + }, + }) +} diff --git a/pkg/dbc/analysis/passes/signalnames/analyzer.go b/pkg/dbc/analysis/passes/signalnames/analyzer.go new file mode 100644 index 0000000..1e7797e --- /dev/null +++ b/pkg/dbc/analysis/passes/signalnames/analyzer.go @@ -0,0 +1,30 @@ +package signalnames + +import ( + "go.einride.tech/can/internal/identifiers" + "go.einride.tech/can/pkg/dbc" + "go.einride.tech/can/pkg/dbc/analysis" +) + +func Analyzer() *analysis.Analyzer { + return &analysis.Analyzer{ + Name: "signalnames", + Doc: "check that signal names are valid CamelCase identifiers", + Run: run, + } +} + +func run(pass *analysis.Pass) error { + for _, d := range pass.File.Defs { + messageDef, ok := d.(*dbc.MessageDef) + if !ok { + continue + } + for _, signalDef := range messageDef.Signals { + if !identifiers.IsCamelCase(string(signalDef.Name)) { + pass.Reportf(signalDef.Pos, "signal names must be CamelCase") + } + } + } + return nil +} diff --git a/pkg/dbc/analysis/passes/signalnames/analyzer_test.go b/pkg/dbc/analysis/passes/signalnames/analyzer_test.go new file mode 100644 index 0000000..d36d1d0 --- /dev/null +++ b/pkg/dbc/analysis/passes/signalnames/analyzer_test.go @@ -0,0 +1,35 @@ +package signalnames + +import ( + "testing" + "text/scanner" + + "go.einride.tech/can/pkg/dbc/analysis" + "go.einride.tech/can/pkg/dbc/analysis/analysistest" +) + +func TestAnalyzer(t *testing.T) { + analysistest.Run(t, Analyzer(), []*analysistest.Case{ + { + Name: "ok", + Data: ` +BO_ 400 MotorStatus: 3 MOTOR + SG_ HasWheelError : 0|1@1+ (1,0) [0|0] "" DRIVER,IO +`, + }, + + { + Name: "not ok", + Data: ` +BO_ 400 MOTOR_STATUS: 3 MOTOR + SG_ IS_OVERHEATED : 0|1@1+ (1,0) [0|0] "" DRIVER,IO +`, + Diagnostics: []*analysis.Diagnostic{ + { + Pos: scanner.Position{Line: 2, Column: 2}, + Message: "signal names must be CamelCase", + }, + }, + }, + }) +} diff --git a/pkg/dbc/analysis/passes/singletondefinitions/analyzer.go b/pkg/dbc/analysis/passes/singletondefinitions/analyzer.go new file mode 100644 index 0000000..c66b275 --- /dev/null +++ b/pkg/dbc/analysis/passes/singletondefinitions/analyzer.go @@ -0,0 +1,42 @@ +package singletondefinitions + +import ( + "reflect" + + "go.einride.tech/can/pkg/dbc" + "go.einride.tech/can/pkg/dbc/analysis" +) + +func Analyzer() *analysis.Analyzer { + return &analysis.Analyzer{ + Name: "singletondefinitions", + Doc: "check that the file contains at most one of all singleton definitions", + Run: run, + } +} + +func singletonDefinitions() []dbc.Def { + return []dbc.Def{ + &dbc.VersionDef{}, + &dbc.NewSymbolsDef{}, + &dbc.BitTimingDef{}, + &dbc.NodesDef{}, + } +} + +func run(pass *analysis.Pass) error { + defsByType := make(map[reflect.Type][]dbc.Def) + for _, def := range pass.File.Defs { + t := reflect.TypeOf(def) + defsByType[t] = append(defsByType[t], def) + } + for _, singletonDef := range singletonDefinitions() { + singletonDefs := defsByType[reflect.TypeOf(singletonDef)] + if len(singletonDefs) > 1 { + for i := 1; i < len(singletonDefs); i++ { + pass.Reportf(singletonDefs[i].Position(), "more than one definition not allowed") + } + } + } + return nil +} diff --git a/pkg/dbc/analysis/passes/singletondefinitions/analyzer_test.go b/pkg/dbc/analysis/passes/singletondefinitions/analyzer_test.go new file mode 100644 index 0000000..87d5afc --- /dev/null +++ b/pkg/dbc/analysis/passes/singletondefinitions/analyzer_test.go @@ -0,0 +1,91 @@ +package singletondefinitions + +import ( + "testing" + "text/scanner" + + "go.einride.tech/can/pkg/dbc/analysis" + "go.einride.tech/can/pkg/dbc/analysis/analysistest" +) + +func TestAnalyzer(t *testing.T) { + analysistest.Run(t, Analyzer(), []*analysistest.Case{ + { + Name: "ok", + Data: ` +VERSION "foo" +NS_: +BS_: +BU_: ECU1 + `, + }, + + { + Name: "multiple versions", + Data: ` +VERSION "foo" +VERSION "foo" +NS_: +BS_: +BU_: ECU1 + `, + Diagnostics: []*analysis.Diagnostic{ + { + Pos: scanner.Position{Line: 2, Column: 1}, + Message: "more than one definition not allowed", + }, + }, + }, + + { + Name: "multiple new symbols", + Data: ` +VERSION "foo" +NS_: +NS_: +BS_: +BU_: ECU1 + `, + Diagnostics: []*analysis.Diagnostic{ + { + Pos: scanner.Position{Line: 3, Column: 1}, + Message: "more than one definition not allowed", + }, + }, + }, + + { + Name: "multiple bit timing", + Data: ` +VERSION "foo" +NS_: +BS_: +BS_: +BU_: ECU1 + `, + Diagnostics: []*analysis.Diagnostic{ + { + Pos: scanner.Position{Line: 4, Column: 1}, + Message: "more than one definition not allowed", + }, + }, + }, + + { + Name: "multiple nodes", + Data: ` +VERSION "foo" +NS_: +BS_: +BU_: ECU1 +BU_: ECU2 + `, + Diagnostics: []*analysis.Diagnostic{ + { + Pos: scanner.Position{Line: 5, Column: 1}, + Message: "more than one definition not allowed", + }, + }, + }, + }) +} diff --git a/pkg/dbc/analysis/passes/siunits/analyzer.go b/pkg/dbc/analysis/passes/siunits/analyzer.go new file mode 100644 index 0000000..6bf1293 --- /dev/null +++ b/pkg/dbc/analysis/passes/siunits/analyzer.go @@ -0,0 +1,51 @@ +package siunits + +import ( + "go.einride.tech/can/pkg/dbc" + "go.einride.tech/can/pkg/dbc/analysis" +) + +func Analyzer() *analysis.Analyzer { + return &analysis.Analyzer{ + Name: "unitsuffixes", + Doc: "check that signals with SI units have the correct symbols", + Run: run, + } +} + +const ( + metersPerSecond = "m/s" + kilometersPerHour = "km/h" + meters = "m" + degrees = "°" + radians = "rad" +) + +// symbolMap returns a map from non-standard unit symbols to SI unit symbols. +func symbolMap() map[string]string { + return map[string]string{ + "kph": kilometersPerHour, + "mps": metersPerSecond, + "meters/sec": metersPerSecond, + "meters": meters, + "deg": degrees, + "degrees": degrees, + "radians": radians, + } +} + +func run(pass *analysis.Pass) error { + symbols := symbolMap() + for _, def := range pass.File.Defs { + message, ok := def.(*dbc.MessageDef) + if !ok { + continue + } + for _, signal := range message.Signals { + if symbol, ok := symbols[signal.Unit]; ok { + pass.Reportf(signal.Pos, "signal with unit %s should have SI unit %s", signal.Unit, symbol) + } + } + } + return nil +} diff --git a/pkg/dbc/analysis/passes/siunits/analyzer_test.go b/pkg/dbc/analysis/passes/siunits/analyzer_test.go new file mode 100644 index 0000000..663bb9d --- /dev/null +++ b/pkg/dbc/analysis/passes/siunits/analyzer_test.go @@ -0,0 +1,35 @@ +package siunits + +import ( + "testing" + "text/scanner" + + "go.einride.tech/can/pkg/dbc/analysis" + "go.einride.tech/can/pkg/dbc/analysis/analysistest" +) + +func TestAnalyzer(t *testing.T) { + analysistest.Run(t, Analyzer(), []*analysistest.Case{ + { + Name: "ok", + Data: ` +BO_ 400 TestMessage: 3 ECU1 + SG_ SpeedMps : 0|1@1+ (1,0) [0|0] "m/s" DRIVER,IO +`, + }, + + { + Name: "not ok", + Data: ` +BO_ 400 TestMessage: 3 ECU1 + SG_ SpeedMps : 0|1@1+ (1,0) [0|0] "meters/sec" DRIVER,IO +`, + Diagnostics: []*analysis.Diagnostic{ + { + Pos: scanner.Position{Line: 2, Column: 2}, + Message: "signal with unit meters/sec should have SI unit m/s", + }, + }, + }, + }) +} diff --git a/pkg/dbc/analysis/passes/uniquenodenames/analyzer.go b/pkg/dbc/analysis/passes/uniquenodenames/analyzer.go new file mode 100644 index 0000000..b14404b --- /dev/null +++ b/pkg/dbc/analysis/passes/uniquenodenames/analyzer.go @@ -0,0 +1,29 @@ +package uniquenodenames + +import ( + "go.einride.tech/can/pkg/dbc" + "go.einride.tech/can/pkg/dbc/analysis" +) + +func Analyzer() *analysis.Analyzer { + return &analysis.Analyzer{ + Name: "uniquenodenames", + Doc: "check that all declared node names are unique", + Run: run, + } +} + +func run(pass *analysis.Pass) error { + nodeNames := make(map[dbc.Identifier]struct{}) + for _, def := range pass.File.Defs { + if def, ok := def.(*dbc.NodesDef); ok { + for _, nodeName := range def.NodeNames { + if _, ok := nodeNames[nodeName]; ok { + pass.Reportf(def.Pos, "non-unique node name") + } + nodeNames[nodeName] = struct{}{} + } + } + } + return nil +} diff --git a/pkg/dbc/analysis/passes/uniquenodenames/analyzer_test.go b/pkg/dbc/analysis/passes/uniquenodenames/analyzer_test.go new file mode 100644 index 0000000..70d549b --- /dev/null +++ b/pkg/dbc/analysis/passes/uniquenodenames/analyzer_test.go @@ -0,0 +1,29 @@ +package uniquenodenames + +import ( + "testing" + "text/scanner" + + "go.einride.tech/can/pkg/dbc/analysis" + "go.einride.tech/can/pkg/dbc/analysis/analysistest" +) + +func TestAnalyzer(t *testing.T) { + analysistest.Run(t, Analyzer(), []*analysistest.Case{ + { + Name: "ok", + Data: `BU_: ECU1 ECU2 ECU3`, + }, + + { + Name: "duplicates", + Data: `BU_: ECU1 ECU2 ECU3 ECU1`, + Diagnostics: []*analysis.Diagnostic{ + { + Pos: scanner.Position{Line: 1, Column: 1}, + Message: "non-unique node name", + }, + }, + }, + }) +} diff --git a/pkg/dbc/analysis/passes/uniquesignalnames/analyzer.go b/pkg/dbc/analysis/passes/uniquesignalnames/analyzer.go new file mode 100644 index 0000000..954213e --- /dev/null +++ b/pkg/dbc/analysis/passes/uniquesignalnames/analyzer.go @@ -0,0 +1,33 @@ +package uniquesignalnames + +import ( + "go.einride.tech/can/pkg/dbc" + "go.einride.tech/can/pkg/dbc/analysis" +) + +func Analyzer() *analysis.Analyzer { + return &analysis.Analyzer{ + Name: "uniquesignalnames", + Doc: "check that all signal names are unique", + Run: run, + } +} + +func run(pass *analysis.Pass) error { + for _, def := range pass.File.Defs { + message, ok := def.(*dbc.MessageDef) + if !ok || dbc.IsIndependentSignalsMessage(message) { + continue + } + signalNames := make(map[dbc.Identifier]struct{}) + for i := range message.Signals { + signal := &message.Signals[i] + if _, ok := signalNames[signal.Name]; ok { + pass.Reportf(signal.Pos, "non-unique signal name") + } else { + signalNames[signal.Name] = struct{}{} + } + } + } + return nil +} diff --git a/pkg/dbc/analysis/passes/uniquesignalnames/analyzer_test.go b/pkg/dbc/analysis/passes/uniquesignalnames/analyzer_test.go new file mode 100644 index 0000000..ea4edd6 --- /dev/null +++ b/pkg/dbc/analysis/passes/uniquesignalnames/analyzer_test.go @@ -0,0 +1,37 @@ +package uniquesignalnames + +import ( + "testing" + "text/scanner" + + "go.einride.tech/can/pkg/dbc/analysis" + "go.einride.tech/can/pkg/dbc/analysis/analysistest" +) + +func TestAnalyzer(t *testing.T) { + analysistest.Run(t, Analyzer(), []*analysistest.Case{ + { + Name: "ok", + Data: ` +BO_ 101 MOTOR_CMD: 1 DRIVER + SG_ MOTOR_CMD_steer : 0|4@1- (1,-5) [-5|5] "" MOTOR + SG_ MOTOR_CMD_drive : 4|4@1+ (1,0) [0|9] "" MOTOR + `, + }, + + { + Name: "duplicate", + Data: ` +BO_ 101 MOTOR_CMD: 1 DRIVER + SG_ MOTOR_CMD_steer : 0|4@1- (1,-5) [-5|5] "" MOTOR + SG_ MOTOR_CMD_steer : 4|4@1+ (1,0) [0|9] "" MOTOR + `, + Diagnostics: []*analysis.Diagnostic{ + { + Pos: scanner.Position{Line: 3, Column: 2}, + Message: "non-unique signal name", + }, + }, + }, + }) +} diff --git a/pkg/dbc/analysis/passes/unitsuffixes/analyzer.go b/pkg/dbc/analysis/passes/unitsuffixes/analyzer.go new file mode 100644 index 0000000..5c3fc4b --- /dev/null +++ b/pkg/dbc/analysis/passes/unitsuffixes/analyzer.go @@ -0,0 +1,44 @@ +package unitsuffixes + +import ( + "strings" + + "go.einride.tech/can/pkg/dbc" + "go.einride.tech/can/pkg/dbc/analysis" +) + +func Analyzer() *analysis.Analyzer { + return &analysis.Analyzer{ + Name: "unitsuffixes", + Doc: "check that signals with units have correct name suffixes", + Run: run, + } +} + +func unitSuffixes() map[string]string { + return map[string]string{ + "°": "Degrees", + "rad": "Radians", + "%": "Percent", + "km/h": "Kph", + "m/s": "Mps", + } +} + +func run(pass *analysis.Pass) error { + suffixes := unitSuffixes() + for _, def := range pass.File.Defs { + message, ok := def.(*dbc.MessageDef) + if !ok { + continue + } + for _, signal := range message.Signals { + if suffix, ok := suffixes[signal.Unit]; ok { + if !strings.HasSuffix(string(signal.Name), suffix) { + pass.Reportf(signal.Pos, "signal with unit %s must have suffix %s", signal.Unit, suffix) + } + } + } + } + return nil +} diff --git a/pkg/dbc/analysis/passes/unitsuffixes/analyzer_test.go b/pkg/dbc/analysis/passes/unitsuffixes/analyzer_test.go new file mode 100644 index 0000000..a66112a --- /dev/null +++ b/pkg/dbc/analysis/passes/unitsuffixes/analyzer_test.go @@ -0,0 +1,35 @@ +package unitsuffixes + +import ( + "testing" + "text/scanner" + + "go.einride.tech/can/pkg/dbc/analysis" + "go.einride.tech/can/pkg/dbc/analysis/analysistest" +) + +func TestAnalyzer(t *testing.T) { + analysistest.Run(t, Analyzer(), []*analysistest.Case{ + { + Name: "ok", + Data: ` +BO_ 400 TestMessage: 3 ECU1 + SG_ ValuePercent : 0|1@1+ (1,0) [0|0] "%" DRIVER,IO +`, + }, + + { + Name: "not ok", + Data: ` +BO_ 400 TestMessage: 3 ECU1 + SG_ ValuePct : 0|1@1+ (1,0) [0|0] "%" DRIVER,IO +`, + Diagnostics: []*analysis.Diagnostic{ + { + Pos: scanner.Position{Line: 2, Column: 2}, + Message: "signal with unit % must have suffix Percent", + }, + }, + }, + }) +} diff --git a/pkg/dbc/analysis/passes/valuedescriptions/analyzer.go b/pkg/dbc/analysis/passes/valuedescriptions/analyzer.go new file mode 100644 index 0000000..888720c --- /dev/null +++ b/pkg/dbc/analysis/passes/valuedescriptions/analyzer.go @@ -0,0 +1,36 @@ +package valuedescriptions + +import ( + "go.einride.tech/can/internal/identifiers" + "go.einride.tech/can/pkg/dbc" + "go.einride.tech/can/pkg/dbc/analysis" +) + +func Analyzer() *analysis.Analyzer { + return &analysis.Analyzer{ + Name: "valuedescriptions", + Doc: "check that value descriptions are valid CamelCase", + Run: run, + } +} + +func run(pass *analysis.Pass) error { + for _, def := range pass.File.Defs { + var valueDescriptions []dbc.ValueDescriptionDef + switch def := def.(type) { + case *dbc.ValueTableDef: + valueDescriptions = def.ValueDescriptions + case *dbc.ValueDescriptionsDef: + valueDescriptions = def.ValueDescriptions + default: + continue + } + for _, vd := range valueDescriptions { + vd := vd + if !identifiers.IsCamelCase(vd.Description) { + pass.Reportf(vd.Pos, "value description must be CamelCase") + } + } + } + return nil +} diff --git a/pkg/dbc/analysis/passes/valuedescriptions/analyzer_test.go b/pkg/dbc/analysis/passes/valuedescriptions/analyzer_test.go new file mode 100644 index 0000000..6bcac94 --- /dev/null +++ b/pkg/dbc/analysis/passes/valuedescriptions/analyzer_test.go @@ -0,0 +1,29 @@ +package valuedescriptions + +import ( + "testing" + "text/scanner" + + "go.einride.tech/can/pkg/dbc/analysis" + "go.einride.tech/can/pkg/dbc/analysis/analysistest" +) + +func TestAnalyzer(t *testing.T) { + analysistest.Run(t, Analyzer(), []*analysistest.Case{ + { + Name: "ok", + Data: `VAL_ 100 Command 2 "Reboot" 1 "Sync" 0 "Noop";`, + }, + + { + Name: "underscore", + Data: `VAL_ 100 Command 2 "Reboot_Command" 1 "Sync" 0 "Noop";`, + Diagnostics: []*analysis.Diagnostic{ + { + Pos: scanner.Position{Line: 1, Column: 18}, + Message: "value description must be CamelCase", + }, + }, + }, + }) +} diff --git a/pkg/dbc/analysis/passes/version/analyzer.go b/pkg/dbc/analysis/passes/version/analyzer.go new file mode 100644 index 0000000..1f36f8e --- /dev/null +++ b/pkg/dbc/analysis/passes/version/analyzer.go @@ -0,0 +1,27 @@ +package version + +import ( + "go.einride.tech/can/pkg/dbc" + "go.einride.tech/can/pkg/dbc/analysis" +) + +func Analyzer() *analysis.Analyzer { + return &analysis.Analyzer{ + Name: "version", + Doc: "check that the version definition is empty", + Run: run, + } +} + +func run(pass *analysis.Pass) error { + for _, def := range pass.File.Defs { + versionDef, ok := def.(*dbc.VersionDef) + if !ok { + continue // not a version definition + } + if len(versionDef.Version) > 0 { + pass.Reportf(versionDef.Pos, "version should be empty") + } + } + return nil +} diff --git a/pkg/dbc/analysis/passes/version/analyzer_test.go b/pkg/dbc/analysis/passes/version/analyzer_test.go new file mode 100644 index 0000000..e71aa8f --- /dev/null +++ b/pkg/dbc/analysis/passes/version/analyzer_test.go @@ -0,0 +1,29 @@ +package version + +import ( + "testing" + "text/scanner" + + "go.einride.tech/can/pkg/dbc/analysis" + "go.einride.tech/can/pkg/dbc/analysis/analysistest" +) + +func TestAnalyzer(t *testing.T) { + analysistest.Run(t, Analyzer(), []*analysistest.Case{ + { + Name: "ok", + Data: `VERSION ""`, + }, + + { + Name: "not ok", + Data: `VERSION "foo"`, + Diagnostics: []*analysis.Diagnostic{ + { + Pos: scanner.Position{Line: 1, Column: 1}, + Message: "version should be empty", + }, + }, + }, + }) +} diff --git a/pkg/dbc/attributevaluetype.go b/pkg/dbc/attributevaluetype.go new file mode 100644 index 0000000..b55f613 --- /dev/null +++ b/pkg/dbc/attributevaluetype.go @@ -0,0 +1,28 @@ +package dbc + +import "fmt" + +// AttributeValueType represents an attribute value type. +type AttributeValueType string + +const ( + AttributeValueTypeInt AttributeValueType = "INT" + AttributeValueTypeHex AttributeValueType = "HEX" + AttributeValueTypeFloat AttributeValueType = "FLOAT" + AttributeValueTypeString AttributeValueType = "STRING" + AttributeValueTypeEnum AttributeValueType = "ENUM" +) + +// Validate returns an error for invalid attribute value types. +func (a AttributeValueType) Validate() error { + switch a { + case AttributeValueTypeInt: + case AttributeValueTypeHex: + case AttributeValueTypeFloat: + case AttributeValueTypeString: + case AttributeValueTypeEnum: + default: + return fmt.Errorf("invalid attribute value type: %v", a) + } + return nil +} diff --git a/pkg/dbc/attributevaluetype_test.go b/pkg/dbc/attributevaluetype_test.go new file mode 100644 index 0000000..e951ced --- /dev/null +++ b/pkg/dbc/attributevaluetype_test.go @@ -0,0 +1,23 @@ +package dbc + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestAttributeValueType_Validate(t *testing.T) { + for _, tt := range []AttributeValueType{ + AttributeValueTypeInt, + AttributeValueTypeHex, + AttributeValueTypeFloat, + AttributeValueTypeString, + AttributeValueTypeEnum, + } { + require.NoError(t, tt.Validate()) + } +} + +func TestAttributeValueType_Validate_Error(t *testing.T) { + require.Error(t, AttributeValueType("foo").Validate()) +} diff --git a/pkg/dbc/def.go b/pkg/dbc/def.go new file mode 100644 index 0000000..b4ccd74 --- /dev/null +++ b/pkg/dbc/def.go @@ -0,0 +1,720 @@ +package dbc + +import ( + "strconv" + "text/scanner" +) + +// Def represents a single definition within a DBC file. +type Def interface { + // Position of the definition. + Position() scanner.Position + + // parseFrom parses the definition from a parser. + parseFrom(*Parser) +} + +// VersionDef defines the version of a DBC file. +type VersionDef struct { + Pos scanner.Position + Version string +} + +var _ Def = &VersionDef{} + +func (d *VersionDef) parseFrom(p *Parser) { + d.Pos = p.keyword(KeywordVersion).pos + d.Version = p.string() +} + +// Position returns the position of the definition. +func (d *VersionDef) Position() scanner.Position { + return d.Pos +} + +// NewSymbolsDef defines new symbol entries in a DBC file. +type NewSymbolsDef struct { + Pos scanner.Position + Symbols []Keyword +} + +var _ Def = &NewSymbolsDef{} + +func (d *NewSymbolsDef) parseFrom(p *Parser) { + p.useWhitespace(significantTab) + defer p.useWhitespace(defaultWhitespace) + d.Pos = p.keyword(KeywordNewSymbols).pos + p.token(':') + for p.peekToken().typ == '\t' { + p.token('\t') + d.Symbols = append(d.Symbols, Keyword(p.identifier())) + } +} + +// Position returns the position of the definition. +func (d *NewSymbolsDef) Position() scanner.Position { + return d.Pos +} + +// BitTimingDef defines the baud rate and the settings of the BTR registers of a CAN network. +// +// This definition is obsolete and not used anymore. +type BitTimingDef struct { + Pos scanner.Position + BaudRate uint64 + BTR1 uint64 + BTR2 uint64 +} + +var _ Def = &BitTimingDef{} + +func (d *BitTimingDef) parseFrom(p *Parser) { + d.Pos = p.keyword(KeywordBitTiming).pos + p.token(':') + d.BaudRate = p.optionalUint() + if p.peekToken().typ == ':' { + d.BTR1 = p.optionalUint() + } + if p.peekToken().typ == ',' { + d.BTR2 = p.optionalUint() + } +} + +// Position returns the position of the definition. +func (d *BitTimingDef) Position() scanner.Position { + return d.Pos +} + +// NodesDef defines the names of all nodes participating in the network. +// +// This definition is required in every DBC file. +// +// All node names must be unique. +type NodesDef struct { + Pos scanner.Position + NodeNames []Identifier +} + +var _ Def = &NodesDef{} + +func (d *NodesDef) parseFrom(p *Parser) { + p.useWhitespace(significantNewline) + defer p.useWhitespace(defaultWhitespace) + d.Pos = p.keyword(KeywordNodes).pos + p.token(':') + for p.peekToken().typ == scanner.Ident { + d.NodeNames = append(d.NodeNames, p.identifier()) + } + if p.peekToken().typ != scanner.EOF { + p.token('\n') + } +} + +// Position returns the position of the definition. +func (d *NodesDef) Position() scanner.Position { + return d.Pos +} + +// ValueDescriptionDef defines a textual description for a single signal value. +// +// The value may either be a signal raw value transferred on the bus or the value of an environment variable in a +// remaining bus simulation. +type ValueDescriptionDef struct { + Pos scanner.Position + Value float64 + Description string +} + +var _ Def = &ValueDescriptionDef{} + +func (d *ValueDescriptionDef) parseFrom(p *Parser) { + d.Pos = p.peekToken().pos + d.Value = p.float() + d.Description = p.string() +} + +// Position returns the position of the definition. +func (d *ValueDescriptionDef) Position() scanner.Position { + return d.Pos +} + +// ValueTableDef defines a global value table. +// +// The value descriptions in value tables define value encodings for signal raw values. +// +// In commonly used DBC files, the global value tables aren't used, but the value descriptions are defined for each +// signal independently. +type ValueTableDef struct { + Pos scanner.Position + TableName Identifier + ValueDescriptions []ValueDescriptionDef +} + +var _ Def = &ValueTableDef{} + +func (d *ValueTableDef) parseFrom(p *Parser) { + d.Pos = p.keyword(KeywordValueTable).pos + d.TableName = p.identifier() + for p.peekToken().typ != ';' { + valueDescriptionDef := ValueDescriptionDef{} + valueDescriptionDef.parseFrom(p) + d.ValueDescriptions = append(d.ValueDescriptions, valueDescriptionDef) + } + p.token(';') +} + +// Position returns the position of the definition. +func (d *ValueTableDef) Position() scanner.Position { + return d.Pos +} + +// MessageDef defines a frame in the network. +// +// The definition includes the name of a frame as well as its properties and the signals transferred. +type MessageDef struct { + // Pos is the position of the message definition. + Pos scanner.Position + + // MessageID contains the message CAN ID. + // + // The CAN ID has to be unique within the DBC file. + // + // If the most significant bit of the message ID is set, the ID is an extended CAN ID. The extended CAN ID can be + // determined by masking out the most significant bit with the mask 0xCFFFFFFF. + MessageID MessageID + + // Name is the name of the message. + // + // The message name has to be unique within the DBC file. + Name Identifier + + // Size specifies the size of the message in bytes. + Size uint64 + + // Transmitter specifies the name of the node transmitting the message. + // + // The transmitter has to be defined in the set of node names in the nodes definition. + // + // If the message has no transmitter, the string 'Vector__XXX' has to be given here. + Transmitter Identifier + + // Signals specifies the signals of the message. + Signals []SignalDef +} + +var _ Def = &MessageDef{} + +func (d *MessageDef) parseFrom(p *Parser) { + d.Pos = p.keyword(KeywordMessage).pos + d.MessageID = p.messageID() + d.Name = p.identifier() + p.token(':') + d.Size = p.uint() + d.Transmitter = p.identifier() + for p.peekToken().typ != scanner.EOF && p.peekKeyword() == KeywordSignal { + signalDef := SignalDef{} + signalDef.parseFrom(p) + d.Signals = append(d.Signals, signalDef) + } +} + +// Position returns the position of the definition. +func (d *MessageDef) Position() scanner.Position { + return d.Pos +} + +// SignalDef defines a signal within a message. +type SignalDef struct { + // Pos is the position of the definition. + Pos scanner.Position + + // Name of the signal. + // + // Has to be unique for all signals within the same message. + Name Identifier + + // StartBit specifies the position of the signal within the data field of the frame. + // + // For signals with byte order Intel (little-endian) the position of the least-significant bit is given. + // + // For signals with byte order Motorola (big-endian) the position of the most significant bit is given. + // + // The bits are counted in a saw-tooth manner. + // + // The start bit has to be in the range of [0 ,8*message_size-1]. + StartBit uint64 + + // Size specifies the size of the signal in bits. + Size uint64 + + // IsBigEndian is true if the signal's byte order is Motorola (big-endian). + IsBigEndian bool + + // IsSigned is true if the signal is signed. + IsSigned bool + + // IsMultiplexerSwitch is true if the signal is a multiplexer switch. + // + // A multiplexer indicator of 'M' defines the signal as the multiplexer switch. + // Only one signal within a single message can be the multiplexer switch. + IsMultiplexerSwitch bool + + // IsMultiplexed is true if the signal is multiplexed by the message's multiplexer switch. + IsMultiplexed bool + + // MultiplexerSwitch is the multiplexer switch value of the signal. + // + // The multiplexed signal is transferred in the message if the switch value of the multiplexer signal is equal to + // its multiplexer switch value. + MultiplexerSwitch uint64 + + // Offset is the signals physical value offset. + // + // Together with the factor, the offset defines the linear conversion rule to convert the signal's raw value into + // the signal's physical value and vice versa. + // + // physical_value = raw_value * factor + offset + // raw_value = (physical_value - offset) / factor + Offset float64 + + // Factor is the signal's physical value factor. + // + // See: Offset. + Factor float64 + + // Minimum defines the signal's minimum physical value. + Minimum float64 + + // Maximum defines the signal's maximum physical value. + Maximum float64 + + // Unit defines the unit of the signal's physical value. + Unit string + + // Receivers specifies the nodes receiving the signal. + // + // If the signal has no receiver, the string 'Vector__XXX' has to be given here. + Receivers []Identifier +} + +var _ Def = &SignalDef{} + +func (d *SignalDef) parseFrom(p *Parser) { + d.Pos = p.keyword(KeywordSignal).pos + d.Name = p.identifier() + // Parse: Multiplexing + if p.peekToken().typ != ':' { + tok := p.nextToken() + if tok.typ != scanner.Ident { + p.fail(tok.pos, "expected ident") + } + switch { + case tok.txt == "M": + d.IsMultiplexerSwitch = true + case tok.txt[0] == 'm' && len(tok.txt) > 1: + d.IsMultiplexed = true + i, err := strconv.Atoi(tok.txt[1:]) + if err != nil || i < 0 { + p.fail(tok.pos, "invalid multiplexer value") + } + d.MultiplexerSwitch = uint64(i) + default: + p.fail(tok.pos, "expected multiplexer") + } + } + p.token(':') + d.StartBit = p.uint() + p.token('|') + d.Size = p.uint() + p.token('@') + d.IsBigEndian = p.intInRange(0, 1) == 0 + d.IsSigned = p.anyOf('-', '+') == '-' + p.token('(') + d.Factor = p.float() + p.token(',') + d.Offset = p.float() + p.token(')') + p.token('[') + d.Minimum = p.float() + p.token('|') + d.Maximum = p.float() + p.token(']') + d.Unit = p.string() + // Parse: Receivers + d.Receivers = append(d.Receivers, p.identifier()) + for p.peekToken().typ == ',' { + p.token(',') + d.Receivers = append(d.Receivers, p.identifier()) + } +} + +// Position returns the position of the definition. +func (d *SignalDef) Position() scanner.Position { + return d.Pos +} + +// SignalValueTypeDef defines an extended type definition for a signal. +type SignalValueTypeDef struct { + Pos scanner.Position + MessageID MessageID + SignalName Identifier + SignalValueType SignalValueType +} + +var _ Def = &SignalValueTypeDef{} + +func (d *SignalValueTypeDef) parseFrom(p *Parser) { + d.Pos = p.keyword(KeywordSignalValueType).pos + d.MessageID = p.messageID() + d.SignalName = p.identifier() + p.optionalToken(':') // SPECIAL-CASE: colon not part of spec, but encountered in the wild + d.SignalValueType = p.signalValueType() + p.token(';') +} + +// Position returns the position of the definition. +func (d *SignalValueTypeDef) Position() scanner.Position { + return d.Pos +} + +// MessageTransmittersDef defines multiple transmitter nodes of a single message. +// +// This definition is used to describe communication data for higher layer protocols. +// +// This is not used to define CAN layer-2 communication. +type MessageTransmittersDef struct { + Pos scanner.Position + MessageID MessageID + Transmitters []Identifier +} + +var _ Def = &MessageTransmittersDef{} + +func (d *MessageTransmittersDef) parseFrom(p *Parser) { + d.Pos = p.keyword(KeywordMessageTransmitters).pos + d.MessageID = p.messageID() + p.token(':') + for p.peekToken().typ != ';' { + d.Transmitters = append(d.Transmitters, p.identifier()) + // SPECIAL-CASE: Comma not included in spec, but encountered in the wild + p.optionalToken(',') + } + p.token(';') +} + +// Position returns the position of the definition. +func (d *MessageTransmittersDef) Position() scanner.Position { + return d.Pos +} + +// ValueDescriptionsDef defines inline descriptions for specific raw signal values. +type ValueDescriptionsDef struct { + Pos scanner.Position + ObjectType ObjectType + MessageID MessageID + SignalName Identifier + EnvironmentVariableName Identifier + ValueDescriptions []ValueDescriptionDef +} + +var _ Def = &ValueDescriptionsDef{} + +func (d *ValueDescriptionsDef) parseFrom(p *Parser) { + d.Pos = p.keyword(KeywordValueDescriptions).pos + if p.peekToken().typ == scanner.Ident { + d.ObjectType = ObjectTypeEnvironmentVariable + d.EnvironmentVariableName = p.identifier() + } else { + d.ObjectType = ObjectTypeSignal + d.MessageID = p.messageID() + d.SignalName = p.identifier() + } + for p.peekToken().typ != ';' { + valueDescriptionDef := ValueDescriptionDef{} + valueDescriptionDef.parseFrom(p) + d.ValueDescriptions = append(d.ValueDescriptions, valueDescriptionDef) + } + p.token(';') +} + +// Position returns the position of the definition. +func (d *ValueDescriptionsDef) Position() scanner.Position { + return d.Pos +} + +// EnvironmentVariableDef defines an environment variable. +// +// DBC files that describe the CAN communication and don't define any additional data for system or remaining bus +// simulations don't include environment variables. +type EnvironmentVariableDef struct { + Pos scanner.Position + Name Identifier + Type EnvironmentVariableType + Minimum float64 + Maximum float64 + Unit string + InitialValue float64 + ID uint64 + AccessType AccessType + AccessNodes []Identifier +} + +var _ Def = &EnvironmentVariableDef{} + +func (d *EnvironmentVariableDef) parseFrom(p *Parser) { + d.Pos = p.keyword(KeywordEnvironmentVariable).pos + d.Name = p.identifier() + p.token(':') + d.Type = p.environmentVariableType() + p.token('[') + d.Minimum = p.float() + p.token('|') + d.Maximum = p.float() + p.token(']') + d.Unit = p.string() + d.InitialValue = p.float() + d.ID = p.uint() + d.AccessType = p.accessType() + d.AccessNodes = append(d.AccessNodes, p.identifier()) + for p.peekToken().typ == ',' { + p.token(',') + d.AccessNodes = append(d.AccessNodes, p.identifier()) + } + p.token(';') +} + +// Position returns the position of the definition. +func (d *EnvironmentVariableDef) Position() scanner.Position { + return d.Pos +} + +// EnvironmentVariableDataDef defines an environment variable as being of type "data". +// +// Environment variables of this type can store an arbitrary binary data of the given length. +// The length is given in bytes. +type EnvironmentVariableDataDef struct { + Pos scanner.Position + // EnvironmentVariableName is the name of the environment variable. + EnvironmentVariableName Identifier + // DataSize is the size of the environment variable data in bytes. + DataSize uint64 +} + +var _ Def = &EnvironmentVariableDataDef{} + +func (d *EnvironmentVariableDataDef) parseFrom(p *Parser) { + d.Pos = p.keyword(KeywordEnvironmentVariableData).pos + d.EnvironmentVariableName = p.identifier() + p.token(':') + d.DataSize = p.uint() + p.token(';') +} + +// Position returns the position of the definition. +func (d *EnvironmentVariableDataDef) Position() scanner.Position { + return d.Pos +} + +// CommentDef defines a comment. +type CommentDef struct { + Pos scanner.Position + ObjectType ObjectType + NodeName Identifier + MessageID MessageID + SignalName Identifier + EnvironmentVariableName Identifier + Comment string +} + +var _ Def = &CommentDef{} + +func (d *CommentDef) parseFrom(p *Parser) { + d.Pos = p.keyword(KeywordComment).pos + d.ObjectType = p.optionalObjectType() + switch d.ObjectType { + case ObjectTypeNetworkNode: + d.NodeName = p.identifier() + case ObjectTypeMessage: + d.MessageID = p.messageID() + case ObjectTypeSignal: + d.MessageID = p.messageID() + d.SignalName = p.identifier() + case ObjectTypeEnvironmentVariable: + d.EnvironmentVariableName = p.identifier() + } + d.Comment = p.string() + p.token(';') +} + +// Position returns the position of the definition. +func (d *CommentDef) Position() scanner.Position { + return d.Pos +} + +// AttributeDef defines a user-defined attribute. +// +// User-defined attributes are a means to extend the object properties of the DBC file. +// +// These additional attributes have to be defined using an attribute definition with an attribute default value. +// +// For each object having a value defined for the attribute, an attribute value entry has to be defined. +// +// If no attribute value entry is defined for an object, the value of the object's attribute is the attribute's default. +type AttributeDef struct { + Pos scanner.Position + ObjectType ObjectType + Name Identifier + Type AttributeValueType + MinimumInt int64 + MaximumInt int64 + MinimumFloat float64 + MaximumFloat float64 + EnumValues []string +} + +var _ Def = &AttributeDef{} + +func (d *AttributeDef) parseFrom(p *Parser) { + d.Pos = p.keyword(KeywordAttribute).pos + d.ObjectType = p.optionalObjectType() + d.Name = p.stringIdentifier() + d.Type = p.attributeValueType() + switch d.Type { + case AttributeValueTypeInt, AttributeValueTypeHex: + if p.peekToken().typ != ';' { + d.MinimumInt = p.int() + d.MaximumInt = p.int() + } + case AttributeValueTypeFloat: + if p.peekToken().typ != ';' { + // SPECIAL CASE: Support attributes without min/max + d.MinimumFloat = p.float() + d.MaximumFloat = p.float() + } + case AttributeValueTypeEnum: + d.EnumValues = append(d.EnumValues, p.string()) + for p.peekToken().typ == ',' { + p.token(',') + d.EnumValues = append(d.EnumValues, p.string()) + } + } + p.token(';') +} + +// Position returns the position of the definition. +func (d *AttributeDef) Position() scanner.Position { + return d.Pos +} + +// AttributeDefaultValueDef defines the default value for an attribute. +type AttributeDefaultValueDef struct { + Pos scanner.Position + AttributeName Identifier + DefaultIntValue int64 + DefaultFloatValue float64 + DefaultStringValue string +} + +var _ Def = &AttributeDefaultValueDef{} + +func (d *AttributeDefaultValueDef) parseFrom(p *Parser) { + d.Pos = p.keyword(KeywordAttributeDefault).pos + d.AttributeName = Identifier(p.string()) + // look up attribute type + for _, prevDef := range p.defs { + if attributeDef, ok := prevDef.(*AttributeDef); ok && attributeDef.Name == d.AttributeName { + switch attributeDef.Type { + case AttributeValueTypeInt, AttributeValueTypeHex: + d.DefaultIntValue = p.int() + case AttributeValueTypeFloat: + d.DefaultFloatValue = p.float() + case AttributeValueTypeString: + d.DefaultStringValue = p.string() + case AttributeValueTypeEnum: + d.DefaultStringValue = p.enumValue(attributeDef.EnumValues) + } + break + } + } + p.token(';') +} + +// Position returns the position of the definition. +func (d *AttributeDefaultValueDef) Position() scanner.Position { + return d.Pos +} + +// AttributeValueForObjectDef defines a value for an attribute and an object. +type AttributeValueForObjectDef struct { + Pos scanner.Position + AttributeName Identifier + ObjectType ObjectType + MessageID MessageID + SignalName Identifier + NodeName Identifier + EnvironmentVariableName Identifier + IntValue int64 + FloatValue float64 + StringValue string +} + +var _ Def = &AttributeValueForObjectDef{} + +func (d *AttributeValueForObjectDef) parseFrom(p *Parser) { + d.Pos = p.keyword(KeywordAttributeValue).pos + d.AttributeName = Identifier(p.string()) + d.ObjectType = p.optionalObjectType() + switch d.ObjectType { + case ObjectTypeMessage: + d.MessageID = p.messageID() + case ObjectTypeSignal: + d.MessageID = p.messageID() + d.SignalName = p.identifier() + case ObjectTypeNetworkNode: + d.NodeName = p.identifier() + case ObjectTypeEnvironmentVariable: + d.EnvironmentVariableName = p.identifier() + } + // look up attribute type + for _, prevDef := range p.defs { + if attributeDef, ok := prevDef.(*AttributeDef); ok && attributeDef.Name == d.AttributeName { + switch attributeDef.Type { + case AttributeValueTypeInt, AttributeValueTypeHex: + d.IntValue = p.int() + case AttributeValueTypeFloat: + d.FloatValue = p.float() + case AttributeValueTypeString: + d.StringValue = p.string() + case AttributeValueTypeEnum: + d.StringValue = p.enumValue(attributeDef.EnumValues) + } + break + } + } + p.token(';') +} + +// Position returns the position of the definition. +func (d *AttributeValueForObjectDef) Position() scanner.Position { + return d.Pos +} + +// UnknownDef represents an unknown or unsupported DBC definition. +type UnknownDef struct { + Pos scanner.Position + Keyword Keyword +} + +var _ Def = &UnknownDef{} + +func (d *UnknownDef) parseFrom(p *Parser) { + tok := p.peekToken() + d.Pos = tok.pos + d.Keyword = Keyword(tok.txt) + p.discardLine() +} + +// Position returns the position of the definition. +func (d *UnknownDef) Position() scanner.Position { + return d.Pos +} diff --git a/pkg/dbc/doc.go b/pkg/dbc/doc.go new file mode 100644 index 0000000..ceafdc9 --- /dev/null +++ b/pkg/dbc/doc.go @@ -0,0 +1,4 @@ +// Package dbc provides primitives for parsing, formatting and linting DBC files. +// +// The implementation adheres to the "DBC File Format Documentation Version 01/2007" unless specified otherwise. +package dbc diff --git a/pkg/dbc/envvartype.go b/pkg/dbc/envvartype.go new file mode 100644 index 0000000..e86f8f3 --- /dev/null +++ b/pkg/dbc/envvartype.go @@ -0,0 +1,24 @@ +package dbc + +import "fmt" + +// EnvironmentVariableType represents the type of an environment variable. +type EnvironmentVariableType uint64 + +const ( + EnvironmentVariableTypeInteger EnvironmentVariableType = 0 + EnvironmentVariableTypeFloat EnvironmentVariableType = 1 + EnvironmentVariableTypeString EnvironmentVariableType = 2 +) + +// Validate returns an error for invalid environment variable types. +func (e EnvironmentVariableType) Validate() error { + switch e { + case EnvironmentVariableTypeInteger: + case EnvironmentVariableTypeFloat: + case EnvironmentVariableTypeString: + default: + return fmt.Errorf("invalid environment variable type: %v", e) + } + return nil +} diff --git a/pkg/dbc/envvartype_test.go b/pkg/dbc/envvartype_test.go new file mode 100644 index 0000000..05738a9 --- /dev/null +++ b/pkg/dbc/envvartype_test.go @@ -0,0 +1,21 @@ +package dbc + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestEnvironmentVariableType_Validate(t *testing.T) { + for _, tt := range []EnvironmentVariableType{ + EnvironmentVariableTypeInteger, + EnvironmentVariableTypeFloat, + EnvironmentVariableTypeString, + } { + require.NoError(t, tt.Validate()) + } +} + +func TestEnvironmentVariableType_Validate_Error(t *testing.T) { + require.Error(t, EnvironmentVariableType(42).Validate()) +} diff --git a/pkg/dbc/error.go b/pkg/dbc/error.go new file mode 100644 index 0000000..befce55 --- /dev/null +++ b/pkg/dbc/error.go @@ -0,0 +1,74 @@ +package dbc + +import ( + "fmt" + "text/scanner" +) + +// Error represents an error in a DBC file. +type Error interface { + error + + // Position of the error in the DBC file. + Position() scanner.Position + + // Reason for the error. + Reason() string +} + +// validationError is an error resulting from an invalid DBC definition. +type validationError struct { + pos scanner.Position + reason string + cause error +} + +func (e *validationError) Unwrap() error { + return e.cause +} + +var _ Error = &validationError{} + +func (e *validationError) Error() string { + return fmt.Sprintf("%v: %s (validate)", e.Position(), e.reason) +} + +// Reason returns the reason for the error. +func (e *validationError) Reason() string { + return e.reason +} + +// Position returns the position of the validation error in the DBC file. +// +// When the validation error results from an invalid nested definition, the position of the nested definition is +// returned. +func (e *validationError) Position() scanner.Position { + if e.cause != nil { + if validationErr, ok := e.cause.(*validationError); ok { + return validationErr.Position() + } + } + return e.pos +} + +// parseError is an error resulting from a failure to parse a DBC file. +type parseError struct { + pos scanner.Position + reason string +} + +var _ Error = &parseError{} + +func (e *parseError) Error() string { + return fmt.Sprintf("%v: %s (parse)", e.pos, e.reason) +} + +// Reason returns the reason for the error. +func (e *parseError) Reason() string { + return e.reason +} + +// Position returns the position of the parse error in the DBC file. +func (e *parseError) Position() scanner.Position { + return e.pos +} diff --git a/pkg/dbc/file.go b/pkg/dbc/file.go new file mode 100644 index 0000000..f80b2bc --- /dev/null +++ b/pkg/dbc/file.go @@ -0,0 +1,11 @@ +package dbc + +// File is a parsed DBC source file. +type File struct { + // Name of the file. + Name string + // Data contains the raw file data. + Data []byte + // Defs in the file. + Defs []Def +} diff --git a/pkg/dbc/identifier.go b/pkg/dbc/identifier.go new file mode 100644 index 0000000..bb33819 --- /dev/null +++ b/pkg/dbc/identifier.go @@ -0,0 +1,31 @@ +package dbc + +import ( + "fmt" + + "go.einride.tech/can/internal/identifiers" +) + +// Identifier represents a DBC identifier. +type Identifier string + +// maxIdentifierLength is the length of the longest valid DBC identifier. +const maxIdentifierLength = 128 + +// Validate returns an error for invalid DBC identifiers. +func (id Identifier) Validate() error { + if len(id) == 0 { + return fmt.Errorf("zero-length") + } + if len(id) > maxIdentifierLength { + return fmt.Errorf("length %v: exceeds max length: %v", len(id), maxIdentifierLength) + } + for i, r := range id { + if i == 0 && r != '_' && !identifiers.IsAlphaChar(r) { // first char + return fmt.Errorf("invalid first char: '%v'", r) + } else if i > 0 && r != '_' && !identifiers.IsAlphaChar(r) && !identifiers.IsNumChar(r) { + return fmt.Errorf("invalid char: '%v'", r) + } + } + return nil +} diff --git a/pkg/dbc/identifier_test.go b/pkg/dbc/identifier_test.go new file mode 100644 index 0000000..d87d87e --- /dev/null +++ b/pkg/dbc/identifier_test.go @@ -0,0 +1,42 @@ +package dbc + +import ( + "fmt" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestIdentifier_Validate(t *testing.T) { + for _, tt := range []Identifier{ + "_", + "_foo", + "foo", + "foo32", + "_43", + Identifier(strings.Repeat("a", maxIdentifierLength)), + } { + tt := tt + t.Run(fmt.Sprintf("%v", tt), func(t *testing.T) { + require.NoError(t, tt.Validate()) + }) + } +} + +func TestIdentifier_Validate_Error(t *testing.T) { + for _, tt := range []Identifier{ + "42", + "", + "42foo", + "☃", + "foo☃", + "foo bar", + Identifier(strings.Repeat("a", maxIdentifierLength+1)), + } { + tt := tt + t.Run(fmt.Sprintf("%v", tt), func(t *testing.T) { + require.Error(t, tt.Validate()) + }) + } +} diff --git a/pkg/dbc/independent_signals.go b/pkg/dbc/independent_signals.go new file mode 100644 index 0000000..fdfab6b --- /dev/null +++ b/pkg/dbc/independent_signals.go @@ -0,0 +1,22 @@ +package dbc + +// Independent signals constants. +// +// DBC files may contain a special message with the following message name and message ID. +// +// This message will have size 0 and may contain duplicate signal names. +const ( + // IndependentSignalsMessageName is the message name used by the special independent signals message. + IndependentSignalsMessageName Identifier = "VECTOR__INDEPENDENT_SIG_MSG" + // IndependentSignalsMessageName is the message ID used by the special independent signals message. + IndependentSignalsMessageID MessageID = 0xc0000000 + // IndependentSignalsMessageSize is the size used by the special independent signals message. + IndependentSignalsMessageSize = 0 +) + +// IsIndependentSignalsMessage returns true if m is the special independent signals message. +func IsIndependentSignalsMessage(m *MessageDef) bool { + return m.Name == IndependentSignalsMessageName && + m.MessageID == IndependentSignalsMessageID && + m.Size == IndependentSignalsMessageSize +} diff --git a/pkg/dbc/keyword.go b/pkg/dbc/keyword.go new file mode 100644 index 0000000..c0193e0 --- /dev/null +++ b/pkg/dbc/keyword.go @@ -0,0 +1,25 @@ +package dbc + +// Keyword represents a DBC keyword. +type Keyword string + +const ( + KeywordAttribute Keyword = "BA_DEF_" + KeywordAttributeDefault Keyword = "BA_DEF_DEF_" + KeywordAttributeValue Keyword = "BA_" + KeywordBitTiming Keyword = "BS_" + KeywordComment Keyword = "CM_" + KeywordEnvironmentVariable Keyword = "EV_" + KeywordEnvironmentVariableData Keyword = "ENVVAR_DATA_" + KeywordMessage Keyword = "BO_" + KeywordMessageTransmitters Keyword = "BO_TX_BU_" + KeywordNewSymbols Keyword = "NS_" + KeywordNodes Keyword = "BU_" + KeywordSignal Keyword = "SG_" + KeywordSignalGroup Keyword = "SIG_GROUP_" + KeywordSignalType Keyword = "SGTYPE_" + KeywordSignalValueType Keyword = "SIG_VALTYPE_" + KeywordValueDescriptions Keyword = "VAL_" + KeywordValueTable Keyword = "VAL_TABLE_" + KeywordVersion Keyword = "VERSION" +) diff --git a/pkg/dbc/messageid.go b/pkg/dbc/messageid.go new file mode 100644 index 0000000..739a136 --- /dev/null +++ b/pkg/dbc/messageid.go @@ -0,0 +1,44 @@ +package dbc + +import "fmt" + +// MessageID represents a message ID. A message ID +type MessageID uint32 + +// ID constants. +const ( + // maxID is the largest valid standard CAN ID. + maxID = 0x7ff + // maxExtendedID is the largest valid extended CAN ID. + maxExtendedID = 0x1fffffff +) + +// messageIDExtendedFlag is a bit flag that is set for extended message IDs. +const messageIDExtendedFlag MessageID = 0x80000000 + +// messageIDIndependentSignals is a special message ID used for the "independent signals" message. +const messageIDIndependentSignals MessageID = 0xc0000000 + +// IsExtended returns true if the message ID is an extended CAN ID. +func (m MessageID) IsExtended() bool { + return m != messageIDIndependentSignals && m&messageIDExtendedFlag > 0 +} + +// ToCAN returns the CAN id value of the message ID (i.e. with bit flags removed). +func (m MessageID) ToCAN() uint32 { + return uint32(m &^ messageIDExtendedFlag) +} + +// Validate returns an error for invalid message IDs. +func (m MessageID) Validate() error { + if m == messageIDIndependentSignals { + return nil + } + if m.IsExtended() && m.ToCAN() > maxExtendedID { + return fmt.Errorf("invalid extended ID: %v", m) + } + if !m.IsExtended() && m.ToCAN() > maxID { + return fmt.Errorf("invalid standard ID: %v", m) + } + return nil +} diff --git a/pkg/dbc/messageid_test.go b/pkg/dbc/messageid_test.go new file mode 100644 index 0000000..0ed3f69 --- /dev/null +++ b/pkg/dbc/messageid_test.go @@ -0,0 +1,71 @@ +package dbc + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestMessageID_Validate(t *testing.T) { + for _, tt := range []MessageID{ + 0, + 1, + maxID, + 0 | messageIDExtendedFlag, + 1 | messageIDExtendedFlag, + maxID | messageIDExtendedFlag, + maxExtendedID | messageIDExtendedFlag, + messageIDIndependentSignals, + } { + tt := tt + t.Run(fmt.Sprintf("%v", tt), func(t *testing.T) { + require.NoError(t, tt.Validate()) + }) + } +} + +func TestMessageID_Validate_Error(t *testing.T) { + for _, tt := range []MessageID{ + maxID + 1, + (maxExtendedID + 1) | messageIDExtendedFlag, + 0xffffffff, + } { + tt := tt + t.Run(fmt.Sprintf("%v", tt), func(t *testing.T) { + require.Error(t, tt.Validate()) + }) + } +} + +func TestMessageID_ToCAN(t *testing.T) { + for _, tt := range []struct { + messageID MessageID + expected uint32 + }{ + {messageID: 1, expected: 1}, + {messageID: messageIDIndependentSignals, expected: 0x40000000}, + {messageID: 2566857156, expected: 419373508}, + } { + tt := tt + t.Run(fmt.Sprintf("%v", tt.messageID), func(t *testing.T) { + require.Equal(t, tt.expected, tt.messageID.ToCAN()) + }) + } +} + +func TestMessageID_IsExtended(t *testing.T) { + for _, tt := range []struct { + messageID MessageID + expected bool + }{ + {messageID: 1, expected: false}, + {messageID: messageIDIndependentSignals, expected: false}, + {messageID: 2566857156, expected: true}, + } { + tt := tt + t.Run(fmt.Sprintf("%v", tt.messageID), func(t *testing.T) { + require.Equal(t, tt.expected, tt.messageID.IsExtended()) + }) + } +} diff --git a/pkg/dbc/objecttype.go b/pkg/dbc/objecttype.go new file mode 100644 index 0000000..307d9da --- /dev/null +++ b/pkg/dbc/objecttype.go @@ -0,0 +1,28 @@ +package dbc + +import "fmt" + +// ObjectType identifies the type of a DBC object. +type ObjectType string + +const ( + ObjectTypeUnspecified ObjectType = "" + ObjectTypeNetworkNode ObjectType = "BU_" + ObjectTypeMessage ObjectType = "BO_" + ObjectTypeSignal ObjectType = "SG_" + ObjectTypeEnvironmentVariable ObjectType = "EV_" +) + +// Validate returns an error for invalid object types. +func (o ObjectType) Validate() error { + switch o { + case ObjectTypeUnspecified: + case ObjectTypeNetworkNode: + case ObjectTypeMessage: + case ObjectTypeSignal: + case ObjectTypeEnvironmentVariable: + default: + return fmt.Errorf("invalid object type: %v", o) + } + return nil +} diff --git a/pkg/dbc/objecttype_test.go b/pkg/dbc/objecttype_test.go new file mode 100644 index 0000000..8a123a1 --- /dev/null +++ b/pkg/dbc/objecttype_test.go @@ -0,0 +1,23 @@ +package dbc + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestObjectType_Validate(t *testing.T) { + for _, tt := range []ObjectType{ + ObjectTypeUnspecified, + ObjectTypeNetworkNode, + ObjectTypeMessage, + ObjectTypeSignal, + ObjectTypeEnvironmentVariable, + } { + require.NoError(t, tt.Validate()) + } +} + +func TestObjectType_Validate_Error(t *testing.T) { + require.Error(t, ObjectType("foo").Validate()) +} diff --git a/pkg/dbc/parser.go b/pkg/dbc/parser.go new file mode 100644 index 0000000..07f34b9 --- /dev/null +++ b/pkg/dbc/parser.go @@ -0,0 +1,438 @@ +package dbc + +import ( + "bytes" + "fmt" + "strconv" + "strings" + "text/scanner" + "unicode/utf8" +) + +const defaultScannerMode = scanner.ScanIdents | scanner.ScanFloats + +const ( + defaultWhitespace = scanner.GoWhitespace + significantNewline = defaultWhitespace & ^uint64(1<<'\n') + significantTab = defaultWhitespace & ^uint64(1<<'\t') +) + +type token struct { + typ rune + pos scanner.Position + txt string +} + +type Parser struct { + sc scanner.Scanner + curr token + lookahead token + hasLookahead bool + data []byte + defs []Def +} + +func NewParser(filename string, data []byte) *Parser { + p := &Parser{data: data} + p.sc.Init(bytes.NewReader(data)) + p.sc.Mode = defaultScannerMode + p.sc.Whitespace = defaultWhitespace + p.sc.Filename = filename + p.sc.Error = func(sc *scanner.Scanner, msg string) { + p.fail(sc.Pos(), msg) + } + return p +} + +func (p *Parser) Defs() []Def { + return p.defs +} + +func (p *Parser) File() *File { + return &File{ + Name: p.sc.Filename, + Data: p.data, + Defs: p.defs, + } +} + +func (p *Parser) Parse() (err Error) { + defer func() { + if r := recover(); r != nil { + // recover from parse errors only + if errParse, ok := r.(*parseError); ok { + err = errParse + } else { + panic(r) + } + } + }() + for p.peekToken().typ != scanner.EOF { + var def Def + switch p.peekKeyword() { + case KeywordVersion: + def = &VersionDef{} + case KeywordBitTiming: + def = &BitTimingDef{} + case KeywordNewSymbols: + def = &NewSymbolsDef{} + case KeywordNodes: + def = &NodesDef{} + case KeywordMessage: + def = &MessageDef{} + case KeywordSignal: + def = &SignalDef{} + case KeywordEnvironmentVariable: + def = &EnvironmentVariableDef{} + case KeywordComment: + def = &CommentDef{} + case KeywordAttribute: + def = &AttributeDef{} + case KeywordAttributeDefault: + def = &AttributeDefaultValueDef{} + case KeywordAttributeValue: + def = &AttributeValueForObjectDef{} + case KeywordValueDescriptions: + def = &ValueDescriptionsDef{} + case KeywordValueTable: + def = &ValueTableDef{} + case KeywordSignalValueType: + def = &SignalValueTypeDef{} + case KeywordMessageTransmitters: + def = &MessageTransmittersDef{} + case KeywordEnvironmentVariableData: + def = &EnvironmentVariableDataDef{} + default: + def = &UnknownDef{} + } + def.parseFrom(p) + p.defs = append(p.defs, def) + } + return nil +} + +func (p *Parser) fail(pos scanner.Position, format string, a ...interface{}) { + panic(&parseError{pos: pos, reason: fmt.Sprintf(format, a...)}) +} + +// +// Whitespace +// + +func (p *Parser) useWhitespace(whitespace uint64) { + p.sc.Whitespace = whitespace +} + +// +// Characters +// + +func (p *Parser) nextRune() rune { + if p.hasLookahead { + if utf8.RuneCountInString(p.lookahead.txt) > 1 { + p.fail(p.lookahead.pos, "cannot get next rune when lookahead contains a token") + } + p.hasLookahead = false + r, _ := utf8.DecodeRuneInString(p.lookahead.txt) + return r + } + return p.sc.Next() +} + +func (p *Parser) peekRune() rune { + if p.hasLookahead { + if utf8.RuneCountInString(p.lookahead.txt) > 1 { + p.fail(p.lookahead.pos, "cannot peek next rune when lookahead contains a token") + } + r, _ := utf8.DecodeRuneInString(p.lookahead.txt) + return r + } + return p.sc.Peek() +} + +func (p *Parser) discardLine() { + p.useWhitespace(significantNewline) + defer p.useWhitespace(defaultWhitespace) + for p.nextToken().typ != '\n' && p.nextToken().typ != scanner.EOF { + // skip all non-newline tokens + } +} + +// +// Tokens +// + +func (p *Parser) nextToken() token { + if p.hasLookahead { + p.hasLookahead = false + p.curr = p.lookahead + return p.lookahead + } + p.curr = token{typ: p.sc.Scan(), pos: p.sc.Position, txt: p.sc.TokenText()} + return p.curr +} + +func (p *Parser) peekToken() token { + if p.hasLookahead { + return p.lookahead + } + p.hasLookahead = true + p.lookahead = token{typ: p.sc.Scan(), pos: p.sc.Position, txt: p.sc.TokenText()} + return p.lookahead +} + +// +// Data types +// + +// string parses a string that may contain newlines +func (p *Parser) string() string { + tok := p.nextToken() + if tok.typ != '"' { + p.fail(tok.pos, `expected token "`) + } + var b strings.Builder +ReadLoop: + for { + switch r := p.nextRune(); r { + case scanner.EOF: + p.fail(tok.pos, "unterminated string") + case '"': + break ReadLoop + case '\n': + if _, err := b.WriteRune(' '); err != nil { + p.fail(tok.pos, err.Error()) + } + case '\\': + if p.peekRune() == '"' { + _ = p.nextRune() // include escaped quotes in string + if _, err := b.WriteString(`\"`); err != nil { + p.fail(tok.pos, err.Error()) + } + continue + } + fallthrough + default: + if _, err := b.WriteRune(r); err != nil { + p.fail(tok.pos, err.Error()) + } + } + } + return b.String() +} + +func (p *Parser) identifier() Identifier { + tok := p.nextToken() + if tok.typ != scanner.Ident { + p.fail(tok.pos, "expected ident") + } + id := Identifier(tok.txt) + if err := id.Validate(); err != nil { + p.fail(tok.pos, err.Error()) + } + return id +} + +func (p *Parser) stringIdentifier() Identifier { + tok := p.peekToken() + id := Identifier(p.string()) + if err := id.Validate(); err != nil { + p.fail(tok.pos, err.Error()) + } + return id +} + +func (p *Parser) keyword(kw Keyword) token { + if p.peekKeyword() != kw { + p.fail(p.peekToken().pos, "expected keyword: %v", kw) + } + return p.nextToken() +} + +func (p *Parser) peekKeyword() Keyword { + tok := p.peekToken() + if tok.typ != scanner.Ident { + p.fail(p.peekToken().pos, "expected ident") + } + return Keyword(tok.txt) +} + +func (p *Parser) token(typ rune) { + tok := p.nextToken() + if tok.typ != typ { + p.fail( + p.peekToken().pos, "expected token: %v, found: %v (%v)", + scanner.TokenString(typ), scanner.TokenString(tok.typ), tok.txt) + } +} + +func (p *Parser) optionalToken(typ rune) { + if p.peekToken().typ == typ { + p.token(typ) + } +} + +func (p *Parser) enumValue(values []string) string { + tok := p.peekToken() + if tok.typ == scanner.Int { + // SPECIAL-CASE: Enum values by index encountered in the wild + i := p.uint() + if i >= uint64(len(values)) { + p.fail(tok.pos, "enum index out of bounds") + } + return values[i] + } + return p.string() +} + +func (p *Parser) float() float64 { + var isNegative bool + if p.peekToken().typ == '-' { + p.token('-') + isNegative = true + } + tok := p.nextToken() + if tok.typ != scanner.Int && tok.typ != scanner.Float { + p.fail(p.peekToken().pos, "expected int or float") + } + f, err := strconv.ParseFloat(tok.txt, 64) + if err != nil { + p.fail(tok.pos, "invalid float") + } + if isNegative { + f *= -1 + } + return f +} + +func (p *Parser) int() int64 { + var isNegative bool + if p.peekToken().typ == '-' { + p.token('-') + isNegative = true + } + tok := p.nextToken() + if tok.typ != scanner.Int { + p.fail(tok.pos, "expected int") + } + i, err := strconv.ParseInt(tok.txt, 10, 64) + if err != nil { + p.fail(tok.pos, "invalid int") + } + if isNegative { + i *= -1 + } + return i +} + +func (p *Parser) uint() uint64 { + tok := p.nextToken() + if tok.typ != scanner.Int { + p.fail(tok.pos, "expected int") + } + i, err := strconv.ParseUint(tok.txt, 10, 64) + if err != nil { + p.fail(tok.pos, "invalid uint") + } + return i +} + +func (p *Parser) intInRange(min, max int) int { + var isNegative bool + if p.peekToken().typ == '-' { + p.token('-') + isNegative = true + } + tok := p.nextToken() + i, err := strconv.Atoi(tok.txt) + if err != nil { + p.fail(tok.pos, "invalid int") + } + if isNegative { + i *= -1 + } + if i < min || i > max { + p.fail(tok.pos, "invalid value") + } + return i +} + +func (p *Parser) optionalUint() uint64 { + if p.peekToken().typ != scanner.Int { + return 0 + } + tok := p.nextToken() + i, err := strconv.ParseUint(tok.txt, 10, 64) + if err != nil { + p.fail(tok.pos, "invalid uint") + } + return i +} + +func (p *Parser) anyOf(tokenTypes ...rune) rune { + tok := p.nextToken() + for _, tokenType := range tokenTypes { + if tok.typ == tokenType { + return tok.typ + } + } + p.fail(tok.pos, "unexpected token") + return 0 +} + +func (p *Parser) optionalObjectType() ObjectType { + tok := p.peekToken() + if tok.typ != scanner.Ident { + return ObjectTypeUnspecified + } + objectType := ObjectType(p.identifier()) + if err := objectType.Validate(); err != nil { + p.fail(tok.pos, err.Error()) + } + return objectType +} + +func (p *Parser) messageID() MessageID { + tok := p.peekToken() + messageID := MessageID(p.uint()) + if err := messageID.Validate(); err != nil { + p.fail(tok.pos, err.Error()) + } + return messageID +} + +func (p *Parser) signalValueType() SignalValueType { + tok := p.peekToken() + signalValueType := SignalValueType(p.uint()) + if err := signalValueType.Validate(); err != nil { + p.fail(tok.pos, err.Error()) + } + return signalValueType +} + +func (p *Parser) environmentVariableType() EnvironmentVariableType { + tok := p.peekToken() + environmentVariableType := EnvironmentVariableType(p.uint()) + if err := environmentVariableType.Validate(); err != nil { + p.fail(tok.pos, err.Error()) + } + return environmentVariableType +} + +func (p *Parser) attributeValueType() AttributeValueType { + tok := p.peekToken() + attributeValueType := AttributeValueType(p.identifier()) + if err := attributeValueType.Validate(); err != nil { + p.fail(tok.pos, err.Error()) + } + return attributeValueType +} + +func (p *Parser) accessType() AccessType { + tok := p.peekToken() + accessType := AccessType(p.identifier()) + if err := accessType.Validate(); err != nil { + p.fail(tok.pos, "invalid access type") + } + return accessType +} diff --git a/pkg/dbc/parser_test.go b/pkg/dbc/parser_test.go new file mode 100644 index 0000000..40d0610 --- /dev/null +++ b/pkg/dbc/parser_test.go @@ -0,0 +1,1082 @@ +package dbc + +import ( + "io/ioutil" + "os" + "strings" + "testing" + "text/scanner" + + "github.com/davecgh/go-spew/spew" + "github.com/stretchr/testify/require" +) + +func shouldUpdateGoldenFiles() bool { + return os.Getenv("GOLDEN") == "true" +} + +func TestParse_ExampleDBC(t *testing.T) { + const inputFile = "../../testdata/dbc/example/example.dbc" + const goldenFile = "../../testdata/dbc/example/example.dbc.golden" + data, err := ioutil.ReadFile(inputFile) + require.NoError(t, err) + p := NewParser(inputFile, data) + require.NoError(t, p.Parse()) + if shouldUpdateGoldenFiles() { + require.NoError(t, ioutil.WriteFile(goldenFile, []byte(dump(p.Defs())), 0644)) + } + goldenFileData, err := ioutil.ReadFile(goldenFile) + require.NoError(t, err) + require.Equal(t, string(goldenFileData), dump(p.Defs())) +} + +func TestParser_Parse(t *testing.T) { + for _, tt := range []struct { + name string + text string + defs []Def + }{ + { + name: "version.dbc", + text: `VERSION "foo"`, + defs: []Def{ + &VersionDef{ + Pos: scanner.Position{ + Filename: "version.dbc", + Line: 1, + Column: 1, + }, + Version: "foo", + }, + }, + }, + + { + name: "multiple_version.dbc", + text: strings.Join([]string{ + `VERSION "foo"`, + `VERSION "bar"`, + }, "\n"), + defs: []Def{ + &VersionDef{ + Pos: scanner.Position{ + Filename: "multiple_version.dbc", + Line: 1, + Column: 1, + }, + Version: "foo", + }, + &VersionDef{ + Pos: scanner.Position{ + Filename: "multiple_version.dbc", + Line: 2, + Column: 1, + Offset: 14, + }, + Version: "bar", + }, + }, + }, + + { + name: "no_bus_speed.dbc", + text: `BS_:`, + defs: []Def{ + &BitTimingDef{ + Pos: scanner.Position{ + Filename: "no_bus_speed.dbc", + Line: 1, + Column: 1, + }, + }, + }, + }, + + { + name: "bus_speed.dbc", + text: `BS_: 250000`, + defs: []Def{ + &BitTimingDef{ + Pos: scanner.Position{ + Filename: "bus_speed.dbc", + Line: 1, + Column: 1, + }, + BaudRate: 250000, + }, + }, + }, + + { + name: "symbols.dbc", + text: strings.Join([]string{ + "NS_ :", + "NS_DESC_", + "CM_", + "BA_DEF_", + "BA_", + "VAL_", + }, "\n\t"), + defs: []Def{ + &NewSymbolsDef{ + Pos: scanner.Position{ + Filename: "symbols.dbc", + Line: 1, + Column: 1, + }, + Symbols: []Keyword{ + "NS_DESC_", + "CM_", + "BA_DEF_", + "BA_", + "VAL_", + }, + }, + }, + }, + + { + name: "standard_message.dbc", + text: "BO_ 804 CRUISE: 8 PCM", + defs: []Def{ + &MessageDef{ + Pos: scanner.Position{ + Filename: "standard_message.dbc", + Line: 1, + Column: 1, + }, + Name: "CRUISE", + MessageID: 804, + Size: 8, + Transmitter: "PCM", + }, + }, + }, + + { + name: "extended_message.dbc", + text: "BO_ 2566857412 BMS2_4: 8 Vector__XXX", + defs: []Def{ + &MessageDef{ + Pos: scanner.Position{ + Filename: "extended_message.dbc", + Line: 1, + Column: 1, + }, + Name: "BMS2_4", + MessageID: 2566857412, + Size: 8, + Transmitter: "Vector__XXX", + }, + }, + }, + + { + name: "signal.dbc", + text: `SG_ CellTempLowest : 32|8@0+ (1,-40) [-40|215] "C" Vector__XXX`, + defs: []Def{ + &SignalDef{ + Pos: scanner.Position{ + Filename: "signal.dbc", + Line: 1, + Column: 1, + }, + Name: "CellTempLowest", + StartBit: 32, + Size: 8, + IsBigEndian: true, + Factor: 1, + Offset: -40, + Minimum: -40, + Maximum: 215, + Unit: "C", + Receivers: []Identifier{"Vector__XXX"}, + }, + }, + }, + + { + name: "multiplexer_signal.dbc", + text: `SG_ TestSignal M : 56|8@1+ (0.001,0) [0|0.255] "l/mm" XXX`, + defs: []Def{ + &SignalDef{ + Pos: scanner.Position{ + Filename: "multiplexer_signal.dbc", + Line: 1, + Column: 1, + }, + Name: "TestSignal", + StartBit: 56, + Size: 8, + Factor: 0.001, + Offset: 0, + Minimum: 0, + Maximum: 0.255, + Unit: "l/mm", + Receivers: []Identifier{"XXX"}, + IsMultiplexerSwitch: true, + }, + }, + }, + + { + name: "multiplexed_signal.dbc", + text: `SG_ TestSignal m2 : 56|8@1+ (0.001,0) [0|0.255] "l/mm" XXX`, + defs: []Def{ + &SignalDef{ + Pos: scanner.Position{ + Filename: "multiplexed_signal.dbc", + Line: 1, + Column: 1, + }, + Name: "TestSignal", + StartBit: 56, + Size: 8, + Factor: 0.001, + Offset: 0, + Minimum: 0, + Maximum: 0.255, + Unit: "l/mm", + Receivers: []Identifier{"XXX"}, + IsMultiplexed: true, + MultiplexerSwitch: 2, + }, + }, + }, + + { + name: "comment.dbc", + text: `CM_ "comment";`, + defs: []Def{ + &CommentDef{ + Pos: scanner.Position{ + Filename: "comment.dbc", + Line: 1, + Column: 1, + }, + Comment: "comment", + }, + }, + }, + + { + name: "node_comment.dbc", + text: `CM_ BU_ NodeName "node comment";`, + defs: []Def{ + &CommentDef{ + Pos: scanner.Position{ + Filename: "node_comment.dbc", + Line: 1, + Column: 1, + }, + ObjectType: ObjectTypeNetworkNode, + NodeName: "NodeName", + Comment: "node comment", + }, + }, + }, + + { + name: "message_comment.dbc", + text: `CM_ BO_ 1234 "message comment";`, + defs: []Def{ + &CommentDef{ + Pos: scanner.Position{ + Filename: "message_comment.dbc", + Line: 1, + Column: 1, + }, + ObjectType: ObjectTypeMessage, + MessageID: 1234, + Comment: "message comment", + }, + }, + }, + + { + name: "signal_comment.dbc", + text: `CM_ SG_ 1234 SignalName "signal comment";`, + defs: []Def{ + &CommentDef{ + Pos: scanner.Position{ + Filename: "signal_comment.dbc", + Line: 1, + Column: 1, + }, + ObjectType: ObjectTypeSignal, + MessageID: 1234, + SignalName: "SignalName", + Comment: "signal comment", + }, + }, + }, + + { + name: "int_attribute_definition.dbc", + text: `BA_DEF_ "AttributeName" INT 5 10;`, + defs: []Def{ + &AttributeDef{ + Pos: scanner.Position{ + Filename: "int_attribute_definition.dbc", + Line: 1, + Column: 1, + }, + Name: "AttributeName", + Type: AttributeValueTypeInt, + MinimumInt: 5, + MaximumInt: 10, + }, + }, + }, + + { + name: "int_attribute_definition_no_min_or_max.dbc", + text: `BA_DEF_ "AttributeName" INT;`, + defs: []Def{ + &AttributeDef{ + Pos: scanner.Position{ + Filename: "int_attribute_definition_no_min_or_max.dbc", + Line: 1, + Column: 1, + }, + Name: "AttributeName", + Type: AttributeValueTypeInt, + }, + }, + }, + + { + name: "float_attribute_definition.dbc", + text: `BA_DEF_ "AttributeName" FLOAT 0.5 1.5;`, + defs: []Def{ + &AttributeDef{ + Pos: scanner.Position{ + Filename: "float_attribute_definition.dbc", + Line: 1, + Column: 1, + }, + Name: "AttributeName", + Type: AttributeValueTypeFloat, + MinimumFloat: 0.5, + MaximumFloat: 1.5, + }, + }, + }, + + { + name: "float_attribute_definition_no_min_or_max.dbc", + text: `BA_DEF_ "AttributeName" FLOAT;`, + defs: []Def{ + &AttributeDef{ + Pos: scanner.Position{ + Filename: "float_attribute_definition_no_min_or_max.dbc", + Line: 1, + Column: 1, + }, + Name: "AttributeName", + Type: AttributeValueTypeFloat, + }, + }, + }, + + { + name: "string_attribute.dbc", + text: `BA_DEF_ "AttributeName" STRING;`, + defs: []Def{ + &AttributeDef{ + Pos: scanner.Position{ + Filename: "string_attribute.dbc", + Line: 1, + Column: 1, + }, + Name: "AttributeName", + Type: AttributeValueTypeString, + }, + }, + }, + + { + name: "enum_attribute.dbc", + text: `BA_DEF_ "AttributeName" ENUM "value1","value2";`, + defs: []Def{ + &AttributeDef{ + Pos: scanner.Position{ + Filename: "enum_attribute.dbc", + Line: 1, + Column: 1, + }, + Name: "AttributeName", + Type: AttributeValueTypeEnum, + EnumValues: []string{"value1", "value2"}, + }, + }, + }, + + { + name: "enum_attribute_for_messages.dbc", + text: `BA_DEF_ BO_ "VFrameFormat" ENUM "StandardCAN","ExtendedCAN","reserved","J1939PG";`, + defs: []Def{ + &AttributeDef{ + Pos: scanner.Position{ + Filename: "enum_attribute_for_messages.dbc", + Line: 1, + Column: 1, + }, + Name: "VFrameFormat", + ObjectType: ObjectTypeMessage, + Type: AttributeValueTypeEnum, + EnumValues: []string{"StandardCAN", "ExtendedCAN", "reserved", "J1939PG"}, + }, + }, + }, + + { + name: "attribute_default_string.dbc", + text: strings.Join([]string{ + `BA_DEF_ "Foo" STRING;`, + `BA_DEF_DEF_ "Foo" "string value";`, + }, "\n"), + defs: []Def{ + &AttributeDef{ + Pos: scanner.Position{ + Filename: "attribute_default_string.dbc", + Line: 1, + Column: 1, + }, + Name: "Foo", + Type: AttributeValueTypeString, + }, + &AttributeDefaultValueDef{ + Pos: scanner.Position{ + Filename: "attribute_default_string.dbc", + Line: 2, + Column: 1, + Offset: 22, + }, + AttributeName: "Foo", + DefaultStringValue: "string value", + }, + }, + }, + + { + name: "attribute_default_int.dbc", + text: strings.Join([]string{ + `BA_DEF_ "Foo" INT 0 200;`, + `BA_DEF_DEF_ "Foo" 100;`, + }, "\n"), + defs: []Def{ + &AttributeDef{ + Pos: scanner.Position{ + Filename: "attribute_default_int.dbc", + Line: 1, + Column: 1, + }, + Name: "Foo", + Type: AttributeValueTypeInt, + MinimumInt: 0, + MaximumInt: 200, + }, + &AttributeDefaultValueDef{ + Pos: scanner.Position{ + Filename: "attribute_default_int.dbc", + Line: 2, + Column: 1, + Offset: 25, + }, + AttributeName: "Foo", + DefaultIntValue: 100, + }, + }, + }, + + { + name: "attribute_default_float.dbc", + text: strings.Join([]string{ + `BA_DEF_ "Foo" FLOAT 0.5 200.5;`, + `BA_DEF_DEF_ "Foo" 100.5;`, + }, "\n"), + defs: []Def{ + &AttributeDef{ + Pos: scanner.Position{ + Filename: "attribute_default_float.dbc", + Line: 1, + Column: 1, + }, + Name: "Foo", + Type: AttributeValueTypeFloat, + MinimumFloat: 0.5, + MaximumFloat: 200.5, + }, + &AttributeDefaultValueDef{ + Pos: scanner.Position{ + Filename: "attribute_default_float.dbc", + Line: 2, + Column: 1, + Offset: 31, + }, + AttributeName: "Foo", + DefaultFloatValue: 100.5, + }, + }, + }, + + { + name: "attribute_value.dbc", + text: strings.Join([]string{ + `BA_DEF_ "Foo" FLOAT;`, + `BA_ "Foo" 100.5;`, + }, "\n"), + defs: []Def{ + &AttributeDef{ + Pos: scanner.Position{ + Filename: "attribute_value.dbc", + Line: 1, + Column: 1, + }, + Name: "Foo", + Type: AttributeValueTypeFloat, + }, + &AttributeValueForObjectDef{ + Pos: scanner.Position{ + Filename: "attribute_value.dbc", + Line: 2, + Column: 1, + Offset: 21, + }, + AttributeName: "Foo", + FloatValue: 100.5, + }, + }, + }, + + { + name: "negative_attribute_value.dbc", + text: strings.Join([]string{ + `BA_DEF_ "Foo" INT;`, + `BA_ "Foo" -100;`, + }, "\n"), + defs: []Def{ + &AttributeDef{ + Pos: scanner.Position{ + Filename: "negative_attribute_value.dbc", + Line: 1, + Column: 1, + }, + Name: "Foo", + Type: AttributeValueTypeInt, + }, + &AttributeValueForObjectDef{ + Pos: scanner.Position{ + Filename: "negative_attribute_value.dbc", + Line: 2, + Column: 1, + Offset: 19, + }, + AttributeName: "Foo", + IntValue: -100, + }, + }, + }, + + { + name: "node_attribute_value.dbc", + text: strings.Join([]string{ + `BA_DEF_ "Foo" INT;`, + `BA_ "Foo" BU_ TestNode 100;`, + }, "\n"), + defs: []Def{ + &AttributeDef{ + Pos: scanner.Position{ + Filename: "node_attribute_value.dbc", + Line: 1, + Column: 1, + }, + Name: "Foo", + Type: AttributeValueTypeInt, + }, + &AttributeValueForObjectDef{ + Pos: scanner.Position{ + Filename: "node_attribute_value.dbc", + Line: 2, + Column: 1, + Offset: 19, + }, + AttributeName: "Foo", + ObjectType: ObjectTypeNetworkNode, + NodeName: "TestNode", + IntValue: 100, + }, + }, + }, + + { + name: "message_attribute_value.dbc", + text: strings.Join([]string{ + `BA_DEF_ "Foo" STRING;`, + `BA_ "Foo" BO_ 1234 "string value";`, + }, "\n"), + defs: []Def{ + &AttributeDef{ + Pos: scanner.Position{ + Filename: "message_attribute_value.dbc", + Line: 1, + Column: 1, + }, + Name: "Foo", + Type: AttributeValueTypeString, + }, + &AttributeValueForObjectDef{ + Pos: scanner.Position{ + Filename: "message_attribute_value.dbc", + Line: 2, + Column: 1, + Offset: 22, + }, + AttributeName: "Foo", + ObjectType: ObjectTypeMessage, + MessageID: 1234, + StringValue: "string value", + }, + }, + }, + + { + name: "signal_attribute_value.dbc", + text: strings.Join([]string{ + `BA_DEF_ "Foo" STRING;`, + `BA_ "Foo" SG_ 1234 SignalName "string value";`, + }, "\n"), + defs: []Def{ + &AttributeDef{ + Pos: scanner.Position{ + Filename: "signal_attribute_value.dbc", + Line: 1, + Column: 1, + }, + Name: "Foo", + Type: AttributeValueTypeString, + }, + &AttributeValueForObjectDef{ + Pos: scanner.Position{ + Filename: "signal_attribute_value.dbc", + Line: 2, + Column: 1, + Offset: 22, + }, + AttributeName: "Foo", + ObjectType: ObjectTypeSignal, + MessageID: 1234, + SignalName: "SignalName", + StringValue: "string value", + }, + }, + }, + + { + name: "enum_attribute_value_by_index.dbc", + text: strings.Join([]string{ + `BA_DEF_ BO_ "VFrameFormat" ENUM "StandardCAN","ExtendedCAN","reserved","J1939PG";`, + `BA_ "VFrameFormat" BO_ 1234 3;`, + }, "\n"), + defs: []Def{ + &AttributeDef{ + Pos: scanner.Position{ + Filename: "enum_attribute_value_by_index.dbc", + Line: 1, + Column: 1, + }, + Name: "VFrameFormat", + ObjectType: ObjectTypeMessage, + Type: AttributeValueTypeEnum, + EnumValues: []string{"StandardCAN", "ExtendedCAN", "reserved", "J1939PG"}, + }, + &AttributeValueForObjectDef{ + Pos: scanner.Position{ + Filename: "enum_attribute_value_by_index.dbc", + Line: 2, + Column: 1, + Offset: 84, + }, + AttributeName: "VFrameFormat", + ObjectType: ObjectTypeMessage, + MessageID: 1234, + StringValue: "J1939PG", + }, + }, + }, + + { + name: "value_descriptions_for_signal.dbc", + text: `VAL_ 3 StW_AnglSens_Id 2 "MUST" 0 "PSBL" 1 "SELF";`, + defs: []Def{ + &ValueDescriptionsDef{ + Pos: scanner.Position{ + Filename: "value_descriptions_for_signal.dbc", + Line: 1, + Column: 1, + }, + ObjectType: ObjectTypeSignal, + MessageID: 3, + SignalName: "StW_AnglSens_Id", + ValueDescriptions: []ValueDescriptionDef{ + { + Pos: scanner.Position{ + Filename: "value_descriptions_for_signal.dbc", + Line: 1, + Column: 24, + Offset: 23, + }, + Value: 2, + Description: "MUST", + }, + { + Pos: scanner.Position{ + Filename: "value_descriptions_for_signal.dbc", + Line: 1, + Column: 33, + Offset: 32, + }, + Value: 0, + Description: "PSBL", + }, + { + Pos: scanner.Position{ + Filename: "value_descriptions_for_signal.dbc", + Line: 1, + Column: 42, + Offset: 41, + }, + Value: 1, + Description: "SELF", + }, + }, + }, + }, + }, + + { + name: "value_table.dbc", + text: `VAL_TABLE_ DI_gear 7 "DI_GEAR_SNA" 4 "DI_GEAR_D";`, + defs: []Def{ + &ValueTableDef{ + Pos: scanner.Position{ + Filename: "value_table.dbc", + Line: 1, + Column: 1, + }, + TableName: "DI_gear", + ValueDescriptions: []ValueDescriptionDef{ + { + Pos: scanner.Position{ + Filename: "value_table.dbc", + Line: 1, + Column: 20, + Offset: 19, + }, + Value: 7, + Description: "DI_GEAR_SNA", + }, + { + Pos: scanner.Position{ + Filename: "value_table.dbc", + Line: 1, + Column: 36, + Offset: 35, + }, + Value: 4, + Description: "DI_GEAR_D", + }, + }, + }, + }, + }, + + { + name: "node_list.dbc", + text: `BU_: RSDS`, + defs: []Def{ + &NodesDef{ + Pos: scanner.Position{ + Filename: "node_list.dbc", + Line: 1, + Column: 1, + }, + NodeNames: []Identifier{"RSDS"}, + }, + }, + }, + + { + name: "node_list_followed_by_single_newline_followed_by_value_table.dbc", + text: strings.Join([]string{ + `BU_: RSDS`, + `VAL_TABLE_ TableName 3 "Value3" 2 "Value2" 1 "Value1" 0 "Value0";`, + }, "\n"), + defs: []Def{ + &NodesDef{ + Pos: scanner.Position{ + Filename: "node_list_followed_by_single_newline_followed_by_value_table.dbc", + Line: 1, + Column: 1, + }, + NodeNames: []Identifier{"RSDS"}, + }, + &ValueTableDef{ + Pos: scanner.Position{ + Filename: "node_list_followed_by_single_newline_followed_by_value_table.dbc", + Line: 2, + Column: 1, + Offset: 10, + }, + TableName: "TableName", + ValueDescriptions: []ValueDescriptionDef{ + { + Pos: scanner.Position{ + Filename: "node_list_followed_by_single_newline_followed_by_value_table.dbc", + Line: 2, + Column: 22, + Offset: 31, + }, + Value: 3, + Description: "Value3", + }, + { + Pos: scanner.Position{ + Filename: "node_list_followed_by_single_newline_followed_by_value_table.dbc", + Line: 2, + Column: 33, + Offset: 42, + }, + Value: 2, + Description: "Value2", + }, + { + Pos: scanner.Position{ + Filename: "node_list_followed_by_single_newline_followed_by_value_table.dbc", + Line: 2, + Column: 44, + Offset: 53, + }, + Value: 1, + Description: "Value1", + }, + { + Pos: scanner.Position{ + Filename: "node_list_followed_by_single_newline_followed_by_value_table.dbc", + Line: 2, + Column: 55, + Offset: 64, + }, + Value: 0, + Description: "Value0", + }, + }, + }, + }, + }, + + { + name: "signal_value_type.dbc", + text: `SIG_VALTYPE_ 42 TestSignal 2;`, + defs: []Def{ + &SignalValueTypeDef{ + Pos: scanner.Position{ + Filename: "signal_value_type.dbc", + Line: 1, + Column: 1, + }, + MessageID: 42, + SignalName: "TestSignal", + SignalValueType: SignalValueTypeFloat64, + }, + }, + }, + + { + name: "signal_value_type_with_colon.dbc", + text: `SIG_VALTYPE_ 42 TestSignal : 2;`, + defs: []Def{ + &SignalValueTypeDef{ + Pos: scanner.Position{ + Filename: "signal_value_type_with_colon.dbc", + Line: 1, + Column: 1, + }, + MessageID: 42, + SignalName: "TestSignal", + SignalValueType: SignalValueTypeFloat64, + }, + }, + }, + + { + name: "message_transmitters.dbc", + text: `BO_TX_BU_ 42: Node1 Node2;`, + defs: []Def{ + &MessageTransmittersDef{ + Pos: scanner.Position{ + Filename: "message_transmitters.dbc", + Line: 1, + Column: 1, + }, + MessageID: 42, + Transmitters: []Identifier{"Node1", "Node2"}, + }, + }, + }, + + { + name: "message_transmitters_comma_separated.dbc", + text: `BO_TX_BU_ 42: Node1,Node2;`, + defs: []Def{ + &MessageTransmittersDef{ + Pos: scanner.Position{ + Filename: "message_transmitters_comma_separated.dbc", + Line: 1, + Column: 1, + }, + MessageID: 42, + Transmitters: []Identifier{"Node1", "Node2"}, + }, + }, + }, + + { + name: "environment_variable_data.dbc", + text: `ENVVAR_DATA_ VariableName: 42;`, + defs: []Def{ + &EnvironmentVariableDataDef{ + Pos: scanner.Position{ + Filename: "environment_variable_data.dbc", + Line: 1, + Column: 1, + }, + EnvironmentVariableName: "VariableName", + DataSize: 42, + }, + }, + }, + + { + name: "environment_variable_value_descriptions.dbc", + text: `VAL_ VariableName 2 "Value2" 1 "Value1" 0 "Value0";`, + defs: []Def{ + &ValueDescriptionsDef{ + Pos: scanner.Position{ + Filename: "environment_variable_value_descriptions.dbc", + Line: 1, + Column: 1, + }, + ObjectType: ObjectTypeEnvironmentVariable, + EnvironmentVariableName: "VariableName", + ValueDescriptions: []ValueDescriptionDef{ + { + Pos: scanner.Position{ + Filename: "environment_variable_value_descriptions.dbc", + Line: 1, + Column: 19, + Offset: 18, + }, + Value: 2, + Description: "Value2", + }, + { + Pos: scanner.Position{ + Filename: "environment_variable_value_descriptions.dbc", + Line: 1, + Column: 30, + Offset: 29, + }, + Value: 1, + Description: "Value1", + }, + { + Pos: scanner.Position{ + Filename: "environment_variable_value_descriptions.dbc", + Line: 1, + Column: 41, + Offset: 40, + }, + Value: 0, + Description: "Value0", + }, + }, + }, + }, + }, + + { + name: "unknown_def.dbc", + text: `FOO_ Bar 2 Baz;`, + defs: []Def{ + &UnknownDef{ + Pos: scanner.Position{ + Filename: "unknown_def.dbc", + Line: 1, + Column: 1, + }, + Keyword: "FOO_", + }, + }, + }, + } { + tt := tt + t.Run(tt.name, func(t *testing.T) { + p := NewParser(tt.name, []byte(tt.text)) + require.NoError(t, p.Parse()) + require.Equal(t, tt.defs, p.Defs()) + }) + } +} + +func TestParser_Parse_Error(t *testing.T) { + for _, tt := range []struct { + name string + text string + err *parseError + }{ + { + name: "non_string_version.dbc", + text: "VERSION foo", + err: &parseError{ + pos: scanner.Position{ + Filename: "non_string_version.dbc", + Line: 1, + Column: 9, + Offset: 8, + }, + reason: "expected token \"", + }, + }, + { + name: "invalid_utf8.dbc", + text: `VERSION "foo` + string([]byte{0xc3, 0x28}) + `"`, + err: &parseError{ + pos: scanner.Position{ + Filename: "invalid_utf8.dbc", + Line: 1, + Column: 13, + Offset: 12, + }, + reason: "invalid UTF-8 encoding", + }, + }, + } { + tt := tt + t.Run(tt.name, func(t *testing.T) { + p := NewParser(tt.name, []byte(tt.text)) + require.Equal(t, tt.err, p.Parse()) + }) + } +} + +func dump(data interface{}) string { + spewConfig := spew.ConfigState{ + Indent: " ", + DisablePointerAddresses: true, + DisableCapacities: true, + SortKeys: true, + } + return spewConfig.Sdump(data) +} diff --git a/pkg/dbc/placeholder.go b/pkg/dbc/placeholder.go new file mode 100644 index 0000000..faf1fd9 --- /dev/null +++ b/pkg/dbc/placeholder.go @@ -0,0 +1,4 @@ +package dbc + +// NodePlaceholder is the placeholder node name used when no actual node is specified. +const NodePlaceholder Identifier = "Vector__XXX" diff --git a/pkg/dbc/signalvaluetype.go b/pkg/dbc/signalvaluetype.go new file mode 100644 index 0000000..f461f6c --- /dev/null +++ b/pkg/dbc/signalvaluetype.go @@ -0,0 +1,24 @@ +package dbc + +import "fmt" + +// SignalValueType represents an extended signal value type. +type SignalValueType uint64 + +const ( + SignalValueTypeInt SignalValueType = 0 + SignalValueTypeFloat32 SignalValueType = 1 + SignalValueTypeFloat64 SignalValueType = 2 +) + +// Validate returns an error for invalid signal value types. +func (s SignalValueType) Validate() error { + switch s { + case SignalValueTypeInt: + case SignalValueTypeFloat32: + case SignalValueTypeFloat64: + default: + return fmt.Errorf("invalid signal value type: %v", s) + } + return nil +} diff --git a/pkg/dbc/signalvaluetype_test.go b/pkg/dbc/signalvaluetype_test.go new file mode 100644 index 0000000..153080d --- /dev/null +++ b/pkg/dbc/signalvaluetype_test.go @@ -0,0 +1,21 @@ +package dbc + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestSignalValueType_Validate(t *testing.T) { + for _, tt := range []SignalValueType{ + SignalValueTypeInt, + SignalValueTypeFloat32, + SignalValueTypeFloat64, + } { + require.NoError(t, tt.Validate()) + } +} + +func TestSignalValueType_Validate_Error(t *testing.T) { + require.Error(t, SignalValueType(42).Validate()) +} diff --git a/pkg/descriptor/database.go b/pkg/descriptor/database.go new file mode 100644 index 0000000..c85b232 --- /dev/null +++ b/pkg/descriptor/database.go @@ -0,0 +1,57 @@ +package descriptor + +import ( + "path" + "strings" +) + +// Database represents a CAN database. +type Database struct { + // SourceFile of the database. + // + // Example: + // github.com/einride/can-databases/dbc/j1939.dbc + SourceFile string + // Version of the database. + Version string + // Messages in the database. + Messages []*Message + // Nodes in the database. + Nodes []*Node +} + +func (d *Database) Node(nodeName string) (*Node, bool) { + for _, n := range d.Nodes { + if n.Name == nodeName { + return n, true + } + } + return nil, false +} + +func (d *Database) Message(id uint32) (*Message, bool) { + for _, m := range d.Messages { + if m.ID == id { + return m, true + } + } + return nil, false +} + +func (d *Database) Signal(messageID uint32, signalName string) (*Signal, bool) { + message, ok := d.Message(messageID) + if !ok { + return nil, false + } + for _, s := range message.Signals { + if s.Name == signalName { + return s, true + } + } + return nil, false +} + +// Description returns the name of the Database. +func (d *Database) Name() string { + return strings.TrimSuffix(path.Base(d.SourceFile), path.Ext(d.SourceFile)) +} diff --git a/pkg/descriptor/message.go b/pkg/descriptor/message.go new file mode 100644 index 0000000..9f6a701 --- /dev/null +++ b/pkg/descriptor/message.go @@ -0,0 +1,37 @@ +package descriptor + +import "time" + +// Message describes a CAN message. +type Message struct { + // Description of the message. + Name string + // ID of the message. + ID uint32 + // IsExtended is true if the message is an extended CAN message. + IsExtended bool + // Length in bytes. + Length uint8 + // SendType is the message's send type. + SendType SendType + // Description of the message. + Description string + // Signals in the message payload. + Signals []*Signal + // SenderNode is the name of the node sending the message. + SenderNode string + // CycleTime is the cycle time of a cyclic message. + CycleTime time.Duration + // DelayTime is the allowed delay between cyclic message sends. + DelayTime time.Duration +} + +// MultiplexerSignal returns the message's multiplexer signal. +func (m *Message) MultiplexerSignal() (*Signal, bool) { + for _, s := range m.Signals { + if s.IsMultiplexer { + return s, true + } + } + return nil, false +} diff --git a/pkg/descriptor/message_test.go b/pkg/descriptor/message_test.go new file mode 100644 index 0000000..1e123b8 --- /dev/null +++ b/pkg/descriptor/message_test.go @@ -0,0 +1,36 @@ +package descriptor + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestMessage_MultiplexerSignal(t *testing.T) { + mux := &Signal{ + Name: "Mux", + IsMultiplexer: true, + } + m := &Message{ + Signals: []*Signal{ + {Name: "NotMux"}, + mux, + {Name: "AlsoNotMux"}, + }, + } + actualMux, ok := m.MultiplexerSignal() + require.True(t, ok) + require.Equal(t, mux, actualMux) +} + +func TestMessage_MultiplexerSignal_NotFound(t *testing.T) { + m := &Message{ + Signals: []*Signal{ + {Name: "NotMux"}, + {Name: "AlsoNotMux"}, + }, + } + actualMux, ok := m.MultiplexerSignal() + require.False(t, ok) + require.Nil(t, actualMux) +} diff --git a/pkg/descriptor/node.go b/pkg/descriptor/node.go new file mode 100644 index 0000000..ac5ac08 --- /dev/null +++ b/pkg/descriptor/node.go @@ -0,0 +1,9 @@ +package descriptor + +// Node describes a CAN node. +type Node struct { + // Description of the CAN node. + Name string + // Description of the CAN node. + Description string +} diff --git a/pkg/descriptor/sendtype.go b/pkg/descriptor/sendtype.go new file mode 100644 index 0000000..67ae7ca --- /dev/null +++ b/pkg/descriptor/sendtype.go @@ -0,0 +1,31 @@ +package descriptor + +import "strings" + +// SendType represents the send type of a message. +type SendType uint8 + +//go:generate gobin -m -run golang.org/x/tools/cmd/stringer -type SendType -trimprefix SendType + +const ( + // SendTypeNone means the send type is unknown or not specified. + SendTypeNone SendType = iota + // SendTypeCyclic means the message is sent cyclically. + SendTypeCyclic + // SendTypeEvent means the message is only sent upon event or request. + SendTypeEvent +) + +// UnmarshalString sets the value of *s from the provided string. +func (s *SendType) UnmarshalString(str string) error { + // TODO: Decide on conventions and make this more strict + switch strings.ToLower(str) { + case "cyclic", "cyclicifactive", "periodic", "fixedperiodic", "enabledperiodic", "eventperiodic": + *s = SendTypeCyclic + case "event", "onevent": + *s = SendTypeEvent + default: + *s = SendTypeNone + } + return nil +} diff --git a/pkg/descriptor/sendtype_string.go b/pkg/descriptor/sendtype_string.go new file mode 100644 index 0000000..8194b4e --- /dev/null +++ b/pkg/descriptor/sendtype_string.go @@ -0,0 +1,25 @@ +// Code generated by "stringer -type SendType -trimprefix SendType"; DO NOT EDIT. + +package descriptor + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[SendTypeNone-0] + _ = x[SendTypeCyclic-1] + _ = x[SendTypeEvent-2] +} + +const _SendType_name = "NoneCyclicEvent" + +var _SendType_index = [...]uint8{0, 4, 10, 15} + +func (i SendType) String() string { + if i >= SendType(len(_SendType_index)-1) { + return "SendType(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _SendType_name[_SendType_index[i]:_SendType_index[i+1]] +} diff --git a/pkg/descriptor/sendtype_test.go b/pkg/descriptor/sendtype_test.go new file mode 100644 index 0000000..097c4c4 --- /dev/null +++ b/pkg/descriptor/sendtype_test.go @@ -0,0 +1,26 @@ +package descriptor + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestSendType_UnmarshalString(t *testing.T) { + for _, tt := range []struct { + str string + expected SendType + }{ + {str: "Cyclic", expected: SendTypeCyclic}, + {str: "Periodic", expected: SendTypeCyclic}, + {str: "OnEvent", expected: SendTypeEvent}, + {str: "Event", expected: SendTypeEvent}, + } { + tt := tt + t.Run(tt.str, func(t *testing.T) { + var actual SendType + require.NoError(t, actual.UnmarshalString(tt.str)) + require.Equal(t, tt.expected, actual) + }) + } +} diff --git a/pkg/descriptor/signal.go b/pkg/descriptor/signal.go new file mode 100644 index 0000000..4073cdb --- /dev/null +++ b/pkg/descriptor/signal.go @@ -0,0 +1,206 @@ +package descriptor + +import ( + "math" + + "go.einride.tech/can" +) + +// Signal describes a CAN signal. +type Signal struct { + // Description of the signal. + Name string + // Start bit. + Start uint8 + // Length in bits. + Length uint8 + // IsBigEndian is true if the signal is big-endian. + IsBigEndian bool + // IsSigned is true if the signal uses raw signed values. + IsSigned bool + // IsMultiplexer is true if the signal is the multiplexor of a multiplexed message. + IsMultiplexer bool + // IsMultiplexed is true if the signal is multiplexed. + IsMultiplexed bool + // MultiplexerValue is the value of the multiplexer when this signal is present. + MultiplexerValue uint + // Offset for real-world transform. + Offset float64 + // Scale for real-world transform. + Scale float64 + // Min real-world value. + Min float64 + // Max real-world value. + Max float64 + // Unit of the signal. + Unit string + // Description of the signal. + Description string + // ValueDescriptions of the signal. + ValueDescriptions []*ValueDescription + // ReceiverNodes is the list of names of the nodes receiving the signal. + ReceiverNodes []string + // DefaultValue of the signal. + DefaultValue int +} + +// ValueDescription returns the value description for the provided value. +func (s *Signal) ValueDescription(value int) (string, bool) { + for _, vd := range s.ValueDescriptions { + if vd.Value == value { + return vd.Description, true + } + } + return "", false +} + +// ToPhysical converts a raw signal value to its physical value. +func (s *Signal) ToPhysical(value float64) float64 { + result := value + result *= s.Scale + result += s.Offset + if s.Min != 0 || s.Max != 0 { + result = math.Max(math.Min(result, s.Max), s.Min) + } + return result +} + +// FromPhysical converts a physical signal value to its raw value. +func (s *Signal) FromPhysical(physical float64) float64 { + result := physical + if s.Min != 0 || s.Max != 0 { + result = math.Max(math.Min(result, s.Max), s.Min) + } + result -= s.Offset + result /= s.Scale + // perform saturated cast + if s.IsSigned { + result = math.Max(float64(s.MinSigned()), math.Min(float64(s.MaxSigned()), result)) + } else { + result = math.Max(0, math.Min(float64(s.MaxUnsigned()), result)) + } + return result +} + +// UnmarshalPhysical returns the physical value of the signal in the provided CAN frame. +func (s *Signal) UnmarshalPhysical(d can.Data) float64 { + switch { + case s.Length == 1: + if d.Bit(s.Start) { + return 1 + } + return 0 + case s.IsSigned: + var value int64 + if s.IsBigEndian { + value = d.SignedBitsBigEndian(s.Start, s.Length) + } else { + value = d.SignedBitsLittleEndian(s.Start, s.Length) + } + return s.ToPhysical(float64(value)) + default: + var value uint64 + if s.IsBigEndian { + value = d.UnsignedBitsBigEndian(s.Start, s.Length) + } else { + value = d.UnsignedBitsLittleEndian(s.Start, s.Length) + } + return s.ToPhysical(float64(value)) + } +} + +// UnmarshalUnsigned returns the unsigned value of the signal in the provided CAN frame. +func (s *Signal) UnmarshalUnsigned(d can.Data) uint64 { + if s.IsBigEndian { + return d.UnsignedBitsBigEndian(s.Start, s.Length) + } + return d.UnsignedBitsLittleEndian(s.Start, s.Length) +} + +// UnmarshalValueDescription returns the value description of the signal in the provided CAN data. +func (s *Signal) UnmarshalValueDescription(d can.Data) (string, bool) { + if len(s.ValueDescriptions) == 0 { + return "", false + } + var intValue int + if s.IsSigned { + intValue = int(s.UnmarshalSigned(d)) + } else { + intValue = int(s.UnmarshalUnsigned(d)) + } + return s.ValueDescription(intValue) +} + +// UnmarshalSigned returns the signed value of the signal in the provided CAN frame. +func (s *Signal) UnmarshalSigned(d can.Data) int64 { + if s.IsBigEndian { + return d.SignedBitsBigEndian(s.Start, s.Length) + } + return d.SignedBitsLittleEndian(s.Start, s.Length) +} + +// UnmarshalBool returns the bool value of the signal in the provided CAN frame. +func (s *Signal) UnmarshalBool(d can.Data) bool { + return d.Bit(s.Start) +} + +// MarshalUnsigned sets the unsigned value of the signal in the provided CAN frame. +func (s *Signal) MarshalUnsigned(d *can.Data, value uint64) { + if s.IsBigEndian { + d.SetUnsignedBitsBigEndian(s.Start, s.Length, value) + } else { + d.SetUnsignedBitsLittleEndian(s.Start, s.Length, value) + } +} + +// MarshalSigned sets the signed value of the signal in the provided CAN frame. +func (s *Signal) MarshalSigned(d *can.Data, value int64) { + if s.IsBigEndian { + d.SetSignedBitsBigEndian(s.Start, s.Length, value) + } else { + d.SetSignedBitsLittleEndian(s.Start, s.Length, value) + } +} + +// MarshalBool sets the bool value of the signal in the provided CAN frame. +func (s *Signal) MarshalBool(d *can.Data, value bool) { + d.SetBit(s.Start, value) +} + +// MaxUnsigned returns the maximum unsigned value representable by the signal. +func (s *Signal) MaxUnsigned() uint64 { + return (2 << (s.Length - 1)) - 1 +} + +// MinSigned returns the minimum signed value representable by the signal. +func (s *Signal) MinSigned() int64 { + return (2 << (s.Length - 1) / 2) * -1 +} + +// MaxSigned returns the maximum signed value representable by the signal. +func (s *Signal) MaxSigned() int64 { + return (2 << (s.Length - 1) / 2) - 1 +} + +// SaturatedCastSigned performs a saturated cast of an int64 to the value domain of the signal. +func (s *Signal) SaturatedCastSigned(value int64) int64 { + min := s.MinSigned() + max := s.MaxSigned() + switch { + case value < min: + return min + case value > max: + return max + default: + return value + } +} + +// SaturatedCastUnsigned performs a saturated cast of a uint64 to the value domain of the signal. +func (s *Signal) SaturatedCastUnsigned(value uint64) uint64 { + max := s.MaxUnsigned() + if value > max { + return max + } + return value +} diff --git a/pkg/descriptor/signal_test.go b/pkg/descriptor/signal_test.go new file mode 100644 index 0000000..bf9c555 --- /dev/null +++ b/pkg/descriptor/signal_test.go @@ -0,0 +1,85 @@ +package descriptor + +import ( + "math" + "testing" + + "github.com/stretchr/testify/require" + "go.einride.tech/can" +) + +func TestSignal_FromPhysical_SaturatedCast(t *testing.T) { + s := &Signal{ + Name: "TestSignal", + Offset: -1, + Scale: 3.0517578125e-05, + Min: -1, + Max: 1, + Length: 16, + } + // without a saturated cast, the result would be math.MaxUint16 + 1, which would wrap around to 0 + require.Equal(t, uint16(math.MaxUint16), uint16(s.FromPhysical(180))) +} + +func TestSignal_SaturatedCastSigned(t *testing.T) { + s := &Signal{ + Name: "TestSignal", + IsSigned: true, + Length: 6, + } + require.Equal(t, int64(31), s.SaturatedCastSigned(254)) + require.Equal(t, int64(-32), s.SaturatedCastSigned(-255)) +} + +func TestSignal_SaturatedCastUnsigned(t *testing.T) { + s := &Signal{ + Name: "TestSignal", + Length: 6, + } + require.Equal(t, uint64(63), s.SaturatedCastUnsigned(255)) +} + +func TestSignal_UnmarshalSigned_BigEndian(t *testing.T) { + s := &Signal{ + Name: "TestSignal", + IsSigned: true, + IsBigEndian: true, + Length: 8, + Start: 32, + } + const value int64 = -8 + var data can.Data + data.SetSignedBitsBigEndian(s.Start, s.Length, value) + require.Equal(t, value, s.UnmarshalSigned(data)) +} + +func TestSignal_MarshalUnsigned_BigEndian(t *testing.T) { + s := &Signal{ + Name: "TestSignal", + IsBigEndian: true, + Length: 8, + Start: 32, + } + const value uint64 = 8 + var expected can.Data + expected.SetUnsignedBitsBigEndian(s.Start, s.Length, value) + var actual can.Data + s.MarshalUnsigned(&actual, value) + require.Equal(t, expected, actual) +} + +func TestSignal_MarshalSigned_BigEndian(t *testing.T) { + s := &Signal{ + Name: "TestSignal", + IsSigned: true, + IsBigEndian: true, + Length: 8, + Start: 32, + } + const value int64 = -8 + var expected can.Data + expected.SetSignedBitsBigEndian(s.Start, s.Length, value) + var actual can.Data + s.MarshalSigned(&actual, value) + require.Equal(t, expected, actual) +} diff --git a/pkg/descriptor/valuedescription.go b/pkg/descriptor/valuedescription.go new file mode 100644 index 0000000..f874c5b --- /dev/null +++ b/pkg/descriptor/valuedescription.go @@ -0,0 +1,6 @@ +package descriptor + +type ValueDescription struct { + Value int + Description string +} diff --git a/pkg/generated/message.go b/pkg/generated/message.go new file mode 100644 index 0000000..9769232 --- /dev/null +++ b/pkg/generated/message.go @@ -0,0 +1,26 @@ +// Package generated provides primitives for working with code-generated CAN messages. +package generated + +import ( + "fmt" + + "go.einride.tech/can" + "go.einride.tech/can/pkg/descriptor" +) + +// Message represents a code-generated CAN message. +type Message interface { + can.Message + fmt.Stringer + + // Descriptor returns the message descriptor. + Descriptor() *descriptor.Message + + // Reset the message signals to their default values. + Reset() + + // Frame returns a CAN frame representing the message. + // + // A generated message ensures that its signals are valid and is always convertible to a CAN frame. + Frame() can.Frame +} diff --git a/pkg/socketcan/canrawaddr.go b/pkg/socketcan/canrawaddr.go new file mode 100644 index 0000000..8b76386 --- /dev/null +++ b/pkg/socketcan/canrawaddr.go @@ -0,0 +1,20 @@ +package socketcan + +import "net" + +const canRawNetwork = "can" + +// canRawAddr represents a CAN_RAW address +type canRawAddr struct { + device string +} + +var _ net.Addr = &canRawAddr{} + +func (a *canRawAddr) Network() string { + return canRawNetwork +} + +func (a *canRawAddr) String() string { + return a.device +} diff --git a/pkg/socketcan/canrawaddr_test.go b/pkg/socketcan/canrawaddr_test.go new file mode 100644 index 0000000..cbec972 --- /dev/null +++ b/pkg/socketcan/canrawaddr_test.go @@ -0,0 +1,17 @@ +package socketcan + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestCanRawAddr_Network(t *testing.T) { + addr := &canRawAddr{device: "can0"} + require.Equal(t, "can0", addr.String()) +} + +func TestCanRawAddr_String(t *testing.T) { + addr := &canRawAddr{device: "can0"} + require.Equal(t, "can", addr.Network()) +} diff --git a/pkg/socketcan/controllererror.go b/pkg/socketcan/controllererror.go new file mode 100644 index 0000000..8d946c4 --- /dev/null +++ b/pkg/socketcan/controllererror.go @@ -0,0 +1,16 @@ +package socketcan + +type ControllerError uint8 + +//go:generate gobin -m -run golang.org/x/tools/cmd/stringer -type ControllerError -trimprefix ControllerError + +const ( + ControllerErrorUnspecified ControllerError = 0x00 + ControllerErrorRxBufferOverflow ControllerError = 0x01 + ControllerErrorTxBufferOverflow ControllerError = 0x02 + ControllerErrorRxWarning ControllerError = 0x04 + ControllerErrorTxWarning ControllerError = 0x08 + ControllerErrorRxPassive ControllerError = 0x10 + ControllerErrorTxPassive ControllerError = 0x20 // at least one error counter exceeds 127 + ControllerErrorActive ControllerError = 0x40 +) diff --git a/pkg/socketcan/controllererror_string.go b/pkg/socketcan/controllererror_string.go new file mode 100644 index 0000000..b1e45eb --- /dev/null +++ b/pkg/socketcan/controllererror_string.go @@ -0,0 +1,51 @@ +// Code generated by "stringer -type ControllerError -trimprefix ControllerError"; DO NOT EDIT. + +package socketcan + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[ControllerErrorUnspecified-0] + _ = x[ControllerErrorRxBufferOverflow-1] + _ = x[ControllerErrorTxBufferOverflow-2] + _ = x[ControllerErrorRxWarning-4] + _ = x[ControllerErrorTxWarning-8] + _ = x[ControllerErrorRxPassive-16] + _ = x[ControllerErrorTxPassive-32] + _ = x[ControllerErrorActive-64] +} + +const ( + _ControllerError_name_0 = "UnspecifiedRxBufferOverflowTxBufferOverflow" + _ControllerError_name_1 = "RxWarning" + _ControllerError_name_2 = "TxWarning" + _ControllerError_name_3 = "RxPassive" + _ControllerError_name_4 = "TxPassive" + _ControllerError_name_5 = "Active" +) + +var ( + _ControllerError_index_0 = [...]uint8{0, 11, 27, 43} +) + +func (i ControllerError) String() string { + switch { + case 0 <= i && i <= 2: + return _ControllerError_name_0[_ControllerError_index_0[i]:_ControllerError_index_0[i+1]] + case i == 4: + return _ControllerError_name_1 + case i == 8: + return _ControllerError_name_2 + case i == 16: + return _ControllerError_name_3 + case i == 32: + return _ControllerError_name_4 + case i == 64: + return _ControllerError_name_5 + default: + return "ControllerError(" + strconv.FormatInt(int64(i), 10) + ")" + } +} diff --git a/pkg/socketcan/dial.go b/pkg/socketcan/dial.go new file mode 100644 index 0000000..e7024a2 --- /dev/null +++ b/pkg/socketcan/dial.go @@ -0,0 +1,79 @@ +package socketcan + +import ( + "context" + "net" +) + +const udp = "udp" + +// Dial connects to the address on the named net. +// +// Linux only: If net is "can" it creates a SocketCAN connection to the device +// (address is interpreted as a device name). +// +// If net is "udp" it assumes UDP multicast and sets up 2 connections, one for +// receiving and one for transmitting. +// See: https://golang.org/pkg/net/#Dial +func Dial(network, address string) (net.Conn, error) { + switch network { + case udp: + return udpTransceiver(network, address) + case canRawNetwork: + return dialRaw(address) // platform-specific + default: + return net.Dial(network, address) + } +} + +// DialContext connects to the address on the named net using +// the provided context. +// +// Linux only: If net is "can" it creates a SocketCAN connection to the device +// (address is interpreted as a device name). +// +// See: https://golang.org/pkg/net/#Dialer.DialContext +func DialContext(ctx context.Context, network, address string) (net.Conn, error) { + switch network { + case canRawNetwork: + return dialCtx(ctx, func() (net.Conn, error) { + return dialRaw(address) + }) + case udp: + return dialCtx(ctx, func() (net.Conn, error) { + return udpTransceiver(network, address) + }) + default: + var d net.Dialer + return d.DialContext(ctx, network, address) + } +} + +func dialCtx(ctx context.Context, connProvider func() (net.Conn, error)) (net.Conn, error) { + resultChan := make(chan struct { + conn net.Conn + err error + }) + go func() { + conn, err := connProvider() + resultChan <- struct { + conn net.Conn + err error + }{conn: conn, err: err} + }() + // wait for connection or timeout + select { + case result := <-resultChan: + return result.conn, result.err + case <-ctx.Done(): + // timeout - make sure we clean up the connection + // error handling not possible since we've already returned + go func() { + result := <-resultChan + if result.conn != nil { + _ = result.conn.Close() + } + }() + return nil, ctx.Err() + } +} diff --git a/pkg/socketcan/dial_test.go b/pkg/socketcan/dial_test.go new file mode 100644 index 0000000..7fc94f2 --- /dev/null +++ b/pkg/socketcan/dial_test.go @@ -0,0 +1,80 @@ +package socketcan + +import ( + "context" + "net" + "testing" + "time" + + "github.com/stretchr/testify/require" + "go.einride.tech/can" + "golang.org/x/sync/errgroup" +) + +func TestDial_TCP(t *testing.T) { + lis, err := net.Listen("tcp", "localhost:0") + require.NoError(t, err) + var g errgroup.Group + g.Go(func() error { + conn, err := lis.Accept() + if err != nil { + return err + } + return conn.Close() + }) + conn, err := Dial("tcp", lis.Addr().String()) + require.NoError(t, err) + require.NoError(t, conn.Close()) + require.NoError(t, g.Wait()) +} + +func TestDialContext_TCP(t *testing.T) { + lis, err := net.Listen("tcp", "localhost:0") + require.NoError(t, err) + var g errgroup.Group + g.Go(func() error { + conn, err := lis.Accept() + if err != nil { + return err + } + return conn.Close() + }) + ctx, done := context.WithTimeout(context.Background(), time.Second) + defer done() + conn, err := DialContext(ctx, "tcp", lis.Addr().String()) + require.NoError(t, err) + require.NoError(t, conn.Close()) + require.NoError(t, g.Wait()) +} + +func TestConn_TransmitReceiveTCP(t *testing.T) { + // Given: A TCP listener that writes a frame on an accepted connection + lis, err := net.Listen("tcp", "localhost:0") + require.NoError(t, err) + var g errgroup.Group + frame := can.Frame{ID: 42, Length: 5, Data: can.Data{'H', 'e', 'l', 'l', 'o'}} + g.Go(func() error { + conn, err := lis.Accept() + if err != nil { + return err + } + tr := NewTransmitter(conn) + ctx, done := context.WithTimeout(context.Background(), time.Second) + defer done() + if err := tr.TransmitFrame(ctx, frame); err != nil { + return err + } + return conn.Close() + }) + // When: We connect to the listener + ctx, done := context.WithTimeout(context.Background(), time.Second) + defer done() + conn, err := DialContext(ctx, "tcp", lis.Addr().String()) + require.NoError(t, err) + rec := NewReceiver(conn) + require.True(t, rec.Receive()) + require.False(t, rec.HasErrorFrame()) + require.Equal(t, frame, rec.Frame()) + require.NoError(t, conn.Close()) + require.NoError(t, g.Wait()) +} diff --git a/pkg/socketcan/dialraw_linux.go b/pkg/socketcan/dialraw_linux.go new file mode 100644 index 0000000..939366f --- /dev/null +++ b/pkg/socketcan/dialraw_linux.go @@ -0,0 +1,36 @@ +// +build linux +// +build go1.12 + +package socketcan + +import ( + "fmt" + "net" + "os" + + "golang.org/x/sys/unix" +) + +func dialRaw(device string) (conn net.Conn, err error) { + defer func() { + if err != nil { + err = &net.OpError{Op: "dial", Net: canRawNetwork, Addr: &canRawAddr{device: device}, Err: err} + } + }() + ifi, err := net.InterfaceByName(device) + if err != nil { + return nil, fmt.Errorf("interface %s: %w", device, err) + } + fd, err := unix.Socket(unix.AF_CAN, unix.SOCK_RAW, unix.CAN_RAW) + if err != nil { + return nil, fmt.Errorf("socket: %w", err) + } + // put fd in non-blocking mode so the created file will be registered by the runtime poller (Go >= 1.12) + if err := unix.SetNonblock(fd, true); err != nil { + return nil, fmt.Errorf("set nonblock: %w", err) + } + if err := unix.Bind(fd, &unix.SockaddrCAN{Ifindex: ifi.Index}); err != nil { + return nil, fmt.Errorf("bind: %w", err) + } + return &fileConn{ra: &canRawAddr{device: device}, f: os.NewFile(uintptr(fd), "can")}, nil +} diff --git a/pkg/socketcan/dialraw_linux_test.go b/pkg/socketcan/dialraw_linux_test.go new file mode 100644 index 0000000..3ed1a3f --- /dev/null +++ b/pkg/socketcan/dialraw_linux_test.go @@ -0,0 +1,169 @@ +// +build linux +// +build go1.12 + +package socketcan + +import ( + "context" + "fmt" + "net" + "runtime" + "testing" + "time" + + "github.com/stretchr/testify/require" + "go.einride.tech/can" + "golang.org/x/sync/errgroup" +) + +func TestDial_CANRaw(t *testing.T) { + requireVCAN0(t) + conn, err := Dial("can", "vcan0") + require.NoError(t, err) + require.NoError(t, conn.Close()) +} + +func TestDialContext_CANRaw(t *testing.T) { + requireVCAN0(t) + ctx, done := context.WithTimeout(context.Background(), time.Second) + defer done() + conn, err := DialContext(ctx, "can", "vcan0") + require.NoError(t, err) + require.NoError(t, conn.Close()) +} + +func TestConn_DialFail(t *testing.T) { + t.Run("bad file name", func(t *testing.T) { + _, err := Dial("can", "badFileName#") + require.Error(t, err, "Dial to a can device that does not exist should not succeed") + }) + t.Run("timeout", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + cancel() + _, err := DialContext(ctx, "can", "vcan0") + require.Error(t, err, "DialContext with closed context should not succeed") + }) +} + +func TestConn_Addr(t *testing.T) { + requireVCAN0(t) + conn, err := Dial("can", "vcan0") + require.NoError(t, err) + require.Nil(t, conn.LocalAddr()) // SocketCAN connections don't have a local connection + require.Equal(t, "can", conn.RemoteAddr().Network()) + require.Equal(t, "vcan0", conn.RemoteAddr().String()) +} + +func TestConn_SetDeadline(t *testing.T) { + requireVCAN0(t) + // Given that a vcan device exists and that I can open a connection to it + receiver, err := Dial("can", "vcan0") + require.NoError(t, err) + // When I set the can + timeout := 20 * time.Millisecond + require.NoError(t, receiver.SetDeadline(time.Now().Add(timeout))) + // Then I expect a read without a corresponding write to time out + data := make([]byte, lengthOfFrame) + n, err := receiver.Read(data) + require.Equal(t, 0, n) + require.Error(t, err) + // When I clear the timeouts + require.NoError(t, receiver.SetDeadline(time.Time{})) + // Then I don't expect the read to timeout anymore + errChan := make(chan error, 1) + go func() { + _, err = receiver.Read(data) + errChan <- err + }() + select { + case <-errChan: + t.Fatal("unexpected read result") + case <-time.After(timeout): + require.NoError(t, receiver.Close()) + require.Error(t, <-errChan) + } +} + +func TestConn_ReadWrite(t *testing.T) { + requireVCAN0(t) + // given a reader and writer + reader, err := Dial("can", "vcan0") + require.NoError(t, err) + writer, err := Dial("can", "vcan0") + require.NoError(t, err) + // when the reader reads + var g errgroup.Group + var readFrame can.Frame + g.Go(func() error { + rec := NewReceiver(reader) + if !rec.Receive() { + return fmt.Errorf("receive") + } + readFrame = rec.Frame() + return reader.Close() + }) + // and the writer writes + writeFrame := can.Frame{ID: 32} + tr := NewTransmitter(writer) + ctx, done := context.WithTimeout(context.Background(), time.Second) + defer done() + require.NoError(t, tr.TransmitFrame(ctx, writeFrame)) + require.NoError(t, writer.Close()) + // then the written and read frames should be identical + require.NoError(t, g.Wait()) + require.Equal(t, writeFrame, readFrame) +} + +func TestConn_WriteOnClosedFails(t *testing.T) { + requireVCAN0(t) + conn, err := Dial("can", "vcan0") + require.NoError(t, err) + tr := NewTransmitter(conn) + ctx, done := context.WithTimeout(context.Background(), time.Second) + defer done() + require.NoError(t, tr.TransmitFrame(ctx, can.Frame{})) + // When I close the connection and then write to it + require.NoError(t, conn.Close()) + // Then it should fail + require.Error(t, tr.TransmitFrame(ctx, can.Frame{}), "WriteFrame on a closed Conn should fail") +} + +func TestConn_ReadOnClose(t *testing.T) { + requireVCAN0(t) + t.Run("close then read", func(t *testing.T) { + conn, err := Dial("can", "vcan0") + require.NoError(t, err) + // When I close the connection and then read from it + require.NoError(t, conn.Close()) + rec := NewReceiver(conn) + require.False(t, rec.Receive()) + require.Error(t, rec.Err()) + }) + t.Run("read then close", func(t *testing.T) { + conn, err := Dial("can", "vcan0") + require.NoError(t, err) + // And when I read from a connection + var g errgroup.Group + var receiveErr error + g.Go(func() error { + rec := NewReceiver(conn) + if rec.Receive() { + return fmt.Errorf("receive") + } + receiveErr = rec.Err() + return nil + }) + runtime.Gosched() + // And then close it + require.NoError(t, conn.Close()) + // Then the read operation should fail + require.NoError(t, g.Wait()) + require.Error(t, receiveErr) + }) +} + +func requireVCAN0(t *testing.T) { + if _, err := net.InterfaceByName("vcan0"); err != nil { + t.Skip("device vcan0 not available") + } +} diff --git a/pkg/socketcan/dialraw_others.go b/pkg/socketcan/dialraw_others.go new file mode 100644 index 0000000..0e18ce6 --- /dev/null +++ b/pkg/socketcan/dialraw_others.go @@ -0,0 +1,13 @@ +// +build !linux !go1.12 + +package socketcan + +import ( + "fmt" + "net" + "runtime" +) + +func dialRaw(interfaceName string) (net.Conn, error) { + return nil, fmt.Errorf("SocketCAN not supported on OS %s and runtime %s", runtime.GOOS, runtime.Version()) +} diff --git a/pkg/socketcan/emulator.go b/pkg/socketcan/emulator.go new file mode 100644 index 0000000..cb8fc34 --- /dev/null +++ b/pkg/socketcan/emulator.go @@ -0,0 +1,270 @@ +package socketcan + +import ( + "context" + "fmt" + "log" + "net" + "os" + "strings" + "sync" + "time" + + "go.einride.tech/can" + "golang.org/x/sync/errgroup" +) + +type emulatorCfg struct { + address string + logger *log.Logger +} + +func defaultCfg() emulatorCfg { + stdLogger := log.New(os.Stderr, "emulator: ", log.Lshortfile|log.Ltime) + return emulatorCfg{ + address: "239.64.142.206:0", + logger: stdLogger, + } +} + +// EmulatorOption represents a way to configure an Emulator prior to creating it. +type EmulatorOption func(*emulatorCfg) + +// WithMulticastAddress sets the address for the multicast group that the Emulator should listen on. +// A multicast address starts with 239.x.x.x, and using an address that does not conform to this +// will lead to undefined behaviour. +func WithMulticastAddress(address string) EmulatorOption { + return func(cfg *emulatorCfg) { + cfg.address = address + } +} + +// WithLogger makes the Emulator print out status messages with the provided logger. +func WithLogger(l *log.Logger) EmulatorOption { + return func(cfg *emulatorCfg) { + cfg.logger = l + } +} + +// NoLogger disables logging in the Emulator. +func NoLogger(cfg *emulatorCfg) { + cfg.logger = log.New(&writeSink{}, "", log.LstdFlags) +} + +// writeSink is an io.Writer which does not write to anything. +// +// Can be thought of as a /dev/null for writers. +type writeSink struct{} + +// Write returns without actually writing to anything. +func (w *writeSink) Write(buf []byte) (int, error) { + return len(buf), nil +} + +// Emulator emulates a CAN bus. +// +// Emulator emulates a CAN bus by using UDP multicast. The emulator itself +// does not own the multicast group but rather establishes a common +// address/port pair for the CAN bus to be emulated on. +// Emulator exposes a thread-safe API to callees and may therefore be +// shared among different goroutines. +type Emulator struct { + transceiver *udpTxRx + logger *log.Logger + reqSenderChan chan chan int + closeChan chan struct{} + sync.Mutex + g *errgroup.Group + rg errgroup.Group +} + +// NewEmulator creates an Emulator to emulate a CAN bus. +// +// If no error is returned it is safe to `socketcan.Dial` the address +// of the Emulator. The emulator will default to using multicast group `239.64.142.206` +// with a random port that's decided when calling Emulator +// +// N.B. It is not possible to simply use `net.Dial` as for UDP multicast both +// a transmitting connection and a writing connection. This is handled +// by `socketcan.Dial` under the hood. +func NewEmulator(options ...EmulatorOption) (*Emulator, error) { + cfg := defaultCfg() + for _, update := range options { + update(&cfg) + } + c, err := udpTransceiver("udp", cfg.address) + if err != nil { + return nil, err + } + return &Emulator{ + transceiver: c, + logger: cfg.logger, + closeChan: make(chan struct{}), + reqSenderChan: make(chan chan int), + }, nil +} + +// Run an Emulator. +// +// This starts the listener and waits until the context is canceled +// before tidying up. +func (e *Emulator) Run(ctx context.Context) error { + e.Lock() + e.g, ctx = errgroup.WithContext(ctx) + ctxDone := ctx.Done() + + // Listen for incoming frames. + // Keep track of unique senders, and notify newSenderChan. + newSenderChan := make(chan struct{}) + e.g.Go(func() error { + e.logger.Printf("waiting for SocketCAN connection requests on udp://%s\n", e.Addr().String()) + registeredSenders := make(map[string]bool) + for { + buffer := make([]byte, 8096) + _, _, src, err := e.transceiver.rx.ReadFrom(buffer) + if err != nil { + if isClosedError(err) { + return nil + } + return fmt.Errorf("read from udp: %w", err) + } + if !registeredSenders[src.String()] { + e.logger.Printf("received first frame from %s", src.String()) + registeredSenders[src.String()] = true + select { + case <-ctxDone: + return nil + case newSenderChan <- struct{}{}: + } + } + } + }) + + // Close multicast listener when ctx is canceled + e.g.Go(func() error { + <-ctxDone + e.logger.Println("closing SocketCAN listener...") + e.Lock() + defer e.Unlock() + return e.transceiver.Close() + }) + + // Stop all started receivers when ctx is canceled + e.g.Go(func() error { + <-ctxDone + e.logger.Println("stopping receivers...") + close(e.closeChan) + e.Lock() + defer e.Unlock() + return e.rg.Wait() + }) + + // Keep track of the number of unique senders of the received frames, when the number of senders + // are requested on the reqSenderChan, send them on the provided channel. + e.g.Go(func() error { + nSenders := 0 + for { + select { + case <-ctxDone: + return nil + case <-newSenderChan: + nSenders++ + case req := <-e.reqSenderChan: + req <- nSenders + } + } + }) + e.Unlock() + e.logger.Println("started emulator, waiting for cancel signal") + return e.g.Wait() +} + +// Addr returns the address of the Emulator's multicast group. +func (e *Emulator) Addr() net.Addr { + return e.transceiver.RemoteAddr() +} + +// Receiver returns a Receiver connected to the Emulator. +// +// The emulator owns the underlying network connection an +// will close it when the emulator is closed. +func (e *Emulator) Receiver() (*Receiver, error) { + conn, err := udpTransceiver(e.Addr().Network(), e.Addr().String()) + if err != nil { + return nil, err + } + e.Lock() + e.rg.Go(func() error { + <-e.closeChan + return conn.Close() + }) + e.Unlock() + return NewReceiver(conn), nil +} + +// TransmitFrame sends a CAN frame to the Emulator's multicast group. +func (e *Emulator) TransmitFrame(ctx context.Context, f can.Frame) error { + conn, err := udpTransceiver(e.Addr().Network(), e.Addr().String()) + if err != nil { + return fmt.Errorf("transmit CAN frame: %w", err) + } + errChan := make(chan error) + go func() { + if err := NewTransmitter(conn).TransmitFrame(ctx, f); err != nil { + errChan <- fmt.Errorf("transmit CAN frame: %w", err) + } + close(errChan) + }() + select { + case <-ctx.Done(): + _ = conn.Close() + return ctx.Err() + case err := <-errChan: + _ = conn.Close() + if err != nil { + return fmt.Errorf("emulator: %w", err) + } + return nil + } +} + +// TransmitMessage sends a CAN message to every emulator connection. +func (e *Emulator) TransmitMessage(ctx context.Context, m can.Message) error { + f, err := m.MarshalFrame() + if err != nil { + return fmt.Errorf("transmit CAN message: %w", err) + } + if err := e.TransmitFrame(ctx, f); err != nil { + return fmt.Errorf("transmit CAN message: %w", err) + } + return nil +} + +// WaitForSenders waits until either, n unique senders have been sending messages to the +// multicast group, or the timeout is reached. +func (e *Emulator) WaitForSenders(n int, timeout time.Duration) error { + reqChan := make(chan int) + timeoutChannel := time.After(timeout) + for { + select { + case <-timeoutChannel: + return fmt.Errorf("emulator timeout waiting for senders") + case e.reqSenderChan <- reqChan: + conns := <-reqChan + if conns < n { + // We don't want to keep the emulator + // busy with our requests all the time. + time.Sleep(time.Millisecond) + continue + } + return nil + } + } +} + +func isClosedError(e error) bool { + if e == nil { + return false + } + return strings.Contains(e.Error(), "closed") +} diff --git a/pkg/socketcan/emulator_test.go b/pkg/socketcan/emulator_test.go new file mode 100644 index 0000000..b9eb210 --- /dev/null +++ b/pkg/socketcan/emulator_test.go @@ -0,0 +1,354 @@ +package socketcan + +import ( + "context" + "fmt" + "log" + "testing" + "time" + + "github.com/stretchr/testify/require" + "go.einride.tech/can" + "golang.org/x/sync/errgroup" +) + +func TestEmulate_Close(t *testing.T) { + // Given: an emulator + e, err := NewEmulator() + require.NoError(t, err) + ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) + defer cancel() + // When: I start the emulator + g, ctx := errgroup.WithContext(ctx) + g.Go(func() error { + return e.Run(ctx) + }) + // Then: I should be able to close it + require.NoError(t, g.Wait()) +} + +func TestEmulate_SendToAll(t *testing.T) { + for _, tt := range []struct { + receivers int + }{ + {receivers: 1}, + {receivers: 5}, + {receivers: 100}, + } { + tt := tt + t.Run(fmt.Sprintf("receivers:%v", tt.receivers), func(t *testing.T) { + // Given: A listener with an Emulator + ctx, cancel := context.WithCancel(context.Background()) + eg, eCtx := errgroup.WithContext(ctx) + e, err := NewEmulator() + require.NoError(t, err) + eg.Go(func() error { + return e.Run(eCtx) + }) + // When: I start multiple receivers connected to the Emulator + g := errgroup.Group{} + for i := 0; i < tt.receivers; i++ { + r, err := e.Receiver() + require.NoError(t, err) + g.Go(func() error { + if ok := r.Receive(); !ok { + return fmt.Errorf("failed to receive CAN frame: %w", r.Err()) + } + if r.HasErrorFrame() { + return fmt.Errorf("received error frame: %v", r.ErrorFrame()) + } + return r.Err() + }) + } + // And then the emulator transmits a CAN frame + txFrame := can.Frame{ID: 42, Length: 4, Data: can.Data{1, 2, 3, 4}} + err = e.TransmitFrame(context.Background(), txFrame) + require.NoError(t, err) + // Then: Every receiver should receive the frame and not return an error + require.NoError(t, g.Wait()) + cancel() + require.NoError(t, eg.Wait()) + }) + } +} + +func TestEmulate_ConnectMany(t *testing.T) { + for _, tt := range []struct { + noTransmitters int + canFrames []can.Frame + }{ + { + noTransmitters: 1, + canFrames: []can.Frame{ + {ID: 42}, + {ID: 43, Length: 4, Data: can.Data{1, 2, 3, 4}}, + }, + }, + { + noTransmitters: 10, + canFrames: []can.Frame{ + {ID: 42}, + {ID: 43, Length: 4, Data: can.Data{1, 2, 3, 4}}, + {ID: 44, IsRemote: true}, + }, + }, + { + noTransmitters: 50, + canFrames: []can.Frame{ + {ID: 42}, + {ID: 43, Length: 4, Data: can.Data{1, 2, 3, 4}}, + {ID: 44, IsRemote: true}, + {ID: 45, Length: 7, Data: can.Data{1, 2, 3, 4, 5, 6, 7}}, + {ID: 46, IsExtended: false}, + {ID: 47, Length: 1, Data: can.Data{1}}, + {ID: 48, IsRemote: false}, + }, + }, + } { + tt := tt + name := fmt.Sprintf("transmitters:%v,frames:%v", tt.noTransmitters, len(tt.canFrames)) + t.Run(name, func(t *testing.T) { + // Given: A listener with an Emulator + e, err := NewEmulator(NoLogger) + require.NoError(t, err) + ctx, cancel := context.WithCancel(context.Background()) + eg, eCtx := errgroup.WithContext(ctx) + eg.Go(func() error { + return e.Run(eCtx) + }) + r, err := e.Receiver() + require.NoError(t, err) + receiver := errgroup.Group{} + receiver.Go(func() error { + for i := 0; i < len(tt.canFrames)*tt.noTransmitters; i++ { + i := i + if ok := r.Receive(); !ok { + cancel() + require.NoError(t, eg.Wait()) + t.Fatal("Not all CAN frames were received", i, r.Err()) + } + require.Contains(t, tt.canFrames, r.Frame()) + } + return nil + }) + // When: I connect multiple transmitters and transmit CAN frame on every transmitter + transmits, txCtx := errgroup.WithContext(ctx) + for i := 0; i < tt.noTransmitters; i++ { + transmits.Go(func() error { + conn, err := DialContext(txCtx, e.Addr().Network(), e.Addr().String()) + if err != nil { + return err + } + tx := NewTransmitter(conn) + for _, frame := range tt.canFrames { + if err := tx.TransmitFrame(txCtx, frame); err != nil { + log.Printf("failed to transmit frame: %+v\n", err) + return err + } + } + return conn.Close() + }) + } + require.NoError(t, transmits.Wait()) + // Then: Every CAN frame should have been delivered to the emulator + require.NoError(t, receiver.Wait()) + cancel() + require.NoError(t, eg.Wait()) + }) + } +} + +func TestEmulate_SendReceive(t *testing.T) { + for _, tt := range []struct { + transmitters int + receivers int + }{ + { + transmitters: 1, + receivers: 2, + }, + { + transmitters: 10, + receivers: 50, + }, + { + transmitters: 50, + receivers: 50, + }, + } { + tt := tt + name := fmt.Sprintf("transmitters: %v,receivers: %v", tt.transmitters, tt.receivers) + t.Run(name, func(t *testing.T) { + // Given: A listener and an emulator + e, err := NewEmulator() + require.NoError(t, err) + ctx, cancel := context.WithCancel(context.Background()) + eg, eCtx := errgroup.WithContext(ctx) + eg.Go(func() error { + return e.Run(eCtx) + }) + canFrames := make(map[uint32]can.Frame) + canFrames[42] = can.Frame{ID: 42} + canFrames[43] = can.Frame{ID: 43, IsRemote: true} + canFrames[44] = can.Frame{ID: 44, IsExtended: true} + // When: I start a number of receivers + rx := errgroup.Group{} + for i := 0; i < tt.receivers; i++ { + r, err := e.Receiver() + require.NoError(t, err) + rx.Go(func() error { + for i := 0; i < tt.transmitters*len(canFrames); i++ { + if ok := r.Receive(); !ok { + return fmt.Errorf("receive frames: %w", r.Err()) + } + if r.HasErrorFrame() { + return fmt.Errorf("received error frame: %v", r.ErrorFrame()) + } + if _, ok := canFrames[r.Frame().ID]; !ok { + return fmt.Errorf("received unexpected frame: %v", r.Frame()) + } + } + return nil + }) + } + // And then start a number of transmitters that will transmit a number of CAN frames + tx, txCtx := errgroup.WithContext(ctx) + for i := 0; i < tt.transmitters; i++ { + conn, err := DialContext(txCtx, e.Addr().Network(), e.Addr().String()) + require.NoError(t, err) + tx.Go(func() (err error) { + t := NewTransmitter(conn) + for _, f := range canFrames { + if err := t.TransmitFrame(txCtx, f); err != nil { + return fmt.Errorf("transmit frame: %w", err) + } + } + if err := conn.Close(); err != nil { + return fmt.Errorf("close transmitter: %w", err) + } + return nil + }) + } + // Then: The transmissions should not fail + require.NoError(t, tx.Wait()) + // And every receiver should receive every CAN frame + require.NoError(t, rx.Wait()) + cancel() + require.NoError(t, eg.Wait()) + }) + } +} + +func TestEmulator_Isolation(t *testing.T) { + // Given 5 separate emulators + const nEmulators = 5 + var emulators []*Emulator + ctx, cancel := context.WithCancel(context.Background()) + eg, eCtx := errgroup.WithContext(ctx) + for i := 0; i < nEmulators; i++ { + e, err := NewEmulator() + require.NoError(t, err) + emulators = append(emulators, e) + eg.Go(func() error { + return e.Run(eCtx) + }) + } + // When starting one transmitter/receiver pair per emulator sending 10 frames + const nFrames = 10 + rx := errgroup.Group{} + tx := errgroup.Group{} + for i := 0; i < nEmulators; i++ { + i := i + r, err := emulators[i].Receiver() + require.NoError(t, err) + rx.Go(func() error { + for j := 0; j < nFrames; j++ { + if ok := r.Receive(); !ok { + return fmt.Errorf("receive frame: %w", r.Err()) + } + if r.HasErrorFrame() { + return fmt.Errorf("received error frame: %v", r.ErrorFrame()) + } + if r.Frame().ID != uint32(i) { + return fmt.Errorf("receiver(%v) received unexpected frame: %v", i, r.Frame()) + } + } + return nil + }) + for j := 0; j < nFrames; j++ { + frame := can.Frame{ID: uint32(i)} + tx.Go(func() error { + return emulators[i].TransmitFrame(context.Background(), frame) + }) + } + } + // Then all transmitted frames should be received by correct receiver + require.NoError(t, rx.Wait()) + require.NoError(t, tx.Wait()) + cancel() + require.NoError(t, eg.Wait()) +} + +func TestEmulator_WaitForSenders(t *testing.T) { + // Given a started emulator + ctx, cancel := context.WithCancel(context.Background()) + eg, eCtx := errgroup.WithContext(ctx) + e, err := NewEmulator() + require.NoError(t, err) + eg.Go(func() error { + return e.Run(eCtx) + }) + // When one transmitter is transmitting a frame + txg := errgroup.Group{} + txg.Go(func() error { + return e.TransmitFrame(context.Background(), can.Frame{ID: 1234}) + }) + // Then WaitForSenders should return without an error + err = e.WaitForSenders(1, time.Second) + require.NoError(t, err) + require.NoError(t, txg.Wait()) + cancel() + require.NoError(t, eg.Wait()) +} + +func TestEmulator_WaitForSenders_Multiple(t *testing.T) { + // Given a started emulator + ctx, cancel := context.WithCancel(context.Background()) + eg, eCtx := errgroup.WithContext(ctx) + e, err := NewEmulator() + require.NoError(t, err) + eg.Go(func() error { + return e.Run(eCtx) + }) + // When one transmitter is transmitting a frame + txg := errgroup.Group{} + txg.Go(func() error { + return e.TransmitFrame(context.Background(), can.Frame{ID: 1234}) + }) + txg.Go(func() error { + return e.TransmitFrame(context.Background(), can.Frame{ID: 4321}) + }) + // Then WaitForSenders should return without an error + err = e.WaitForSenders(2, time.Second) + require.NoError(t, err) + require.NoError(t, txg.Wait()) + cancel() + require.NoError(t, eg.Wait()) +} + +func TestEmulator_WaitForSenders_Timeout(t *testing.T) { + // Given a started emulator + ctx, cancel := context.WithCancel(context.Background()) + eg, eCtx := errgroup.WithContext(ctx) + e, err := NewEmulator() + require.NoError(t, err) + eg.Go(func() error { + return e.Run(eCtx) + }) + // When no transmitters have connected and transmitted frames + // Then WaitForSenders should timeout + err = e.WaitForSenders(1, 100*time.Millisecond) + require.Error(t, err) + cancel() + require.NoError(t, eg.Wait()) +} diff --git a/pkg/socketcan/errorclass.go b/pkg/socketcan/errorclass.go new file mode 100644 index 0000000..ba07ec7 --- /dev/null +++ b/pkg/socketcan/errorclass.go @@ -0,0 +1,17 @@ +package socketcan + +type ErrorClass uint32 + +//go:generate gobin -m -run golang.org/x/tools/cmd/stringer -type ErrorClass -trimprefix ErrorClass + +const ( + ErrorClassTxTimeout ErrorClass = 0x00000001 + ErrorClassLostArbitration ErrorClass = 0x00000002 + ErrorClassController ErrorClass = 0x00000004 + ErrorClassProtocolViolation ErrorClass = 0x00000008 + ErrorClassTransceiver ErrorClass = 0x00000010 + ErrorClassNoAck ErrorClass = 0x00000020 + ErrorClassBusOff ErrorClass = 0x00000040 + ErrorClassBusError ErrorClass = 0x00000080 + ErrorClassRestarted ErrorClass = 0x00000100 +) diff --git a/pkg/socketcan/errorclass_string.go b/pkg/socketcan/errorclass_string.go new file mode 100644 index 0000000..98d7621 --- /dev/null +++ b/pkg/socketcan/errorclass_string.go @@ -0,0 +1,59 @@ +// Code generated by "stringer -type ErrorClass -trimprefix ErrorClass"; DO NOT EDIT. + +package socketcan + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[ErrorClassTxTimeout-1] + _ = x[ErrorClassLostArbitration-2] + _ = x[ErrorClassController-4] + _ = x[ErrorClassProtocolViolation-8] + _ = x[ErrorClassTransceiver-16] + _ = x[ErrorClassNoAck-32] + _ = x[ErrorClassBusOff-64] + _ = x[ErrorClassBusError-128] + _ = x[ErrorClassRestarted-256] +} + +const ( + _ErrorClass_name_0 = "TxTimeoutLostArbitration" + _ErrorClass_name_1 = "Controller" + _ErrorClass_name_2 = "ProtocolViolation" + _ErrorClass_name_3 = "Transceiver" + _ErrorClass_name_4 = "NoAck" + _ErrorClass_name_5 = "BusOff" + _ErrorClass_name_6 = "BusError" + _ErrorClass_name_7 = "Restarted" +) + +var ( + _ErrorClass_index_0 = [...]uint8{0, 9, 24} +) + +func (i ErrorClass) String() string { + switch { + case 1 <= i && i <= 2: + i -= 1 + return _ErrorClass_name_0[_ErrorClass_index_0[i]:_ErrorClass_index_0[i+1]] + case i == 4: + return _ErrorClass_name_1 + case i == 8: + return _ErrorClass_name_2 + case i == 16: + return _ErrorClass_name_3 + case i == 32: + return _ErrorClass_name_4 + case i == 64: + return _ErrorClass_name_5 + case i == 128: + return _ErrorClass_name_6 + case i == 256: + return _ErrorClass_name_7 + default: + return "ErrorClass(" + strconv.FormatInt(int64(i), 10) + ")" + } +} diff --git a/pkg/socketcan/errorframe.go b/pkg/socketcan/errorframe.go new file mode 100644 index 0000000..11114d0 --- /dev/null +++ b/pkg/socketcan/errorframe.go @@ -0,0 +1,63 @@ +package socketcan + +import ( + "encoding/hex" + "fmt" +) + +type ErrorFrame struct { + // Class is the error class + ErrorClass ErrorClass + // LostArbitrationBit contains the bit number when the error class is LostArbitration. + LostArbitrationBit uint8 + // ControllerError contains error information when the error class is Controller. + ControllerError ControllerError + // ProtocolViolationError contains error information when the error class is Protocol. + ProtocolError ProtocolViolationError + // ProtocolViolationErrorLocation contains error location when the error class is Protocol. + ProtocolViolationErrorLocation ProtocolViolationErrorLocation + // TransceiverError contains error information when the error class is Transceiver. + TransceiverError TransceiverError + // ControllerSpecificInformation contains controller-specific additional error information. + ControllerSpecificInformation [3]byte +} + +func (e *ErrorFrame) String() string { + switch e.ErrorClass { + case ErrorClassLostArbitration: + return fmt.Sprintf( + "%s in bit %d (%s)", + e.ErrorClass, + e.LostArbitrationBit, + hex.EncodeToString(e.ControllerSpecificInformation[:]), + ) + case ErrorClassController: + return fmt.Sprintf( + "%s: %s (%v)", + e.ErrorClass, + e.ControllerError, + hex.EncodeToString(e.ControllerSpecificInformation[:]), + ) + case ErrorClassProtocolViolation: + return fmt.Sprintf( + "%s: %s: location %s (%v)", + e.ErrorClass, + e.ProtocolError, + e.ProtocolViolationErrorLocation, + hex.EncodeToString(e.ControllerSpecificInformation[:]), + ) + case ErrorClassTransceiver: + return fmt.Sprintf( + "%s: %s (%v)", + e.ErrorClass, + e.TransceiverError, + hex.EncodeToString(e.ControllerSpecificInformation[:]), + ) + default: + return fmt.Sprintf( + "%s (%v)", + e.ErrorClass, + hex.EncodeToString(e.ControllerSpecificInformation[:]), + ) + } +} diff --git a/pkg/socketcan/errorframe_test.go b/pkg/socketcan/errorframe_test.go new file mode 100644 index 0000000..bf6ea25 --- /dev/null +++ b/pkg/socketcan/errorframe_test.go @@ -0,0 +1,62 @@ +package socketcan + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestErrorFrame_String(t *testing.T) { + for _, tt := range []struct { + msg string + f ErrorFrame + expected string + }{ + { + msg: "lost arbitration", + f: ErrorFrame{ + ErrorClass: ErrorClassLostArbitration, + LostArbitrationBit: 42, + }, + expected: "LostArbitration in bit 42 (000000)", + }, + { + msg: "controller", + f: ErrorFrame{ + ErrorClass: ErrorClassController, + ControllerError: ControllerErrorRxBufferOverflow, + }, + expected: "Controller: RxBufferOverflow (000000)", + }, + { + msg: "protocol violation", + f: ErrorFrame{ + ErrorClass: ErrorClassProtocolViolation, + ProtocolError: ProtocolViolationErrorFrameFormat, + ProtocolViolationErrorLocation: ProtocolViolationErrorLocationID20To18, + }, + expected: "ProtocolViolation: FrameFormat: location ID20To18 (000000)", + }, + { + msg: "transceiver", + f: ErrorFrame{ + ErrorClass: ErrorClassTransceiver, + TransceiverError: TransceiverErrorCANHShortToGND, + }, + expected: "Transceiver: CANHShortToGND (000000)", + }, + { + msg: "controller specific information", + f: ErrorFrame{ + ErrorClass: ErrorClassTxTimeout, + ControllerSpecificInformation: [3]byte{0x12, 0x34, 0x56}, + }, + expected: "TxTimeout (123456)", + }, + } { + tt := tt + t.Run(tt.msg, func(t *testing.T) { + require.Equal(t, tt.expected, tt.f.String()) + }) + } +} diff --git a/pkg/socketcan/fileconn.go b/pkg/socketcan/fileconn.go new file mode 100644 index 0000000..5d66d7e --- /dev/null +++ b/pkg/socketcan/fileconn.go @@ -0,0 +1,95 @@ +package socketcan + +import ( + "net" + "os" + "time" + + "golang.org/x/xerrors" +) + +// file is an interface for mocking file operations performed by fileConn. +type file interface { + Read([]byte) (int, error) + Write([]byte) (int, error) + SetDeadline(time.Time) error + SetReadDeadline(time.Time) error + SetWriteDeadline(time.Time) error + Close() error +} + +// fileConn provides a net.Conn API for file-like types. +type fileConn struct { + // f is the file to provide a net.Conn API for. + f file + // net is the connection's network. + net string + // la is the connection's local address, if any. + la net.Addr + // ra is the connection's remote address, if any. + ra net.Addr +} + +// ensure net.Conn interface +var _ net.Conn = &fileConn{} + +func (c *fileConn) Read(b []byte) (int, error) { + n, err := c.f.Read(b) + if err != nil { + return n, &net.OpError{Op: "read", Net: c.net, Source: c.la, Addr: c.ra, Err: unwrapPathError(err)} + } + return n, nil +} + +func (c *fileConn) Write(b []byte) (int, error) { + n, err := c.f.Write(b) + if err != nil { + return n, &net.OpError{Op: "write", Net: c.net, Source: c.la, Addr: c.ra, Err: unwrapPathError(err)} + } + return n, nil +} + +func (c *fileConn) LocalAddr() net.Addr { + return c.la +} + +func (c *fileConn) RemoteAddr() net.Addr { + return c.ra +} + +func (c *fileConn) SetDeadline(t time.Time) error { + if err := c.f.SetDeadline(t); err != nil { + return &net.OpError{Op: "set deadline", Net: c.net, Source: c.la, Addr: c.ra, Err: unwrapPathError(err)} + } + return nil +} + +func (c *fileConn) SetReadDeadline(t time.Time) error { + if err := c.f.SetReadDeadline(t); err != nil { + return &net.OpError{Op: "set read deadline", Net: c.net, Source: c.la, Addr: c.ra, Err: unwrapPathError(err)} + } + return nil +} + +func (c *fileConn) SetWriteDeadline(t time.Time) error { + if err := c.f.SetWriteDeadline(t); err != nil { + return &net.OpError{Op: "set write deadline", Net: c.net, Source: c.la, Addr: c.ra, Err: unwrapPathError(err)} + } + return nil +} + +func (c *fileConn) Close() error { + if err := c.f.Close(); err != nil { + return &net.OpError{Op: "close", Net: c.net, Source: c.la, Addr: c.ra, Err: unwrapPathError(err)} + } + return nil +} + +// unwrapPathError unwraps one level of *os.PathError from the provided error. +func unwrapPathError(err error) error { + var pe *os.PathError + if xerrors.As(err, &pe) { + return pe.Err + } + return err +} diff --git a/pkg/socketcan/fileconn_test.go b/pkg/socketcan/fileconn_test.go new file mode 100644 index 0000000..53ac0c3 --- /dev/null +++ b/pkg/socketcan/fileconn_test.go @@ -0,0 +1,154 @@ +package socketcan + +import ( + "fmt" + "net" + "os" + "testing" + "time" + + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/require" + "go.einride.tech/can/internal/mocks/mocksocketcan" +) + +func TestUnwrapPathError(t *testing.T) { + innerErr := fmt.Errorf("inner error") + for _, tt := range []struct { + msg string + err error + expected error + }{ + { + msg: "no path error", + err: innerErr, + expected: innerErr, + }, + { + msg: "single path error", + err: &os.PathError{Op: "read", Err: innerErr}, + expected: innerErr, + }, + { + msg: "double path error", + err: &os.PathError{Op: "read", Err: &os.PathError{Op: "read", Err: innerErr}}, + expected: &os.PathError{Op: "read", Err: innerErr}, + }, + } { + tt := tt + t.Run(tt.msg, func(t *testing.T) { + require.Equal(t, tt.expected, unwrapPathError(tt.err)) + }) + } +} + +func TestFileConn_ReadWrite(t *testing.T) { + for _, tt := range []struct { + op string + fn func(file, []byte) (int, error) + mockFn func(*mocksocketcan.MockfileMockRecorder, interface{}) *gomock.Call + }{ + { + op: "read", + fn: file.Read, + mockFn: (*mocksocketcan.MockfileMockRecorder).Read, + }, + { + op: "write", + fn: file.Write, + mockFn: (*mocksocketcan.MockfileMockRecorder).Write, + }, + } { + tt := tt + t.Run(tt.op, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + f := mocksocketcan.NewMockfile(ctrl) + fc := &fileConn{f: f, net: "can", ra: &canRawAddr{device: "can0"}} + t.Run("no error", func(t *testing.T) { + var data []byte + tt.mockFn(f.EXPECT(), data).Return(42, nil) + n, err := tt.fn(fc, data) + require.Equal(t, 42, n) + require.NoError(t, err) + }) + t.Run("error", func(t *testing.T) { + var data []byte + cause := fmt.Errorf("boom") + tt.mockFn(f.EXPECT(), data).Return(0, &os.PathError{Err: cause}) + n, err := tt.fn(fc, data) + require.Equal(t, 0, n) + require.Error(t, &net.OpError{Op: tt.op, Net: fc.net, Addr: fc.RemoteAddr(), Err: err}) + }) + }) + } +} + +func TestFileConn_Addr(t *testing.T) { + fc := &fileConn{la: &canRawAddr{device: "can0"}, ra: &canRawAddr{device: "can1"}} + t.Run("local", func(t *testing.T) { + require.Equal(t, fc.la, fc.LocalAddr()) + }) + t.Run("remote", func(t *testing.T) { + require.Equal(t, fc.ra, fc.RemoteAddr()) + }) +} + +func TestFileConn_SetDeadlines(t *testing.T) { + for _, tt := range []struct { + op string + fn func(file, time.Time) error + mockFn func(*mocksocketcan.MockfileMockRecorder, interface{}) *gomock.Call + }{ + { + op: "set deadline", + fn: file.SetDeadline, + mockFn: (*mocksocketcan.MockfileMockRecorder).SetDeadline, + }, + { + op: "set read deadline", + fn: file.SetReadDeadline, + mockFn: (*mocksocketcan.MockfileMockRecorder).SetReadDeadline, + }, + { + op: "set write deadline", + fn: file.SetWriteDeadline, + mockFn: (*mocksocketcan.MockfileMockRecorder).SetWriteDeadline, + }, + } { + tt := tt + t.Run(tt.op, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + f := mocksocketcan.NewMockfile(ctrl) + fc := &fileConn{f: f, net: "can", ra: &canRawAddr{device: "can0"}} + t.Run("no error", func(t *testing.T) { + tt.mockFn(f.EXPECT(), time.Unix(0, 1)).Return(nil) + require.NoError(t, tt.fn(fc, time.Unix(0, 1))) + }) + t.Run("error", func(t *testing.T) { + cause := fmt.Errorf("boom") + tt.mockFn(f.EXPECT(), time.Unix(0, 1)).Return(&os.PathError{Err: cause}) + err := tt.fn(fc, time.Unix(0, 1)) + require.Equal(t, &net.OpError{Op: tt.op, Net: fc.net, Addr: fc.RemoteAddr(), Err: cause}, err) + }) + }) + } +} + +func TestFileConn_Close(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + f := mocksocketcan.NewMockfile(ctrl) + fc := &fileConn{f: f, net: "can", ra: &canRawAddr{device: "can0"}} + t.Run("no error", func(t *testing.T) { + f.EXPECT().Close().Return(nil) + require.NoError(t, fc.Close()) + }) + t.Run("error", func(t *testing.T) { + cause := fmt.Errorf("boom") + f.EXPECT().Close().Return(&os.PathError{Err: cause}) + err := fc.Close() + require.Equal(t, &net.OpError{Op: "close", Net: fc.net, Addr: fc.RemoteAddr(), Err: cause}, err) + }) +} diff --git a/pkg/socketcan/frame.go b/pkg/socketcan/frame.go new file mode 100644 index 0000000..c3e8224 --- /dev/null +++ b/pkg/socketcan/frame.go @@ -0,0 +1,184 @@ +package socketcan + +import ( + "encoding/binary" + + "go.einride.tech/can" +) + +const ( + // lengthOfFrame is the length of a SocketCAN frame in bytes. + lengthOfFrame = 16 + // maxLengthOfData is the max length of a SocketCAN frame payload in bytes. + maxLengthOfData = 8 + // indexOfID is the index of the first byte of the frame ID. + indexOfID = 0 + // lengthOfID is the length of a frame ID in bytes. + lengthOfID = 4 + // indexOfDataLengthCode is the index of the first byte of the frame dataLengthCode. + indexOfDataLengthCode = indexOfID + lengthOfID + // lengthOfDataLengthCode is the length of a frame dataLengthCode in bytes. + lengthOfDataLengthCode = 1 + // indexOfPadding is the index of the first byte of frame padding. + indexOfPadding = indexOfDataLengthCode + lengthOfDataLengthCode + // lengthOfPadding is the length of frame padding in bytes. + lengthOfPadding = 3 + // indexOfData is the index of the first byte of data in a frame. + indexOfData = indexOfPadding + lengthOfPadding +) + +// error frame flag indices +const ( + // indexOfLostArbitrationBit is the byte index of the lost arbitration bit in an error frame. + indexOfLostArbitrationBit = 0 + // indexOfControllerError is the byte index of the controller error in an error frame. + indexOfControllerError = 1 + // indexOfProtocolError is the byte index of the protocol error in an error frame. + indexOfProtocolError = 2 + // indexOfProtocolViolationErrorLocation is the byte index of the protocol error location in an error frame. + indexOfProtocolViolationErrorLocation = 3 + // indexOfTransceiverError is the byte index of the transceiver error in an error frame. + indexOfTransceiverError = 4 + // indexOfControllerSpecificInformation is the starting byte of controller specific information in an error frame. + indexOfControllerSpecificInformation = 5 + // LengthOfControllerSpecificInformation is the number of error frame bytes with controller-specific information. + LengthOfControllerSpecificInformation = 3 +) + +// compile-time assertion that lengthOfFrame = indexOfData + maxLengthOfData +var _ [lengthOfFrame]struct{} = [indexOfData + maxLengthOfData]struct{}{} + +// id flags (copied from x/sys/unix) +const ( + idFlagExtended = 0x80000000 + idFlagError = 0x20000000 + idFlagRemote = 0x40000000 + idMaskExtended = 0x1fffffff + idMaskStandard = 0x7ff +) + +// FrameInterceptor provides a hook to intercept the transmission of a CAN frame. +// The interceptor is called if and only if the frame transmission/receival is a success. +type FrameInterceptor func(fr can.Frame) + +// frame represents a SocketCAN frame. +// +// The format specified in the Linux SocketCAN kernel module: +// +// struct can_frame { +// canid_t can_id; /* 32 bit CAN_ID + EFF/RTR/ERR flags */ +// __u8 can_dlc; /* frame payload length in byte (0 .. 8) */ +// __u8 __pad; /* padding */ +// __u8 __res0; /* reserved / padding */ +// __u8 __res1; /* reserved / padding */ +// __u8 data[8] __attribute__((aligned(8))); +// }; +type frame struct { + // idAndFlags is the combined CAN ID and flags. + idAndFlags uint32 + // dataLengthCode is the frame payload length in bytes. + dataLengthCode uint8 + // padding+reserved fields + _ [3]byte + // bytes contains the frame payload. + data [8]byte +} + +func (f *frame) unmarshalBinary(b []byte) { + _ = b[lengthOfFrame-1] // bounds check + f.idAndFlags = binary.LittleEndian.Uint32(b[indexOfID : indexOfID+lengthOfID]) + f.dataLengthCode = b[indexOfDataLengthCode] + copy(f.data[:], b[indexOfData:lengthOfFrame]) +} + +func (f *frame) marshalBinary(b []byte) { + _ = b[lengthOfFrame-1] // bounds check + binary.LittleEndian.PutUint32(b[indexOfID:indexOfID+lengthOfID], f.idAndFlags) + b[indexOfDataLengthCode] = f.dataLengthCode + copy(b[indexOfData:], f.data[:]) +} + +func (f *frame) decodeFrame() can.Frame { + return can.Frame{ + ID: f.id(), + Length: f.dataLengthCode, + Data: can.Data(f.data), + IsExtended: f.isExtended(), + IsRemote: f.isRemote(), + } +} + +func (f *frame) encodeFrame(cf can.Frame) { + f.idAndFlags = cf.ID + if cf.IsRemote { + f.idAndFlags |= idFlagRemote + } + if cf.IsExtended { + f.idAndFlags |= idFlagExtended + } + f.dataLengthCode = cf.Length + f.data = cf.Data +} + +func (f *frame) isExtended() bool { + return f.idAndFlags&idFlagExtended > 0 +} + +func (f *frame) isRemote() bool { + return f.idAndFlags&idFlagRemote > 0 +} + +func (f *frame) isError() bool { + return f.idAndFlags&idFlagError > 0 +} + +func (f *frame) id() uint32 { + if f.isExtended() { + return f.idAndFlags & idMaskExtended + } + return f.idAndFlags & idMaskStandard +} + +func (f *frame) decodeErrorFrame() ErrorFrame { + return ErrorFrame{ + ErrorClass: f.errorClass(), + LostArbitrationBit: f.lostArbitrationBit(), + ControllerError: f.controllerError(), + ProtocolError: f.protocolError(), + ProtocolViolationErrorLocation: f.protocolErrorLocation(), + TransceiverError: f.transceiverError(), + ControllerSpecificInformation: f.controllerSpecificInformation(), + } +} + +func (f *frame) errorClass() ErrorClass { + return ErrorClass(f.idAndFlags &^ idFlagError) +} + +func (f *frame) lostArbitrationBit() uint8 { + return f.data[indexOfLostArbitrationBit] +} + +func (f *frame) controllerError() ControllerError { + return ControllerError(f.data[indexOfControllerError]) +} + +func (f *frame) protocolError() ProtocolViolationError { + return ProtocolViolationError(f.data[indexOfProtocolError]) +} + +func (f *frame) protocolErrorLocation() ProtocolViolationErrorLocation { + return ProtocolViolationErrorLocation(f.data[indexOfProtocolViolationErrorLocation]) +} + +func (f *frame) transceiverError() TransceiverError { + return TransceiverError(f.data[indexOfTransceiverError]) +} + +func (f *frame) controllerSpecificInformation() [LengthOfControllerSpecificInformation]byte { + var ret [LengthOfControllerSpecificInformation]byte + start := indexOfControllerSpecificInformation + end := start + LengthOfControllerSpecificInformation + copy(ret[:], f.data[start:end]) + return ret +} diff --git a/pkg/socketcan/frame_test.go b/pkg/socketcan/frame_test.go new file mode 100644 index 0000000..659601c --- /dev/null +++ b/pkg/socketcan/frame_test.go @@ -0,0 +1,194 @@ +package socketcan + +import ( + "testing" + "testing/quick" + + "github.com/stretchr/testify/require" + "go.einride.tech/can" +) + +func TestFrame_MarshalUnmarshalBinary_Property_Idempotent(t *testing.T) { + f := func(data [lengthOfFrame]byte) [lengthOfFrame]byte { + data[5], data[6], data[7] = 0, 0, 0 // padding+reserved fields + return data + } + g := func(data [lengthOfFrame]byte) [lengthOfFrame]byte { + var f frame + f.unmarshalBinary(data[:]) + var newData [lengthOfFrame]byte + f.marshalBinary(newData[:]) + return newData + } + require.NoError(t, quick.CheckEqual(f, g, nil)) +} + +func TestFrame_EncodeDecode(t *testing.T) { + for _, tt := range []struct { + msg string + frame can.Frame + socketCANFrame frame + }{ + { + msg: "data", + frame: can.Frame{ + ID: 0x00000001, + Length: 8, + Data: can.Data{1, 2, 3, 4, 5, 6, 7, 8}, + }, + socketCANFrame: frame{ + idAndFlags: 0x00000001, + dataLengthCode: 8, + data: [8]byte{1, 2, 3, 4, 5, 6, 7, 8}, + }, + }, + { + msg: "extended", + frame: can.Frame{ + ID: 0x00000001, + IsExtended: true, + }, + socketCANFrame: frame{ + idAndFlags: 0x80000001, + }, + }, + { + msg: "remote", + frame: can.Frame{ + ID: 0x00000001, + IsRemote: true, + }, + socketCANFrame: frame{ + idAndFlags: 0x40000001, + }, + }, + { + msg: "extended and remote", + frame: can.Frame{ + ID: 0x00000001, + IsExtended: true, + IsRemote: true, + }, + socketCANFrame: frame{ + idAndFlags: 0xc0000001, + }, + }, + } { + tt := tt + t.Run(tt.msg, func(t *testing.T) { + t.Run("encode", func(t *testing.T) { + var actual frame + actual.encodeFrame(tt.frame) + require.Equal(t, tt.socketCANFrame, actual) + }) + t.Run("decode", func(t *testing.T) { + require.Equal(t, tt.frame, tt.socketCANFrame.decodeFrame()) + }) + }) + } +} + +func TestFrame_IsError(t *testing.T) { + require.True(t, (&frame{idAndFlags: 0x20000001}).isError()) + require.False(t, (&frame{idAndFlags: 0x00000001}).isError()) +} + +func TestFrame_DecodeErrorFrame(t *testing.T) { + for _, tt := range []struct { + msg string + f frame + expected ErrorFrame + }{ + { + msg: "lost arbitration", + f: frame{ + idAndFlags: 0x20000002, + dataLengthCode: 8, + data: [8]byte{ + 42, + }, + }, + expected: ErrorFrame{ + ErrorClass: ErrorClassLostArbitration, + LostArbitrationBit: 42, + }, + }, + { + msg: "controller", + f: frame{ + idAndFlags: 0x20000004, + dataLengthCode: 8, + data: [8]byte{ + 0, + 0x04, + }, + }, + expected: ErrorFrame{ + ErrorClass: ErrorClassController, + ControllerError: ControllerErrorRxWarning, + }, + }, + { + msg: "protocol violation", + f: frame{ + idAndFlags: 0x20000008, + dataLengthCode: 8, + data: [8]byte{ + 0, + 0, + 0x10, + 0x02, + }, + }, + expected: ErrorFrame{ + ErrorClass: ErrorClassProtocolViolation, + ProtocolError: ProtocolViolationErrorBit1, + ProtocolViolationErrorLocation: ProtocolViolationErrorLocationID28To21, + }, + }, + { + msg: "transceiver", + f: frame{ + idAndFlags: 0x20000010, + dataLengthCode: 8, + data: [8]byte{ + 0, + 0, + 0, + 0, + 0x07, + }, + }, + expected: ErrorFrame{ + ErrorClass: ErrorClassTransceiver, + TransceiverError: TransceiverErrorCANHShortToGND, + }, + }, + { + msg: "controller-specific information", + f: frame{ + idAndFlags: 0x20000001, + dataLengthCode: 8, + data: [8]byte{ + 0, + 0, + 0, + 0, + 0, + 1, + 2, + 3, + }, + }, + expected: ErrorFrame{ + ErrorClass: ErrorClassTxTimeout, + ControllerSpecificInformation: [3]byte{1, 2, 3}, + }, + }, + } { + tt := tt + t.Run(tt.msg, func(t *testing.T) { + require.Equal(t, tt.expected, tt.f.decodeErrorFrame()) + }) + } +} diff --git a/pkg/socketcan/main_test.go b/pkg/socketcan/main_test.go new file mode 100644 index 0000000..74cb38f --- /dev/null +++ b/pkg/socketcan/main_test.go @@ -0,0 +1,13 @@ +package socketcan + +import ( + "os" + "testing" + + "go.uber.org/goleak" +) + +func TestMain(m *testing.M) { + goleak.VerifyTestMain(m) + os.Exit(m.Run()) +} diff --git a/pkg/socketcan/protocolviolationerror.go b/pkg/socketcan/protocolviolationerror.go new file mode 100644 index 0000000..f783878 --- /dev/null +++ b/pkg/socketcan/protocolviolationerror.go @@ -0,0 +1,17 @@ +package socketcan + +type ProtocolViolationError uint8 + +//go:generate gobin -m -run golang.org/x/tools/cmd/stringer -type ProtocolViolationError -trimprefix ProtocolViolationError + +const ( + ProtocolViolationErrorUnspecified ProtocolViolationError = 0x00 + ProtocolViolationErrorSingleBit ProtocolViolationError = 0x01 + ProtocolViolationErrorFrameFormat ProtocolViolationError = 0x02 + ProtocolViolationErrorBitStuffing ProtocolViolationError = 0x04 + ProtocolViolationErrorBit0 ProtocolViolationError = 0x08 // unable to send dominant bit + ProtocolViolationErrorBit1 ProtocolViolationError = 0x10 // unable to send recessive bit + ProtocolViolationErrorBusOverload ProtocolViolationError = 0x20 + ProtocolViolationErrorActive ProtocolViolationError = 0x40 // active error announcement + ProtocolViolationErrorTx ProtocolViolationError = 0x80 // error occurred on transmission +) diff --git a/pkg/socketcan/protocolviolationerror_string.go b/pkg/socketcan/protocolviolationerror_string.go new file mode 100644 index 0000000..da7738e --- /dev/null +++ b/pkg/socketcan/protocolviolationerror_string.go @@ -0,0 +1,55 @@ +// Code generated by "stringer -type ProtocolViolationError -trimprefix ProtocolViolationError"; DO NOT EDIT. + +package socketcan + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[ProtocolViolationErrorUnspecified-0] + _ = x[ProtocolViolationErrorSingleBit-1] + _ = x[ProtocolViolationErrorFrameFormat-2] + _ = x[ProtocolViolationErrorBitStuffing-4] + _ = x[ProtocolViolationErrorBit0-8] + _ = x[ProtocolViolationErrorBit1-16] + _ = x[ProtocolViolationErrorBusOverload-32] + _ = x[ProtocolViolationErrorActive-64] + _ = x[ProtocolViolationErrorTx-128] +} + +const ( + _ProtocolViolationError_name_0 = "UnspecifiedSingleBitFrameFormat" + _ProtocolViolationError_name_1 = "BitStuffing" + _ProtocolViolationError_name_2 = "Bit0" + _ProtocolViolationError_name_3 = "Bit1" + _ProtocolViolationError_name_4 = "BusOverload" + _ProtocolViolationError_name_5 = "Active" + _ProtocolViolationError_name_6 = "Tx" +) + +var ( + _ProtocolViolationError_index_0 = [...]uint8{0, 11, 20, 31} +) + +func (i ProtocolViolationError) String() string { + switch { + case 0 <= i && i <= 2: + return _ProtocolViolationError_name_0[_ProtocolViolationError_index_0[i]:_ProtocolViolationError_index_0[i+1]] + case i == 4: + return _ProtocolViolationError_name_1 + case i == 8: + return _ProtocolViolationError_name_2 + case i == 16: + return _ProtocolViolationError_name_3 + case i == 32: + return _ProtocolViolationError_name_4 + case i == 64: + return _ProtocolViolationError_name_5 + case i == 128: + return _ProtocolViolationError_name_6 + default: + return "ProtocolViolationError(" + strconv.FormatInt(int64(i), 10) + ")" + } +} diff --git a/pkg/socketcan/protocolviolationerrorlocation.go b/pkg/socketcan/protocolviolationerrorlocation.go new file mode 100644 index 0000000..7c42a03 --- /dev/null +++ b/pkg/socketcan/protocolviolationerrorlocation.go @@ -0,0 +1,28 @@ +package socketcan + +type ProtocolViolationErrorLocation uint8 + +//go:generate gobin -m -run golang.org/x/tools/cmd/stringer -type ProtocolViolationErrorLocation -trimprefix ProtocolViolationErrorLocation + +const ( + ProtocolViolationErrorLocationUnspecified ProtocolViolationErrorLocation = 0x00 + ProtocolViolationErrorLocationStartOfFrame ProtocolViolationErrorLocation = 0x03 + ProtocolViolationErrorLocationID28To21 ProtocolViolationErrorLocation = 0x02 // standard frames: 10 - 3 + ProtocolViolationErrorLocationID20To18 ProtocolViolationErrorLocation = 0x06 // standard frames: 2 - 0 + ProtocolViolationErrorLocationSubstituteRTR ProtocolViolationErrorLocation = 0x04 // standard frames: RTR + ProtocolViolationErrorLocationIDExtension ProtocolViolationErrorLocation = 0x05 + ProtocolViolationErrorLocationIDBits17To13 ProtocolViolationErrorLocation = 0x07 + ProtocolViolationErrorLocationIDBits12To05 ProtocolViolationErrorLocation = 0x0F + ProtocolViolationErrorLocationIDBits04To00 ProtocolViolationErrorLocation = 0x0E + ProtocolViolationErrorLocationRTR ProtocolViolationErrorLocation = 0x0C + ProtocolViolationErrorLocationReservedBit1 ProtocolViolationErrorLocation = 0x0D + ProtocolViolationErrorLocationReservedBit0 ProtocolViolationErrorLocation = 0x09 + ProtocolViolationErrorLocationDataLengthCode ProtocolViolationErrorLocation = 0x0B + ProtocolViolationErrorLocationData ProtocolViolationErrorLocation = 0x0A + ProtocolViolationErrorLocationCRCSequence ProtocolViolationErrorLocation = 0x08 + ProtocolViolationErrorLocationCRCDelimiter ProtocolViolationErrorLocation = 0x18 + ProtocolViolationErrorLocationACKSlot ProtocolViolationErrorLocation = 0x19 + ProtocolViolationErrorLocationACKDelimiter ProtocolViolationErrorLocation = 0x1B + ProtocolViolationErrorLocationEndOfFrame ProtocolViolationErrorLocation = 0x1A + ProtocolViolationErrorLocationIntermission ProtocolViolationErrorLocation = 0x12 +) diff --git a/pkg/socketcan/protocolviolationerrorlocation_string.go b/pkg/socketcan/protocolviolationerrorlocation_string.go new file mode 100644 index 0000000..dab3963 --- /dev/null +++ b/pkg/socketcan/protocolviolationerrorlocation_string.go @@ -0,0 +1,60 @@ +// Code generated by "stringer -type ProtocolViolationErrorLocation -trimprefix ProtocolViolationErrorLocation"; DO NOT EDIT. + +package socketcan + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[ProtocolViolationErrorLocationUnspecified-0] + _ = x[ProtocolViolationErrorLocationStartOfFrame-3] + _ = x[ProtocolViolationErrorLocationID28To21-2] + _ = x[ProtocolViolationErrorLocationID20To18-6] + _ = x[ProtocolViolationErrorLocationSubstituteRTR-4] + _ = x[ProtocolViolationErrorLocationIDExtension-5] + _ = x[ProtocolViolationErrorLocationIDBits17To13-7] + _ = x[ProtocolViolationErrorLocationIDBits12To05-15] + _ = x[ProtocolViolationErrorLocationIDBits04To00-14] + _ = x[ProtocolViolationErrorLocationRTR-12] + _ = x[ProtocolViolationErrorLocationReservedBit1-13] + _ = x[ProtocolViolationErrorLocationReservedBit0-9] + _ = x[ProtocolViolationErrorLocationDataLengthCode-11] + _ = x[ProtocolViolationErrorLocationData-10] + _ = x[ProtocolViolationErrorLocationCRCSequence-8] + _ = x[ProtocolViolationErrorLocationCRCDelimiter-24] + _ = x[ProtocolViolationErrorLocationACKSlot-25] + _ = x[ProtocolViolationErrorLocationACKDelimiter-27] + _ = x[ProtocolViolationErrorLocationEndOfFrame-26] + _ = x[ProtocolViolationErrorLocationIntermission-18] +} + +const ( + _ProtocolViolationErrorLocation_name_0 = "Unspecified" + _ProtocolViolationErrorLocation_name_1 = "ID28To21StartOfFrameSubstituteRTRIDExtensionID20To18IDBits17To13CRCSequenceReservedBit0DataDataLengthCodeRTRReservedBit1IDBits04To00IDBits12To05" + _ProtocolViolationErrorLocation_name_2 = "Intermission" + _ProtocolViolationErrorLocation_name_3 = "CRCDelimiterACKSlotEndOfFrameACKDelimiter" +) + +var ( + _ProtocolViolationErrorLocation_index_1 = [...]uint8{0, 8, 20, 33, 44, 52, 64, 75, 87, 91, 105, 108, 120, 132, 144} + _ProtocolViolationErrorLocation_index_3 = [...]uint8{0, 12, 19, 29, 41} +) + +func (i ProtocolViolationErrorLocation) String() string { + switch { + case i == 0: + return _ProtocolViolationErrorLocation_name_0 + case 2 <= i && i <= 15: + i -= 2 + return _ProtocolViolationErrorLocation_name_1[_ProtocolViolationErrorLocation_index_1[i]:_ProtocolViolationErrorLocation_index_1[i+1]] + case i == 18: + return _ProtocolViolationErrorLocation_name_2 + case 24 <= i && i <= 27: + i -= 24 + return _ProtocolViolationErrorLocation_name_3[_ProtocolViolationErrorLocation_index_3[i]:_ProtocolViolationErrorLocation_index_3[i+1]] + default: + return "ProtocolViolationErrorLocation(" + strconv.FormatInt(int64(i), 10) + ")" + } +} diff --git a/pkg/socketcan/receiver.go b/pkg/socketcan/receiver.go new file mode 100644 index 0000000..aa5d52c --- /dev/null +++ b/pkg/socketcan/receiver.go @@ -0,0 +1,83 @@ +package socketcan + +import ( + "bufio" + "io" + + "go.einride.tech/can" +) + +type ReceiverOption func(*receiverOpts) + +type receiverOpts struct { + frameInterceptor FrameInterceptor +} + +type Receiver struct { + opts receiverOpts + rc io.ReadCloser + sc *bufio.Scanner + frame frame +} + +func NewReceiver(rc io.ReadCloser, opt ...ReceiverOption) *Receiver { + opts := receiverOpts{} + for _, f := range opt { + f(&opts) + } + sc := bufio.NewScanner(rc) + sc.Split(scanFrames) + return &Receiver{ + rc: rc, + opts: opts, + sc: sc, + } +} + +func scanFrames(data []byte, _ bool) (int, []byte, error) { + if len(data) < lengthOfFrame { + // not enough data for a full frame + return 0, nil, nil + } + return lengthOfFrame, data[0:lengthOfFrame], nil +} + +func (r *Receiver) Receive() bool { + ok := r.sc.Scan() + r.frame = frame{} + if ok { + r.frame.unmarshalBinary(r.sc.Bytes()) + if r.opts.frameInterceptor != nil { + r.opts.frameInterceptor(r.frame.decodeFrame()) + } + } + return ok +} + +func (r *Receiver) HasErrorFrame() bool { + return r.frame.isError() +} + +func (r *Receiver) Frame() can.Frame { + return r.frame.decodeFrame() +} + +func (r *Receiver) ErrorFrame() ErrorFrame { + return r.frame.decodeErrorFrame() +} + +func (r *Receiver) Err() error { + return r.sc.Err() +} + +func (r *Receiver) Close() error { + return r.rc.Close() +} + +// ReceiverFrameInterceptor returns a ReceiverOption that sets the FrameInterceptor for the +// receiver. Only one frame interceptor can be installed. +func ReceiverFrameInterceptor(i FrameInterceptor) ReceiverOption { + return func(o *receiverOpts) { + o.frameInterceptor = i + } +} diff --git a/pkg/socketcan/receiver_test.go b/pkg/socketcan/receiver_test.go new file mode 100644 index 0000000..1479556 --- /dev/null +++ b/pkg/socketcan/receiver_test.go @@ -0,0 +1,136 @@ +package socketcan + +import ( + "bytes" + "io/ioutil" + "testing" + + "github.com/stretchr/testify/require" + "go.einride.tech/can" +) + +func TestReceiver_ReceiveFrames_Options(t *testing.T) { + testReceive := func(opt ReceiverOption) { + input := []byte{ + // id---------------> | dlc | padding-------> | data----------------------------------------> | + 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x12, 0x34, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + } + expected := can.Frame{ID: 0x01, Length: 2, Data: can.Data{0x12, 0x34}} + receiver := NewReceiver(ioutil.NopCloser(bytes.NewReader(input)), opt) + require.True(t, receiver.Receive(), "expecting 1 CAN frames") + require.NoError(t, receiver.Err()) + require.False(t, receiver.HasErrorFrame()) + require.Equal(t, expected, receiver.Frame()) + require.False(t, receiver.Receive(), "expecting exactly 1 CAN frames") + require.NoError(t, receiver.Err()) + } + + // no options + testReceive(func(*receiverOpts) {}) + + // frame interceptor + run := false + intFunc := func(can.Frame) { + run = true + } + testReceive(ReceiverFrameInterceptor(intFunc)) + require.True(t, run) +} + +func TestReceiver_ReceiveFrames(t *testing.T) { + for _, tt := range []struct { + msg string + input []byte + expectedFrames []can.Frame + }{ + { + msg: "no data", + input: []byte{}, + expectedFrames: []can.Frame{}, + }, + { + msg: "incomplete frame", + input: []byte{ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + }, + expectedFrames: []can.Frame{}, + }, + { + msg: "whole single frame", + input: []byte{ + // id---------------> | dlc | padding-------> | data----------------------------------------> | + 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x12, 0x34, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + }, + expectedFrames: []can.Frame{ + {ID: 0x01, Length: 2, Data: can.Data{0x12, 0x34}}, + }, + }, + { + msg: "one whole one incomplete", + input: []byte{ + // id---------------> | dlc | padding-------> | data----------------------------------------> | + 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x12, 0x34, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, + }, + expectedFrames: []can.Frame{ + {ID: 0x01, Length: 2, Data: can.Data{0x12, 0x34}}, + }, + }, + { + msg: "two whole frames", + input: []byte{ + // id---------------> | dlc | padding-------> | data----------------------------------------> | + 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x12, 0x34, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // id---------------> | dlc | padding-------> | data----------------------------------------> | + 0x02, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x56, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + }, + expectedFrames: []can.Frame{ + {ID: 0x01, Length: 2, Data: can.Data{0x12, 0x34}}, + {ID: 0x02, Length: 2, Data: can.Data{0x56, 0x78}}, + }, + }, + } { + tt := tt + t.Run(tt.msg, func(t *testing.T) { + receiver := NewReceiver(ioutil.NopCloser(bytes.NewReader(tt.input))) + for i, expected := range tt.expectedFrames { + require.True(t, receiver.Receive(), "expecting %d CAN frames", i+1) + require.NoError(t, receiver.Err()) + require.False(t, receiver.HasErrorFrame()) + require.Equal(t, expected, receiver.Frame()) + } + require.False(t, receiver.Receive(), "expecting exactly %d CAN frames", len(tt.expectedFrames)) + require.NoError(t, receiver.Err()) + }) + } +} + +func TestReceiver_ReceiveErrorFrame(t *testing.T) { + input := []byte{ + // frame + // id---------------> | dlc | padding-------> | data----------------------------------------> | + 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x12, 0x34, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // error frame + // id---------------> | dlc | padding-------> | data----------------------------------------> | + 0x01, 0x00, 0x00, 0x20, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // frame + // id---------------> | dlc | padding-------> | data----------------------------------------> | + 0x02, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x12, 0x34, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + } + receiver := NewReceiver(ioutil.NopCloser(bytes.NewReader(input))) + // expect frame + require.True(t, receiver.Receive()) + require.False(t, receiver.HasErrorFrame()) + require.Equal(t, can.Frame{ID: 0x01, Length: 2, Data: can.Data{0x12, 0x34}}, receiver.Frame()) + // expect error frame + require.True(t, receiver.Receive()) + require.True(t, receiver.HasErrorFrame()) + require.Equal(t, ErrorFrame{ErrorClass: ErrorClassTxTimeout}, receiver.ErrorFrame()) + // expect frame + require.True(t, receiver.Receive()) + require.False(t, receiver.HasErrorFrame()) + require.Equal(t, can.Frame{ID: 0x02, Length: 2, Data: can.Data{0x12, 0x34}}, receiver.Frame()) + // expect end of stream + require.False(t, receiver.Receive()) + require.NoError(t, receiver.Err()) +} diff --git a/pkg/socketcan/tools.go b/pkg/socketcan/tools.go new file mode 100644 index 0000000..79e0973 --- /dev/null +++ b/pkg/socketcan/tools.go @@ -0,0 +1,5 @@ +// +build tools + +package socketcan + +import _ "github.com/golang/mock/mockgen" diff --git a/pkg/socketcan/transceivererror.go b/pkg/socketcan/transceivererror.go new file mode 100644 index 0000000..92c3f70 --- /dev/null +++ b/pkg/socketcan/transceivererror.go @@ -0,0 +1,18 @@ +package socketcan + +type TransceiverError uint8 + +//go:generate gobin -m -run golang.org/x/tools/cmd/stringer -type TransceiverError -trimprefix TransceiverError + +const ( + TransceiverErrorUnspecified TransceiverError = 0x00 + TransceiverErrorCANHNoWire TransceiverError = 0x04 + TransceiverErrorCANHShortToBat TransceiverError = 0x05 + TransceiverErrorCANHShortToVCC TransceiverError = 0x06 + TransceiverErrorCANHShortToGND TransceiverError = 0x07 + TransceiverErrorCANLNoWire TransceiverError = 0x40 + TransceiverErrorCANLShortToBat TransceiverError = 0x50 + TransceiverErrorCANLShortToVcc TransceiverError = 0x60 + TransceiverErrorCANLShortToGND TransceiverError = 0x70 + TransceiverErrorCANLShortToCANH TransceiverError = 0x80 +) diff --git a/pkg/socketcan/transceivererror_string.go b/pkg/socketcan/transceivererror_string.go new file mode 100644 index 0000000..26ef4bd --- /dev/null +++ b/pkg/socketcan/transceivererror_string.go @@ -0,0 +1,57 @@ +// Code generated by "stringer -type TransceiverError -trimprefix TransceiverError"; DO NOT EDIT. + +package socketcan + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[TransceiverErrorUnspecified-0] + _ = x[TransceiverErrorCANHNoWire-4] + _ = x[TransceiverErrorCANHShortToBat-5] + _ = x[TransceiverErrorCANHShortToVCC-6] + _ = x[TransceiverErrorCANHShortToGND-7] + _ = x[TransceiverErrorCANLNoWire-64] + _ = x[TransceiverErrorCANLShortToBat-80] + _ = x[TransceiverErrorCANLShortToVcc-96] + _ = x[TransceiverErrorCANLShortToGND-112] + _ = x[TransceiverErrorCANLShortToCANH-128] +} + +const ( + _TransceiverError_name_0 = "Unspecified" + _TransceiverError_name_1 = "CANHNoWireCANHShortToBatCANHShortToVCCCANHShortToGND" + _TransceiverError_name_2 = "CANLNoWire" + _TransceiverError_name_3 = "CANLShortToBat" + _TransceiverError_name_4 = "CANLShortToVcc" + _TransceiverError_name_5 = "CANLShortToGND" + _TransceiverError_name_6 = "CANLShortToCANH" +) + +var ( + _TransceiverError_index_1 = [...]uint8{0, 10, 24, 38, 52} +) + +func (i TransceiverError) String() string { + switch { + case i == 0: + return _TransceiverError_name_0 + case 4 <= i && i <= 7: + i -= 4 + return _TransceiverError_name_1[_TransceiverError_index_1[i]:_TransceiverError_index_1[i+1]] + case i == 64: + return _TransceiverError_name_2 + case i == 80: + return _TransceiverError_name_3 + case i == 96: + return _TransceiverError_name_4 + case i == 112: + return _TransceiverError_name_5 + case i == 128: + return _TransceiverError_name_6 + default: + return "TransceiverError(" + strconv.FormatInt(int64(i), 10) + ")" + } +} diff --git a/pkg/socketcan/transmitter.go b/pkg/socketcan/transmitter.go new file mode 100644 index 0000000..2d61e1b --- /dev/null +++ b/pkg/socketcan/transmitter.go @@ -0,0 +1,75 @@ +package socketcan + +import ( + "context" + "fmt" + "net" + + "go.einride.tech/can" +) + +type TransmitterOption func(*transmitterOpts) + +type transmitterOpts struct { + frameInterceptor FrameInterceptor +} + +// Transmitter transmits CAN frames. +type Transmitter struct { + opts transmitterOpts + conn net.Conn +} + +// NewTransmitter creates a new transmitter that transmits CAN frames to the provided io.Writer. +func NewTransmitter(conn net.Conn, opt ...TransmitterOption) *Transmitter { + opts := transmitterOpts{} + for _, f := range opt { + f(&opts) + } + return &Transmitter{ + conn: conn, + opts: opts, + } +} + +// TransmitMessage transmits a CAN message. +func (t *Transmitter) TransmitMessage(ctx context.Context, m can.Message) error { + f, err := m.MarshalFrame() + if err != nil { + return fmt.Errorf("transmit message: %w", err) + } + return t.TransmitFrame(ctx, f) +} + +// TransmitFrame transmits a CAN frame. +func (t *Transmitter) TransmitFrame(ctx context.Context, f can.Frame) error { + var scf frame + scf.encodeFrame(f) + data := make([]byte, lengthOfFrame) + scf.marshalBinary(data) + if deadline, ok := ctx.Deadline(); ok { + if err := t.conn.SetWriteDeadline(deadline); err != nil { + return fmt.Errorf("transmit frame: %w", err) + } + } + if _, err := t.conn.Write(data); err != nil { + return fmt.Errorf("transmit frame: %w", err) + } + if t.opts.frameInterceptor != nil { + t.opts.frameInterceptor(f) + } + return nil +} + +// Close the transmitter's underlying connection. +func (t *Transmitter) Close() error { + return t.conn.Close() +} + +// TransmitterFrameInterceptor returns a TransmitterOption that sets the FrameInterceptor for the +// transmitter. Only one frame interceptor can be installed. +func TransmitterFrameInterceptor(i FrameInterceptor) TransmitterOption { + return func(o *transmitterOpts) { + o.frameInterceptor = i + } +} diff --git a/pkg/socketcan/transmitter_test.go b/pkg/socketcan/transmitter_test.go new file mode 100644 index 0000000..34cfa59 --- /dev/null +++ b/pkg/socketcan/transmitter_test.go @@ -0,0 +1,145 @@ +package socketcan + +import ( + "context" + "fmt" + "io" + "net" + "testing" + "time" + + "github.com/stretchr/testify/require" + "go.einride.tech/can" + "golang.org/x/sync/errgroup" + "golang.org/x/xerrors" +) + +func TestTransmitter_TransmitMessage(t *testing.T) { + testTransmit := func(opt TransmitterOption) { + w, r := net.Pipe() + f := can.Frame{ + ID: 0x12, + Length: 8, + Data: can.Data{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0}, + } + msg := &testMessage{frame: f} + expected := []byte{ + // id---------------> | dlc | padding-------> | data----------------------------------------> | + 0x12, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, + } + // write + var g errgroup.Group + g.Go(func() error { + tr := NewTransmitter(w, opt) + ctx, done := context.WithTimeout(context.Background(), time.Second) + defer done() + if err := tr.TransmitMessage(ctx, msg); err != nil { + return err + } + return w.Close() + }) + // read + actual := make([]byte, len(expected)) + _, err := io.ReadFull(r, actual) + require.NoError(t, err) + require.NoError(t, r.Close()) + // assert + require.Equal(t, expected, actual) + require.NoError(t, g.Wait()) + } + + // No opts + testTransmit(func(*transmitterOpts) {}) + + // Frame Interceptor + run := false + intFunc := func(fr can.Frame) { + run = true + } + testTransmit(TransmitterFrameInterceptor(intFunc)) + require.True(t, run) +} + +func TestTransmitter_TransmitMessage_Error(t *testing.T) { + cause := fmt.Errorf("boom") + msg := &testMessage{err: cause} + tr := NewTransmitter(nil) + ctx, done := context.WithTimeout(context.Background(), time.Second) + defer done() + err := tr.TransmitMessage(ctx, msg) + require.Error(t, err) + require.Equal(t, cause, xerrors.Unwrap(err)) +} + +func TestTransmitter_TransmitFrame_Error(t *testing.T) { + t.Run("set deadline", func(t *testing.T) { + cause := fmt.Errorf("boom") + w := &errCon{deadlineErr: cause} + tr := NewTransmitter(w) + ctx, done := context.WithTimeout(context.Background(), time.Second) + defer done() + err := tr.TransmitFrame(ctx, can.Frame{}) + require.Error(t, err) + require.Equal(t, cause, xerrors.Unwrap(err)) + }) + t.Run("write", func(t *testing.T) { + cause := fmt.Errorf("boom") + w := &errCon{writeErr: cause} + tr := NewTransmitter(w) + ctx, done := context.WithTimeout(context.Background(), time.Second) + defer done() + err := tr.TransmitFrame(ctx, can.Frame{}) + require.Error(t, err) + require.Equal(t, cause, xerrors.Unwrap(err)) + }) +} + +type testMessage struct { + frame can.Frame + err error +} + +func (t *testMessage) MarshalFrame() (can.Frame, error) { + return t.frame, t.err +} + +func (t *testMessage) UnmarshalFrame(can.Frame) error { + panic("should not be called") +} + +type errCon struct { + deadlineErr error + writeErr error +} + +func (e *errCon) Write(b []byte) (n int, err error) { + return 0, e.writeErr +} + +func (e *errCon) SetWriteDeadline(t time.Time) error { + return e.deadlineErr +} + +func (e *errCon) Read(b []byte) (n int, err error) { + panic("should not be called") +} + +func (e *errCon) Close() error { + panic("should not be called") +} + +func (e *errCon) LocalAddr() net.Addr { + panic("should not be called") +} + +func (e *errCon) RemoteAddr() net.Addr { + panic("should not be called") +} + +func (e *errCon) SetDeadline(t time.Time) error { + panic("should not be called") +} + +func (e *errCon) SetReadDeadline(t time.Time) error { + panic("should not be called") +} diff --git a/pkg/socketcan/udp.go b/pkg/socketcan/udp.go new file mode 100644 index 0000000..72db7ea --- /dev/null +++ b/pkg/socketcan/udp.go @@ -0,0 +1,157 @@ +package socketcan + +import ( + "fmt" + "net" + "strconv" + "time" + + "golang.org/x/net/ipv4" + "golang.org/x/net/nettest" +) + +// udpTxRx emulates a single `net.Conn` that can be used for both transmitting +// and receiving UDP multicast packets. +type udpTxRx struct { + tx *ipv4.PacketConn + rx *ipv4.PacketConn + groupAddr *net.UDPAddr +} + +func (utr *udpTxRx) Close() error { + if err := utr.tx.Close(); err != nil { + _ = utr.rx.Close() + return err + } + return utr.rx.Close() +} + +func (utr *udpTxRx) LocalAddr() net.Addr { + return utr.rx.LocalAddr() +} + +func (utr *udpTxRx) SetDeadline(t time.Time) error { + if err := utr.rx.SetReadDeadline(t); err != nil { + return err + } + if err := utr.tx.SetWriteDeadline(t); err != nil { + return err + } + return nil +} + +func (utr *udpTxRx) SetReadDeadline(t time.Time) error { + return utr.rx.SetReadDeadline(t) +} + +func (utr *udpTxRx) SetWriteDeadline(t time.Time) error { + return utr.tx.SetWriteDeadline(t) +} + +func (utr *udpTxRx) Read(b []byte) (n int, err error) { + n, _, _, err = utr.rx.ReadFrom(b) + return +} + +func (utr *udpTxRx) Write(b []byte) (n int, err error) { + return utr.tx.WriteTo(b, nil, nil) +} + +func (utr *udpTxRx) RemoteAddr() net.Addr { + return utr.groupAddr +} + +func udpTransceiver(network, address string) (*udpTxRx, error) { + if network != udp { + return nil, fmt.Errorf("[%v] is not a udp network", network) + } + ifi, err := getMulticastInterface() + if err != nil { + return nil, fmt.Errorf("new UDP transceiver: %w", err) + } + rx, groupAddr, err := udpReceiver(address, ifi) + if err != nil { + return nil, fmt.Errorf("new UDP transceiver: %w", err) + } + tx, err := udpTransmitter(groupAddr, ifi) + if err != nil { + return nil, fmt.Errorf("new UDP transceiver: %w", err) + } + return &udpTxRx{rx: rx, tx: tx, groupAddr: groupAddr}, nil +} + +func getMulticastInterface() (*net.Interface, error) { + ifi, err := nettest.RoutedInterface("ip4", net.FlagUp|net.FlagMulticast|net.FlagLoopback) + if err == nil { + return ifi, nil + } + return nettest.RoutedInterface("ip4", net.FlagUp|net.FlagMulticast) +} + +func hostPortToUDPAddr(hostport string) (*net.UDPAddr, error) { + host, portStr, err := net.SplitHostPort(hostport) + if err != nil { + return nil, fmt.Errorf("convert hostport to udp addr: %w", err) + } + port, err := strconv.Atoi(portStr) + if err != nil { + return nil, fmt.Errorf("convert hostport to udp addr: %w", err) + } + ip := net.ParseIP(host) + return &net.UDPAddr{Port: port, IP: ip}, nil +} + +func setMulticastOpts(p *ipv4.PacketConn, ifi *net.Interface, groupAddr net.Addr) error { + if err := p.JoinGroup(ifi, groupAddr); err != nil { + return err + } + if err := p.SetMulticastInterface(ifi); err != nil { + return err + } + if err := p.SetMulticastLoopback(true); err != nil { + return err + } + if err := p.SetMulticastTTL(0); err != nil { + return err + } + if err := p.SetTOS(0x0); err != nil { + return err + } + return nil +} + +func udpReceiver(address string, ifi *net.Interface) (*ipv4.PacketConn, *net.UDPAddr, error) { + c, err := net.ListenPacket("udp4", address) + if err != nil { + return nil, nil, fmt.Errorf("create udp receiver: %w", err) + } + groupAddr, err := hostPortToUDPAddr(address) + if err != nil { + return nil, nil, fmt.Errorf("create udp receiver: %w", err) + } + // If requested port is 0, one is provided when creating the packet listener + if groupAddr.Port == 0 { + localAddr, err := hostPortToUDPAddr(c.LocalAddr().String()) + if err != nil { + return nil, nil, fmt.Errorf("create udp receiver: %w", err) + } + groupAddr.Port = localAddr.Port + } + rx := ipv4.NewPacketConn(c) + if err := setMulticastOpts(rx, ifi, groupAddr); err != nil { + return nil, nil, fmt.Errorf("new UDP transceiver: %w", err) + } + return rx, groupAddr, nil +} + +func udpTransmitter(groupAddr *net.UDPAddr, ifi *net.Interface) (*ipv4.PacketConn, error) { + c, err := net.DialUDP("udp4", nil, groupAddr) + if err != nil { + return nil, fmt.Errorf("new UDP transmitter: %w", err) + } + tx := ipv4.NewPacketConn(c) + if err := tx.SetMulticastInterface(ifi); err != nil { + return nil, fmt.Errorf("new UDP transmitter: %w", err) + } + return tx, nil +} diff --git a/testdata/dbc/example/example.dbc b/testdata/dbc/example/example.dbc new file mode 100644 index 0000000..7ea4f35 --- /dev/null +++ b/testdata/dbc/example/example.dbc @@ -0,0 +1,78 @@ +VERSION "" + +NS_ : + +BS_: + +BU_: DBG DRIVER IO MOTOR SENSOR + +BO_ 1 EmptyMessage: 0 DBG + +BO_ 100 DriverHeartbeat: 1 DRIVER + SG_ Command : 0|8@1+ (1,0) [0|0] "" SENSOR,MOTOR + +BO_ 101 MotorCommand: 1 DRIVER + SG_ Steer : 0|4@1- (1,-5) [-5|5] "" MOTOR + SG_ Drive : 4|4@1+ (1,0) [0|9] "" MOTOR + +BO_ 400 MotorStatus: 3 MOTOR + SG_ WheelError : 0|1@1+ (1,0) [0|0] "" DRIVER,IO + SG_ SpeedKph : 8|16@1+ (0.001,0) [0|0] "km/h" DRIVER,IO + +BO_ 200 SensorSonars: 8 SENSOR + SG_ Mux M : 0|4@1+ (1,0) [0|0] "" DRIVER,IO + SG_ ErrCount : 4|12@1+ (1,0) [0|0] "" DRIVER,IO + SG_ Left m0 : 16|12@1+ (0.1,0) [0|0] "" DRIVER,IO + SG_ Middle m0 : 28|12@1+ (0.1,0) [0|0] "" DRIVER,IO + SG_ Right m0 : 40|12@1+ (0.1,0) [0|0] "" DRIVER,IO + SG_ Rear m0 : 52|12@1+ (0.1,0) [0|0] "" DRIVER,IO + SG_ NoFiltLeft m1 : 16|12@1+ (0.1,0) [0|0] "" DBG + SG_ NoFiltMiddle m1 : 28|12@1+ (0.1,0) [0|0] "" DBG + SG_ NoFiltRight m1 : 40|12@1+ (0.1,0) [0|0] "" DBG + SG_ NoFiltRear m1 : 52|12@1+ (0.1,0) [0|0] "" DBG + +BO_ 500 IODebug: 6 IO + SG_ TestUnsigned : 0|8@1+ (1,0) [0|0] "" DBG + SG_ TestEnum : 8|6@1+ (1,0) [0|0] "" DBG + SG_ TestSigned : 16|8@1- (1,0) [0|0] "" DBG + SG_ TestFloat : 24|8@1+ (0.5,0) [0|0] "" DBG + SG_ TestBoolEnum : 32|1@1+ (1,0) [0|0] "" DBG + SG_ TestScaledEnum : 40|2@1+ (2,0) [0|6] "" DBG + +EV_ BrakeEngaged: 0 [0|1] "" 0 10 DUMMY_NODE_VECTOR0 Vector__XXX; +EV_ Torque: 1 [0|30000] "mNm" 500 16 DUMMY_NODE_VECTOR0 Vector__XXX; + +CM_ EV_ BrakeEngaged "Brake fully engaged"; +CM_ BU_ DRIVER "The driver controller driving the car"; +CM_ BU_ MOTOR "The motor controller of the car"; +CM_ BU_ SENSOR "The sensor controller of the car"; +CM_ BO_ 100 "Sync message used to synchronize the controllers"; + +BA_DEF_ "BusType" STRING ; +BA_DEF_ BO_ "GenMsgSendType" ENUM "None","Cyclic","OnEvent"; +BA_DEF_ BO_ "GenMsgCycleTime" INT 0 0; +BA_DEF_ SG_ "FieldType" STRING ; +BA_DEF_ SG_ "GenSigStartValue" INT 0 10000; +BA_DEF_DEF_ "BusType" "CAN"; +BA_DEF_DEF_ "FieldType" ""; +BA_DEF_DEF_ "GenMsgCycleTime" 0; +BA_DEF_DEF_ "GenSigStartValue" 0; + +BA_ "GenMsgSendType" BO_ 1 0; +BA_ "GenMsgSendType" BO_ 100 1; +BA_ "GenMsgCycleTime" BO_ 100 1000; +BA_ "GenMsgSendType" BO_ 101 1; +BA_ "GenMsgCycleTime" BO_ 101 100; +BA_ "GenMsgSendType" BO_ 200 1; +BA_ "GenMsgCycleTime" BO_ 200 100; +BA_ "GenMsgSendType" BO_ 400 1; +BA_ "GenMsgCycleTime" BO_ 400 100; +BA_ "GenMsgSendType" BO_ 500 2; +BA_ "FieldType" SG_ 100 Command "Command"; +BA_ "FieldType" SG_ 500 TestEnum "TestEnum"; +BA_ "GenSigStartValue" SG_ 500 TestEnum 2; + +VAL_ 100 Command 2 "Reboot" 1 "Sync" 0 "None" ; +VAL_ 500 TestEnum 2 "Two" 1 "One" ; +VAL_ 500 TestScaledEnum 3 "Six" 2 "Four" 1 "Two" 0 "Zero" ; +VAL_ 500 TestBoolEnum 1 "One" 0 "Zero" ; diff --git a/testdata/dbc/example/example.dbc.golden b/testdata/dbc/example/example.dbc.golden new file mode 100644 index 0000000..e6e18d9 --- /dev/null +++ b/testdata/dbc/example/example.dbc.golden @@ -0,0 +1,894 @@ +([]dbc.Def) (len=43) { + (*dbc.VersionDef)({ + Pos: (scanner.Position) ../../testdata/dbc/example/example.dbc:1:1, + Version: (string) "" + }), + (*dbc.NewSymbolsDef)({ + Pos: (scanner.Position) ../../testdata/dbc/example/example.dbc:3:1, + Symbols: ([]dbc.Keyword) + }), + (*dbc.BitTimingDef)({ + Pos: (scanner.Position) ../../testdata/dbc/example/example.dbc:5:1, + BaudRate: (uint64) 0, + BTR1: (uint64) 0, + BTR2: (uint64) 0 + }), + (*dbc.NodesDef)({ + Pos: (scanner.Position) ../../testdata/dbc/example/example.dbc:7:1, + NodeNames: ([]dbc.Identifier) (len=5) { + (dbc.Identifier) (len=3) "DBG", + (dbc.Identifier) (len=6) "DRIVER", + (dbc.Identifier) (len=2) "IO", + (dbc.Identifier) (len=5) "MOTOR", + (dbc.Identifier) (len=6) "SENSOR" + } + }), + (*dbc.MessageDef)({ + Pos: (scanner.Position) ../../testdata/dbc/example/example.dbc:9:1, + MessageID: (dbc.MessageID) 1, + Name: (dbc.Identifier) (len=12) "EmptyMessage", + Size: (uint64) 0, + Transmitter: (dbc.Identifier) (len=3) "DBG", + Signals: ([]dbc.SignalDef) + }), + (*dbc.MessageDef)({ + Pos: (scanner.Position) ../../testdata/dbc/example/example.dbc:11:1, + MessageID: (dbc.MessageID) 100, + Name: (dbc.Identifier) (len=15) "DriverHeartbeat", + Size: (uint64) 1, + Transmitter: (dbc.Identifier) (len=6) "DRIVER", + Signals: ([]dbc.SignalDef) (len=1) { + (dbc.SignalDef) { + Pos: (scanner.Position) ../../testdata/dbc/example/example.dbc:12:2, + Name: (dbc.Identifier) (len=7) "Command", + StartBit: (uint64) 0, + Size: (uint64) 8, + IsBigEndian: (bool) false, + IsSigned: (bool) false, + IsMultiplexerSwitch: (bool) false, + IsMultiplexed: (bool) false, + MultiplexerSwitch: (uint64) 0, + Offset: (float64) 0, + Factor: (float64) 1, + Minimum: (float64) 0, + Maximum: (float64) 0, + Unit: (string) "", + Receivers: ([]dbc.Identifier) (len=2) { + (dbc.Identifier) (len=6) "SENSOR", + (dbc.Identifier) (len=5) "MOTOR" + } + } + } + }), + (*dbc.MessageDef)({ + Pos: (scanner.Position) ../../testdata/dbc/example/example.dbc:14:1, + MessageID: (dbc.MessageID) 101, + Name: (dbc.Identifier) (len=12) "MotorCommand", + Size: (uint64) 1, + Transmitter: (dbc.Identifier) (len=6) "DRIVER", + Signals: ([]dbc.SignalDef) (len=2) { + (dbc.SignalDef) { + Pos: (scanner.Position) ../../testdata/dbc/example/example.dbc:15:2, + Name: (dbc.Identifier) (len=5) "Steer", + StartBit: (uint64) 0, + Size: (uint64) 4, + IsBigEndian: (bool) false, + IsSigned: (bool) true, + IsMultiplexerSwitch: (bool) false, + IsMultiplexed: (bool) false, + MultiplexerSwitch: (uint64) 0, + Offset: (float64) -5, + Factor: (float64) 1, + Minimum: (float64) -5, + Maximum: (float64) 5, + Unit: (string) "", + Receivers: ([]dbc.Identifier) (len=1) { + (dbc.Identifier) (len=5) "MOTOR" + } + }, + (dbc.SignalDef) { + Pos: (scanner.Position) ../../testdata/dbc/example/example.dbc:16:2, + Name: (dbc.Identifier) (len=5) "Drive", + StartBit: (uint64) 4, + Size: (uint64) 4, + IsBigEndian: (bool) false, + IsSigned: (bool) false, + IsMultiplexerSwitch: (bool) false, + IsMultiplexed: (bool) false, + MultiplexerSwitch: (uint64) 0, + Offset: (float64) 0, + Factor: (float64) 1, + Minimum: (float64) 0, + Maximum: (float64) 9, + Unit: (string) "", + Receivers: ([]dbc.Identifier) (len=1) { + (dbc.Identifier) (len=5) "MOTOR" + } + } + } + }), + (*dbc.MessageDef)({ + Pos: (scanner.Position) ../../testdata/dbc/example/example.dbc:18:1, + MessageID: (dbc.MessageID) 400, + Name: (dbc.Identifier) (len=11) "MotorStatus", + Size: (uint64) 3, + Transmitter: (dbc.Identifier) (len=5) "MOTOR", + Signals: ([]dbc.SignalDef) (len=2) { + (dbc.SignalDef) { + Pos: (scanner.Position) ../../testdata/dbc/example/example.dbc:19:2, + Name: (dbc.Identifier) (len=10) "WheelError", + StartBit: (uint64) 0, + Size: (uint64) 1, + IsBigEndian: (bool) false, + IsSigned: (bool) false, + IsMultiplexerSwitch: (bool) false, + IsMultiplexed: (bool) false, + MultiplexerSwitch: (uint64) 0, + Offset: (float64) 0, + Factor: (float64) 1, + Minimum: (float64) 0, + Maximum: (float64) 0, + Unit: (string) "", + Receivers: ([]dbc.Identifier) (len=2) { + (dbc.Identifier) (len=6) "DRIVER", + (dbc.Identifier) (len=2) "IO" + } + }, + (dbc.SignalDef) { + Pos: (scanner.Position) ../../testdata/dbc/example/example.dbc:20:2, + Name: (dbc.Identifier) (len=8) "SpeedKph", + StartBit: (uint64) 8, + Size: (uint64) 16, + IsBigEndian: (bool) false, + IsSigned: (bool) false, + IsMultiplexerSwitch: (bool) false, + IsMultiplexed: (bool) false, + MultiplexerSwitch: (uint64) 0, + Offset: (float64) 0, + Factor: (float64) 0.001, + Minimum: (float64) 0, + Maximum: (float64) 0, + Unit: (string) (len=4) "km/h", + Receivers: ([]dbc.Identifier) (len=2) { + (dbc.Identifier) (len=6) "DRIVER", + (dbc.Identifier) (len=2) "IO" + } + } + } + }), + (*dbc.MessageDef)({ + Pos: (scanner.Position) ../../testdata/dbc/example/example.dbc:22:1, + MessageID: (dbc.MessageID) 200, + Name: (dbc.Identifier) (len=12) "SensorSonars", + Size: (uint64) 8, + Transmitter: (dbc.Identifier) (len=6) "SENSOR", + Signals: ([]dbc.SignalDef) (len=10) { + (dbc.SignalDef) { + Pos: (scanner.Position) ../../testdata/dbc/example/example.dbc:23:2, + Name: (dbc.Identifier) (len=3) "Mux", + StartBit: (uint64) 0, + Size: (uint64) 4, + IsBigEndian: (bool) false, + IsSigned: (bool) false, + IsMultiplexerSwitch: (bool) true, + IsMultiplexed: (bool) false, + MultiplexerSwitch: (uint64) 0, + Offset: (float64) 0, + Factor: (float64) 1, + Minimum: (float64) 0, + Maximum: (float64) 0, + Unit: (string) "", + Receivers: ([]dbc.Identifier) (len=2) { + (dbc.Identifier) (len=6) "DRIVER", + (dbc.Identifier) (len=2) "IO" + } + }, + (dbc.SignalDef) { + Pos: (scanner.Position) ../../testdata/dbc/example/example.dbc:24:2, + Name: (dbc.Identifier) (len=8) "ErrCount", + StartBit: (uint64) 4, + Size: (uint64) 12, + IsBigEndian: (bool) false, + IsSigned: (bool) false, + IsMultiplexerSwitch: (bool) false, + IsMultiplexed: (bool) false, + MultiplexerSwitch: (uint64) 0, + Offset: (float64) 0, + Factor: (float64) 1, + Minimum: (float64) 0, + Maximum: (float64) 0, + Unit: (string) "", + Receivers: ([]dbc.Identifier) (len=2) { + (dbc.Identifier) (len=6) "DRIVER", + (dbc.Identifier) (len=2) "IO" + } + }, + (dbc.SignalDef) { + Pos: (scanner.Position) ../../testdata/dbc/example/example.dbc:25:2, + Name: (dbc.Identifier) (len=4) "Left", + StartBit: (uint64) 16, + Size: (uint64) 12, + IsBigEndian: (bool) false, + IsSigned: (bool) false, + IsMultiplexerSwitch: (bool) false, + IsMultiplexed: (bool) true, + MultiplexerSwitch: (uint64) 0, + Offset: (float64) 0, + Factor: (float64) 0.1, + Minimum: (float64) 0, + Maximum: (float64) 0, + Unit: (string) "", + Receivers: ([]dbc.Identifier) (len=2) { + (dbc.Identifier) (len=6) "DRIVER", + (dbc.Identifier) (len=2) "IO" + } + }, + (dbc.SignalDef) { + Pos: (scanner.Position) ../../testdata/dbc/example/example.dbc:26:2, + Name: (dbc.Identifier) (len=6) "Middle", + StartBit: (uint64) 28, + Size: (uint64) 12, + IsBigEndian: (bool) false, + IsSigned: (bool) false, + IsMultiplexerSwitch: (bool) false, + IsMultiplexed: (bool) true, + MultiplexerSwitch: (uint64) 0, + Offset: (float64) 0, + Factor: (float64) 0.1, + Minimum: (float64) 0, + Maximum: (float64) 0, + Unit: (string) "", + Receivers: ([]dbc.Identifier) (len=2) { + (dbc.Identifier) (len=6) "DRIVER", + (dbc.Identifier) (len=2) "IO" + } + }, + (dbc.SignalDef) { + Pos: (scanner.Position) ../../testdata/dbc/example/example.dbc:27:2, + Name: (dbc.Identifier) (len=5) "Right", + StartBit: (uint64) 40, + Size: (uint64) 12, + IsBigEndian: (bool) false, + IsSigned: (bool) false, + IsMultiplexerSwitch: (bool) false, + IsMultiplexed: (bool) true, + MultiplexerSwitch: (uint64) 0, + Offset: (float64) 0, + Factor: (float64) 0.1, + Minimum: (float64) 0, + Maximum: (float64) 0, + Unit: (string) "", + Receivers: ([]dbc.Identifier) (len=2) { + (dbc.Identifier) (len=6) "DRIVER", + (dbc.Identifier) (len=2) "IO" + } + }, + (dbc.SignalDef) { + Pos: (scanner.Position) ../../testdata/dbc/example/example.dbc:28:2, + Name: (dbc.Identifier) (len=4) "Rear", + StartBit: (uint64) 52, + Size: (uint64) 12, + IsBigEndian: (bool) false, + IsSigned: (bool) false, + IsMultiplexerSwitch: (bool) false, + IsMultiplexed: (bool) true, + MultiplexerSwitch: (uint64) 0, + Offset: (float64) 0, + Factor: (float64) 0.1, + Minimum: (float64) 0, + Maximum: (float64) 0, + Unit: (string) "", + Receivers: ([]dbc.Identifier) (len=2) { + (dbc.Identifier) (len=6) "DRIVER", + (dbc.Identifier) (len=2) "IO" + } + }, + (dbc.SignalDef) { + Pos: (scanner.Position) ../../testdata/dbc/example/example.dbc:29:2, + Name: (dbc.Identifier) (len=10) "NoFiltLeft", + StartBit: (uint64) 16, + Size: (uint64) 12, + IsBigEndian: (bool) false, + IsSigned: (bool) false, + IsMultiplexerSwitch: (bool) false, + IsMultiplexed: (bool) true, + MultiplexerSwitch: (uint64) 1, + Offset: (float64) 0, + Factor: (float64) 0.1, + Minimum: (float64) 0, + Maximum: (float64) 0, + Unit: (string) "", + Receivers: ([]dbc.Identifier) (len=1) { + (dbc.Identifier) (len=3) "DBG" + } + }, + (dbc.SignalDef) { + Pos: (scanner.Position) ../../testdata/dbc/example/example.dbc:30:2, + Name: (dbc.Identifier) (len=12) "NoFiltMiddle", + StartBit: (uint64) 28, + Size: (uint64) 12, + IsBigEndian: (bool) false, + IsSigned: (bool) false, + IsMultiplexerSwitch: (bool) false, + IsMultiplexed: (bool) true, + MultiplexerSwitch: (uint64) 1, + Offset: (float64) 0, + Factor: (float64) 0.1, + Minimum: (float64) 0, + Maximum: (float64) 0, + Unit: (string) "", + Receivers: ([]dbc.Identifier) (len=1) { + (dbc.Identifier) (len=3) "DBG" + } + }, + (dbc.SignalDef) { + Pos: (scanner.Position) ../../testdata/dbc/example/example.dbc:31:2, + Name: (dbc.Identifier) (len=11) "NoFiltRight", + StartBit: (uint64) 40, + Size: (uint64) 12, + IsBigEndian: (bool) false, + IsSigned: (bool) false, + IsMultiplexerSwitch: (bool) false, + IsMultiplexed: (bool) true, + MultiplexerSwitch: (uint64) 1, + Offset: (float64) 0, + Factor: (float64) 0.1, + Minimum: (float64) 0, + Maximum: (float64) 0, + Unit: (string) "", + Receivers: ([]dbc.Identifier) (len=1) { + (dbc.Identifier) (len=3) "DBG" + } + }, + (dbc.SignalDef) { + Pos: (scanner.Position) ../../testdata/dbc/example/example.dbc:32:2, + Name: (dbc.Identifier) (len=10) "NoFiltRear", + StartBit: (uint64) 52, + Size: (uint64) 12, + IsBigEndian: (bool) false, + IsSigned: (bool) false, + IsMultiplexerSwitch: (bool) false, + IsMultiplexed: (bool) true, + MultiplexerSwitch: (uint64) 1, + Offset: (float64) 0, + Factor: (float64) 0.1, + Minimum: (float64) 0, + Maximum: (float64) 0, + Unit: (string) "", + Receivers: ([]dbc.Identifier) (len=1) { + (dbc.Identifier) (len=3) "DBG" + } + } + } + }), + (*dbc.MessageDef)({ + Pos: (scanner.Position) ../../testdata/dbc/example/example.dbc:34:1, + MessageID: (dbc.MessageID) 500, + Name: (dbc.Identifier) (len=7) "IODebug", + Size: (uint64) 6, + Transmitter: (dbc.Identifier) (len=2) "IO", + Signals: ([]dbc.SignalDef) (len=6) { + (dbc.SignalDef) { + Pos: (scanner.Position) ../../testdata/dbc/example/example.dbc:35:2, + Name: (dbc.Identifier) (len=12) "TestUnsigned", + StartBit: (uint64) 0, + Size: (uint64) 8, + IsBigEndian: (bool) false, + IsSigned: (bool) false, + IsMultiplexerSwitch: (bool) false, + IsMultiplexed: (bool) false, + MultiplexerSwitch: (uint64) 0, + Offset: (float64) 0, + Factor: (float64) 1, + Minimum: (float64) 0, + Maximum: (float64) 0, + Unit: (string) "", + Receivers: ([]dbc.Identifier) (len=1) { + (dbc.Identifier) (len=3) "DBG" + } + }, + (dbc.SignalDef) { + Pos: (scanner.Position) ../../testdata/dbc/example/example.dbc:36:2, + Name: (dbc.Identifier) (len=8) "TestEnum", + StartBit: (uint64) 8, + Size: (uint64) 6, + IsBigEndian: (bool) false, + IsSigned: (bool) false, + IsMultiplexerSwitch: (bool) false, + IsMultiplexed: (bool) false, + MultiplexerSwitch: (uint64) 0, + Offset: (float64) 0, + Factor: (float64) 1, + Minimum: (float64) 0, + Maximum: (float64) 0, + Unit: (string) "", + Receivers: ([]dbc.Identifier) (len=1) { + (dbc.Identifier) (len=3) "DBG" + } + }, + (dbc.SignalDef) { + Pos: (scanner.Position) ../../testdata/dbc/example/example.dbc:37:2, + Name: (dbc.Identifier) (len=10) "TestSigned", + StartBit: (uint64) 16, + Size: (uint64) 8, + IsBigEndian: (bool) false, + IsSigned: (bool) true, + IsMultiplexerSwitch: (bool) false, + IsMultiplexed: (bool) false, + MultiplexerSwitch: (uint64) 0, + Offset: (float64) 0, + Factor: (float64) 1, + Minimum: (float64) 0, + Maximum: (float64) 0, + Unit: (string) "", + Receivers: ([]dbc.Identifier) (len=1) { + (dbc.Identifier) (len=3) "DBG" + } + }, + (dbc.SignalDef) { + Pos: (scanner.Position) ../../testdata/dbc/example/example.dbc:38:2, + Name: (dbc.Identifier) (len=9) "TestFloat", + StartBit: (uint64) 24, + Size: (uint64) 8, + IsBigEndian: (bool) false, + IsSigned: (bool) false, + IsMultiplexerSwitch: (bool) false, + IsMultiplexed: (bool) false, + MultiplexerSwitch: (uint64) 0, + Offset: (float64) 0, + Factor: (float64) 0.5, + Minimum: (float64) 0, + Maximum: (float64) 0, + Unit: (string) "", + Receivers: ([]dbc.Identifier) (len=1) { + (dbc.Identifier) (len=3) "DBG" + } + }, + (dbc.SignalDef) { + Pos: (scanner.Position) ../../testdata/dbc/example/example.dbc:39:2, + Name: (dbc.Identifier) (len=12) "TestBoolEnum", + StartBit: (uint64) 32, + Size: (uint64) 1, + IsBigEndian: (bool) false, + IsSigned: (bool) false, + IsMultiplexerSwitch: (bool) false, + IsMultiplexed: (bool) false, + MultiplexerSwitch: (uint64) 0, + Offset: (float64) 0, + Factor: (float64) 1, + Minimum: (float64) 0, + Maximum: (float64) 0, + Unit: (string) "", + Receivers: ([]dbc.Identifier) (len=1) { + (dbc.Identifier) (len=3) "DBG" + } + }, + (dbc.SignalDef) { + Pos: (scanner.Position) ../../testdata/dbc/example/example.dbc:40:2, + Name: (dbc.Identifier) (len=14) "TestScaledEnum", + StartBit: (uint64) 40, + Size: (uint64) 2, + IsBigEndian: (bool) false, + IsSigned: (bool) false, + IsMultiplexerSwitch: (bool) false, + IsMultiplexed: (bool) false, + MultiplexerSwitch: (uint64) 0, + Offset: (float64) 0, + Factor: (float64) 2, + Minimum: (float64) 0, + Maximum: (float64) 6, + Unit: (string) "", + Receivers: ([]dbc.Identifier) (len=1) { + (dbc.Identifier) (len=3) "DBG" + } + } + } + }), + (*dbc.EnvironmentVariableDef)({ + Pos: (scanner.Position) ../../testdata/dbc/example/example.dbc:42:1, + Name: (dbc.Identifier) (len=12) "BrakeEngaged", + Type: (dbc.EnvironmentVariableType) 0, + Minimum: (float64) 0, + Maximum: (float64) 1, + Unit: (string) "", + InitialValue: (float64) 0, + ID: (uint64) 10, + AccessType: (dbc.AccessType) (len=18) "DUMMY_NODE_VECTOR0", + AccessNodes: ([]dbc.Identifier) (len=1) { + (dbc.Identifier) (len=11) "Vector__XXX" + } + }), + (*dbc.EnvironmentVariableDef)({ + Pos: (scanner.Position) ../../testdata/dbc/example/example.dbc:43:1, + Name: (dbc.Identifier) (len=6) "Torque", + Type: (dbc.EnvironmentVariableType) 1, + Minimum: (float64) 0, + Maximum: (float64) 30000, + Unit: (string) (len=3) "mNm", + InitialValue: (float64) 500, + ID: (uint64) 16, + AccessType: (dbc.AccessType) (len=18) "DUMMY_NODE_VECTOR0", + AccessNodes: ([]dbc.Identifier) (len=1) { + (dbc.Identifier) (len=11) "Vector__XXX" + } + }), + (*dbc.CommentDef)({ + Pos: (scanner.Position) ../../testdata/dbc/example/example.dbc:45:1, + ObjectType: (dbc.ObjectType) (len=3) "EV_", + NodeName: (dbc.Identifier) "", + MessageID: (dbc.MessageID) 0, + SignalName: (dbc.Identifier) "", + EnvironmentVariableName: (dbc.Identifier) (len=12) "BrakeEngaged", + Comment: (string) (len=19) "Brake fully engaged" + }), + (*dbc.CommentDef)({ + Pos: (scanner.Position) ../../testdata/dbc/example/example.dbc:46:1, + ObjectType: (dbc.ObjectType) (len=3) "BU_", + NodeName: (dbc.Identifier) (len=6) "DRIVER", + MessageID: (dbc.MessageID) 0, + SignalName: (dbc.Identifier) "", + EnvironmentVariableName: (dbc.Identifier) "", + Comment: (string) (len=37) "The driver controller driving the car" + }), + (*dbc.CommentDef)({ + Pos: (scanner.Position) ../../testdata/dbc/example/example.dbc:47:1, + ObjectType: (dbc.ObjectType) (len=3) "BU_", + NodeName: (dbc.Identifier) (len=5) "MOTOR", + MessageID: (dbc.MessageID) 0, + SignalName: (dbc.Identifier) "", + EnvironmentVariableName: (dbc.Identifier) "", + Comment: (string) (len=31) "The motor controller of the car" + }), + (*dbc.CommentDef)({ + Pos: (scanner.Position) ../../testdata/dbc/example/example.dbc:48:1, + ObjectType: (dbc.ObjectType) (len=3) "BU_", + NodeName: (dbc.Identifier) (len=6) "SENSOR", + MessageID: (dbc.MessageID) 0, + SignalName: (dbc.Identifier) "", + EnvironmentVariableName: (dbc.Identifier) "", + Comment: (string) (len=32) "The sensor controller of the car" + }), + (*dbc.CommentDef)({ + Pos: (scanner.Position) ../../testdata/dbc/example/example.dbc:49:1, + ObjectType: (dbc.ObjectType) (len=3) "BO_", + NodeName: (dbc.Identifier) "", + MessageID: (dbc.MessageID) 100, + SignalName: (dbc.Identifier) "", + EnvironmentVariableName: (dbc.Identifier) "", + Comment: (string) (len=48) "Sync message used to synchronize the controllers" + }), + (*dbc.AttributeDef)({ + Pos: (scanner.Position) ../../testdata/dbc/example/example.dbc:51:1, + ObjectType: (dbc.ObjectType) "", + Name: (dbc.Identifier) (len=7) "BusType", + Type: (dbc.AttributeValueType) (len=6) "STRING", + MinimumInt: (int64) 0, + MaximumInt: (int64) 0, + MinimumFloat: (float64) 0, + MaximumFloat: (float64) 0, + EnumValues: ([]string) + }), + (*dbc.AttributeDef)({ + Pos: (scanner.Position) ../../testdata/dbc/example/example.dbc:52:1, + ObjectType: (dbc.ObjectType) (len=3) "BO_", + Name: (dbc.Identifier) (len=14) "GenMsgSendType", + Type: (dbc.AttributeValueType) (len=4) "ENUM", + MinimumInt: (int64) 0, + MaximumInt: (int64) 0, + MinimumFloat: (float64) 0, + MaximumFloat: (float64) 0, + EnumValues: ([]string) (len=3) { + (string) (len=4) "None", + (string) (len=6) "Cyclic", + (string) (len=7) "OnEvent" + } + }), + (*dbc.AttributeDef)({ + Pos: (scanner.Position) ../../testdata/dbc/example/example.dbc:53:1, + ObjectType: (dbc.ObjectType) (len=3) "BO_", + Name: (dbc.Identifier) (len=15) "GenMsgCycleTime", + Type: (dbc.AttributeValueType) (len=3) "INT", + MinimumInt: (int64) 0, + MaximumInt: (int64) 0, + MinimumFloat: (float64) 0, + MaximumFloat: (float64) 0, + EnumValues: ([]string) + }), + (*dbc.AttributeDef)({ + Pos: (scanner.Position) ../../testdata/dbc/example/example.dbc:54:1, + ObjectType: (dbc.ObjectType) (len=3) "SG_", + Name: (dbc.Identifier) (len=9) "FieldType", + Type: (dbc.AttributeValueType) (len=6) "STRING", + MinimumInt: (int64) 0, + MaximumInt: (int64) 0, + MinimumFloat: (float64) 0, + MaximumFloat: (float64) 0, + EnumValues: ([]string) + }), + (*dbc.AttributeDef)({ + Pos: (scanner.Position) ../../testdata/dbc/example/example.dbc:55:1, + ObjectType: (dbc.ObjectType) (len=3) "SG_", + Name: (dbc.Identifier) (len=16) "GenSigStartValue", + Type: (dbc.AttributeValueType) (len=3) "INT", + MinimumInt: (int64) 0, + MaximumInt: (int64) 10000, + MinimumFloat: (float64) 0, + MaximumFloat: (float64) 0, + EnumValues: ([]string) + }), + (*dbc.AttributeDefaultValueDef)({ + Pos: (scanner.Position) ../../testdata/dbc/example/example.dbc:56:1, + AttributeName: (dbc.Identifier) (len=7) "BusType", + DefaultIntValue: (int64) 0, + DefaultFloatValue: (float64) 0, + DefaultStringValue: (string) (len=3) "CAN" + }), + (*dbc.AttributeDefaultValueDef)({ + Pos: (scanner.Position) ../../testdata/dbc/example/example.dbc:57:1, + AttributeName: (dbc.Identifier) (len=9) "FieldType", + DefaultIntValue: (int64) 0, + DefaultFloatValue: (float64) 0, + DefaultStringValue: (string) "" + }), + (*dbc.AttributeDefaultValueDef)({ + Pos: (scanner.Position) ../../testdata/dbc/example/example.dbc:58:1, + AttributeName: (dbc.Identifier) (len=15) "GenMsgCycleTime", + DefaultIntValue: (int64) 0, + DefaultFloatValue: (float64) 0, + DefaultStringValue: (string) "" + }), + (*dbc.AttributeDefaultValueDef)({ + Pos: (scanner.Position) ../../testdata/dbc/example/example.dbc:59:1, + AttributeName: (dbc.Identifier) (len=16) "GenSigStartValue", + DefaultIntValue: (int64) 0, + DefaultFloatValue: (float64) 0, + DefaultStringValue: (string) "" + }), + (*dbc.AttributeValueForObjectDef)({ + Pos: (scanner.Position) ../../testdata/dbc/example/example.dbc:61:1, + AttributeName: (dbc.Identifier) (len=14) "GenMsgSendType", + ObjectType: (dbc.ObjectType) (len=3) "BO_", + MessageID: (dbc.MessageID) 1, + SignalName: (dbc.Identifier) "", + NodeName: (dbc.Identifier) "", + EnvironmentVariableName: (dbc.Identifier) "", + IntValue: (int64) 0, + FloatValue: (float64) 0, + StringValue: (string) (len=4) "None" + }), + (*dbc.AttributeValueForObjectDef)({ + Pos: (scanner.Position) ../../testdata/dbc/example/example.dbc:62:1, + AttributeName: (dbc.Identifier) (len=14) "GenMsgSendType", + ObjectType: (dbc.ObjectType) (len=3) "BO_", + MessageID: (dbc.MessageID) 100, + SignalName: (dbc.Identifier) "", + NodeName: (dbc.Identifier) "", + EnvironmentVariableName: (dbc.Identifier) "", + IntValue: (int64) 0, + FloatValue: (float64) 0, + StringValue: (string) (len=6) "Cyclic" + }), + (*dbc.AttributeValueForObjectDef)({ + Pos: (scanner.Position) ../../testdata/dbc/example/example.dbc:63:1, + AttributeName: (dbc.Identifier) (len=15) "GenMsgCycleTime", + ObjectType: (dbc.ObjectType) (len=3) "BO_", + MessageID: (dbc.MessageID) 100, + SignalName: (dbc.Identifier) "", + NodeName: (dbc.Identifier) "", + EnvironmentVariableName: (dbc.Identifier) "", + IntValue: (int64) 1000, + FloatValue: (float64) 0, + StringValue: (string) "" + }), + (*dbc.AttributeValueForObjectDef)({ + Pos: (scanner.Position) ../../testdata/dbc/example/example.dbc:64:1, + AttributeName: (dbc.Identifier) (len=14) "GenMsgSendType", + ObjectType: (dbc.ObjectType) (len=3) "BO_", + MessageID: (dbc.MessageID) 101, + SignalName: (dbc.Identifier) "", + NodeName: (dbc.Identifier) "", + EnvironmentVariableName: (dbc.Identifier) "", + IntValue: (int64) 0, + FloatValue: (float64) 0, + StringValue: (string) (len=6) "Cyclic" + }), + (*dbc.AttributeValueForObjectDef)({ + Pos: (scanner.Position) ../../testdata/dbc/example/example.dbc:65:1, + AttributeName: (dbc.Identifier) (len=15) "GenMsgCycleTime", + ObjectType: (dbc.ObjectType) (len=3) "BO_", + MessageID: (dbc.MessageID) 101, + SignalName: (dbc.Identifier) "", + NodeName: (dbc.Identifier) "", + EnvironmentVariableName: (dbc.Identifier) "", + IntValue: (int64) 100, + FloatValue: (float64) 0, + StringValue: (string) "" + }), + (*dbc.AttributeValueForObjectDef)({ + Pos: (scanner.Position) ../../testdata/dbc/example/example.dbc:66:1, + AttributeName: (dbc.Identifier) (len=14) "GenMsgSendType", + ObjectType: (dbc.ObjectType) (len=3) "BO_", + MessageID: (dbc.MessageID) 200, + SignalName: (dbc.Identifier) "", + NodeName: (dbc.Identifier) "", + EnvironmentVariableName: (dbc.Identifier) "", + IntValue: (int64) 0, + FloatValue: (float64) 0, + StringValue: (string) (len=6) "Cyclic" + }), + (*dbc.AttributeValueForObjectDef)({ + Pos: (scanner.Position) ../../testdata/dbc/example/example.dbc:67:1, + AttributeName: (dbc.Identifier) (len=15) "GenMsgCycleTime", + ObjectType: (dbc.ObjectType) (len=3) "BO_", + MessageID: (dbc.MessageID) 200, + SignalName: (dbc.Identifier) "", + NodeName: (dbc.Identifier) "", + EnvironmentVariableName: (dbc.Identifier) "", + IntValue: (int64) 100, + FloatValue: (float64) 0, + StringValue: (string) "" + }), + (*dbc.AttributeValueForObjectDef)({ + Pos: (scanner.Position) ../../testdata/dbc/example/example.dbc:68:1, + AttributeName: (dbc.Identifier) (len=14) "GenMsgSendType", + ObjectType: (dbc.ObjectType) (len=3) "BO_", + MessageID: (dbc.MessageID) 400, + SignalName: (dbc.Identifier) "", + NodeName: (dbc.Identifier) "", + EnvironmentVariableName: (dbc.Identifier) "", + IntValue: (int64) 0, + FloatValue: (float64) 0, + StringValue: (string) (len=6) "Cyclic" + }), + (*dbc.AttributeValueForObjectDef)({ + Pos: (scanner.Position) ../../testdata/dbc/example/example.dbc:69:1, + AttributeName: (dbc.Identifier) (len=15) "GenMsgCycleTime", + ObjectType: (dbc.ObjectType) (len=3) "BO_", + MessageID: (dbc.MessageID) 400, + SignalName: (dbc.Identifier) "", + NodeName: (dbc.Identifier) "", + EnvironmentVariableName: (dbc.Identifier) "", + IntValue: (int64) 100, + FloatValue: (float64) 0, + StringValue: (string) "" + }), + (*dbc.AttributeValueForObjectDef)({ + Pos: (scanner.Position) ../../testdata/dbc/example/example.dbc:70:1, + AttributeName: (dbc.Identifier) (len=14) "GenMsgSendType", + ObjectType: (dbc.ObjectType) (len=3) "BO_", + MessageID: (dbc.MessageID) 500, + SignalName: (dbc.Identifier) "", + NodeName: (dbc.Identifier) "", + EnvironmentVariableName: (dbc.Identifier) "", + IntValue: (int64) 0, + FloatValue: (float64) 0, + StringValue: (string) (len=7) "OnEvent" + }), + (*dbc.AttributeValueForObjectDef)({ + Pos: (scanner.Position) ../../testdata/dbc/example/example.dbc:71:1, + AttributeName: (dbc.Identifier) (len=9) "FieldType", + ObjectType: (dbc.ObjectType) (len=3) "SG_", + MessageID: (dbc.MessageID) 100, + SignalName: (dbc.Identifier) (len=7) "Command", + NodeName: (dbc.Identifier) "", + EnvironmentVariableName: (dbc.Identifier) "", + IntValue: (int64) 0, + FloatValue: (float64) 0, + StringValue: (string) (len=7) "Command" + }), + (*dbc.AttributeValueForObjectDef)({ + Pos: (scanner.Position) ../../testdata/dbc/example/example.dbc:72:1, + AttributeName: (dbc.Identifier) (len=9) "FieldType", + ObjectType: (dbc.ObjectType) (len=3) "SG_", + MessageID: (dbc.MessageID) 500, + SignalName: (dbc.Identifier) (len=8) "TestEnum", + NodeName: (dbc.Identifier) "", + EnvironmentVariableName: (dbc.Identifier) "", + IntValue: (int64) 0, + FloatValue: (float64) 0, + StringValue: (string) (len=8) "TestEnum" + }), + (*dbc.AttributeValueForObjectDef)({ + Pos: (scanner.Position) ../../testdata/dbc/example/example.dbc:73:1, + AttributeName: (dbc.Identifier) (len=16) "GenSigStartValue", + ObjectType: (dbc.ObjectType) (len=3) "SG_", + MessageID: (dbc.MessageID) 500, + SignalName: (dbc.Identifier) (len=8) "TestEnum", + NodeName: (dbc.Identifier) "", + EnvironmentVariableName: (dbc.Identifier) "", + IntValue: (int64) 2, + FloatValue: (float64) 0, + StringValue: (string) "" + }), + (*dbc.ValueDescriptionsDef)({ + Pos: (scanner.Position) ../../testdata/dbc/example/example.dbc:75:1, + ObjectType: (dbc.ObjectType) (len=3) "SG_", + MessageID: (dbc.MessageID) 100, + SignalName: (dbc.Identifier) (len=7) "Command", + EnvironmentVariableName: (dbc.Identifier) "", + ValueDescriptions: ([]dbc.ValueDescriptionDef) (len=3) { + (dbc.ValueDescriptionDef) { + Pos: (scanner.Position) ../../testdata/dbc/example/example.dbc:75:18, + Value: (float64) 2, + Description: (string) (len=6) "Reboot" + }, + (dbc.ValueDescriptionDef) { + Pos: (scanner.Position) ../../testdata/dbc/example/example.dbc:75:29, + Value: (float64) 1, + Description: (string) (len=4) "Sync" + }, + (dbc.ValueDescriptionDef) { + Pos: (scanner.Position) ../../testdata/dbc/example/example.dbc:75:38, + Value: (float64) 0, + Description: (string) (len=4) "None" + } + } + }), + (*dbc.ValueDescriptionsDef)({ + Pos: (scanner.Position) ../../testdata/dbc/example/example.dbc:76:1, + ObjectType: (dbc.ObjectType) (len=3) "SG_", + MessageID: (dbc.MessageID) 500, + SignalName: (dbc.Identifier) (len=8) "TestEnum", + EnvironmentVariableName: (dbc.Identifier) "", + ValueDescriptions: ([]dbc.ValueDescriptionDef) (len=2) { + (dbc.ValueDescriptionDef) { + Pos: (scanner.Position) ../../testdata/dbc/example/example.dbc:76:19, + Value: (float64) 2, + Description: (string) (len=3) "Two" + }, + (dbc.ValueDescriptionDef) { + Pos: (scanner.Position) ../../testdata/dbc/example/example.dbc:76:27, + Value: (float64) 1, + Description: (string) (len=3) "One" + } + } + }), + (*dbc.ValueDescriptionsDef)({ + Pos: (scanner.Position) ../../testdata/dbc/example/example.dbc:77:1, + ObjectType: (dbc.ObjectType) (len=3) "SG_", + MessageID: (dbc.MessageID) 500, + SignalName: (dbc.Identifier) (len=14) "TestScaledEnum", + EnvironmentVariableName: (dbc.Identifier) "", + ValueDescriptions: ([]dbc.ValueDescriptionDef) (len=4) { + (dbc.ValueDescriptionDef) { + Pos: (scanner.Position) ../../testdata/dbc/example/example.dbc:77:25, + Value: (float64) 3, + Description: (string) (len=3) "Six" + }, + (dbc.ValueDescriptionDef) { + Pos: (scanner.Position) ../../testdata/dbc/example/example.dbc:77:33, + Value: (float64) 2, + Description: (string) (len=4) "Four" + }, + (dbc.ValueDescriptionDef) { + Pos: (scanner.Position) ../../testdata/dbc/example/example.dbc:77:42, + Value: (float64) 1, + Description: (string) (len=3) "Two" + }, + (dbc.ValueDescriptionDef) { + Pos: (scanner.Position) ../../testdata/dbc/example/example.dbc:77:50, + Value: (float64) 0, + Description: (string) (len=4) "Zero" + } + } + }), + (*dbc.ValueDescriptionsDef)({ + Pos: (scanner.Position) ../../testdata/dbc/example/example.dbc:78:1, + ObjectType: (dbc.ObjectType) (len=3) "SG_", + MessageID: (dbc.MessageID) 500, + SignalName: (dbc.Identifier) (len=12) "TestBoolEnum", + EnvironmentVariableName: (dbc.Identifier) "", + ValueDescriptions: ([]dbc.ValueDescriptionDef) (len=2) { + (dbc.ValueDescriptionDef) { + Pos: (scanner.Position) ../../testdata/dbc/example/example.dbc:78:23, + Value: (float64) 1, + Description: (string) (len=3) "One" + }, + (dbc.ValueDescriptionDef) { + Pos: (scanner.Position) ../../testdata/dbc/example/example.dbc:78:31, + Value: (float64) 0, + Description: (string) (len=4) "Zero" + } + } + }) +} diff --git a/testdata/gen/go/example/example.dbc.go b/testdata/gen/go/example/example.dbc.go new file mode 100644 index 0000000..c9d3773 --- /dev/null +++ b/testdata/gen/go/example/example.dbc.go @@ -0,0 +1,3046 @@ +// Package examplecan provides primitives for encoding and decoding example CAN messages. +// +// Source: testdata/dbc/example/example.dbc +package examplecan + +import ( + "context" + "fmt" + "net" + "net/http" + "sync" + "time" + + "go.einride.tech/can" + "go.einride.tech/can/pkg/candebug" + "go.einride.tech/can/pkg/canrunner" + "go.einride.tech/can/pkg/cantext" + "go.einride.tech/can/pkg/descriptor" + "go.einride.tech/can/pkg/generated" + "go.einride.tech/can/pkg/socketcan" +) + +// prevent unused imports +var ( + _ = context.Background + _ = fmt.Print + _ = net.Dial + _ = http.Error + _ = sync.Mutex{} + _ = time.Now + _ = socketcan.Dial + _ = candebug.ServeMessagesHTTP + _ = canrunner.Run +) + +// Generated code. DO NOT EDIT. +// EmptyMessageReader provides read access to a EmptyMessage message. +type EmptyMessageReader interface { +} + +// EmptyMessageWriter provides write access to a EmptyMessage message. +type EmptyMessageWriter interface { + // CopyFrom copies all values from EmptyMessageReader. + CopyFrom(EmptyMessageReader) *EmptyMessage +} + +type EmptyMessage struct { +} + +func NewEmptyMessage() *EmptyMessage { + m := &EmptyMessage{} + m.Reset() + return m +} + +func (m *EmptyMessage) Reset() { +} + +func (m *EmptyMessage) CopyFrom(o EmptyMessageReader) *EmptyMessage { + return m +} + +// Descriptor returns the EmptyMessage descriptor. +func (m *EmptyMessage) Descriptor() *descriptor.Message { + return Messages().EmptyMessage.Message +} + +// String returns a compact string representation of the message. +func (m *EmptyMessage) String() string { + return cantext.MessageString(m) +} + +// Frame returns a CAN frame representing the message. +func (m *EmptyMessage) Frame() can.Frame { + md := Messages().EmptyMessage + f := can.Frame{ID: md.ID, IsExtended: md.IsExtended, Length: md.Length} + return f +} + +// MarshalFrame encodes the message as a CAN frame. +func (m *EmptyMessage) MarshalFrame() (can.Frame, error) { + return m.Frame(), nil +} + +// UnmarshalFrame decodes the message from a CAN frame. +func (m *EmptyMessage) UnmarshalFrame(f can.Frame) error { + md := Messages().EmptyMessage + switch { + case f.ID != md.ID: + return fmt.Errorf( + "unmarshal EmptyMessage: expects ID 1 (got %s with ID %d)", f.String(), f.ID, + ) + case f.Length != md.Length: + return fmt.Errorf( + "unmarshal EmptyMessage: expects length 0 (got %s with length %d)", f.String(), f.Length, + ) + case f.IsRemote: + return fmt.Errorf( + "unmarshal EmptyMessage: expects non-remote frame (got remote frame %s)", f.String(), + ) + case f.IsExtended != md.IsExtended: + return fmt.Errorf( + "unmarshal EmptyMessage: expects standard ID (got %s with extended ID)", f.String(), + ) + } + return nil +} + +// DriverHeartbeatReader provides read access to a DriverHeartbeat message. +type DriverHeartbeatReader interface { + // Command returns the value of the Command signal. + Command() DriverHeartbeat_Command +} + +// DriverHeartbeatWriter provides write access to a DriverHeartbeat message. +type DriverHeartbeatWriter interface { + // CopyFrom copies all values from DriverHeartbeatReader. + CopyFrom(DriverHeartbeatReader) *DriverHeartbeat + // SetCommand sets the value of the Command signal. + SetCommand(DriverHeartbeat_Command) *DriverHeartbeat +} + +type DriverHeartbeat struct { + xxx_Command DriverHeartbeat_Command +} + +func NewDriverHeartbeat() *DriverHeartbeat { + m := &DriverHeartbeat{} + m.Reset() + return m +} + +func (m *DriverHeartbeat) Reset() { + m.xxx_Command = 0 +} + +func (m *DriverHeartbeat) CopyFrom(o DriverHeartbeatReader) *DriverHeartbeat { + m.xxx_Command = o.Command() + return m +} + +// Descriptor returns the DriverHeartbeat descriptor. +func (m *DriverHeartbeat) Descriptor() *descriptor.Message { + return Messages().DriverHeartbeat.Message +} + +// String returns a compact string representation of the message. +func (m *DriverHeartbeat) String() string { + return cantext.MessageString(m) +} + +func (m *DriverHeartbeat) Command() DriverHeartbeat_Command { + return m.xxx_Command +} + +func (m *DriverHeartbeat) SetCommand(v DriverHeartbeat_Command) *DriverHeartbeat { + m.xxx_Command = DriverHeartbeat_Command(Messages().DriverHeartbeat.Command.SaturatedCastUnsigned(uint64(v))) + return m +} + +// DriverHeartbeat_Command models the Command signal of the DriverHeartbeat message. +type DriverHeartbeat_Command uint8 + +// Value descriptions for the Command signal of the DriverHeartbeat message. +const ( + DriverHeartbeat_Command_None DriverHeartbeat_Command = 0 + DriverHeartbeat_Command_Sync DriverHeartbeat_Command = 1 + DriverHeartbeat_Command_Reboot DriverHeartbeat_Command = 2 +) + +func (v DriverHeartbeat_Command) String() string { + switch v { + case 0: + return "None" + case 1: + return "Sync" + case 2: + return "Reboot" + default: + return fmt.Sprintf("DriverHeartbeat_Command(%d)", v) + } +} + +// Frame returns a CAN frame representing the message. +func (m *DriverHeartbeat) Frame() can.Frame { + md := Messages().DriverHeartbeat + f := can.Frame{ID: md.ID, IsExtended: md.IsExtended, Length: md.Length} + md.Command.MarshalUnsigned(&f.Data, uint64(m.xxx_Command)) + return f +} + +// MarshalFrame encodes the message as a CAN frame. +func (m *DriverHeartbeat) MarshalFrame() (can.Frame, error) { + return m.Frame(), nil +} + +// UnmarshalFrame decodes the message from a CAN frame. +func (m *DriverHeartbeat) UnmarshalFrame(f can.Frame) error { + md := Messages().DriverHeartbeat + switch { + case f.ID != md.ID: + return fmt.Errorf( + "unmarshal DriverHeartbeat: expects ID 100 (got %s with ID %d)", f.String(), f.ID, + ) + case f.Length != md.Length: + return fmt.Errorf( + "unmarshal DriverHeartbeat: expects length 1 (got %s with length %d)", f.String(), f.Length, + ) + case f.IsRemote: + return fmt.Errorf( + "unmarshal DriverHeartbeat: expects non-remote frame (got remote frame %s)", f.String(), + ) + case f.IsExtended != md.IsExtended: + return fmt.Errorf( + "unmarshal DriverHeartbeat: expects standard ID (got %s with extended ID)", f.String(), + ) + } + m.xxx_Command = DriverHeartbeat_Command(md.Command.UnmarshalUnsigned(f.Data)) + return nil +} + +// MotorCommandReader provides read access to a MotorCommand message. +type MotorCommandReader interface { + // Steer returns the physical value of the Steer signal. + Steer() float64 + // Drive returns the physical value of the Drive signal. + Drive() float64 +} + +// MotorCommandWriter provides write access to a MotorCommand message. +type MotorCommandWriter interface { + // CopyFrom copies all values from MotorCommandReader. + CopyFrom(MotorCommandReader) *MotorCommand + // SetSteer sets the physical value of the Steer signal. + SetSteer(float64) *MotorCommand + // SetDrive sets the physical value of the Drive signal. + SetDrive(float64) *MotorCommand +} + +type MotorCommand struct { + xxx_Steer int8 + xxx_Drive uint8 +} + +func NewMotorCommand() *MotorCommand { + m := &MotorCommand{} + m.Reset() + return m +} + +func (m *MotorCommand) Reset() { + m.xxx_Steer = 0 + m.xxx_Drive = 0 +} + +func (m *MotorCommand) CopyFrom(o MotorCommandReader) *MotorCommand { + m.SetSteer(o.Steer()) + m.SetDrive(o.Drive()) + return m +} + +// Descriptor returns the MotorCommand descriptor. +func (m *MotorCommand) Descriptor() *descriptor.Message { + return Messages().MotorCommand.Message +} + +// String returns a compact string representation of the message. +func (m *MotorCommand) String() string { + return cantext.MessageString(m) +} + +func (m *MotorCommand) Steer() float64 { + return Messages().MotorCommand.Steer.ToPhysical(float64(m.xxx_Steer)) +} + +func (m *MotorCommand) SetSteer(v float64) *MotorCommand { + m.xxx_Steer = int8(Messages().MotorCommand.Steer.FromPhysical(v)) + return m +} + +func (m *MotorCommand) Drive() float64 { + return Messages().MotorCommand.Drive.ToPhysical(float64(m.xxx_Drive)) +} + +func (m *MotorCommand) SetDrive(v float64) *MotorCommand { + m.xxx_Drive = uint8(Messages().MotorCommand.Drive.FromPhysical(v)) + return m +} + +// Frame returns a CAN frame representing the message. +func (m *MotorCommand) Frame() can.Frame { + md := Messages().MotorCommand + f := can.Frame{ID: md.ID, IsExtended: md.IsExtended, Length: md.Length} + md.Steer.MarshalSigned(&f.Data, int64(m.xxx_Steer)) + md.Drive.MarshalUnsigned(&f.Data, uint64(m.xxx_Drive)) + return f +} + +// MarshalFrame encodes the message as a CAN frame. +func (m *MotorCommand) MarshalFrame() (can.Frame, error) { + return m.Frame(), nil +} + +// UnmarshalFrame decodes the message from a CAN frame. +func (m *MotorCommand) UnmarshalFrame(f can.Frame) error { + md := Messages().MotorCommand + switch { + case f.ID != md.ID: + return fmt.Errorf( + "unmarshal MotorCommand: expects ID 101 (got %s with ID %d)", f.String(), f.ID, + ) + case f.Length != md.Length: + return fmt.Errorf( + "unmarshal MotorCommand: expects length 1 (got %s with length %d)", f.String(), f.Length, + ) + case f.IsRemote: + return fmt.Errorf( + "unmarshal MotorCommand: expects non-remote frame (got remote frame %s)", f.String(), + ) + case f.IsExtended != md.IsExtended: + return fmt.Errorf( + "unmarshal MotorCommand: expects standard ID (got %s with extended ID)", f.String(), + ) + } + m.xxx_Steer = int8(md.Steer.UnmarshalSigned(f.Data)) + m.xxx_Drive = uint8(md.Drive.UnmarshalUnsigned(f.Data)) + return nil +} + +// SensorSonarsReader provides read access to a SensorSonars message. +type SensorSonarsReader interface { + // Mux returns the value of the Mux signal. + Mux() uint8 + // ErrCount returns the value of the ErrCount signal. + ErrCount() uint16 + // Left returns the physical value of the Left signal. + Left() float64 + // NoFiltLeft returns the physical value of the NoFiltLeft signal. + NoFiltLeft() float64 + // Middle returns the physical value of the Middle signal. + Middle() float64 + // NoFiltMiddle returns the physical value of the NoFiltMiddle signal. + NoFiltMiddle() float64 + // Right returns the physical value of the Right signal. + Right() float64 + // NoFiltRight returns the physical value of the NoFiltRight signal. + NoFiltRight() float64 + // Rear returns the physical value of the Rear signal. + Rear() float64 + // NoFiltRear returns the physical value of the NoFiltRear signal. + NoFiltRear() float64 +} + +// SensorSonarsWriter provides write access to a SensorSonars message. +type SensorSonarsWriter interface { + // CopyFrom copies all values from SensorSonarsReader. + CopyFrom(SensorSonarsReader) *SensorSonars + // SetMux sets the value of the Mux signal. + SetMux(uint8) *SensorSonars + // SetErrCount sets the value of the ErrCount signal. + SetErrCount(uint16) *SensorSonars + // SetLeft sets the physical value of the Left signal. + SetLeft(float64) *SensorSonars + // SetNoFiltLeft sets the physical value of the NoFiltLeft signal. + SetNoFiltLeft(float64) *SensorSonars + // SetMiddle sets the physical value of the Middle signal. + SetMiddle(float64) *SensorSonars + // SetNoFiltMiddle sets the physical value of the NoFiltMiddle signal. + SetNoFiltMiddle(float64) *SensorSonars + // SetRight sets the physical value of the Right signal. + SetRight(float64) *SensorSonars + // SetNoFiltRight sets the physical value of the NoFiltRight signal. + SetNoFiltRight(float64) *SensorSonars + // SetRear sets the physical value of the Rear signal. + SetRear(float64) *SensorSonars + // SetNoFiltRear sets the physical value of the NoFiltRear signal. + SetNoFiltRear(float64) *SensorSonars +} + +type SensorSonars struct { + xxx_Mux uint8 + xxx_ErrCount uint16 + xxx_Left uint16 + xxx_NoFiltLeft uint16 + xxx_Middle uint16 + xxx_NoFiltMiddle uint16 + xxx_Right uint16 + xxx_NoFiltRight uint16 + xxx_Rear uint16 + xxx_NoFiltRear uint16 +} + +func NewSensorSonars() *SensorSonars { + m := &SensorSonars{} + m.Reset() + return m +} + +func (m *SensorSonars) Reset() { + m.xxx_Mux = 0 + m.xxx_ErrCount = 0 + m.xxx_Left = 0 + m.xxx_NoFiltLeft = 0 + m.xxx_Middle = 0 + m.xxx_NoFiltMiddle = 0 + m.xxx_Right = 0 + m.xxx_NoFiltRight = 0 + m.xxx_Rear = 0 + m.xxx_NoFiltRear = 0 +} + +func (m *SensorSonars) CopyFrom(o SensorSonarsReader) *SensorSonars { + m.xxx_Mux = o.Mux() + m.xxx_ErrCount = o.ErrCount() + m.SetLeft(o.Left()) + m.SetNoFiltLeft(o.NoFiltLeft()) + m.SetMiddle(o.Middle()) + m.SetNoFiltMiddle(o.NoFiltMiddle()) + m.SetRight(o.Right()) + m.SetNoFiltRight(o.NoFiltRight()) + m.SetRear(o.Rear()) + m.SetNoFiltRear(o.NoFiltRear()) + return m +} + +// Descriptor returns the SensorSonars descriptor. +func (m *SensorSonars) Descriptor() *descriptor.Message { + return Messages().SensorSonars.Message +} + +// String returns a compact string representation of the message. +func (m *SensorSonars) String() string { + return cantext.MessageString(m) +} + +func (m *SensorSonars) Mux() uint8 { + return m.xxx_Mux +} + +func (m *SensorSonars) SetMux(v uint8) *SensorSonars { + m.xxx_Mux = uint8(Messages().SensorSonars.Mux.SaturatedCastUnsigned(uint64(v))) + return m +} + +func (m *SensorSonars) ErrCount() uint16 { + return m.xxx_ErrCount +} + +func (m *SensorSonars) SetErrCount(v uint16) *SensorSonars { + m.xxx_ErrCount = uint16(Messages().SensorSonars.ErrCount.SaturatedCastUnsigned(uint64(v))) + return m +} + +func (m *SensorSonars) Left() float64 { + return Messages().SensorSonars.Left.ToPhysical(float64(m.xxx_Left)) +} + +func (m *SensorSonars) SetLeft(v float64) *SensorSonars { + m.xxx_Left = uint16(Messages().SensorSonars.Left.FromPhysical(v)) + return m +} + +func (m *SensorSonars) NoFiltLeft() float64 { + return Messages().SensorSonars.NoFiltLeft.ToPhysical(float64(m.xxx_NoFiltLeft)) +} + +func (m *SensorSonars) SetNoFiltLeft(v float64) *SensorSonars { + m.xxx_NoFiltLeft = uint16(Messages().SensorSonars.NoFiltLeft.FromPhysical(v)) + return m +} + +func (m *SensorSonars) Middle() float64 { + return Messages().SensorSonars.Middle.ToPhysical(float64(m.xxx_Middle)) +} + +func (m *SensorSonars) SetMiddle(v float64) *SensorSonars { + m.xxx_Middle = uint16(Messages().SensorSonars.Middle.FromPhysical(v)) + return m +} + +func (m *SensorSonars) NoFiltMiddle() float64 { + return Messages().SensorSonars.NoFiltMiddle.ToPhysical(float64(m.xxx_NoFiltMiddle)) +} + +func (m *SensorSonars) SetNoFiltMiddle(v float64) *SensorSonars { + m.xxx_NoFiltMiddle = uint16(Messages().SensorSonars.NoFiltMiddle.FromPhysical(v)) + return m +} + +func (m *SensorSonars) Right() float64 { + return Messages().SensorSonars.Right.ToPhysical(float64(m.xxx_Right)) +} + +func (m *SensorSonars) SetRight(v float64) *SensorSonars { + m.xxx_Right = uint16(Messages().SensorSonars.Right.FromPhysical(v)) + return m +} + +func (m *SensorSonars) NoFiltRight() float64 { + return Messages().SensorSonars.NoFiltRight.ToPhysical(float64(m.xxx_NoFiltRight)) +} + +func (m *SensorSonars) SetNoFiltRight(v float64) *SensorSonars { + m.xxx_NoFiltRight = uint16(Messages().SensorSonars.NoFiltRight.FromPhysical(v)) + return m +} + +func (m *SensorSonars) Rear() float64 { + return Messages().SensorSonars.Rear.ToPhysical(float64(m.xxx_Rear)) +} + +func (m *SensorSonars) SetRear(v float64) *SensorSonars { + m.xxx_Rear = uint16(Messages().SensorSonars.Rear.FromPhysical(v)) + return m +} + +func (m *SensorSonars) NoFiltRear() float64 { + return Messages().SensorSonars.NoFiltRear.ToPhysical(float64(m.xxx_NoFiltRear)) +} + +func (m *SensorSonars) SetNoFiltRear(v float64) *SensorSonars { + m.xxx_NoFiltRear = uint16(Messages().SensorSonars.NoFiltRear.FromPhysical(v)) + return m +} + +// Frame returns a CAN frame representing the message. +func (m *SensorSonars) Frame() can.Frame { + md := Messages().SensorSonars + f := can.Frame{ID: md.ID, IsExtended: md.IsExtended, Length: md.Length} + md.Mux.MarshalUnsigned(&f.Data, uint64(m.xxx_Mux)) + md.ErrCount.MarshalUnsigned(&f.Data, uint64(m.xxx_ErrCount)) + if m.xxx_Mux == 0 { + md.Left.MarshalUnsigned(&f.Data, uint64(m.xxx_Left)) + } + if m.xxx_Mux == 1 { + md.NoFiltLeft.MarshalUnsigned(&f.Data, uint64(m.xxx_NoFiltLeft)) + } + if m.xxx_Mux == 0 { + md.Middle.MarshalUnsigned(&f.Data, uint64(m.xxx_Middle)) + } + if m.xxx_Mux == 1 { + md.NoFiltMiddle.MarshalUnsigned(&f.Data, uint64(m.xxx_NoFiltMiddle)) + } + if m.xxx_Mux == 0 { + md.Right.MarshalUnsigned(&f.Data, uint64(m.xxx_Right)) + } + if m.xxx_Mux == 1 { + md.NoFiltRight.MarshalUnsigned(&f.Data, uint64(m.xxx_NoFiltRight)) + } + if m.xxx_Mux == 0 { + md.Rear.MarshalUnsigned(&f.Data, uint64(m.xxx_Rear)) + } + if m.xxx_Mux == 1 { + md.NoFiltRear.MarshalUnsigned(&f.Data, uint64(m.xxx_NoFiltRear)) + } + return f +} + +// MarshalFrame encodes the message as a CAN frame. +func (m *SensorSonars) MarshalFrame() (can.Frame, error) { + return m.Frame(), nil +} + +// UnmarshalFrame decodes the message from a CAN frame. +func (m *SensorSonars) UnmarshalFrame(f can.Frame) error { + md := Messages().SensorSonars + switch { + case f.ID != md.ID: + return fmt.Errorf( + "unmarshal SensorSonars: expects ID 200 (got %s with ID %d)", f.String(), f.ID, + ) + case f.Length != md.Length: + return fmt.Errorf( + "unmarshal SensorSonars: expects length 8 (got %s with length %d)", f.String(), f.Length, + ) + case f.IsRemote: + return fmt.Errorf( + "unmarshal SensorSonars: expects non-remote frame (got remote frame %s)", f.String(), + ) + case f.IsExtended != md.IsExtended: + return fmt.Errorf( + "unmarshal SensorSonars: expects standard ID (got %s with extended ID)", f.String(), + ) + } + m.xxx_Mux = uint8(md.Mux.UnmarshalUnsigned(f.Data)) + m.xxx_ErrCount = uint16(md.ErrCount.UnmarshalUnsigned(f.Data)) + if m.xxx_Mux == 0 { + m.xxx_Left = uint16(md.Left.UnmarshalUnsigned(f.Data)) + } + if m.xxx_Mux == 1 { + m.xxx_NoFiltLeft = uint16(md.NoFiltLeft.UnmarshalUnsigned(f.Data)) + } + if m.xxx_Mux == 0 { + m.xxx_Middle = uint16(md.Middle.UnmarshalUnsigned(f.Data)) + } + if m.xxx_Mux == 1 { + m.xxx_NoFiltMiddle = uint16(md.NoFiltMiddle.UnmarshalUnsigned(f.Data)) + } + if m.xxx_Mux == 0 { + m.xxx_Right = uint16(md.Right.UnmarshalUnsigned(f.Data)) + } + if m.xxx_Mux == 1 { + m.xxx_NoFiltRight = uint16(md.NoFiltRight.UnmarshalUnsigned(f.Data)) + } + if m.xxx_Mux == 0 { + m.xxx_Rear = uint16(md.Rear.UnmarshalUnsigned(f.Data)) + } + if m.xxx_Mux == 1 { + m.xxx_NoFiltRear = uint16(md.NoFiltRear.UnmarshalUnsigned(f.Data)) + } + return nil +} + +// MotorStatusReader provides read access to a MotorStatus message. +type MotorStatusReader interface { + // WheelError returns the value of the WheelError signal. + WheelError() bool + // SpeedKph returns the physical value of the SpeedKph signal. + SpeedKph() float64 +} + +// MotorStatusWriter provides write access to a MotorStatus message. +type MotorStatusWriter interface { + // CopyFrom copies all values from MotorStatusReader. + CopyFrom(MotorStatusReader) *MotorStatus + // SetWheelError sets the value of the WheelError signal. + SetWheelError(bool) *MotorStatus + // SetSpeedKph sets the physical value of the SpeedKph signal. + SetSpeedKph(float64) *MotorStatus +} + +type MotorStatus struct { + xxx_WheelError bool + xxx_SpeedKph uint16 +} + +func NewMotorStatus() *MotorStatus { + m := &MotorStatus{} + m.Reset() + return m +} + +func (m *MotorStatus) Reset() { + m.xxx_WheelError = false + m.xxx_SpeedKph = 0 +} + +func (m *MotorStatus) CopyFrom(o MotorStatusReader) *MotorStatus { + m.xxx_WheelError = o.WheelError() + m.SetSpeedKph(o.SpeedKph()) + return m +} + +// Descriptor returns the MotorStatus descriptor. +func (m *MotorStatus) Descriptor() *descriptor.Message { + return Messages().MotorStatus.Message +} + +// String returns a compact string representation of the message. +func (m *MotorStatus) String() string { + return cantext.MessageString(m) +} + +func (m *MotorStatus) WheelError() bool { + return m.xxx_WheelError +} + +func (m *MotorStatus) SetWheelError(v bool) *MotorStatus { + m.xxx_WheelError = v + return m +} + +func (m *MotorStatus) SpeedKph() float64 { + return Messages().MotorStatus.SpeedKph.ToPhysical(float64(m.xxx_SpeedKph)) +} + +func (m *MotorStatus) SetSpeedKph(v float64) *MotorStatus { + m.xxx_SpeedKph = uint16(Messages().MotorStatus.SpeedKph.FromPhysical(v)) + return m +} + +// Frame returns a CAN frame representing the message. +func (m *MotorStatus) Frame() can.Frame { + md := Messages().MotorStatus + f := can.Frame{ID: md.ID, IsExtended: md.IsExtended, Length: md.Length} + md.WheelError.MarshalBool(&f.Data, bool(m.xxx_WheelError)) + md.SpeedKph.MarshalUnsigned(&f.Data, uint64(m.xxx_SpeedKph)) + return f +} + +// MarshalFrame encodes the message as a CAN frame. +func (m *MotorStatus) MarshalFrame() (can.Frame, error) { + return m.Frame(), nil +} + +// UnmarshalFrame decodes the message from a CAN frame. +func (m *MotorStatus) UnmarshalFrame(f can.Frame) error { + md := Messages().MotorStatus + switch { + case f.ID != md.ID: + return fmt.Errorf( + "unmarshal MotorStatus: expects ID 400 (got %s with ID %d)", f.String(), f.ID, + ) + case f.Length != md.Length: + return fmt.Errorf( + "unmarshal MotorStatus: expects length 3 (got %s with length %d)", f.String(), f.Length, + ) + case f.IsRemote: + return fmt.Errorf( + "unmarshal MotorStatus: expects non-remote frame (got remote frame %s)", f.String(), + ) + case f.IsExtended != md.IsExtended: + return fmt.Errorf( + "unmarshal MotorStatus: expects standard ID (got %s with extended ID)", f.String(), + ) + } + m.xxx_WheelError = bool(md.WheelError.UnmarshalBool(f.Data)) + m.xxx_SpeedKph = uint16(md.SpeedKph.UnmarshalUnsigned(f.Data)) + return nil +} + +// IODebugReader provides read access to a IODebug message. +type IODebugReader interface { + // TestUnsigned returns the value of the TestUnsigned signal. + TestUnsigned() uint8 + // TestEnum returns the value of the TestEnum signal. + TestEnum() IODebug_TestEnum + // TestSigned returns the value of the TestSigned signal. + TestSigned() int8 + // TestFloat returns the physical value of the TestFloat signal. + TestFloat() float64 + // TestBoolEnum returns the value of the TestBoolEnum signal. + TestBoolEnum() IODebug_TestBoolEnum + // TestScaledEnum returns the physical value of the TestScaledEnum signal. + TestScaledEnum() float64 + + // TestScaledEnum returns the raw (encoded) value of the TestScaledEnum signal. + RawTestScaledEnum() IODebug_TestScaledEnum +} + +// IODebugWriter provides write access to a IODebug message. +type IODebugWriter interface { + // CopyFrom copies all values from IODebugReader. + CopyFrom(IODebugReader) *IODebug + // SetTestUnsigned sets the value of the TestUnsigned signal. + SetTestUnsigned(uint8) *IODebug + // SetTestEnum sets the value of the TestEnum signal. + SetTestEnum(IODebug_TestEnum) *IODebug + // SetTestSigned sets the value of the TestSigned signal. + SetTestSigned(int8) *IODebug + // SetTestFloat sets the physical value of the TestFloat signal. + SetTestFloat(float64) *IODebug + // SetTestBoolEnum sets the value of the TestBoolEnum signal. + SetTestBoolEnum(IODebug_TestBoolEnum) *IODebug + // SetTestScaledEnum sets the physical value of the TestScaledEnum signal. + SetTestScaledEnum(float64) *IODebug + + // SetRawTestScaledEnum sets the raw (encoded) value of the TestScaledEnum signal. + SetRawTestScaledEnum(IODebug_TestScaledEnum) *IODebug +} + +type IODebug struct { + xxx_TestUnsigned uint8 + xxx_TestEnum IODebug_TestEnum + xxx_TestSigned int8 + xxx_TestFloat uint8 + xxx_TestBoolEnum IODebug_TestBoolEnum + xxx_TestScaledEnum IODebug_TestScaledEnum +} + +func NewIODebug() *IODebug { + m := &IODebug{} + m.Reset() + return m +} + +func (m *IODebug) Reset() { + m.xxx_TestUnsigned = 0 + m.xxx_TestEnum = 2 + m.xxx_TestSigned = 0 + m.xxx_TestFloat = 0 + m.xxx_TestBoolEnum = false + m.xxx_TestScaledEnum = 0 +} + +func (m *IODebug) CopyFrom(o IODebugReader) *IODebug { + m.xxx_TestUnsigned = o.TestUnsigned() + m.xxx_TestEnum = o.TestEnum() + m.xxx_TestSigned = o.TestSigned() + m.SetTestFloat(o.TestFloat()) + m.xxx_TestBoolEnum = o.TestBoolEnum() + m.SetTestScaledEnum(o.TestScaledEnum()) + return m +} + +// Descriptor returns the IODebug descriptor. +func (m *IODebug) Descriptor() *descriptor.Message { + return Messages().IODebug.Message +} + +// String returns a compact string representation of the message. +func (m *IODebug) String() string { + return cantext.MessageString(m) +} + +func (m *IODebug) TestUnsigned() uint8 { + return m.xxx_TestUnsigned +} + +func (m *IODebug) SetTestUnsigned(v uint8) *IODebug { + m.xxx_TestUnsigned = uint8(Messages().IODebug.TestUnsigned.SaturatedCastUnsigned(uint64(v))) + return m +} + +func (m *IODebug) TestEnum() IODebug_TestEnum { + return m.xxx_TestEnum +} + +func (m *IODebug) SetTestEnum(v IODebug_TestEnum) *IODebug { + m.xxx_TestEnum = IODebug_TestEnum(Messages().IODebug.TestEnum.SaturatedCastUnsigned(uint64(v))) + return m +} + +func (m *IODebug) TestSigned() int8 { + return m.xxx_TestSigned +} + +func (m *IODebug) SetTestSigned(v int8) *IODebug { + m.xxx_TestSigned = int8(Messages().IODebug.TestSigned.SaturatedCastSigned(int64(v))) + return m +} + +func (m *IODebug) TestFloat() float64 { + return Messages().IODebug.TestFloat.ToPhysical(float64(m.xxx_TestFloat)) +} + +func (m *IODebug) SetTestFloat(v float64) *IODebug { + m.xxx_TestFloat = uint8(Messages().IODebug.TestFloat.FromPhysical(v)) + return m +} + +func (m *IODebug) TestBoolEnum() IODebug_TestBoolEnum { + return m.xxx_TestBoolEnum +} + +func (m *IODebug) SetTestBoolEnum(v IODebug_TestBoolEnum) *IODebug { + m.xxx_TestBoolEnum = v + return m +} + +func (m *IODebug) TestScaledEnum() float64 { + return Messages().IODebug.TestScaledEnum.ToPhysical(float64(m.xxx_TestScaledEnum)) +} + +func (m *IODebug) SetTestScaledEnum(v float64) *IODebug { + m.xxx_TestScaledEnum = IODebug_TestScaledEnum(Messages().IODebug.TestScaledEnum.FromPhysical(v)) + return m +} + +func (m *IODebug) RawTestScaledEnum() IODebug_TestScaledEnum { + return m.xxx_TestScaledEnum +} + +func (m *IODebug) SetRawTestScaledEnum(v IODebug_TestScaledEnum) *IODebug { + m.xxx_TestScaledEnum = IODebug_TestScaledEnum(Messages().IODebug.TestScaledEnum.SaturatedCastUnsigned(uint64(v))) + return m +} + +// IODebug_TestEnum models the TestEnum signal of the IODebug message. +type IODebug_TestEnum uint8 + +// Value descriptions for the TestEnum signal of the IODebug message. +const ( + IODebug_TestEnum_One IODebug_TestEnum = 1 + IODebug_TestEnum_Two IODebug_TestEnum = 2 +) + +func (v IODebug_TestEnum) String() string { + switch v { + case 1: + return "One" + case 2: + return "Two" + default: + return fmt.Sprintf("IODebug_TestEnum(%d)", v) + } +} + +// IODebug_TestBoolEnum models the TestBoolEnum signal of the IODebug message. +type IODebug_TestBoolEnum bool + +// Value descriptions for the TestBoolEnum signal of the IODebug message. +const ( + IODebug_TestBoolEnum_Zero IODebug_TestBoolEnum = false + IODebug_TestBoolEnum_One IODebug_TestBoolEnum = true +) + +func (v IODebug_TestBoolEnum) String() string { + switch bool(v) { + case false: + return "Zero" + case true: + return "One" + } + return fmt.Sprintf("IODebug_TestBoolEnum(%t)", v) +} + +// IODebug_TestScaledEnum models the TestScaledEnum signal of the IODebug message. +type IODebug_TestScaledEnum uint8 + +// Value descriptions for the TestScaledEnum signal of the IODebug message. +const ( + IODebug_TestScaledEnum_Zero IODebug_TestScaledEnum = 0 + IODebug_TestScaledEnum_Two IODebug_TestScaledEnum = 1 + IODebug_TestScaledEnum_Four IODebug_TestScaledEnum = 2 + IODebug_TestScaledEnum_Six IODebug_TestScaledEnum = 3 +) + +func (v IODebug_TestScaledEnum) String() string { + switch v { + case 0: + return "Zero" + case 1: + return "Two" + case 2: + return "Four" + case 3: + return "Six" + default: + return fmt.Sprintf("IODebug_TestScaledEnum(%d)", v) + } +} + +// Frame returns a CAN frame representing the message. +func (m *IODebug) Frame() can.Frame { + md := Messages().IODebug + f := can.Frame{ID: md.ID, IsExtended: md.IsExtended, Length: md.Length} + md.TestUnsigned.MarshalUnsigned(&f.Data, uint64(m.xxx_TestUnsigned)) + md.TestEnum.MarshalUnsigned(&f.Data, uint64(m.xxx_TestEnum)) + md.TestSigned.MarshalSigned(&f.Data, int64(m.xxx_TestSigned)) + md.TestFloat.MarshalUnsigned(&f.Data, uint64(m.xxx_TestFloat)) + md.TestBoolEnum.MarshalBool(&f.Data, bool(m.xxx_TestBoolEnum)) + md.TestScaledEnum.MarshalUnsigned(&f.Data, uint64(m.xxx_TestScaledEnum)) + return f +} + +// MarshalFrame encodes the message as a CAN frame. +func (m *IODebug) MarshalFrame() (can.Frame, error) { + return m.Frame(), nil +} + +// UnmarshalFrame decodes the message from a CAN frame. +func (m *IODebug) UnmarshalFrame(f can.Frame) error { + md := Messages().IODebug + switch { + case f.ID != md.ID: + return fmt.Errorf( + "unmarshal IODebug: expects ID 500 (got %s with ID %d)", f.String(), f.ID, + ) + case f.Length != md.Length: + return fmt.Errorf( + "unmarshal IODebug: expects length 6 (got %s with length %d)", f.String(), f.Length, + ) + case f.IsRemote: + return fmt.Errorf( + "unmarshal IODebug: expects non-remote frame (got remote frame %s)", f.String(), + ) + case f.IsExtended != md.IsExtended: + return fmt.Errorf( + "unmarshal IODebug: expects standard ID (got %s with extended ID)", f.String(), + ) + } + m.xxx_TestUnsigned = uint8(md.TestUnsigned.UnmarshalUnsigned(f.Data)) + m.xxx_TestEnum = IODebug_TestEnum(md.TestEnum.UnmarshalUnsigned(f.Data)) + m.xxx_TestSigned = int8(md.TestSigned.UnmarshalSigned(f.Data)) + m.xxx_TestFloat = uint8(md.TestFloat.UnmarshalUnsigned(f.Data)) + m.xxx_TestBoolEnum = IODebug_TestBoolEnum(md.TestBoolEnum.UnmarshalBool(f.Data)) + m.xxx_TestScaledEnum = IODebug_TestScaledEnum(md.TestScaledEnum.UnmarshalUnsigned(f.Data)) + return nil +} + +type DBG interface { + sync.Locker + Tx() DBG_Tx + Rx() DBG_Rx + Run(ctx context.Context) error +} + +type DBG_Rx interface { + http.Handler // for debugging + SensorSonars() DBG_Rx_SensorSonars + IODebug() DBG_Rx_IODebug +} + +type DBG_Tx interface { + http.Handler // for debugging +} + +type DBG_Rx_SensorSonars interface { + SensorSonarsReader + ReceiveTime() time.Time + SetAfterReceiveHook(h func(context.Context) error) +} + +type DBG_Rx_IODebug interface { + IODebugReader + ReceiveTime() time.Time + SetAfterReceiveHook(h func(context.Context) error) +} + +type xxx_DBG struct { + sync.Mutex // protects all node state + network string + address string + rx xxx_DBG_Rx + tx xxx_DBG_Tx +} + +var _ DBG = &xxx_DBG{} +var _ canrunner.Node = &xxx_DBG{} + +func NewDBG(network, address string) DBG { + n := &xxx_DBG{network: network, address: address} + n.rx.parentMutex = &n.Mutex + n.tx.parentMutex = &n.Mutex + n.rx.xxx_SensorSonars.init() + n.rx.xxx_SensorSonars.Reset() + n.rx.xxx_IODebug.init() + n.rx.xxx_IODebug.Reset() + return n +} + +func (n *xxx_DBG) Run(ctx context.Context) error { + return canrunner.Run(ctx, n) +} + +func (n *xxx_DBG) Rx() DBG_Rx { + return &n.rx +} + +func (n *xxx_DBG) Tx() DBG_Tx { + return &n.tx +} + +type xxx_DBG_Rx struct { + parentMutex *sync.Mutex + xxx_SensorSonars xxx_DBG_Rx_SensorSonars + xxx_IODebug xxx_DBG_Rx_IODebug +} + +var _ DBG_Rx = &xxx_DBG_Rx{} + +func (rx *xxx_DBG_Rx) ServeHTTP(w http.ResponseWriter, r *http.Request) { + rx.parentMutex.Lock() + defer rx.parentMutex.Unlock() + candebug.ServeMessagesHTTP(w, r, []generated.Message{ + &rx.xxx_SensorSonars, + &rx.xxx_IODebug, + }) +} + +func (rx *xxx_DBG_Rx) SensorSonars() DBG_Rx_SensorSonars { + return &rx.xxx_SensorSonars +} + +func (rx *xxx_DBG_Rx) IODebug() DBG_Rx_IODebug { + return &rx.xxx_IODebug +} + +type xxx_DBG_Tx struct { + parentMutex *sync.Mutex +} + +var _ DBG_Tx = &xxx_DBG_Tx{} + +func (tx *xxx_DBG_Tx) ServeHTTP(w http.ResponseWriter, r *http.Request) { + tx.parentMutex.Lock() + defer tx.parentMutex.Unlock() + candebug.ServeMessagesHTTP(w, r, []generated.Message{}) +} + +func (n *xxx_DBG) Descriptor() *descriptor.Node { + return Nodes().DBG +} + +func (n *xxx_DBG) Connect() (net.Conn, error) { + return socketcan.Dial(n.network, n.address) +} + +func (n *xxx_DBG) ReceivedMessage(id uint32) (canrunner.ReceivedMessage, bool) { + switch id { + case 200: + return &n.rx.xxx_SensorSonars, true + case 500: + return &n.rx.xxx_IODebug, true + default: + return nil, false + } +} + +func (n *xxx_DBG) TransmittedMessages() []canrunner.TransmittedMessage { + return []canrunner.TransmittedMessage{} +} + +type xxx_DBG_Rx_SensorSonars struct { + SensorSonars + receiveTime time.Time + afterReceiveHook func(context.Context) error +} + +func (m *xxx_DBG_Rx_SensorSonars) init() { + m.afterReceiveHook = func(context.Context) error { return nil } +} + +func (m *xxx_DBG_Rx_SensorSonars) SetAfterReceiveHook(h func(context.Context) error) { + m.afterReceiveHook = h +} + +func (m *xxx_DBG_Rx_SensorSonars) AfterReceiveHook() func(context.Context) error { + return m.afterReceiveHook +} + +func (m *xxx_DBG_Rx_SensorSonars) ReceiveTime() time.Time { + return m.receiveTime +} + +func (m *xxx_DBG_Rx_SensorSonars) SetReceiveTime(t time.Time) { + m.receiveTime = t +} + +var _ canrunner.ReceivedMessage = &xxx_DBG_Rx_SensorSonars{} + +type xxx_DBG_Rx_IODebug struct { + IODebug + receiveTime time.Time + afterReceiveHook func(context.Context) error +} + +func (m *xxx_DBG_Rx_IODebug) init() { + m.afterReceiveHook = func(context.Context) error { return nil } +} + +func (m *xxx_DBG_Rx_IODebug) SetAfterReceiveHook(h func(context.Context) error) { + m.afterReceiveHook = h +} + +func (m *xxx_DBG_Rx_IODebug) AfterReceiveHook() func(context.Context) error { + return m.afterReceiveHook +} + +func (m *xxx_DBG_Rx_IODebug) ReceiveTime() time.Time { + return m.receiveTime +} + +func (m *xxx_DBG_Rx_IODebug) SetReceiveTime(t time.Time) { + m.receiveTime = t +} + +var _ canrunner.ReceivedMessage = &xxx_DBG_Rx_IODebug{} + +type DRIVER interface { + sync.Locker + Tx() DRIVER_Tx + Rx() DRIVER_Rx + Run(ctx context.Context) error +} + +type DRIVER_Rx interface { + http.Handler // for debugging + SensorSonars() DRIVER_Rx_SensorSonars + MotorStatus() DRIVER_Rx_MotorStatus +} + +type DRIVER_Tx interface { + http.Handler // for debugging + DriverHeartbeat() DRIVER_Tx_DriverHeartbeat + MotorCommand() DRIVER_Tx_MotorCommand +} + +type DRIVER_Rx_SensorSonars interface { + SensorSonarsReader + ReceiveTime() time.Time + SetAfterReceiveHook(h func(context.Context) error) +} + +type DRIVER_Rx_MotorStatus interface { + MotorStatusReader + ReceiveTime() time.Time + SetAfterReceiveHook(h func(context.Context) error) +} + +type DRIVER_Tx_DriverHeartbeat interface { + DriverHeartbeatReader + DriverHeartbeatWriter + TransmitTime() time.Time + Transmit(ctx context.Context) error + SetBeforeTransmitHook(h func(context.Context) error) + // SetCyclicTransmissionEnabled enables/disables cyclic transmission. + SetCyclicTransmissionEnabled(bool) + // IsCyclicTransmissionEnabled returns whether cyclic transmission is enabled/disabled. + IsCyclicTransmissionEnabled() bool +} + +type DRIVER_Tx_MotorCommand interface { + MotorCommandReader + MotorCommandWriter + TransmitTime() time.Time + Transmit(ctx context.Context) error + SetBeforeTransmitHook(h func(context.Context) error) + // SetCyclicTransmissionEnabled enables/disables cyclic transmission. + SetCyclicTransmissionEnabled(bool) + // IsCyclicTransmissionEnabled returns whether cyclic transmission is enabled/disabled. + IsCyclicTransmissionEnabled() bool +} + +type xxx_DRIVER struct { + sync.Mutex // protects all node state + network string + address string + rx xxx_DRIVER_Rx + tx xxx_DRIVER_Tx +} + +var _ DRIVER = &xxx_DRIVER{} +var _ canrunner.Node = &xxx_DRIVER{} + +func NewDRIVER(network, address string) DRIVER { + n := &xxx_DRIVER{network: network, address: address} + n.rx.parentMutex = &n.Mutex + n.tx.parentMutex = &n.Mutex + n.rx.xxx_SensorSonars.init() + n.rx.xxx_SensorSonars.Reset() + n.rx.xxx_MotorStatus.init() + n.rx.xxx_MotorStatus.Reset() + n.tx.xxx_DriverHeartbeat.init() + n.tx.xxx_DriverHeartbeat.Reset() + n.tx.xxx_MotorCommand.init() + n.tx.xxx_MotorCommand.Reset() + return n +} + +func (n *xxx_DRIVER) Run(ctx context.Context) error { + return canrunner.Run(ctx, n) +} + +func (n *xxx_DRIVER) Rx() DRIVER_Rx { + return &n.rx +} + +func (n *xxx_DRIVER) Tx() DRIVER_Tx { + return &n.tx +} + +type xxx_DRIVER_Rx struct { + parentMutex *sync.Mutex + xxx_SensorSonars xxx_DRIVER_Rx_SensorSonars + xxx_MotorStatus xxx_DRIVER_Rx_MotorStatus +} + +var _ DRIVER_Rx = &xxx_DRIVER_Rx{} + +func (rx *xxx_DRIVER_Rx) ServeHTTP(w http.ResponseWriter, r *http.Request) { + rx.parentMutex.Lock() + defer rx.parentMutex.Unlock() + candebug.ServeMessagesHTTP(w, r, []generated.Message{ + &rx.xxx_SensorSonars, + &rx.xxx_MotorStatus, + }) +} + +func (rx *xxx_DRIVER_Rx) SensorSonars() DRIVER_Rx_SensorSonars { + return &rx.xxx_SensorSonars +} + +func (rx *xxx_DRIVER_Rx) MotorStatus() DRIVER_Rx_MotorStatus { + return &rx.xxx_MotorStatus +} + +type xxx_DRIVER_Tx struct { + parentMutex *sync.Mutex + xxx_DriverHeartbeat xxx_DRIVER_Tx_DriverHeartbeat + xxx_MotorCommand xxx_DRIVER_Tx_MotorCommand +} + +var _ DRIVER_Tx = &xxx_DRIVER_Tx{} + +func (tx *xxx_DRIVER_Tx) ServeHTTP(w http.ResponseWriter, r *http.Request) { + tx.parentMutex.Lock() + defer tx.parentMutex.Unlock() + candebug.ServeMessagesHTTP(w, r, []generated.Message{ + &tx.xxx_DriverHeartbeat, + &tx.xxx_MotorCommand, + }) +} + +func (tx *xxx_DRIVER_Tx) DriverHeartbeat() DRIVER_Tx_DriverHeartbeat { + return &tx.xxx_DriverHeartbeat +} + +func (tx *xxx_DRIVER_Tx) MotorCommand() DRIVER_Tx_MotorCommand { + return &tx.xxx_MotorCommand +} + +func (n *xxx_DRIVER) Descriptor() *descriptor.Node { + return Nodes().DRIVER +} + +func (n *xxx_DRIVER) Connect() (net.Conn, error) { + return socketcan.Dial(n.network, n.address) +} + +func (n *xxx_DRIVER) ReceivedMessage(id uint32) (canrunner.ReceivedMessage, bool) { + switch id { + case 200: + return &n.rx.xxx_SensorSonars, true + case 400: + return &n.rx.xxx_MotorStatus, true + default: + return nil, false + } +} + +func (n *xxx_DRIVER) TransmittedMessages() []canrunner.TransmittedMessage { + return []canrunner.TransmittedMessage{ + &n.tx.xxx_DriverHeartbeat, + &n.tx.xxx_MotorCommand, + } +} + +type xxx_DRIVER_Rx_SensorSonars struct { + SensorSonars + receiveTime time.Time + afterReceiveHook func(context.Context) error +} + +func (m *xxx_DRIVER_Rx_SensorSonars) init() { + m.afterReceiveHook = func(context.Context) error { return nil } +} + +func (m *xxx_DRIVER_Rx_SensorSonars) SetAfterReceiveHook(h func(context.Context) error) { + m.afterReceiveHook = h +} + +func (m *xxx_DRIVER_Rx_SensorSonars) AfterReceiveHook() func(context.Context) error { + return m.afterReceiveHook +} + +func (m *xxx_DRIVER_Rx_SensorSonars) ReceiveTime() time.Time { + return m.receiveTime +} + +func (m *xxx_DRIVER_Rx_SensorSonars) SetReceiveTime(t time.Time) { + m.receiveTime = t +} + +var _ canrunner.ReceivedMessage = &xxx_DRIVER_Rx_SensorSonars{} + +type xxx_DRIVER_Rx_MotorStatus struct { + MotorStatus + receiveTime time.Time + afterReceiveHook func(context.Context) error +} + +func (m *xxx_DRIVER_Rx_MotorStatus) init() { + m.afterReceiveHook = func(context.Context) error { return nil } +} + +func (m *xxx_DRIVER_Rx_MotorStatus) SetAfterReceiveHook(h func(context.Context) error) { + m.afterReceiveHook = h +} + +func (m *xxx_DRIVER_Rx_MotorStatus) AfterReceiveHook() func(context.Context) error { + return m.afterReceiveHook +} + +func (m *xxx_DRIVER_Rx_MotorStatus) ReceiveTime() time.Time { + return m.receiveTime +} + +func (m *xxx_DRIVER_Rx_MotorStatus) SetReceiveTime(t time.Time) { + m.receiveTime = t +} + +var _ canrunner.ReceivedMessage = &xxx_DRIVER_Rx_MotorStatus{} + +type xxx_DRIVER_Tx_DriverHeartbeat struct { + DriverHeartbeat + transmitTime time.Time + beforeTransmitHook func(context.Context) error + isCyclicEnabled bool + wakeUpChan chan struct{} + transmitEventChan chan struct{} +} + +var _ DRIVER_Tx_DriverHeartbeat = &xxx_DRIVER_Tx_DriverHeartbeat{} +var _ canrunner.TransmittedMessage = &xxx_DRIVER_Tx_DriverHeartbeat{} + +func (m *xxx_DRIVER_Tx_DriverHeartbeat) init() { + m.beforeTransmitHook = func(context.Context) error { return nil } + m.wakeUpChan = make(chan struct{}, 1) + m.transmitEventChan = make(chan struct{}) +} + +func (m *xxx_DRIVER_Tx_DriverHeartbeat) SetBeforeTransmitHook(h func(context.Context) error) { + m.beforeTransmitHook = h +} + +func (m *xxx_DRIVER_Tx_DriverHeartbeat) BeforeTransmitHook() func(context.Context) error { + return m.beforeTransmitHook +} + +func (m *xxx_DRIVER_Tx_DriverHeartbeat) TransmitTime() time.Time { + return m.transmitTime +} + +func (m *xxx_DRIVER_Tx_DriverHeartbeat) SetTransmitTime(t time.Time) { + m.transmitTime = t +} + +func (m *xxx_DRIVER_Tx_DriverHeartbeat) IsCyclicTransmissionEnabled() bool { + return m.isCyclicEnabled +} + +func (m *xxx_DRIVER_Tx_DriverHeartbeat) SetCyclicTransmissionEnabled(b bool) { + m.isCyclicEnabled = b + select { + case m.wakeUpChan <- struct{}{}: + default: + } +} + +func (m *xxx_DRIVER_Tx_DriverHeartbeat) WakeUpChan() <-chan struct{} { + return m.wakeUpChan +} + +func (m *xxx_DRIVER_Tx_DriverHeartbeat) Transmit(ctx context.Context) error { + select { + case m.transmitEventChan <- struct{}{}: + return nil + case <-ctx.Done(): + return fmt.Errorf("event-triggered transmit of DriverHeartbeat: %w", ctx.Err()) + } +} + +func (m *xxx_DRIVER_Tx_DriverHeartbeat) TransmitEventChan() <-chan struct{} { + return m.transmitEventChan +} + +var _ canrunner.TransmittedMessage = &xxx_DRIVER_Tx_DriverHeartbeat{} + +type xxx_DRIVER_Tx_MotorCommand struct { + MotorCommand + transmitTime time.Time + beforeTransmitHook func(context.Context) error + isCyclicEnabled bool + wakeUpChan chan struct{} + transmitEventChan chan struct{} +} + +var _ DRIVER_Tx_MotorCommand = &xxx_DRIVER_Tx_MotorCommand{} +var _ canrunner.TransmittedMessage = &xxx_DRIVER_Tx_MotorCommand{} + +func (m *xxx_DRIVER_Tx_MotorCommand) init() { + m.beforeTransmitHook = func(context.Context) error { return nil } + m.wakeUpChan = make(chan struct{}, 1) + m.transmitEventChan = make(chan struct{}) +} + +func (m *xxx_DRIVER_Tx_MotorCommand) SetBeforeTransmitHook(h func(context.Context) error) { + m.beforeTransmitHook = h +} + +func (m *xxx_DRIVER_Tx_MotorCommand) BeforeTransmitHook() func(context.Context) error { + return m.beforeTransmitHook +} + +func (m *xxx_DRIVER_Tx_MotorCommand) TransmitTime() time.Time { + return m.transmitTime +} + +func (m *xxx_DRIVER_Tx_MotorCommand) SetTransmitTime(t time.Time) { + m.transmitTime = t +} + +func (m *xxx_DRIVER_Tx_MotorCommand) IsCyclicTransmissionEnabled() bool { + return m.isCyclicEnabled +} + +func (m *xxx_DRIVER_Tx_MotorCommand) SetCyclicTransmissionEnabled(b bool) { + m.isCyclicEnabled = b + select { + case m.wakeUpChan <- struct{}{}: + default: + } +} + +func (m *xxx_DRIVER_Tx_MotorCommand) WakeUpChan() <-chan struct{} { + return m.wakeUpChan +} + +func (m *xxx_DRIVER_Tx_MotorCommand) Transmit(ctx context.Context) error { + select { + case m.transmitEventChan <- struct{}{}: + return nil + case <-ctx.Done(): + return fmt.Errorf("event-triggered transmit of MotorCommand: %w", ctx.Err()) + } +} + +func (m *xxx_DRIVER_Tx_MotorCommand) TransmitEventChan() <-chan struct{} { + return m.transmitEventChan +} + +var _ canrunner.TransmittedMessage = &xxx_DRIVER_Tx_MotorCommand{} + +type IO interface { + sync.Locker + Tx() IO_Tx + Rx() IO_Rx + Run(ctx context.Context) error +} + +type IO_Rx interface { + http.Handler // for debugging + SensorSonars() IO_Rx_SensorSonars + MotorStatus() IO_Rx_MotorStatus +} + +type IO_Tx interface { + http.Handler // for debugging + IODebug() IO_Tx_IODebug +} + +type IO_Rx_SensorSonars interface { + SensorSonarsReader + ReceiveTime() time.Time + SetAfterReceiveHook(h func(context.Context) error) +} + +type IO_Rx_MotorStatus interface { + MotorStatusReader + ReceiveTime() time.Time + SetAfterReceiveHook(h func(context.Context) error) +} + +type IO_Tx_IODebug interface { + IODebugReader + IODebugWriter + TransmitTime() time.Time + Transmit(ctx context.Context) error + SetBeforeTransmitHook(h func(context.Context) error) +} + +type xxx_IO struct { + sync.Mutex // protects all node state + network string + address string + rx xxx_IO_Rx + tx xxx_IO_Tx +} + +var _ IO = &xxx_IO{} +var _ canrunner.Node = &xxx_IO{} + +func NewIO(network, address string) IO { + n := &xxx_IO{network: network, address: address} + n.rx.parentMutex = &n.Mutex + n.tx.parentMutex = &n.Mutex + n.rx.xxx_SensorSonars.init() + n.rx.xxx_SensorSonars.Reset() + n.rx.xxx_MotorStatus.init() + n.rx.xxx_MotorStatus.Reset() + n.tx.xxx_IODebug.init() + n.tx.xxx_IODebug.Reset() + return n +} + +func (n *xxx_IO) Run(ctx context.Context) error { + return canrunner.Run(ctx, n) +} + +func (n *xxx_IO) Rx() IO_Rx { + return &n.rx +} + +func (n *xxx_IO) Tx() IO_Tx { + return &n.tx +} + +type xxx_IO_Rx struct { + parentMutex *sync.Mutex + xxx_SensorSonars xxx_IO_Rx_SensorSonars + xxx_MotorStatus xxx_IO_Rx_MotorStatus +} + +var _ IO_Rx = &xxx_IO_Rx{} + +func (rx *xxx_IO_Rx) ServeHTTP(w http.ResponseWriter, r *http.Request) { + rx.parentMutex.Lock() + defer rx.parentMutex.Unlock() + candebug.ServeMessagesHTTP(w, r, []generated.Message{ + &rx.xxx_SensorSonars, + &rx.xxx_MotorStatus, + }) +} + +func (rx *xxx_IO_Rx) SensorSonars() IO_Rx_SensorSonars { + return &rx.xxx_SensorSonars +} + +func (rx *xxx_IO_Rx) MotorStatus() IO_Rx_MotorStatus { + return &rx.xxx_MotorStatus +} + +type xxx_IO_Tx struct { + parentMutex *sync.Mutex + xxx_IODebug xxx_IO_Tx_IODebug +} + +var _ IO_Tx = &xxx_IO_Tx{} + +func (tx *xxx_IO_Tx) ServeHTTP(w http.ResponseWriter, r *http.Request) { + tx.parentMutex.Lock() + defer tx.parentMutex.Unlock() + candebug.ServeMessagesHTTP(w, r, []generated.Message{ + &tx.xxx_IODebug, + }) +} + +func (tx *xxx_IO_Tx) IODebug() IO_Tx_IODebug { + return &tx.xxx_IODebug +} + +func (n *xxx_IO) Descriptor() *descriptor.Node { + return Nodes().IO +} + +func (n *xxx_IO) Connect() (net.Conn, error) { + return socketcan.Dial(n.network, n.address) +} + +func (n *xxx_IO) ReceivedMessage(id uint32) (canrunner.ReceivedMessage, bool) { + switch id { + case 200: + return &n.rx.xxx_SensorSonars, true + case 400: + return &n.rx.xxx_MotorStatus, true + default: + return nil, false + } +} + +func (n *xxx_IO) TransmittedMessages() []canrunner.TransmittedMessage { + return []canrunner.TransmittedMessage{ + &n.tx.xxx_IODebug, + } +} + +type xxx_IO_Rx_SensorSonars struct { + SensorSonars + receiveTime time.Time + afterReceiveHook func(context.Context) error +} + +func (m *xxx_IO_Rx_SensorSonars) init() { + m.afterReceiveHook = func(context.Context) error { return nil } +} + +func (m *xxx_IO_Rx_SensorSonars) SetAfterReceiveHook(h func(context.Context) error) { + m.afterReceiveHook = h +} + +func (m *xxx_IO_Rx_SensorSonars) AfterReceiveHook() func(context.Context) error { + return m.afterReceiveHook +} + +func (m *xxx_IO_Rx_SensorSonars) ReceiveTime() time.Time { + return m.receiveTime +} + +func (m *xxx_IO_Rx_SensorSonars) SetReceiveTime(t time.Time) { + m.receiveTime = t +} + +var _ canrunner.ReceivedMessage = &xxx_IO_Rx_SensorSonars{} + +type xxx_IO_Rx_MotorStatus struct { + MotorStatus + receiveTime time.Time + afterReceiveHook func(context.Context) error +} + +func (m *xxx_IO_Rx_MotorStatus) init() { + m.afterReceiveHook = func(context.Context) error { return nil } +} + +func (m *xxx_IO_Rx_MotorStatus) SetAfterReceiveHook(h func(context.Context) error) { + m.afterReceiveHook = h +} + +func (m *xxx_IO_Rx_MotorStatus) AfterReceiveHook() func(context.Context) error { + return m.afterReceiveHook +} + +func (m *xxx_IO_Rx_MotorStatus) ReceiveTime() time.Time { + return m.receiveTime +} + +func (m *xxx_IO_Rx_MotorStatus) SetReceiveTime(t time.Time) { + m.receiveTime = t +} + +var _ canrunner.ReceivedMessage = &xxx_IO_Rx_MotorStatus{} + +type xxx_IO_Tx_IODebug struct { + IODebug + transmitTime time.Time + beforeTransmitHook func(context.Context) error + isCyclicEnabled bool + wakeUpChan chan struct{} + transmitEventChan chan struct{} +} + +var _ IO_Tx_IODebug = &xxx_IO_Tx_IODebug{} +var _ canrunner.TransmittedMessage = &xxx_IO_Tx_IODebug{} + +func (m *xxx_IO_Tx_IODebug) init() { + m.beforeTransmitHook = func(context.Context) error { return nil } + m.wakeUpChan = make(chan struct{}, 1) + m.transmitEventChan = make(chan struct{}) +} + +func (m *xxx_IO_Tx_IODebug) SetBeforeTransmitHook(h func(context.Context) error) { + m.beforeTransmitHook = h +} + +func (m *xxx_IO_Tx_IODebug) BeforeTransmitHook() func(context.Context) error { + return m.beforeTransmitHook +} + +func (m *xxx_IO_Tx_IODebug) TransmitTime() time.Time { + return m.transmitTime +} + +func (m *xxx_IO_Tx_IODebug) SetTransmitTime(t time.Time) { + m.transmitTime = t +} + +func (m *xxx_IO_Tx_IODebug) IsCyclicTransmissionEnabled() bool { + return m.isCyclicEnabled +} + +func (m *xxx_IO_Tx_IODebug) SetCyclicTransmissionEnabled(b bool) { + m.isCyclicEnabled = b + select { + case m.wakeUpChan <- struct{}{}: + default: + } +} + +func (m *xxx_IO_Tx_IODebug) WakeUpChan() <-chan struct{} { + return m.wakeUpChan +} + +func (m *xxx_IO_Tx_IODebug) Transmit(ctx context.Context) error { + select { + case m.transmitEventChan <- struct{}{}: + return nil + case <-ctx.Done(): + return fmt.Errorf("event-triggered transmit of IODebug: %w", ctx.Err()) + } +} + +func (m *xxx_IO_Tx_IODebug) TransmitEventChan() <-chan struct{} { + return m.transmitEventChan +} + +var _ canrunner.TransmittedMessage = &xxx_IO_Tx_IODebug{} + +type MOTOR interface { + sync.Locker + Tx() MOTOR_Tx + Rx() MOTOR_Rx + Run(ctx context.Context) error +} + +type MOTOR_Rx interface { + http.Handler // for debugging + DriverHeartbeat() MOTOR_Rx_DriverHeartbeat + MotorCommand() MOTOR_Rx_MotorCommand +} + +type MOTOR_Tx interface { + http.Handler // for debugging + MotorStatus() MOTOR_Tx_MotorStatus +} + +type MOTOR_Rx_DriverHeartbeat interface { + DriverHeartbeatReader + ReceiveTime() time.Time + SetAfterReceiveHook(h func(context.Context) error) +} + +type MOTOR_Rx_MotorCommand interface { + MotorCommandReader + ReceiveTime() time.Time + SetAfterReceiveHook(h func(context.Context) error) +} + +type MOTOR_Tx_MotorStatus interface { + MotorStatusReader + MotorStatusWriter + TransmitTime() time.Time + Transmit(ctx context.Context) error + SetBeforeTransmitHook(h func(context.Context) error) + // SetCyclicTransmissionEnabled enables/disables cyclic transmission. + SetCyclicTransmissionEnabled(bool) + // IsCyclicTransmissionEnabled returns whether cyclic transmission is enabled/disabled. + IsCyclicTransmissionEnabled() bool +} + +type xxx_MOTOR struct { + sync.Mutex // protects all node state + network string + address string + rx xxx_MOTOR_Rx + tx xxx_MOTOR_Tx +} + +var _ MOTOR = &xxx_MOTOR{} +var _ canrunner.Node = &xxx_MOTOR{} + +func NewMOTOR(network, address string) MOTOR { + n := &xxx_MOTOR{network: network, address: address} + n.rx.parentMutex = &n.Mutex + n.tx.parentMutex = &n.Mutex + n.rx.xxx_DriverHeartbeat.init() + n.rx.xxx_DriverHeartbeat.Reset() + n.rx.xxx_MotorCommand.init() + n.rx.xxx_MotorCommand.Reset() + n.tx.xxx_MotorStatus.init() + n.tx.xxx_MotorStatus.Reset() + return n +} + +func (n *xxx_MOTOR) Run(ctx context.Context) error { + return canrunner.Run(ctx, n) +} + +func (n *xxx_MOTOR) Rx() MOTOR_Rx { + return &n.rx +} + +func (n *xxx_MOTOR) Tx() MOTOR_Tx { + return &n.tx +} + +type xxx_MOTOR_Rx struct { + parentMutex *sync.Mutex + xxx_DriverHeartbeat xxx_MOTOR_Rx_DriverHeartbeat + xxx_MotorCommand xxx_MOTOR_Rx_MotorCommand +} + +var _ MOTOR_Rx = &xxx_MOTOR_Rx{} + +func (rx *xxx_MOTOR_Rx) ServeHTTP(w http.ResponseWriter, r *http.Request) { + rx.parentMutex.Lock() + defer rx.parentMutex.Unlock() + candebug.ServeMessagesHTTP(w, r, []generated.Message{ + &rx.xxx_DriverHeartbeat, + &rx.xxx_MotorCommand, + }) +} + +func (rx *xxx_MOTOR_Rx) DriverHeartbeat() MOTOR_Rx_DriverHeartbeat { + return &rx.xxx_DriverHeartbeat +} + +func (rx *xxx_MOTOR_Rx) MotorCommand() MOTOR_Rx_MotorCommand { + return &rx.xxx_MotorCommand +} + +type xxx_MOTOR_Tx struct { + parentMutex *sync.Mutex + xxx_MotorStatus xxx_MOTOR_Tx_MotorStatus +} + +var _ MOTOR_Tx = &xxx_MOTOR_Tx{} + +func (tx *xxx_MOTOR_Tx) ServeHTTP(w http.ResponseWriter, r *http.Request) { + tx.parentMutex.Lock() + defer tx.parentMutex.Unlock() + candebug.ServeMessagesHTTP(w, r, []generated.Message{ + &tx.xxx_MotorStatus, + }) +} + +func (tx *xxx_MOTOR_Tx) MotorStatus() MOTOR_Tx_MotorStatus { + return &tx.xxx_MotorStatus +} + +func (n *xxx_MOTOR) Descriptor() *descriptor.Node { + return Nodes().MOTOR +} + +func (n *xxx_MOTOR) Connect() (net.Conn, error) { + return socketcan.Dial(n.network, n.address) +} + +func (n *xxx_MOTOR) ReceivedMessage(id uint32) (canrunner.ReceivedMessage, bool) { + switch id { + case 100: + return &n.rx.xxx_DriverHeartbeat, true + case 101: + return &n.rx.xxx_MotorCommand, true + default: + return nil, false + } +} + +func (n *xxx_MOTOR) TransmittedMessages() []canrunner.TransmittedMessage { + return []canrunner.TransmittedMessage{ + &n.tx.xxx_MotorStatus, + } +} + +type xxx_MOTOR_Rx_DriverHeartbeat struct { + DriverHeartbeat + receiveTime time.Time + afterReceiveHook func(context.Context) error +} + +func (m *xxx_MOTOR_Rx_DriverHeartbeat) init() { + m.afterReceiveHook = func(context.Context) error { return nil } +} + +func (m *xxx_MOTOR_Rx_DriverHeartbeat) SetAfterReceiveHook(h func(context.Context) error) { + m.afterReceiveHook = h +} + +func (m *xxx_MOTOR_Rx_DriverHeartbeat) AfterReceiveHook() func(context.Context) error { + return m.afterReceiveHook +} + +func (m *xxx_MOTOR_Rx_DriverHeartbeat) ReceiveTime() time.Time { + return m.receiveTime +} + +func (m *xxx_MOTOR_Rx_DriverHeartbeat) SetReceiveTime(t time.Time) { + m.receiveTime = t +} + +var _ canrunner.ReceivedMessage = &xxx_MOTOR_Rx_DriverHeartbeat{} + +type xxx_MOTOR_Rx_MotorCommand struct { + MotorCommand + receiveTime time.Time + afterReceiveHook func(context.Context) error +} + +func (m *xxx_MOTOR_Rx_MotorCommand) init() { + m.afterReceiveHook = func(context.Context) error { return nil } +} + +func (m *xxx_MOTOR_Rx_MotorCommand) SetAfterReceiveHook(h func(context.Context) error) { + m.afterReceiveHook = h +} + +func (m *xxx_MOTOR_Rx_MotorCommand) AfterReceiveHook() func(context.Context) error { + return m.afterReceiveHook +} + +func (m *xxx_MOTOR_Rx_MotorCommand) ReceiveTime() time.Time { + return m.receiveTime +} + +func (m *xxx_MOTOR_Rx_MotorCommand) SetReceiveTime(t time.Time) { + m.receiveTime = t +} + +var _ canrunner.ReceivedMessage = &xxx_MOTOR_Rx_MotorCommand{} + +type xxx_MOTOR_Tx_MotorStatus struct { + MotorStatus + transmitTime time.Time + beforeTransmitHook func(context.Context) error + isCyclicEnabled bool + wakeUpChan chan struct{} + transmitEventChan chan struct{} +} + +var _ MOTOR_Tx_MotorStatus = &xxx_MOTOR_Tx_MotorStatus{} +var _ canrunner.TransmittedMessage = &xxx_MOTOR_Tx_MotorStatus{} + +func (m *xxx_MOTOR_Tx_MotorStatus) init() { + m.beforeTransmitHook = func(context.Context) error { return nil } + m.wakeUpChan = make(chan struct{}, 1) + m.transmitEventChan = make(chan struct{}) +} + +func (m *xxx_MOTOR_Tx_MotorStatus) SetBeforeTransmitHook(h func(context.Context) error) { + m.beforeTransmitHook = h +} + +func (m *xxx_MOTOR_Tx_MotorStatus) BeforeTransmitHook() func(context.Context) error { + return m.beforeTransmitHook +} + +func (m *xxx_MOTOR_Tx_MotorStatus) TransmitTime() time.Time { + return m.transmitTime +} + +func (m *xxx_MOTOR_Tx_MotorStatus) SetTransmitTime(t time.Time) { + m.transmitTime = t +} + +func (m *xxx_MOTOR_Tx_MotorStatus) IsCyclicTransmissionEnabled() bool { + return m.isCyclicEnabled +} + +func (m *xxx_MOTOR_Tx_MotorStatus) SetCyclicTransmissionEnabled(b bool) { + m.isCyclicEnabled = b + select { + case m.wakeUpChan <- struct{}{}: + default: + } +} + +func (m *xxx_MOTOR_Tx_MotorStatus) WakeUpChan() <-chan struct{} { + return m.wakeUpChan +} + +func (m *xxx_MOTOR_Tx_MotorStatus) Transmit(ctx context.Context) error { + select { + case m.transmitEventChan <- struct{}{}: + return nil + case <-ctx.Done(): + return fmt.Errorf("event-triggered transmit of MotorStatus: %w", ctx.Err()) + } +} + +func (m *xxx_MOTOR_Tx_MotorStatus) TransmitEventChan() <-chan struct{} { + return m.transmitEventChan +} + +var _ canrunner.TransmittedMessage = &xxx_MOTOR_Tx_MotorStatus{} + +type SENSOR interface { + sync.Locker + Tx() SENSOR_Tx + Rx() SENSOR_Rx + Run(ctx context.Context) error +} + +type SENSOR_Rx interface { + http.Handler // for debugging + DriverHeartbeat() SENSOR_Rx_DriverHeartbeat +} + +type SENSOR_Tx interface { + http.Handler // for debugging + SensorSonars() SENSOR_Tx_SensorSonars +} + +type SENSOR_Rx_DriverHeartbeat interface { + DriverHeartbeatReader + ReceiveTime() time.Time + SetAfterReceiveHook(h func(context.Context) error) +} + +type SENSOR_Tx_SensorSonars interface { + SensorSonarsReader + SensorSonarsWriter + TransmitTime() time.Time + Transmit(ctx context.Context) error + SetBeforeTransmitHook(h func(context.Context) error) + // SetCyclicTransmissionEnabled enables/disables cyclic transmission. + SetCyclicTransmissionEnabled(bool) + // IsCyclicTransmissionEnabled returns whether cyclic transmission is enabled/disabled. + IsCyclicTransmissionEnabled() bool +} + +type xxx_SENSOR struct { + sync.Mutex // protects all node state + network string + address string + rx xxx_SENSOR_Rx + tx xxx_SENSOR_Tx +} + +var _ SENSOR = &xxx_SENSOR{} +var _ canrunner.Node = &xxx_SENSOR{} + +func NewSENSOR(network, address string) SENSOR { + n := &xxx_SENSOR{network: network, address: address} + n.rx.parentMutex = &n.Mutex + n.tx.parentMutex = &n.Mutex + n.rx.xxx_DriverHeartbeat.init() + n.rx.xxx_DriverHeartbeat.Reset() + n.tx.xxx_SensorSonars.init() + n.tx.xxx_SensorSonars.Reset() + return n +} + +func (n *xxx_SENSOR) Run(ctx context.Context) error { + return canrunner.Run(ctx, n) +} + +func (n *xxx_SENSOR) Rx() SENSOR_Rx { + return &n.rx +} + +func (n *xxx_SENSOR) Tx() SENSOR_Tx { + return &n.tx +} + +type xxx_SENSOR_Rx struct { + parentMutex *sync.Mutex + xxx_DriverHeartbeat xxx_SENSOR_Rx_DriverHeartbeat +} + +var _ SENSOR_Rx = &xxx_SENSOR_Rx{} + +func (rx *xxx_SENSOR_Rx) ServeHTTP(w http.ResponseWriter, r *http.Request) { + rx.parentMutex.Lock() + defer rx.parentMutex.Unlock() + candebug.ServeMessagesHTTP(w, r, []generated.Message{ + &rx.xxx_DriverHeartbeat, + }) +} + +func (rx *xxx_SENSOR_Rx) DriverHeartbeat() SENSOR_Rx_DriverHeartbeat { + return &rx.xxx_DriverHeartbeat +} + +type xxx_SENSOR_Tx struct { + parentMutex *sync.Mutex + xxx_SensorSonars xxx_SENSOR_Tx_SensorSonars +} + +var _ SENSOR_Tx = &xxx_SENSOR_Tx{} + +func (tx *xxx_SENSOR_Tx) ServeHTTP(w http.ResponseWriter, r *http.Request) { + tx.parentMutex.Lock() + defer tx.parentMutex.Unlock() + candebug.ServeMessagesHTTP(w, r, []generated.Message{ + &tx.xxx_SensorSonars, + }) +} + +func (tx *xxx_SENSOR_Tx) SensorSonars() SENSOR_Tx_SensorSonars { + return &tx.xxx_SensorSonars +} + +func (n *xxx_SENSOR) Descriptor() *descriptor.Node { + return Nodes().SENSOR +} + +func (n *xxx_SENSOR) Connect() (net.Conn, error) { + return socketcan.Dial(n.network, n.address) +} + +func (n *xxx_SENSOR) ReceivedMessage(id uint32) (canrunner.ReceivedMessage, bool) { + switch id { + case 100: + return &n.rx.xxx_DriverHeartbeat, true + default: + return nil, false + } +} + +func (n *xxx_SENSOR) TransmittedMessages() []canrunner.TransmittedMessage { + return []canrunner.TransmittedMessage{ + &n.tx.xxx_SensorSonars, + } +} + +type xxx_SENSOR_Rx_DriverHeartbeat struct { + DriverHeartbeat + receiveTime time.Time + afterReceiveHook func(context.Context) error +} + +func (m *xxx_SENSOR_Rx_DriverHeartbeat) init() { + m.afterReceiveHook = func(context.Context) error { return nil } +} + +func (m *xxx_SENSOR_Rx_DriverHeartbeat) SetAfterReceiveHook(h func(context.Context) error) { + m.afterReceiveHook = h +} + +func (m *xxx_SENSOR_Rx_DriverHeartbeat) AfterReceiveHook() func(context.Context) error { + return m.afterReceiveHook +} + +func (m *xxx_SENSOR_Rx_DriverHeartbeat) ReceiveTime() time.Time { + return m.receiveTime +} + +func (m *xxx_SENSOR_Rx_DriverHeartbeat) SetReceiveTime(t time.Time) { + m.receiveTime = t +} + +var _ canrunner.ReceivedMessage = &xxx_SENSOR_Rx_DriverHeartbeat{} + +type xxx_SENSOR_Tx_SensorSonars struct { + SensorSonars + transmitTime time.Time + beforeTransmitHook func(context.Context) error + isCyclicEnabled bool + wakeUpChan chan struct{} + transmitEventChan chan struct{} +} + +var _ SENSOR_Tx_SensorSonars = &xxx_SENSOR_Tx_SensorSonars{} +var _ canrunner.TransmittedMessage = &xxx_SENSOR_Tx_SensorSonars{} + +func (m *xxx_SENSOR_Tx_SensorSonars) init() { + m.beforeTransmitHook = func(context.Context) error { return nil } + m.wakeUpChan = make(chan struct{}, 1) + m.transmitEventChan = make(chan struct{}) +} + +func (m *xxx_SENSOR_Tx_SensorSonars) SetBeforeTransmitHook(h func(context.Context) error) { + m.beforeTransmitHook = h +} + +func (m *xxx_SENSOR_Tx_SensorSonars) BeforeTransmitHook() func(context.Context) error { + return m.beforeTransmitHook +} + +func (m *xxx_SENSOR_Tx_SensorSonars) TransmitTime() time.Time { + return m.transmitTime +} + +func (m *xxx_SENSOR_Tx_SensorSonars) SetTransmitTime(t time.Time) { + m.transmitTime = t +} + +func (m *xxx_SENSOR_Tx_SensorSonars) IsCyclicTransmissionEnabled() bool { + return m.isCyclicEnabled +} + +func (m *xxx_SENSOR_Tx_SensorSonars) SetCyclicTransmissionEnabled(b bool) { + m.isCyclicEnabled = b + select { + case m.wakeUpChan <- struct{}{}: + default: + } +} + +func (m *xxx_SENSOR_Tx_SensorSonars) WakeUpChan() <-chan struct{} { + return m.wakeUpChan +} + +func (m *xxx_SENSOR_Tx_SensorSonars) Transmit(ctx context.Context) error { + select { + case m.transmitEventChan <- struct{}{}: + return nil + case <-ctx.Done(): + return fmt.Errorf("event-triggered transmit of SensorSonars: %w", ctx.Err()) + } +} + +func (m *xxx_SENSOR_Tx_SensorSonars) TransmitEventChan() <-chan struct{} { + return m.transmitEventChan +} + +var _ canrunner.TransmittedMessage = &xxx_SENSOR_Tx_SensorSonars{} + +// Nodes returns the example node descriptors. +func Nodes() *NodesDescriptor { + return nd +} + +// NodesDescriptor contains all example node descriptors. +type NodesDescriptor struct { + DBG *descriptor.Node + DRIVER *descriptor.Node + IO *descriptor.Node + MOTOR *descriptor.Node + SENSOR *descriptor.Node +} + +// Messages returns the example message descriptors. +func Messages() *MessagesDescriptor { + return md +} + +// MessagesDescriptor contains all example message descriptors. +type MessagesDescriptor struct { + EmptyMessage *EmptyMessageDescriptor + DriverHeartbeat *DriverHeartbeatDescriptor + MotorCommand *MotorCommandDescriptor + SensorSonars *SensorSonarsDescriptor + MotorStatus *MotorStatusDescriptor + IODebug *IODebugDescriptor +} + +// UnmarshalFrame unmarshals the provided example CAN frame. +func (md *MessagesDescriptor) UnmarshalFrame(f can.Frame) (generated.Message, error) { + switch f.ID { + case md.EmptyMessage.ID: + var msg EmptyMessage + if err := msg.UnmarshalFrame(f); err != nil { + return nil, fmt.Errorf("unmarshal example frame: %w", err) + } + return &msg, nil + case md.DriverHeartbeat.ID: + var msg DriverHeartbeat + if err := msg.UnmarshalFrame(f); err != nil { + return nil, fmt.Errorf("unmarshal example frame: %w", err) + } + return &msg, nil + case md.MotorCommand.ID: + var msg MotorCommand + if err := msg.UnmarshalFrame(f); err != nil { + return nil, fmt.Errorf("unmarshal example frame: %w", err) + } + return &msg, nil + case md.SensorSonars.ID: + var msg SensorSonars + if err := msg.UnmarshalFrame(f); err != nil { + return nil, fmt.Errorf("unmarshal example frame: %w", err) + } + return &msg, nil + case md.MotorStatus.ID: + var msg MotorStatus + if err := msg.UnmarshalFrame(f); err != nil { + return nil, fmt.Errorf("unmarshal example frame: %w", err) + } + return &msg, nil + case md.IODebug.ID: + var msg IODebug + if err := msg.UnmarshalFrame(f); err != nil { + return nil, fmt.Errorf("unmarshal example frame: %w", err) + } + return &msg, nil + default: + return nil, fmt.Errorf("unmarshal example frame: ID not in database: %d", f.ID) + } +} + +type EmptyMessageDescriptor struct { + *descriptor.Message +} + +type DriverHeartbeatDescriptor struct { + *descriptor.Message + Command *descriptor.Signal +} + +type MotorCommandDescriptor struct { + *descriptor.Message + Steer *descriptor.Signal + Drive *descriptor.Signal +} + +type SensorSonarsDescriptor struct { + *descriptor.Message + Mux *descriptor.Signal + ErrCount *descriptor.Signal + Left *descriptor.Signal + NoFiltLeft *descriptor.Signal + Middle *descriptor.Signal + NoFiltMiddle *descriptor.Signal + Right *descriptor.Signal + NoFiltRight *descriptor.Signal + Rear *descriptor.Signal + NoFiltRear *descriptor.Signal +} + +type MotorStatusDescriptor struct { + *descriptor.Message + WheelError *descriptor.Signal + SpeedKph *descriptor.Signal +} + +type IODebugDescriptor struct { + *descriptor.Message + TestUnsigned *descriptor.Signal + TestEnum *descriptor.Signal + TestSigned *descriptor.Signal + TestFloat *descriptor.Signal + TestBoolEnum *descriptor.Signal + TestScaledEnum *descriptor.Signal +} + +// Database returns the example database descriptor. +func (md *MessagesDescriptor) Database() *descriptor.Database { + return d +} + +var nd = &NodesDescriptor{ + DBG: d.Nodes[0], + DRIVER: d.Nodes[1], + IO: d.Nodes[2], + MOTOR: d.Nodes[3], + SENSOR: d.Nodes[4], +} + +var md = &MessagesDescriptor{ + EmptyMessage: &EmptyMessageDescriptor{ + Message: d.Messages[0], + }, + DriverHeartbeat: &DriverHeartbeatDescriptor{ + Message: d.Messages[1], + Command: d.Messages[1].Signals[0], + }, + MotorCommand: &MotorCommandDescriptor{ + Message: d.Messages[2], + Steer: d.Messages[2].Signals[0], + Drive: d.Messages[2].Signals[1], + }, + SensorSonars: &SensorSonarsDescriptor{ + Message: d.Messages[3], + Mux: d.Messages[3].Signals[0], + ErrCount: d.Messages[3].Signals[1], + Left: d.Messages[3].Signals[2], + NoFiltLeft: d.Messages[3].Signals[3], + Middle: d.Messages[3].Signals[4], + NoFiltMiddle: d.Messages[3].Signals[5], + Right: d.Messages[3].Signals[6], + NoFiltRight: d.Messages[3].Signals[7], + Rear: d.Messages[3].Signals[8], + NoFiltRear: d.Messages[3].Signals[9], + }, + MotorStatus: &MotorStatusDescriptor{ + Message: d.Messages[4], + WheelError: d.Messages[4].Signals[0], + SpeedKph: d.Messages[4].Signals[1], + }, + IODebug: &IODebugDescriptor{ + Message: d.Messages[5], + TestUnsigned: d.Messages[5].Signals[0], + TestEnum: d.Messages[5].Signals[1], + TestSigned: d.Messages[5].Signals[2], + TestFloat: d.Messages[5].Signals[3], + TestBoolEnum: d.Messages[5].Signals[4], + TestScaledEnum: d.Messages[5].Signals[5], + }, +} + +var d = (*descriptor.Database)(&descriptor.Database{ + SourceFile: (string)("testdata/dbc/example/example.dbc"), + Version: (string)(""), + Messages: ([]*descriptor.Message)([]*descriptor.Message{ + (*descriptor.Message)(&descriptor.Message{ + Name: (string)("EmptyMessage"), + ID: (uint32)(1), + IsExtended: (bool)(false), + Length: (uint8)(0), + SendType: (descriptor.SendType)(0), + Description: (string)(""), + Signals: ([]*descriptor.Signal)(nil), + SenderNode: (string)("DBG"), + CycleTime: (time.Duration)(0), + DelayTime: (time.Duration)(0), + }), + (*descriptor.Message)(&descriptor.Message{ + Name: (string)("DriverHeartbeat"), + ID: (uint32)(100), + IsExtended: (bool)(false), + Length: (uint8)(1), + SendType: (descriptor.SendType)(1), + Description: (string)("Sync message used to synchronize the controllers"), + Signals: ([]*descriptor.Signal)([]*descriptor.Signal{ + (*descriptor.Signal)(&descriptor.Signal{ + Name: (string)("Command"), + Start: (uint8)(0), + Length: (uint8)(8), + IsBigEndian: (bool)(false), + IsSigned: (bool)(false), + IsMultiplexer: (bool)(false), + IsMultiplexed: (bool)(false), + MultiplexerValue: (uint)(0), + Offset: (float64)(0), + Scale: (float64)(1), + Min: (float64)(0), + Max: (float64)(0), + Unit: (string)(""), + Description: (string)(""), + ValueDescriptions: ([]*descriptor.ValueDescription)([]*descriptor.ValueDescription{ + (*descriptor.ValueDescription)(&descriptor.ValueDescription{ + Value: (int)(0), + Description: (string)("None"), + }), + (*descriptor.ValueDescription)(&descriptor.ValueDescription{ + Value: (int)(1), + Description: (string)("Sync"), + }), + (*descriptor.ValueDescription)(&descriptor.ValueDescription{ + Value: (int)(2), + Description: (string)("Reboot"), + }), + }), + ReceiverNodes: ([]string)([]string{ + (string)("SENSOR"), + (string)("MOTOR"), + }), + DefaultValue: (int)(0), + }), + }), + SenderNode: (string)("DRIVER"), + CycleTime: (time.Duration)(1000000000), + DelayTime: (time.Duration)(0), + }), + (*descriptor.Message)(&descriptor.Message{ + Name: (string)("MotorCommand"), + ID: (uint32)(101), + IsExtended: (bool)(false), + Length: (uint8)(1), + SendType: (descriptor.SendType)(1), + Description: (string)(""), + Signals: ([]*descriptor.Signal)([]*descriptor.Signal{ + (*descriptor.Signal)(&descriptor.Signal{ + Name: (string)("Steer"), + Start: (uint8)(0), + Length: (uint8)(4), + IsBigEndian: (bool)(false), + IsSigned: (bool)(true), + IsMultiplexer: (bool)(false), + IsMultiplexed: (bool)(false), + MultiplexerValue: (uint)(0), + Offset: (float64)(-5), + Scale: (float64)(1), + Min: (float64)(-5), + Max: (float64)(5), + Unit: (string)(""), + Description: (string)(""), + ValueDescriptions: ([]*descriptor.ValueDescription)(nil), + ReceiverNodes: ([]string)([]string{ + (string)("MOTOR"), + }), + DefaultValue: (int)(0), + }), + (*descriptor.Signal)(&descriptor.Signal{ + Name: (string)("Drive"), + Start: (uint8)(4), + Length: (uint8)(4), + IsBigEndian: (bool)(false), + IsSigned: (bool)(false), + IsMultiplexer: (bool)(false), + IsMultiplexed: (bool)(false), + MultiplexerValue: (uint)(0), + Offset: (float64)(0), + Scale: (float64)(1), + Min: (float64)(0), + Max: (float64)(9), + Unit: (string)(""), + Description: (string)(""), + ValueDescriptions: ([]*descriptor.ValueDescription)(nil), + ReceiverNodes: ([]string)([]string{ + (string)("MOTOR"), + }), + DefaultValue: (int)(0), + }), + }), + SenderNode: (string)("DRIVER"), + CycleTime: (time.Duration)(100000000), + DelayTime: (time.Duration)(0), + }), + (*descriptor.Message)(&descriptor.Message{ + Name: (string)("SensorSonars"), + ID: (uint32)(200), + IsExtended: (bool)(false), + Length: (uint8)(8), + SendType: (descriptor.SendType)(1), + Description: (string)(""), + Signals: ([]*descriptor.Signal)([]*descriptor.Signal{ + (*descriptor.Signal)(&descriptor.Signal{ + Name: (string)("Mux"), + Start: (uint8)(0), + Length: (uint8)(4), + IsBigEndian: (bool)(false), + IsSigned: (bool)(false), + IsMultiplexer: (bool)(true), + IsMultiplexed: (bool)(false), + MultiplexerValue: (uint)(0), + Offset: (float64)(0), + Scale: (float64)(1), + Min: (float64)(0), + Max: (float64)(0), + Unit: (string)(""), + Description: (string)(""), + ValueDescriptions: ([]*descriptor.ValueDescription)(nil), + ReceiverNodes: ([]string)([]string{ + (string)("DRIVER"), + (string)("IO"), + }), + DefaultValue: (int)(0), + }), + (*descriptor.Signal)(&descriptor.Signal{ + Name: (string)("ErrCount"), + Start: (uint8)(4), + Length: (uint8)(12), + IsBigEndian: (bool)(false), + IsSigned: (bool)(false), + IsMultiplexer: (bool)(false), + IsMultiplexed: (bool)(false), + MultiplexerValue: (uint)(0), + Offset: (float64)(0), + Scale: (float64)(1), + Min: (float64)(0), + Max: (float64)(0), + Unit: (string)(""), + Description: (string)(""), + ValueDescriptions: ([]*descriptor.ValueDescription)(nil), + ReceiverNodes: ([]string)([]string{ + (string)("DRIVER"), + (string)("IO"), + }), + DefaultValue: (int)(0), + }), + (*descriptor.Signal)(&descriptor.Signal{ + Name: (string)("Left"), + Start: (uint8)(16), + Length: (uint8)(12), + IsBigEndian: (bool)(false), + IsSigned: (bool)(false), + IsMultiplexer: (bool)(false), + IsMultiplexed: (bool)(true), + MultiplexerValue: (uint)(0), + Offset: (float64)(0), + Scale: (float64)(0.1), + Min: (float64)(0), + Max: (float64)(0), + Unit: (string)(""), + Description: (string)(""), + ValueDescriptions: ([]*descriptor.ValueDescription)(nil), + ReceiverNodes: ([]string)([]string{ + (string)("DRIVER"), + (string)("IO"), + }), + DefaultValue: (int)(0), + }), + (*descriptor.Signal)(&descriptor.Signal{ + Name: (string)("NoFiltLeft"), + Start: (uint8)(16), + Length: (uint8)(12), + IsBigEndian: (bool)(false), + IsSigned: (bool)(false), + IsMultiplexer: (bool)(false), + IsMultiplexed: (bool)(true), + MultiplexerValue: (uint)(1), + Offset: (float64)(0), + Scale: (float64)(0.1), + Min: (float64)(0), + Max: (float64)(0), + Unit: (string)(""), + Description: (string)(""), + ValueDescriptions: ([]*descriptor.ValueDescription)(nil), + ReceiverNodes: ([]string)([]string{ + (string)("DBG"), + }), + DefaultValue: (int)(0), + }), + (*descriptor.Signal)(&descriptor.Signal{ + Name: (string)("Middle"), + Start: (uint8)(28), + Length: (uint8)(12), + IsBigEndian: (bool)(false), + IsSigned: (bool)(false), + IsMultiplexer: (bool)(false), + IsMultiplexed: (bool)(true), + MultiplexerValue: (uint)(0), + Offset: (float64)(0), + Scale: (float64)(0.1), + Min: (float64)(0), + Max: (float64)(0), + Unit: (string)(""), + Description: (string)(""), + ValueDescriptions: ([]*descriptor.ValueDescription)(nil), + ReceiverNodes: ([]string)([]string{ + (string)("DRIVER"), + (string)("IO"), + }), + DefaultValue: (int)(0), + }), + (*descriptor.Signal)(&descriptor.Signal{ + Name: (string)("NoFiltMiddle"), + Start: (uint8)(28), + Length: (uint8)(12), + IsBigEndian: (bool)(false), + IsSigned: (bool)(false), + IsMultiplexer: (bool)(false), + IsMultiplexed: (bool)(true), + MultiplexerValue: (uint)(1), + Offset: (float64)(0), + Scale: (float64)(0.1), + Min: (float64)(0), + Max: (float64)(0), + Unit: (string)(""), + Description: (string)(""), + ValueDescriptions: ([]*descriptor.ValueDescription)(nil), + ReceiverNodes: ([]string)([]string{ + (string)("DBG"), + }), + DefaultValue: (int)(0), + }), + (*descriptor.Signal)(&descriptor.Signal{ + Name: (string)("Right"), + Start: (uint8)(40), + Length: (uint8)(12), + IsBigEndian: (bool)(false), + IsSigned: (bool)(false), + IsMultiplexer: (bool)(false), + IsMultiplexed: (bool)(true), + MultiplexerValue: (uint)(0), + Offset: (float64)(0), + Scale: (float64)(0.1), + Min: (float64)(0), + Max: (float64)(0), + Unit: (string)(""), + Description: (string)(""), + ValueDescriptions: ([]*descriptor.ValueDescription)(nil), + ReceiverNodes: ([]string)([]string{ + (string)("DRIVER"), + (string)("IO"), + }), + DefaultValue: (int)(0), + }), + (*descriptor.Signal)(&descriptor.Signal{ + Name: (string)("NoFiltRight"), + Start: (uint8)(40), + Length: (uint8)(12), + IsBigEndian: (bool)(false), + IsSigned: (bool)(false), + IsMultiplexer: (bool)(false), + IsMultiplexed: (bool)(true), + MultiplexerValue: (uint)(1), + Offset: (float64)(0), + Scale: (float64)(0.1), + Min: (float64)(0), + Max: (float64)(0), + Unit: (string)(""), + Description: (string)(""), + ValueDescriptions: ([]*descriptor.ValueDescription)(nil), + ReceiverNodes: ([]string)([]string{ + (string)("DBG"), + }), + DefaultValue: (int)(0), + }), + (*descriptor.Signal)(&descriptor.Signal{ + Name: (string)("Rear"), + Start: (uint8)(52), + Length: (uint8)(12), + IsBigEndian: (bool)(false), + IsSigned: (bool)(false), + IsMultiplexer: (bool)(false), + IsMultiplexed: (bool)(true), + MultiplexerValue: (uint)(0), + Offset: (float64)(0), + Scale: (float64)(0.1), + Min: (float64)(0), + Max: (float64)(0), + Unit: (string)(""), + Description: (string)(""), + ValueDescriptions: ([]*descriptor.ValueDescription)(nil), + ReceiverNodes: ([]string)([]string{ + (string)("DRIVER"), + (string)("IO"), + }), + DefaultValue: (int)(0), + }), + (*descriptor.Signal)(&descriptor.Signal{ + Name: (string)("NoFiltRear"), + Start: (uint8)(52), + Length: (uint8)(12), + IsBigEndian: (bool)(false), + IsSigned: (bool)(false), + IsMultiplexer: (bool)(false), + IsMultiplexed: (bool)(true), + MultiplexerValue: (uint)(1), + Offset: (float64)(0), + Scale: (float64)(0.1), + Min: (float64)(0), + Max: (float64)(0), + Unit: (string)(""), + Description: (string)(""), + ValueDescriptions: ([]*descriptor.ValueDescription)(nil), + ReceiverNodes: ([]string)([]string{ + (string)("DBG"), + }), + DefaultValue: (int)(0), + }), + }), + SenderNode: (string)("SENSOR"), + CycleTime: (time.Duration)(100000000), + DelayTime: (time.Duration)(0), + }), + (*descriptor.Message)(&descriptor.Message{ + Name: (string)("MotorStatus"), + ID: (uint32)(400), + IsExtended: (bool)(false), + Length: (uint8)(3), + SendType: (descriptor.SendType)(1), + Description: (string)(""), + Signals: ([]*descriptor.Signal)([]*descriptor.Signal{ + (*descriptor.Signal)(&descriptor.Signal{ + Name: (string)("WheelError"), + Start: (uint8)(0), + Length: (uint8)(1), + IsBigEndian: (bool)(false), + IsSigned: (bool)(false), + IsMultiplexer: (bool)(false), + IsMultiplexed: (bool)(false), + MultiplexerValue: (uint)(0), + Offset: (float64)(0), + Scale: (float64)(1), + Min: (float64)(0), + Max: (float64)(0), + Unit: (string)(""), + Description: (string)(""), + ValueDescriptions: ([]*descriptor.ValueDescription)(nil), + ReceiverNodes: ([]string)([]string{ + (string)("DRIVER"), + (string)("IO"), + }), + DefaultValue: (int)(0), + }), + (*descriptor.Signal)(&descriptor.Signal{ + Name: (string)("SpeedKph"), + Start: (uint8)(8), + Length: (uint8)(16), + IsBigEndian: (bool)(false), + IsSigned: (bool)(false), + IsMultiplexer: (bool)(false), + IsMultiplexed: (bool)(false), + MultiplexerValue: (uint)(0), + Offset: (float64)(0), + Scale: (float64)(0.001), + Min: (float64)(0), + Max: (float64)(0), + Unit: (string)("km/h"), + Description: (string)(""), + ValueDescriptions: ([]*descriptor.ValueDescription)(nil), + ReceiverNodes: ([]string)([]string{ + (string)("DRIVER"), + (string)("IO"), + }), + DefaultValue: (int)(0), + }), + }), + SenderNode: (string)("MOTOR"), + CycleTime: (time.Duration)(100000000), + DelayTime: (time.Duration)(0), + }), + (*descriptor.Message)(&descriptor.Message{ + Name: (string)("IODebug"), + ID: (uint32)(500), + IsExtended: (bool)(false), + Length: (uint8)(6), + SendType: (descriptor.SendType)(2), + Description: (string)(""), + Signals: ([]*descriptor.Signal)([]*descriptor.Signal{ + (*descriptor.Signal)(&descriptor.Signal{ + Name: (string)("TestUnsigned"), + Start: (uint8)(0), + Length: (uint8)(8), + IsBigEndian: (bool)(false), + IsSigned: (bool)(false), + IsMultiplexer: (bool)(false), + IsMultiplexed: (bool)(false), + MultiplexerValue: (uint)(0), + Offset: (float64)(0), + Scale: (float64)(1), + Min: (float64)(0), + Max: (float64)(0), + Unit: (string)(""), + Description: (string)(""), + ValueDescriptions: ([]*descriptor.ValueDescription)(nil), + ReceiverNodes: ([]string)([]string{ + (string)("DBG"), + }), + DefaultValue: (int)(0), + }), + (*descriptor.Signal)(&descriptor.Signal{ + Name: (string)("TestEnum"), + Start: (uint8)(8), + Length: (uint8)(6), + IsBigEndian: (bool)(false), + IsSigned: (bool)(false), + IsMultiplexer: (bool)(false), + IsMultiplexed: (bool)(false), + MultiplexerValue: (uint)(0), + Offset: (float64)(0), + Scale: (float64)(1), + Min: (float64)(0), + Max: (float64)(0), + Unit: (string)(""), + Description: (string)(""), + ValueDescriptions: ([]*descriptor.ValueDescription)([]*descriptor.ValueDescription{ + (*descriptor.ValueDescription)(&descriptor.ValueDescription{ + Value: (int)(1), + Description: (string)("One"), + }), + (*descriptor.ValueDescription)(&descriptor.ValueDescription{ + Value: (int)(2), + Description: (string)("Two"), + }), + }), + ReceiverNodes: ([]string)([]string{ + (string)("DBG"), + }), + DefaultValue: (int)(2), + }), + (*descriptor.Signal)(&descriptor.Signal{ + Name: (string)("TestSigned"), + Start: (uint8)(16), + Length: (uint8)(8), + IsBigEndian: (bool)(false), + IsSigned: (bool)(true), + IsMultiplexer: (bool)(false), + IsMultiplexed: (bool)(false), + MultiplexerValue: (uint)(0), + Offset: (float64)(0), + Scale: (float64)(1), + Min: (float64)(0), + Max: (float64)(0), + Unit: (string)(""), + Description: (string)(""), + ValueDescriptions: ([]*descriptor.ValueDescription)(nil), + ReceiverNodes: ([]string)([]string{ + (string)("DBG"), + }), + DefaultValue: (int)(0), + }), + (*descriptor.Signal)(&descriptor.Signal{ + Name: (string)("TestFloat"), + Start: (uint8)(24), + Length: (uint8)(8), + IsBigEndian: (bool)(false), + IsSigned: (bool)(false), + IsMultiplexer: (bool)(false), + IsMultiplexed: (bool)(false), + MultiplexerValue: (uint)(0), + Offset: (float64)(0), + Scale: (float64)(0.5), + Min: (float64)(0), + Max: (float64)(0), + Unit: (string)(""), + Description: (string)(""), + ValueDescriptions: ([]*descriptor.ValueDescription)(nil), + ReceiverNodes: ([]string)([]string{ + (string)("DBG"), + }), + DefaultValue: (int)(0), + }), + (*descriptor.Signal)(&descriptor.Signal{ + Name: (string)("TestBoolEnum"), + Start: (uint8)(32), + Length: (uint8)(1), + IsBigEndian: (bool)(false), + IsSigned: (bool)(false), + IsMultiplexer: (bool)(false), + IsMultiplexed: (bool)(false), + MultiplexerValue: (uint)(0), + Offset: (float64)(0), + Scale: (float64)(1), + Min: (float64)(0), + Max: (float64)(0), + Unit: (string)(""), + Description: (string)(""), + ValueDescriptions: ([]*descriptor.ValueDescription)([]*descriptor.ValueDescription{ + (*descriptor.ValueDescription)(&descriptor.ValueDescription{ + Value: (int)(0), + Description: (string)("Zero"), + }), + (*descriptor.ValueDescription)(&descriptor.ValueDescription{ + Value: (int)(1), + Description: (string)("One"), + }), + }), + ReceiverNodes: ([]string)([]string{ + (string)("DBG"), + }), + DefaultValue: (int)(0), + }), + (*descriptor.Signal)(&descriptor.Signal{ + Name: (string)("TestScaledEnum"), + Start: (uint8)(40), + Length: (uint8)(2), + IsBigEndian: (bool)(false), + IsSigned: (bool)(false), + IsMultiplexer: (bool)(false), + IsMultiplexed: (bool)(false), + MultiplexerValue: (uint)(0), + Offset: (float64)(0), + Scale: (float64)(2), + Min: (float64)(0), + Max: (float64)(6), + Unit: (string)(""), + Description: (string)(""), + ValueDescriptions: ([]*descriptor.ValueDescription)([]*descriptor.ValueDescription{ + (*descriptor.ValueDescription)(&descriptor.ValueDescription{ + Value: (int)(0), + Description: (string)("Zero"), + }), + (*descriptor.ValueDescription)(&descriptor.ValueDescription{ + Value: (int)(1), + Description: (string)("Two"), + }), + (*descriptor.ValueDescription)(&descriptor.ValueDescription{ + Value: (int)(2), + Description: (string)("Four"), + }), + (*descriptor.ValueDescription)(&descriptor.ValueDescription{ + Value: (int)(3), + Description: (string)("Six"), + }), + }), + ReceiverNodes: ([]string)([]string{ + (string)("DBG"), + }), + DefaultValue: (int)(0), + }), + }), + SenderNode: (string)("IO"), + CycleTime: (time.Duration)(0), + DelayTime: (time.Duration)(0), + }), + }), + Nodes: ([]*descriptor.Node)([]*descriptor.Node{ + (*descriptor.Node)(&descriptor.Node{ + Name: (string)("DBG"), + Description: (string)(""), + }), + (*descriptor.Node)(&descriptor.Node{ + Name: (string)("DRIVER"), + Description: (string)("The driver controller driving the car"), + }), + (*descriptor.Node)(&descriptor.Node{ + Name: (string)("IO"), + Description: (string)(""), + }), + (*descriptor.Node)(&descriptor.Node{ + Name: (string)("MOTOR"), + Description: (string)("The motor controller of the car"), + }), + (*descriptor.Node)(&descriptor.Node{ + Name: (string)("SENSOR"), + Description: (string)("The sensor controller of the car"), + }), + }), +}) diff --git a/tools.go b/tools.go new file mode 100644 index 0000000..3c324cb --- /dev/null +++ b/tools.go @@ -0,0 +1,8 @@ +// +build tools + +package tools + +import ( + _ "github.com/golang/mock/mockgen" + _ "golang.org/x/tools/cmd/stringer" +)