├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ ├── release.yml │ └── review.yml ├── .gitignore ├── .goreleaser.yml ├── .sage ├── go.mod ├── go.sum └── main.go ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── SECURITY.md ├── can.go ├── cmd └── cantool │ └── main.go ├── data.go ├── data_test.go ├── frame.go ├── frame_json.go ├── frame_json_test.go ├── frame_string_test.go ├── frame_test.go ├── go.mod ├── go.sum ├── internal ├── clock │ ├── clock.go │ └── system.go ├── generate │ ├── compile.go │ ├── compile_test.go │ ├── example_test.go │ ├── file.go │ └── file_test.go ├── identifiers │ ├── case.go │ ├── case_test.go │ ├── char.go │ └── char_test.go ├── mocks │ ├── gen.go │ └── gen │ │ ├── mockcanrunner │ │ └── mocks.go │ │ ├── mockclock │ │ └── mocks.go │ │ └── mocksocketcan │ │ └── mocks.go └── reinterpret │ ├── reinterpret.go │ └── reinterpret_test.go ├── message.go ├── pkg ├── candebug │ ├── http.go │ └── http_test.go ├── candevice │ ├── device_integration_test.go │ ├── device_linux.go │ └── device_others.go ├── canjson │ ├── encode.go │ └── encode_test.go ├── canrunner │ ├── run.go │ └── run_test.go ├── cantext │ ├── encode.go │ └── encode_test.go ├── dbc │ ├── accesstype.go │ ├── accesstype_test.go │ ├── analysis │ │ ├── analysis.go │ │ ├── analysistest │ │ │ └── analysistest.go │ │ └── passes │ │ │ ├── boolprefix │ │ │ ├── analyzer.go │ │ │ └── analyzer_test.go │ │ │ ├── definitiontypeorder │ │ │ ├── analyzer.go │ │ │ └── analyzer_test.go │ │ │ ├── intervals │ │ │ ├── analyzer.go │ │ │ └── analyzer_test.go │ │ │ ├── lineendings │ │ │ ├── analyzer.go │ │ │ └── analyzer_test.go │ │ │ ├── messagenames │ │ │ ├── analyzer.go │ │ │ └── analyzer_test.go │ │ │ ├── multiplexedsignals │ │ │ ├── analyzer.go │ │ │ └── analyzer_test.go │ │ │ ├── newsymbols │ │ │ ├── analyzer.go │ │ │ └── analyzer_test.go │ │ │ ├── nodereferences │ │ │ ├── analyzer.go │ │ │ └── analyzer_test.go │ │ │ ├── noreservedsignals │ │ │ ├── analyzer.go │ │ │ └── analyzer_test.go │ │ │ ├── requireddefinitions │ │ │ ├── analyzer.go │ │ │ └── analyzer_test.go │ │ │ ├── signalbounds │ │ │ ├── analyzer.go │ │ │ └── analyzer_test.go │ │ │ ├── signalnames │ │ │ ├── analyzer.go │ │ │ └── analyzer_test.go │ │ │ ├── singletondefinitions │ │ │ ├── analyzer.go │ │ │ └── analyzer_test.go │ │ │ ├── siunits │ │ │ ├── analyzer.go │ │ │ └── analyzer_test.go │ │ │ ├── uniquemessageids │ │ │ ├── analyzer.go │ │ │ └── analyzer_test.go │ │ │ ├── uniquenodenames │ │ │ ├── analyzer.go │ │ │ └── analyzer_test.go │ │ │ ├── uniquesignalnames │ │ │ ├── analyzer.go │ │ │ └── analyzer_test.go │ │ │ ├── unitsuffixes │ │ │ ├── analyzer.go │ │ │ └── analyzer_test.go │ │ │ ├── valuedescriptions │ │ │ ├── analyzer.go │ │ │ └── analyzer_test.go │ │ │ └── version │ │ │ ├── analyzer.go │ │ │ └── analyzer_test.go │ ├── attributevaluetype.go │ ├── attributevaluetype_test.go │ ├── def.go │ ├── doc.go │ ├── envvartype.go │ ├── envvartype_test.go │ ├── error.go │ ├── file.go │ ├── identifier.go │ ├── identifier_test.go │ ├── independent_signals.go │ ├── keyword.go │ ├── messageid.go │ ├── messageid_test.go │ ├── objecttype.go │ ├── objecttype_test.go │ ├── parser.go │ ├── parser_test.go │ ├── placeholder.go │ ├── signalvaluetype.go │ └── signalvaluetype_test.go ├── descriptor │ ├── database.go │ ├── message.go │ ├── message_test.go │ ├── node.go │ ├── sendtype.go │ ├── sendtype_string.go │ ├── sendtype_test.go │ ├── signal.go │ ├── signal_test.go │ └── valuedescription.go ├── generated │ └── message.go └── socketcan │ ├── canrawaddr.go │ ├── canrawaddr_test.go │ ├── controllererror.go │ ├── controllererror_string.go │ ├── dial.go │ ├── dial_test.go │ ├── dialraw_linux.go │ ├── dialraw_linux_test.go │ ├── dialraw_others.go │ ├── emulator.go │ ├── emulator_test.go │ ├── errorclass.go │ ├── errorclass_string.go │ ├── errorframe.go │ ├── errorframe_test.go │ ├── fileconn.go │ ├── fileconn_test.go │ ├── frame.go │ ├── frame_test.go │ ├── main_test.go │ ├── protocolviolationerror.go │ ├── protocolviolationerror_string.go │ ├── protocolviolationerrorlocation.go │ ├── protocolviolationerrorlocation_string.go │ ├── receiver.go │ ├── receiver_test.go │ ├── transceivererror.go │ ├── transceivererror_string.go │ ├── transmitter.go │ ├── transmitter_test.go │ └── udp.go ├── testdata ├── dbc-invalid │ └── example │ │ ├── example_float32_invalid_signal_length.dbc │ │ ├── example_float32_invalid_signal_name.dbc │ │ ├── example_float64_signal.dbc │ │ └── example_metadata_invalid_signal_reference.dbc ├── dbc │ └── example │ │ ├── example.dbc │ │ └── example.dbc.golden └── gen │ └── go │ └── example │ └── example.dbc.go └── tools.go /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @Jassob @ericwenn 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: github-actions 5 | directory: / 6 | schedule: 7 | interval: weekly 8 | day: "monday" 9 | time: "05:06" 10 | timezone: "Europe/Stockholm" 11 | 12 | - package-ecosystem: gomod 13 | directory: / 14 | schedule: 15 | interval: weekly 16 | day: "monday" 17 | time: "05:06" 18 | timezone: "Europe/Stockholm" 19 | groups: 20 | go: 21 | patterns: 22 | - "*" # Include all dependencies in one PR 23 | update-types: 24 | - "minor" 25 | - "patch" 26 | 27 | - package-ecosystem: gomod 28 | directory: .sage 29 | schedule: 30 | interval: weekly 31 | day: "monday" 32 | time: "05:06" 33 | timezone: "Europe/Stockholm" 34 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: write 13 | 14 | steps: 15 | - name: Setup Sage 16 | uses: einride/sage/actions/setup@master 17 | with: 18 | go-version: "~1.22" 19 | 20 | - name: Make 21 | run: make 22 | 23 | - name: Create Release 24 | id: release 25 | uses: go-semantic-release/action@v1.23 26 | with: 27 | github-token: ${{ secrets.GITHUB_TOKEN }} 28 | allow-initial-development-versions: true 29 | 30 | # Update tags for goreleaser to choose latest version 31 | - name: Fetch tags 32 | if: steps.release.outputs.version != '' 33 | run: git fetch --force --tags 34 | 35 | - name: Run goreleaser 36 | if: steps.release.outputs.version != '' 37 | uses: goreleaser/goreleaser-action@v5.1.0 38 | with: 39 | version: latest 40 | args: release --rm-dist 41 | env: 42 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 43 | -------------------------------------------------------------------------------- /.github/workflows/review.yml: -------------------------------------------------------------------------------- 1 | name: Review 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | make: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Setup Sage 11 | uses: einride/sage/actions/setup@master 12 | with: 13 | go-version: "~1.22" 14 | 15 | - name: Make 16 | run: make 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | tools/*/*/ 3 | build/ 4 | 5 | # files generated during release 6 | .generated-go-semantic-release-changelog.md 7 | .semrel/ 8 | /testdata/gen/go/ 9 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | before: 2 | hooks: 3 | - go mod download 4 | 5 | builds: 6 | - id: cantool 7 | binary: cantool 8 | dir: ./cmd/cantool 9 | main: main.go 10 | env: 11 | - CGO_ENABLED=0 12 | goos: 13 | - linux 14 | - windows 15 | - darwin 16 | 17 | checksum: 18 | name_template: "checksums.txt" 19 | 20 | snapshot: 21 | name_template: "{{ .Tag }}-next" 22 | 23 | release: 24 | github: 25 | prerelease: auto 26 | -------------------------------------------------------------------------------- /.sage/go.mod: -------------------------------------------------------------------------------- 1 | module go.einride.tech/can/.sage 2 | 3 | go 1.22.12 4 | 5 | toolchain go1.24.2 6 | 7 | require go.einride.tech/sage v0.362.0 8 | -------------------------------------------------------------------------------- /.sage/go.sum: -------------------------------------------------------------------------------- 1 | go.einride.tech/sage v0.362.0 h1:TjoNeO9vr0w+4KP0+ESYLV46TCemggl3SWnh2f7VW24= 2 | go.einride.tech/sage v0.362.0/go.mod h1:sy9YuK//XVwEZ2wD3f19xVSKEtN8CYtgtBZGpzC3p80= 3 | -------------------------------------------------------------------------------- /.sage/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "path/filepath" 7 | 8 | "go.einride.tech/sage/sg" 9 | "go.einride.tech/sage/sgtool" 10 | "go.einride.tech/sage/tools/sgconvco" 11 | "go.einride.tech/sage/tools/sggit" 12 | "go.einride.tech/sage/tools/sggo" 13 | "go.einride.tech/sage/tools/sggolangcilint" 14 | "go.einride.tech/sage/tools/sggolicenses" 15 | "go.einride.tech/sage/tools/sgmdformat" 16 | "go.einride.tech/sage/tools/sgyamlfmt" 17 | ) 18 | 19 | func main() { 20 | sg.GenerateMakefiles( 21 | sg.Makefile{ 22 | Path: sg.FromGitRoot("Makefile"), 23 | DefaultTarget: Default, 24 | }, 25 | ) 26 | } 27 | 28 | func Default(ctx context.Context) error { 29 | sg.Deps(ctx, ConvcoCheck, FormatMarkdown, FormatYaml, GoGenerate, GenerateTestdata) 30 | sg.Deps(ctx, GoLint) 31 | sg.Deps(ctx, GoTest) 32 | sg.Deps(ctx, GoModTidy) 33 | sg.Deps(ctx, GoLicenses, GitVerifyNoDiff) 34 | return nil 35 | } 36 | 37 | func GoModTidy(ctx context.Context) error { 38 | sg.Logger(ctx).Println("tidying Go module files...") 39 | return sg.Command(ctx, "go", "mod", "tidy", "-v").Run() 40 | } 41 | 42 | func GoTest(ctx context.Context) error { 43 | sg.Logger(ctx).Println("running Go tests...") 44 | return sggo.TestCommand(ctx).Run() 45 | } 46 | 47 | func GoLint(ctx context.Context) error { 48 | sg.Logger(ctx).Println("linting Go files...") 49 | return sggolangcilint.Run(ctx) 50 | } 51 | 52 | func GoLicenses(ctx context.Context) error { 53 | sg.Logger(ctx).Println("checking Go licenses...") 54 | return sggolicenses.Check(ctx) 55 | } 56 | 57 | func FormatMarkdown(ctx context.Context) error { 58 | sg.Logger(ctx).Println("formatting Markdown files...") 59 | return sgmdformat.Command(ctx).Run() 60 | } 61 | 62 | func FormatYaml(ctx context.Context) error { 63 | sg.Logger(ctx).Println("formatting Yaml files...") 64 | return sgyamlfmt.Run(ctx) 65 | } 66 | 67 | func ConvcoCheck(ctx context.Context) error { 68 | sg.Logger(ctx).Println("checking git commits...") 69 | return sgconvco.Command(ctx, "check", "origin/master..HEAD").Run() 70 | } 71 | 72 | func GitVerifyNoDiff(ctx context.Context) error { 73 | sg.Logger(ctx).Println("verifying that git has no diff...") 74 | return sggit.VerifyNoDiff(ctx) 75 | } 76 | 77 | func GoGenerate(ctx context.Context) error { 78 | sg.Deps(ctx, Mockgen, Stringer) 79 | sg.Logger(ctx).Println("generating Go code...") 80 | return sg.Command(ctx, "go", "generate", "./...").Run() 81 | } 82 | 83 | func Mockgen(ctx context.Context) error { 84 | sg.Logger(ctx).Println("installing mockgen...") 85 | _, err := sgtool.GoInstallWithModfile(ctx, "github.com/golang/mock/mockgen", sg.FromGitRoot("go.mod")) 86 | return err 87 | } 88 | 89 | func Stringer(ctx context.Context) error { 90 | sg.Logger(ctx).Println("installing stringer...") 91 | _, err := sgtool.GoInstallWithModfile(ctx, "golang.org/x/tools/cmd/stringer", sg.FromGitRoot("go.mod")) 92 | return err 93 | } 94 | 95 | func GenerateTestdata(ctx context.Context) error { 96 | sg.Logger(ctx).Println("generating testdata...") 97 | // don't use "sg.FromGitRoot" in paths to avoid embedding user paths in generated files 98 | cmd := sg.Command( 99 | ctx, 100 | "go", 101 | "run", 102 | "cmd/cantool/main.go", 103 | "generate", 104 | "testdata/dbc", 105 | "testdata/gen/go", 106 | ) 107 | cmd.Dir = sg.FromGitRoot() 108 | return cmd.Run() 109 | } 110 | 111 | func BuildIntegrationTests(ctx context.Context) error { 112 | sg.Logger(ctx).Println("building integration test...") 113 | testDir := sg.FromGitRoot("build", "tests") 114 | if err := os.MkdirAll(testDir, 0o775); err != nil { 115 | return err 116 | } 117 | return sg.Command( 118 | ctx, 119 | "go", 120 | "test", 121 | "-tags=integration", 122 | "-c", 123 | sg.FromGitRoot("pkg", "candevice"), 124 | "-o", 125 | filepath.Join(testDir, "candevice.test"), 126 | ).Run() 127 | } 128 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thanks for showing interest in contributing to [CAN-go]! 4 | 5 | ## Suggesting Features 6 | 7 | Before creating a PR, please consider that [CAN-go] is a tool that Einride uses 8 | internally and as such, features that are not aligned with how we are using CAN 9 | will probably not be accepted. In that case feel free to fork our project and 10 | make the changes you want there instead. 11 | 12 | ## Reporting Issues 13 | 14 | If you find a bug, please create an 15 | [issue](https://github.com/einride/can-go/issues) for it, or create a PR 16 | following the [Pull Request Guidelines](#pull-request-guidelines). 17 | 18 | ## Development 19 | 20 | To start developing on [CAN-go] it is enough to clone the repo and run `make`. 21 | 22 | ## Pull Request Guidelines 23 | 24 | [CAN-go] is using the 25 | [Conventional Commits](https://www.conventionalcommits.org/) commit message 26 | convention. 27 | 28 | Keep your commits as small as possible, but still keep all changes related to a 29 | logical change in the same commit. When receiving review feedback, fix up the 30 | commits with the changes addressing the feedback and force-push, please don't 31 | send fix commits. 32 | 33 | Before opening a PR, please make sure you get no errors when running `make` and 34 | that there is sufficient test coverage for added or changed functionality. 35 | 36 | [can-go]: https://go.einride.tech/can 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Einride AB 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Code generated by go.einride.tech/sage. DO NOT EDIT. 2 | # To learn more, see .sage/main.go and https://github.com/einride/sage. 3 | 4 | .DEFAULT_GOAL := default 5 | 6 | cwd := $(dir $(realpath $(firstword $(MAKEFILE_LIST)))) 7 | sagefile := $(abspath $(cwd)/.sage/bin/sagefile) 8 | 9 | # Setup Go. 10 | go := $(shell command -v go 2>/dev/null) 11 | export GOWORK ?= off 12 | ifndef go 13 | SAGE_GO_VERSION ?= 1.23.4 14 | export GOROOT := $(abspath $(cwd)/.sage/tools/go/$(SAGE_GO_VERSION)/go) 15 | export PATH := $(PATH):$(GOROOT)/bin 16 | go := $(GOROOT)/bin/go 17 | os := $(shell uname | tr '[:upper:]' '[:lower:]') 18 | arch := $(shell uname -m) 19 | ifeq ($(arch),x86_64) 20 | arch := amd64 21 | endif 22 | $(go): 23 | $(info installing Go $(SAGE_GO_VERSION)...) 24 | @mkdir -p $(dir $(GOROOT)) 25 | @curl -sSL https://go.dev/dl/go$(SAGE_GO_VERSION).$(os)-$(arch).tar.gz | tar xz -C $(dir $(GOROOT)) 26 | @touch $(GOROOT)/go.mod 27 | @chmod +x $(go) 28 | endif 29 | 30 | .PHONY: $(sagefile) 31 | $(sagefile): $(go) 32 | @cd .sage && $(go) mod tidy && $(go) run . 33 | 34 | .PHONY: sage 35 | sage: 36 | @$(MAKE) $(sagefile) 37 | 38 | .PHONY: update-sage 39 | update-sage: $(go) 40 | @cd .sage && $(go) get go.einride.tech/sage@latest && $(go) mod tidy && $(go) run . 41 | 42 | .PHONY: clean-sage 43 | clean-sage: 44 | @git clean -fdx .sage/tools .sage/bin .sage/build 45 | 46 | .PHONY: build-integration-tests 47 | build-integration-tests: $(sagefile) 48 | @$(sagefile) BuildIntegrationTests 49 | 50 | .PHONY: convco-check 51 | convco-check: $(sagefile) 52 | @$(sagefile) ConvcoCheck 53 | 54 | .PHONY: default 55 | default: $(sagefile) 56 | @$(sagefile) Default 57 | 58 | .PHONY: format-markdown 59 | format-markdown: $(sagefile) 60 | @$(sagefile) FormatMarkdown 61 | 62 | .PHONY: format-yaml 63 | format-yaml: $(sagefile) 64 | @$(sagefile) FormatYaml 65 | 66 | .PHONY: generate-testdata 67 | generate-testdata: $(sagefile) 68 | @$(sagefile) GenerateTestdata 69 | 70 | .PHONY: git-verify-no-diff 71 | git-verify-no-diff: $(sagefile) 72 | @$(sagefile) GitVerifyNoDiff 73 | 74 | .PHONY: go-generate 75 | go-generate: $(sagefile) 76 | @$(sagefile) GoGenerate 77 | 78 | .PHONY: go-licenses 79 | go-licenses: $(sagefile) 80 | @$(sagefile) GoLicenses 81 | 82 | .PHONY: go-lint 83 | go-lint: $(sagefile) 84 | @$(sagefile) GoLint 85 | 86 | .PHONY: go-mod-tidy 87 | go-mod-tidy: $(sagefile) 88 | @$(sagefile) GoModTidy 89 | 90 | .PHONY: go-test 91 | go-test: $(sagefile) 92 | @$(sagefile) GoTest 93 | 94 | .PHONY: mockgen 95 | mockgen: $(sagefile) 96 | @$(sagefile) Mockgen 97 | 98 | .PHONY: stringer 99 | stringer: $(sagefile) 100 | @$(sagefile) Stringer 101 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # :electric_plug: go.einride.tech/can 2 | 3 | [![PkgGoDev](https://pkg.go.dev/badge/go.einride.tech/can)](https://pkg.go.dev/go.einride.tech/can) 4 | [![GoReportCard](https://goreportcard.com/badge/go.einride.tech/can)](https://goreportcard.com/report/go.einride.tech/can) 5 | [![Codecov](https://codecov.io/gh/einride/can-go/branch/master/graph/badge.svg)](https://codecov.io/gh/einride/can-go) 6 | 7 | CAN toolkit for Go programmers. 8 | 9 | can-go makes use of the Linux SocketCAN abstraction for CAN communication. (See 10 | the [SocketCAN](https://www.kernel.org/doc/Documentation/networking/can.txt) 11 | documentation for more details). 12 | 13 | ## Installation 14 | 15 | ``` 16 | go get -u go.einride.tech/can 17 | ``` 18 | 19 | ## Examples 20 | 21 | ### Setting up a CAN interface 22 | 23 | ```go 24 | 25 | import "go.einride.tech/can/pkg/candevice" 26 | 27 | func main() { 28 | // Error handling omitted to keep example simple 29 | d, _ := candevice.New("can0") 30 | _ := d.SetBitrate(250000) 31 | _ := d.SetUp() 32 | defer d.SetDown() 33 | } 34 | ``` 35 | 36 | ### Receiving CAN frames 37 | 38 | Receiving CAN frames from a socketcan interface. 39 | 40 | ```go 41 | import "go.einride.tech/can/pkg/socketcan" 42 | 43 | func main() { 44 | // Error handling omitted to keep example simple 45 | conn, _ := socketcan.DialContext(context.Background(), "can", "can0") 46 | 47 | recv := socketcan.NewReceiver(conn) 48 | for recv.Receive() { 49 | frame := recv.Frame() 50 | fmt.Println(frame.String()) 51 | } 52 | } 53 | ``` 54 | 55 | ### Sending CAN frames/messages 56 | 57 | Sending CAN frames to a socketcan interface. 58 | 59 | ```go 60 | import "go.einride.tech/can/pkg/socketcan" 61 | 62 | func main() { 63 | // Error handling omitted to keep example simple 64 | 65 | conn, _ := socketcan.DialContext(context.Background(), "can", "can0") 66 | 67 | frame := can.Frame{} 68 | tx := socketcan.NewTransmitter(conn) 69 | _ = tx.TransmitFrame(context.Background(), frame) 70 | } 71 | ``` 72 | 73 | ### Generating Go code from a DBC file 74 | 75 | It is possible to generate Go code from a `.dbc` file. 76 | 77 | ``` 78 | $ go run go.einride.tech/can/cmd/cantool generate 79 | ``` 80 | 81 | In order to generate Go code that makes sense, we currently perform some 82 | validations when parsing the DBC file so there may need to be some changes on 83 | the DBC file to make it work 84 | 85 | After generating Go code we can marshal a message to a frame: 86 | 87 | ```go 88 | // import etruckcan "github.com/myproject/myrepo/gen" 89 | 90 | auxMsg := etruckcan.NewAuxiliary().SetHeadLights(etruckcan.Auxiliary_HeadLights_LowBeam) 91 | frame := auxMsg.Frame() 92 | ``` 93 | 94 | Or unmarshal a frame to a message: 95 | 96 | ```go 97 | // import etruckcan "github.com/myproject/myrepo/gen" 98 | 99 | // Error handling omitted for simplicity 100 | _ := recv.Receive() 101 | frame := recv.Frame() 102 | 103 | var auxMsg *etruckcan.Auxiliary 104 | _ = auxMsg.UnmarshalFrame(frame) 105 | 106 | ``` 107 | 108 | ## Running integration tests 109 | 110 | Building the tests: 111 | 112 | ```shell 113 | $ make build-integration-tests 114 | ``` 115 | 116 | Built tests are placed in build/tests. 117 | 118 | The candevice test requires access to physical HW, so run it using sudo. 119 | Example: 120 | 121 | ```shell 122 | $ sudo ./build/tests/candevice.test 123 | > PASS 124 | ``` 125 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | Einride welcomes feedback from security researchers and the general public to 4 | help improve our security. If you believe you have discovered a vulnerability, 5 | privacy issue, exposed data, or other security issues in relation to this 6 | project, we want to hear from you. This policy outlines steps for reporting 7 | security issues to us, what we expect, and what you can expect from us. 8 | 9 | ## Supported versions 10 | 11 | We release patches for security issues according to semantic versioning. This 12 | project is currently unstable (v0.x) and only the latest version will receive 13 | security patches. 14 | 15 | ## Reporting a vulnerability 16 | 17 | Please do not report security vulnerabilities through public issues, 18 | discussions, or change requests. 19 | 20 | Please report security issues via [oss-security@einride.tech][email]. Provide 21 | all relevant information, including steps to reproduce the issue, any affected 22 | versions, and known mitigations. The more details you provide, the easier it 23 | will be for us to triage and fix the issue. You will receive a response from us 24 | within 2 business days. If the issue is confirmed, a patch will be released as 25 | soon as possible. 26 | 27 | For more information, or security issues not relating to open source code, 28 | please consult our [Vulnerability Disclosure Policy][vdp]. 29 | 30 | ## Preferred languages 31 | 32 | English is our preferred language of communication. 33 | 34 | ## Contributions and recognition 35 | 36 | We appreciate every contribution and will do our best to publicly 37 | [acknowledge][acknowledgments] your contributions. 38 | 39 | [acknowledgments]: https://einride.tech/security-acknowledgments.txt 40 | [email]: mailto:oss-security@einride.tech 41 | [vdp]: https://www.einride.tech/vulnerability-disclosure-policy 42 | -------------------------------------------------------------------------------- /can.go: -------------------------------------------------------------------------------- 1 | // Package can provides primitives for working with CAN. 2 | // 3 | // See: https://en.wikipedia.org/wiki/CAN_bus 4 | package can // import "go.einride.tech/can" 5 | -------------------------------------------------------------------------------- /frame.go: -------------------------------------------------------------------------------- 1 | package can 2 | 3 | import ( 4 | "encoding/hex" 5 | "fmt" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | const ( 11 | idBits = 11 12 | extendedIDBits = 29 13 | ) 14 | 15 | // CAN format constants. 16 | const ( 17 | MaxID = 0x7ff 18 | MaxExtendedID = 0x1fffffff 19 | ) 20 | 21 | // Frame represents a CAN frame. 22 | // 23 | // A Frame is intentionally designed to fit into 16 bytes on common architectures 24 | // and is therefore amenable to pass-by-value and judicious copying. 25 | type Frame struct { 26 | // ID is the CAN ID 27 | ID uint32 28 | // Length is the number of bytes of data in the frame. 29 | Length uint8 30 | // Data is the frame data. 31 | Data Data 32 | // IsRemote is true for remote frames. 33 | IsRemote bool 34 | // IsExtended is true for extended frames, i.e. frames with 29-bit IDs. 35 | IsExtended bool 36 | } 37 | 38 | // Validate returns an error if the Frame is not a valid CAN frame. 39 | func (f *Frame) Validate() error { 40 | // Validate: ID 41 | if f.IsExtended && f.ID > MaxExtendedID { 42 | return fmt.Errorf( 43 | "invalid extended CAN id: %v does not fit in %v bits", 44 | f.ID, 45 | extendedIDBits, 46 | ) 47 | } else if !f.IsExtended && f.ID > MaxID { 48 | return fmt.Errorf( 49 | "invalid standard CAN id: %v does not fit in %v bits", 50 | f.ID, 51 | idBits, 52 | ) 53 | } 54 | // Validate: Data 55 | if f.Length > MaxDataLength { 56 | return fmt.Errorf("invalid data length: %v", f.Length) 57 | } 58 | return nil 59 | } 60 | 61 | // String returns an ASCII representation the CAN frame. 62 | // 63 | // Format: 64 | // 65 | // ([0-9A-F]{3}|[0-9A-F]{3})#(R[0-8]?|[0-9A-F]{0,16}) 66 | // 67 | // The format is compatible with the candump(1) log file format. 68 | func (f Frame) String() string { 69 | var id string 70 | if f.IsExtended { 71 | id = fmt.Sprintf("%08X", f.ID) 72 | } else { 73 | id = fmt.Sprintf("%03X", f.ID) 74 | } 75 | if f.IsRemote && f.Length == 0 { 76 | return id + "#R" 77 | } else if f.IsRemote { 78 | return id + "#R" + strconv.Itoa(int(f.Length)) 79 | } 80 | return id + "#" + strings.ToUpper(hex.EncodeToString(f.Data[:f.Length])) 81 | } 82 | 83 | // UnmarshalString sets *f using the provided ASCII representation of a Frame. 84 | func (f *Frame) UnmarshalString(s string) error { 85 | // Split split into parts 86 | parts := strings.Split(s, "#") 87 | if len(parts) != 2 { 88 | return fmt.Errorf("invalid frame format: %v", s) 89 | } 90 | idPart, dataPart := parts[0], parts[1] 91 | var frame Frame 92 | // Parse: IsExtended 93 | if len(idPart) != 3 && len(idPart) != 8 { 94 | return fmt.Errorf("invalid ID length: %v", s) 95 | } 96 | frame.IsExtended = len(idPart) == 8 97 | // Parse: ID 98 | id, err := strconv.ParseUint(idPart, 16, 32) 99 | if err != nil { 100 | return fmt.Errorf("invalid frame ID: %v", s) 101 | } 102 | frame.ID = uint32(id) 103 | if len(dataPart) == 0 { 104 | *f = frame 105 | return nil 106 | } 107 | // Parse: IsRemote 108 | if dataPart[0] == 'R' { 109 | frame.IsRemote = true 110 | if len(dataPart) > 2 { 111 | return fmt.Errorf("invalid remote length: %v", s) 112 | } else if len(dataPart) == 2 { 113 | dataLength, err := strconv.Atoi(dataPart[1:2]) 114 | if err != nil { 115 | return fmt.Errorf("invalid remote length: %v: %w", s, err) 116 | } 117 | frame.Length = uint8(dataLength) 118 | } 119 | *f = frame 120 | return nil 121 | } 122 | // Parse: Length 123 | if len(dataPart) > 16 || len(dataPart)%2 != 0 { 124 | return fmt.Errorf("invalid data length: %v", s) 125 | } 126 | frame.Length = uint8(len(dataPart) / 2) 127 | // Parse: Data 128 | decodedData, err := hex.DecodeString(dataPart) 129 | if err != nil { 130 | return fmt.Errorf("invalid data: %v: %w", s, err) 131 | } 132 | copy(frame.Data[:], decodedData) 133 | *f = frame 134 | return nil 135 | } 136 | -------------------------------------------------------------------------------- /frame_json.go: -------------------------------------------------------------------------------- 1 | package can 2 | 3 | import ( 4 | "encoding/hex" 5 | "encoding/json" 6 | "fmt" 7 | "strconv" 8 | ) 9 | 10 | type jsonFrame struct { 11 | ID uint32 `json:"id"` 12 | Data *string `json:"data"` 13 | Length *uint8 `json:"length"` 14 | Extended *bool `json:"extended"` 15 | Remote *bool `json:"remote"` 16 | } 17 | 18 | // JSON returns the JSON-encoding of f, using hex-encoding for the data. 19 | // 20 | // Examples: 21 | // 22 | // {"id":32,"data":"0102030405060708"} 23 | // {"id":32,"extended":true,"remote":true,"length":4} 24 | // 25 | //nolint:goconst 26 | func (f Frame) JSON() string { 27 | switch { 28 | case f.IsRemote && f.IsExtended: 29 | return `{"id":` + strconv.Itoa(int(f.ID)) + 30 | `,"extended":true,"remote":true,"length":` + 31 | strconv.Itoa(int(f.Length)) + `}` 32 | case f.IsRemote: 33 | return `{"id":` + strconv.Itoa(int(f.ID)) + 34 | `,"remote":true,"length":` + 35 | strconv.Itoa(int(f.Length)) + `}` 36 | case f.IsExtended && f.Length == 0: 37 | return `{"id":` + strconv.Itoa(int(f.ID)) + `,"extended":true}` 38 | case f.IsExtended: 39 | return `{"id":` + strconv.Itoa(int(f.ID)) + 40 | `,"data":"` + hex.EncodeToString(f.Data[:f.Length]) + `"` + 41 | `,"extended":true}` 42 | case f.Length == 0: 43 | return `{"id":` + strconv.Itoa(int(f.ID)) + `}` 44 | default: 45 | return `{"id":` + strconv.Itoa(int(f.ID)) + 46 | `,"data":"` + hex.EncodeToString(f.Data[:f.Length]) + `"}` 47 | } 48 | } 49 | 50 | // MarshalJSON returns the JSON-encoding of f, using hex-encoding for the data. 51 | // 52 | // See JSON for an example of the JSON schema. 53 | func (f Frame) MarshalJSON() ([]byte, error) { 54 | return []byte(f.JSON()), nil 55 | } 56 | 57 | // UnmarshalJSON sets *f using the provided JSON-encoded values. 58 | // 59 | // See MarshalJSON for an example of the expected JSON schema. 60 | // 61 | // The result should be checked with Validate to guard against invalid JSON data. 62 | func (f *Frame) UnmarshalJSON(jsonData []byte) error { 63 | jf := jsonFrame{} 64 | if err := json.Unmarshal(jsonData, &jf); err != nil { 65 | return err 66 | } 67 | if jf.Data != nil { 68 | data, err := hex.DecodeString(*jf.Data) 69 | if err != nil { 70 | return fmt.Errorf("failed to hex-decode CAN data: %v: %w", string(jsonData), err) 71 | } 72 | f.Data = Data{} 73 | copy(f.Data[:], data) 74 | f.Length = uint8(len(data)) 75 | } else { 76 | f.Data = Data{} 77 | f.Length = 0 78 | } 79 | f.ID = jf.ID 80 | if jf.Remote != nil { 81 | f.IsRemote = *jf.Remote 82 | } else { 83 | f.IsRemote = false 84 | } 85 | if f.IsRemote { 86 | if jf.Length == nil { 87 | return fmt.Errorf("missing length field for remote JSON frame: %v", string(jsonData)) 88 | } 89 | f.Length = *jf.Length 90 | } 91 | if jf.Extended != nil { 92 | f.IsExtended = *jf.Extended 93 | } else { 94 | f.IsExtended = false 95 | } 96 | return nil 97 | } 98 | -------------------------------------------------------------------------------- /frame_json_test.go: -------------------------------------------------------------------------------- 1 | package can 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "math/rand" 7 | "reflect" 8 | "testing" 9 | "testing/quick" 10 | 11 | "gotest.tools/v3/assert" 12 | is "gotest.tools/v3/assert/cmp" 13 | ) 14 | 15 | func TestFrame_JSON(t *testing.T) { 16 | for _, tt := range []struct { 17 | jsonFrame string 18 | frame Frame 19 | }{ 20 | { 21 | // Standard frame 22 | jsonFrame: `{"id":42,"data":"00010203"}`, 23 | frame: Frame{ 24 | ID: 42, 25 | Length: 4, 26 | Data: Data{0x00, 0x01, 0x02, 0x03}, 27 | }, 28 | }, 29 | { 30 | // Standard frame, no data 31 | jsonFrame: `{"id":42}`, 32 | frame: Frame{ID: 42}, 33 | }, 34 | { 35 | // Standard remote frame 36 | jsonFrame: `{"id":42,"remote":true,"length":4}`, 37 | frame: Frame{ 38 | ID: 42, 39 | IsRemote: true, 40 | Length: 4, 41 | }, 42 | }, 43 | { 44 | // Extended frame 45 | jsonFrame: `{"id":42,"data":"0001020304050607","extended":true}`, 46 | frame: Frame{ 47 | ID: 42, 48 | IsExtended: true, 49 | Length: 8, 50 | Data: Data{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07}, 51 | }, 52 | }, 53 | { 54 | // Extended frame, no data 55 | jsonFrame: `{"id":42,"extended":true}`, 56 | frame: Frame{ID: 42, IsExtended: true}, 57 | }, 58 | { 59 | // Extended remote frame 60 | jsonFrame: `{"id":42,"extended":true,"remote":true,"length":8}`, 61 | frame: Frame{ 62 | ID: 42, 63 | IsExtended: true, 64 | IsRemote: true, 65 | Length: 8, 66 | }, 67 | }, 68 | } { 69 | t.Run(fmt.Sprintf("JSON|frame=%v", tt.frame), func(t *testing.T) { 70 | assert.Check(t, is.Equal(tt.jsonFrame, tt.frame.JSON())) 71 | }) 72 | t.Run(fmt.Sprintf("UnmarshalJSON|frame=%v", tt.frame), func(t *testing.T) { 73 | var frame Frame 74 | if err := json.Unmarshal([]byte(tt.jsonFrame), &frame); err != nil { 75 | t.Fatal(err) 76 | } 77 | assert.Check(t, is.DeepEqual(tt.frame, frame)) 78 | }) 79 | } 80 | } 81 | 82 | func TestFrame_UnmarshalJSON_Invalid(t *testing.T) { 83 | var f Frame 84 | t.Run("invalid JSON", func(t *testing.T) { 85 | data := `foobar` 86 | assert.Check(t, f.UnmarshalJSON([]uint8(data)) != nil) 87 | }) 88 | t.Run("invalid payload", func(t *testing.T) { 89 | data := `{"id":1,"data":"foobar","extended":false,"remote":false}` 90 | assert.Check(t, f.UnmarshalJSON([]uint8(data)) != nil) 91 | }) 92 | } 93 | 94 | func (Frame) Generate(rand *rand.Rand, _ int) reflect.Value { 95 | f := Frame{ 96 | IsExtended: rand.Intn(2) == 0, 97 | IsRemote: rand.Intn(2) == 0, 98 | } 99 | if f.IsExtended { 100 | f.ID = rand.Uint32() & MaxExtendedID 101 | } else { 102 | f.ID = rand.Uint32() & MaxID 103 | } 104 | f.Length = uint8(rand.Intn(9)) 105 | if !f.IsRemote { 106 | _, _ = rand.Read(f.Data[:f.Length]) 107 | } 108 | return reflect.ValueOf(f) 109 | } 110 | 111 | func TestPropertyFrame_MarshalUnmarshalJSON(t *testing.T) { 112 | f := func(f Frame) Frame { 113 | return f 114 | } 115 | g := func(f Frame) Frame { 116 | f2 := Frame{} 117 | if err := json.Unmarshal([]uint8(f.JSON()), &f2); err != nil { 118 | t.Fatal(err) 119 | } 120 | return f2 121 | } 122 | if err := quick.CheckEqual(f, g, nil); err != nil { 123 | t.Fatal(err) 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /frame_string_test.go: -------------------------------------------------------------------------------- 1 | package can 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "gotest.tools/v3/assert" 8 | is "gotest.tools/v3/assert/cmp" 9 | ) 10 | 11 | func TestFrame_String(t *testing.T) { 12 | for _, tt := range []struct { 13 | frame Frame 14 | str string 15 | }{ 16 | { 17 | frame: Frame{ 18 | ID: 0x62e, 19 | Length: 2, 20 | Data: Data{0x10, 0x44}, 21 | }, 22 | str: "62E#1044", 23 | }, 24 | { 25 | frame: Frame{ 26 | ID: 0x410, 27 | IsRemote: true, 28 | Length: 3, 29 | }, 30 | str: "410#R3", 31 | }, 32 | { 33 | frame: Frame{ 34 | ID: 0xd2, 35 | Length: 2, 36 | Data: Data{0xf0, 0x31}, 37 | }, 38 | str: "0D2#F031", 39 | }, 40 | { 41 | frame: Frame{ID: 0xee}, 42 | str: "0EE#", 43 | }, 44 | { 45 | frame: Frame{ID: 0}, 46 | str: "000#", 47 | }, 48 | { 49 | frame: Frame{ID: 0, IsExtended: true}, 50 | str: "00000000#", 51 | }, 52 | { 53 | frame: Frame{ID: 0x1234abcd, IsExtended: true}, 54 | str: "1234ABCD#", 55 | }, 56 | } { 57 | t.Run(fmt.Sprintf("String|frame=%v,str=%v", tt.frame, tt.str), func(t *testing.T) { 58 | assert.Check(t, is.Equal(tt.str, tt.frame.String())) 59 | }) 60 | t.Run(fmt.Sprintf("UnmarshalString|frame=%v,str=%v", tt.frame, tt.str), func(t *testing.T) { 61 | var actual Frame 62 | if err := actual.UnmarshalString(tt.str); err != nil { 63 | t.Fatal(err) 64 | } 65 | assert.Check(t, is.DeepEqual(actual, tt.frame)) 66 | }) 67 | } 68 | } 69 | 70 | func TestParseFrame_Errors(t *testing.T) { 71 | for _, tt := range []string{ 72 | "foo", // invalid 73 | "foo#", // invalid ID 74 | "0D23#F031", // invalid ID length 75 | "62E#104400000000000000", // invalid data length 76 | } { 77 | t.Run(fmt.Sprintf("str=%v", tt), func(t *testing.T) { 78 | var frame Frame 79 | err := frame.UnmarshalString(tt) 80 | assert.ErrorContains(t, err, "invalid") 81 | assert.Check(t, is.DeepEqual(Frame{}, frame)) 82 | }) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /frame_test.go: -------------------------------------------------------------------------------- 1 | package can 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "unsafe" 7 | 8 | "gotest.tools/v3/assert" 9 | ) 10 | 11 | // If this mocks ever starts failing, the documentation needs to be updated 12 | // to prefer pass-by-pointer over pass-by-value. 13 | func TestFrame_Size(t *testing.T) { 14 | assert.Assert(t, unsafe.Sizeof(Frame{}) <= 16, "Frame size is <= 16 bytes") 15 | } 16 | 17 | func TestFrame_Validate_Error(t *testing.T) { 18 | for _, tt := range []Frame{ 19 | {ID: MaxID + 1}, 20 | {ID: MaxExtendedID + 1, IsExtended: true}, 21 | } { 22 | t.Run(fmt.Sprintf("%v", tt), func(t *testing.T) { 23 | assert.Check(t, tt.Validate() != nil, "should return validation error") 24 | }) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module go.einride.tech/can 2 | 3 | go 1.22.12 4 | 5 | toolchain go1.24.2 6 | 7 | require ( 8 | github.com/alecthomas/kingpin/v2 v2.4.0 9 | github.com/davecgh/go-spew v1.1.1 10 | github.com/fatih/color v1.17.0 11 | github.com/golang/mock v1.6.0 12 | github.com/mdlayher/netlink v1.7.2 13 | github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041 14 | go.uber.org/goleak v1.3.0 15 | golang.org/x/net v0.35.0 16 | golang.org/x/sync v0.11.0 17 | golang.org/x/sys v0.30.0 18 | golang.org/x/tools v0.30.0 19 | gotest.tools/v3 v3.5.1 20 | ) 21 | 22 | require ( 23 | github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect 24 | github.com/google/go-cmp v0.6.0 // indirect 25 | github.com/josharian/native v1.1.0 // indirect 26 | github.com/mattn/go-colorable v0.1.13 // indirect 27 | github.com/mattn/go-isatty v0.0.20 // indirect 28 | github.com/mdlayher/socket v0.4.1 // indirect 29 | github.com/shurcooL/go v0.0.0-20190704215121-7189cc372560 // indirect 30 | github.com/xhit/go-str2duration/v2 v2.1.0 // indirect 31 | golang.org/x/mod v0.23.0 // indirect 32 | ) 33 | -------------------------------------------------------------------------------- /internal/clock/clock.go: -------------------------------------------------------------------------------- 1 | // Package clock provides primitives for mocking time. 2 | package clock 3 | 4 | import ( 5 | "time" 6 | ) 7 | 8 | // Clock provides capabilities from the time standard library package. 9 | type Clock interface { 10 | // After waits for the duration to elapse and then sends the current time on the returned channel. 11 | After(duration time.Duration) <-chan time.Time 12 | 13 | // NewTicker returns a new Ticker. 14 | NewTicker(d time.Duration) Ticker 15 | 16 | // Now returns the current local time. 17 | Now() time.Time 18 | } 19 | 20 | // Ticker wraps the time.Ticker class. 21 | type Ticker interface { 22 | // C returns the channel on which the ticks are delivered. 23 | C() <-chan time.Time 24 | 25 | // Stop the Ticker. 26 | Stop() 27 | } 28 | -------------------------------------------------------------------------------- /internal/clock/system.go: -------------------------------------------------------------------------------- 1 | package clock 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // System returns a Clock implementation that delegate to the time package. 8 | func System() Clock { 9 | return &systemClock{} 10 | } 11 | 12 | type systemClock struct{} 13 | 14 | var _ Clock = &systemClock{} 15 | 16 | func (c systemClock) After(d time.Duration) <-chan time.Time { 17 | return time.After(d) 18 | } 19 | 20 | func (c systemClock) NewTicker(d time.Duration) Ticker { 21 | return &systemTicker{Ticker: *time.NewTicker(d)} 22 | } 23 | 24 | func (c systemClock) Now() time.Time { 25 | return time.Now() 26 | } 27 | 28 | type systemTicker struct { 29 | time.Ticker 30 | } 31 | 32 | func (t systemTicker) C() <-chan time.Time { 33 | return t.Ticker.C 34 | } 35 | -------------------------------------------------------------------------------- /internal/generate/file_test.go: -------------------------------------------------------------------------------- 1 | package generate 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "gotest.tools/v3/assert" 8 | ) 9 | 10 | func runTestInDir(t *testing.T, dir string) func() { 11 | // change working directory to project root 12 | wd, err := os.Getwd() 13 | assert.NilError(t, err) 14 | assert.NilError(t, os.Chdir(dir)) 15 | return func() { 16 | assert.NilError(t, os.Chdir(wd)) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /internal/identifiers/case.go: -------------------------------------------------------------------------------- 1 | package identifiers 2 | 3 | import "unicode" 4 | 5 | func IsCamelCase(s string) bool { 6 | i := 0 7 | for _, r := range s { 8 | if unicode.IsDigit(r) { 9 | continue 10 | } 11 | if i == 0 && !unicode.IsUpper(r) || !IsAlphaChar(r) && !IsNumChar(r) { 12 | return false 13 | } 14 | i++ 15 | } 16 | return true 17 | } 18 | -------------------------------------------------------------------------------- /internal/identifiers/case_test.go: -------------------------------------------------------------------------------- 1 | package identifiers 2 | 3 | import ( 4 | "testing" 5 | 6 | "gotest.tools/v3/assert" 7 | ) 8 | 9 | func TestIsCamelCase(t *testing.T) { 10 | assert.Assert(t, IsCamelCase("SOC")) 11 | assert.Assert(t, IsCamelCase("Camel")) 12 | assert.Assert(t, IsCamelCase("CamelCase")) 13 | assert.Assert(t, IsCamelCase("111CamelCaseNr")) 14 | assert.Assert(t, !IsCamelCase("camelCase")) 15 | assert.Assert(t, !IsCamelCase("snake_case")) 16 | assert.Assert(t, !IsCamelCase("kebab-case")) 17 | assert.Assert(t, !IsCamelCase("111camelCaseNr")) 18 | } 19 | -------------------------------------------------------------------------------- /internal/identifiers/char.go: -------------------------------------------------------------------------------- 1 | package identifiers 2 | 3 | func IsAlphaChar(r rune) bool { 4 | return ('A' <= r && r <= 'Z') || ('a' <= r && r <= 'z') 5 | } 6 | 7 | func IsNumChar(r rune) bool { 8 | return '0' <= r && r <= '9' 9 | } 10 | -------------------------------------------------------------------------------- /internal/identifiers/char_test.go: -------------------------------------------------------------------------------- 1 | package identifiers 2 | 3 | import ( 4 | "testing" 5 | 6 | "gotest.tools/v3/assert" 7 | ) 8 | 9 | func TestIsAlphaChar(t *testing.T) { 10 | assert.Assert(t, IsAlphaChar('b')) 11 | assert.Assert(t, IsAlphaChar('C')) 12 | assert.Assert(t, !IsAlphaChar('Ö')) 13 | assert.Assert(t, !IsAlphaChar('_')) 14 | } 15 | 16 | func TestIsNumChar(t *testing.T) { 17 | assert.Assert(t, IsNumChar('0')) 18 | assert.Assert(t, IsNumChar('1')) 19 | assert.Assert(t, IsNumChar('2')) 20 | assert.Assert(t, IsNumChar('9')) 21 | assert.Assert(t, !IsNumChar('/')) 22 | assert.Assert(t, !IsNumChar('a')) 23 | } 24 | -------------------------------------------------------------------------------- /internal/mocks/gen.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | //go:generate mockgen -destination gen/mockclock/mocks.go -package mockclock go.einride.tech/can/internal/clock Clock,Ticker 4 | //go:generate mockgen -destination gen/mocksocketcan/mocks.go -package mocksocketcan -source ../../pkg/socketcan/fileconn.go 5 | //go:generate mockgen -destination gen/mockcanrunner/mocks.go -package mockcanrunner go.einride.tech/can/pkg/canrunner Node,TransmittedMessage,ReceivedMessage,FrameTransmitter,FrameReceiver 6 | -------------------------------------------------------------------------------- /internal/mocks/gen/mockclock/mocks.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: go.einride.tech/can/internal/clock (interfaces: Clock,Ticker) 3 | 4 | // Package mockclock is a generated GoMock package. 5 | package mockclock 6 | 7 | import ( 8 | reflect "reflect" 9 | time "time" 10 | 11 | gomock "github.com/golang/mock/gomock" 12 | clock "go.einride.tech/can/internal/clock" 13 | ) 14 | 15 | // MockClock is a mock of Clock interface. 16 | type MockClock struct { 17 | ctrl *gomock.Controller 18 | recorder *MockClockMockRecorder 19 | } 20 | 21 | // MockClockMockRecorder is the mock recorder for MockClock. 22 | type MockClockMockRecorder struct { 23 | mock *MockClock 24 | } 25 | 26 | // NewMockClock creates a new mock instance. 27 | func NewMockClock(ctrl *gomock.Controller) *MockClock { 28 | mock := &MockClock{ctrl: ctrl} 29 | mock.recorder = &MockClockMockRecorder{mock} 30 | return mock 31 | } 32 | 33 | // EXPECT returns an object that allows the caller to indicate expected use. 34 | func (m *MockClock) EXPECT() *MockClockMockRecorder { 35 | return m.recorder 36 | } 37 | 38 | // After mocks base method. 39 | func (m *MockClock) After(arg0 time.Duration) <-chan time.Time { 40 | m.ctrl.T.Helper() 41 | ret := m.ctrl.Call(m, "After", arg0) 42 | ret0, _ := ret[0].(<-chan time.Time) 43 | return ret0 44 | } 45 | 46 | // After indicates an expected call of After. 47 | func (mr *MockClockMockRecorder) After(arg0 interface{}) *gomock.Call { 48 | mr.mock.ctrl.T.Helper() 49 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "After", reflect.TypeOf((*MockClock)(nil).After), arg0) 50 | } 51 | 52 | // NewTicker mocks base method. 53 | func (m *MockClock) NewTicker(arg0 time.Duration) clock.Ticker { 54 | m.ctrl.T.Helper() 55 | ret := m.ctrl.Call(m, "NewTicker", arg0) 56 | ret0, _ := ret[0].(clock.Ticker) 57 | return ret0 58 | } 59 | 60 | // NewTicker indicates an expected call of NewTicker. 61 | func (mr *MockClockMockRecorder) NewTicker(arg0 interface{}) *gomock.Call { 62 | mr.mock.ctrl.T.Helper() 63 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewTicker", reflect.TypeOf((*MockClock)(nil).NewTicker), arg0) 64 | } 65 | 66 | // Now mocks base method. 67 | func (m *MockClock) Now() time.Time { 68 | m.ctrl.T.Helper() 69 | ret := m.ctrl.Call(m, "Now") 70 | ret0, _ := ret[0].(time.Time) 71 | return ret0 72 | } 73 | 74 | // Now indicates an expected call of Now. 75 | func (mr *MockClockMockRecorder) Now() *gomock.Call { 76 | mr.mock.ctrl.T.Helper() 77 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Now", reflect.TypeOf((*MockClock)(nil).Now)) 78 | } 79 | 80 | // MockTicker is a mock of Ticker interface. 81 | type MockTicker struct { 82 | ctrl *gomock.Controller 83 | recorder *MockTickerMockRecorder 84 | } 85 | 86 | // MockTickerMockRecorder is the mock recorder for MockTicker. 87 | type MockTickerMockRecorder struct { 88 | mock *MockTicker 89 | } 90 | 91 | // NewMockTicker creates a new mock instance. 92 | func NewMockTicker(ctrl *gomock.Controller) *MockTicker { 93 | mock := &MockTicker{ctrl: ctrl} 94 | mock.recorder = &MockTickerMockRecorder{mock} 95 | return mock 96 | } 97 | 98 | // EXPECT returns an object that allows the caller to indicate expected use. 99 | func (m *MockTicker) EXPECT() *MockTickerMockRecorder { 100 | return m.recorder 101 | } 102 | 103 | // C mocks base method. 104 | func (m *MockTicker) C() <-chan time.Time { 105 | m.ctrl.T.Helper() 106 | ret := m.ctrl.Call(m, "C") 107 | ret0, _ := ret[0].(<-chan time.Time) 108 | return ret0 109 | } 110 | 111 | // C indicates an expected call of C. 112 | func (mr *MockTickerMockRecorder) C() *gomock.Call { 113 | mr.mock.ctrl.T.Helper() 114 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "C", reflect.TypeOf((*MockTicker)(nil).C)) 115 | } 116 | 117 | // Stop mocks base method. 118 | func (m *MockTicker) Stop() { 119 | m.ctrl.T.Helper() 120 | m.ctrl.Call(m, "Stop") 121 | } 122 | 123 | // Stop indicates an expected call of Stop. 124 | func (mr *MockTickerMockRecorder) Stop() *gomock.Call { 125 | mr.mock.ctrl.T.Helper() 126 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Stop", reflect.TypeOf((*MockTicker)(nil).Stop)) 127 | } 128 | -------------------------------------------------------------------------------- /internal/mocks/gen/mocksocketcan/mocks.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: ../../pkg/socketcan/fileconn.go 3 | 4 | // Package mocksocketcan is a generated GoMock package. 5 | package mocksocketcan 6 | 7 | import ( 8 | reflect "reflect" 9 | time "time" 10 | 11 | gomock "github.com/golang/mock/gomock" 12 | ) 13 | 14 | // Mockfile is a mock of file interface. 15 | type Mockfile struct { 16 | ctrl *gomock.Controller 17 | recorder *MockfileMockRecorder 18 | } 19 | 20 | // MockfileMockRecorder is the mock recorder for Mockfile. 21 | type MockfileMockRecorder struct { 22 | mock *Mockfile 23 | } 24 | 25 | // NewMockfile creates a new mock instance. 26 | func NewMockfile(ctrl *gomock.Controller) *Mockfile { 27 | mock := &Mockfile{ctrl: ctrl} 28 | mock.recorder = &MockfileMockRecorder{mock} 29 | return mock 30 | } 31 | 32 | // EXPECT returns an object that allows the caller to indicate expected use. 33 | func (m *Mockfile) EXPECT() *MockfileMockRecorder { 34 | return m.recorder 35 | } 36 | 37 | // Close mocks base method. 38 | func (m *Mockfile) Close() error { 39 | m.ctrl.T.Helper() 40 | ret := m.ctrl.Call(m, "Close") 41 | ret0, _ := ret[0].(error) 42 | return ret0 43 | } 44 | 45 | // Close indicates an expected call of Close. 46 | func (mr *MockfileMockRecorder) Close() *gomock.Call { 47 | mr.mock.ctrl.T.Helper() 48 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*Mockfile)(nil).Close)) 49 | } 50 | 51 | // Read mocks base method. 52 | func (m *Mockfile) Read(arg0 []byte) (int, error) { 53 | m.ctrl.T.Helper() 54 | ret := m.ctrl.Call(m, "Read", arg0) 55 | ret0, _ := ret[0].(int) 56 | ret1, _ := ret[1].(error) 57 | return ret0, ret1 58 | } 59 | 60 | // Read indicates an expected call of Read. 61 | func (mr *MockfileMockRecorder) Read(arg0 interface{}) *gomock.Call { 62 | mr.mock.ctrl.T.Helper() 63 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*Mockfile)(nil).Read), arg0) 64 | } 65 | 66 | // SetDeadline mocks base method. 67 | func (m *Mockfile) SetDeadline(arg0 time.Time) error { 68 | m.ctrl.T.Helper() 69 | ret := m.ctrl.Call(m, "SetDeadline", arg0) 70 | ret0, _ := ret[0].(error) 71 | return ret0 72 | } 73 | 74 | // SetDeadline indicates an expected call of SetDeadline. 75 | func (mr *MockfileMockRecorder) SetDeadline(arg0 interface{}) *gomock.Call { 76 | mr.mock.ctrl.T.Helper() 77 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetDeadline", reflect.TypeOf((*Mockfile)(nil).SetDeadline), arg0) 78 | } 79 | 80 | // SetReadDeadline mocks base method. 81 | func (m *Mockfile) SetReadDeadline(arg0 time.Time) error { 82 | m.ctrl.T.Helper() 83 | ret := m.ctrl.Call(m, "SetReadDeadline", arg0) 84 | ret0, _ := ret[0].(error) 85 | return ret0 86 | } 87 | 88 | // SetReadDeadline indicates an expected call of SetReadDeadline. 89 | func (mr *MockfileMockRecorder) SetReadDeadline(arg0 interface{}) *gomock.Call { 90 | mr.mock.ctrl.T.Helper() 91 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetReadDeadline", reflect.TypeOf((*Mockfile)(nil).SetReadDeadline), arg0) 92 | } 93 | 94 | // SetWriteDeadline mocks base method. 95 | func (m *Mockfile) SetWriteDeadline(arg0 time.Time) error { 96 | m.ctrl.T.Helper() 97 | ret := m.ctrl.Call(m, "SetWriteDeadline", arg0) 98 | ret0, _ := ret[0].(error) 99 | return ret0 100 | } 101 | 102 | // SetWriteDeadline indicates an expected call of SetWriteDeadline. 103 | func (mr *MockfileMockRecorder) SetWriteDeadline(arg0 interface{}) *gomock.Call { 104 | mr.mock.ctrl.T.Helper() 105 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetWriteDeadline", reflect.TypeOf((*Mockfile)(nil).SetWriteDeadline), arg0) 106 | } 107 | 108 | // Write mocks base method. 109 | func (m *Mockfile) Write(arg0 []byte) (int, error) { 110 | m.ctrl.T.Helper() 111 | ret := m.ctrl.Call(m, "Write", arg0) 112 | ret0, _ := ret[0].(int) 113 | ret1, _ := ret[1].(error) 114 | return ret0, ret1 115 | } 116 | 117 | // Write indicates an expected call of Write. 118 | func (mr *MockfileMockRecorder) Write(arg0 interface{}) *gomock.Call { 119 | mr.mock.ctrl.T.Helper() 120 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Write", reflect.TypeOf((*Mockfile)(nil).Write), arg0) 121 | } 122 | -------------------------------------------------------------------------------- /internal/reinterpret/reinterpret.go: -------------------------------------------------------------------------------- 1 | // Package reinterpret provides primitives for reinterpreting arbitrary-length values as signed or unsigned. 2 | package reinterpret 3 | 4 | // AsSigned reinterprets the provided unsigned value as a signed value. 5 | func AsSigned(unsigned uint64, bits uint8) int64 { 6 | switch bits { 7 | case 8: 8 | return int64(int8(uint8(unsigned))) 9 | case 16: 10 | return int64(int16(uint16(unsigned))) 11 | case 32: 12 | return int64(int32(uint32(unsigned))) 13 | case 64: 14 | return int64(unsigned) 15 | default: 16 | // calculate bit mask for sign bit 17 | signBitMask := uint64(1 << (bits - 1)) 18 | // check if sign bit is set 19 | isNegative := unsigned&signBitMask > 0 20 | if !isNegative { 21 | // sign bit not set means we can reinterpret the value as-is 22 | return int64(unsigned) 23 | } 24 | // calculate bit mask for extracting value bits (all bits except the sign bit) 25 | valueBitMask := signBitMask - 1 26 | // calculate two's complement of the value bits 27 | value := ((^unsigned) & valueBitMask) + 1 28 | // result is the negative value of the two's complement 29 | return -1 * int64(value) 30 | } 31 | } 32 | 33 | // AsUnsigned reinterprets the provided signed value as an unsigned value. 34 | func AsUnsigned(signed int64, bits uint8) uint64 { 35 | switch bits { 36 | case 8: 37 | return uint64(uint8(int8(signed))) 38 | case 16: 39 | return uint64(uint16(int16(signed))) 40 | case 32: 41 | return uint64(uint32(int32(signed))) 42 | case 64: 43 | return uint64(signed) 44 | default: 45 | // calculate bit mask for extracting relevant bits 46 | valueBitMask := uint64(1< minOrder { 50 | pass.Reportf(def.Position(), "definition out of order") 51 | } else { 52 | minOrder = currOrder 53 | } 54 | } 55 | return nil 56 | } 57 | -------------------------------------------------------------------------------- /pkg/dbc/analysis/passes/definitiontypeorder/analyzer_test.go: -------------------------------------------------------------------------------- 1 | package definitiontypeorder 2 | 3 | import ( 4 | "testing" 5 | "text/scanner" 6 | 7 | "go.einride.tech/can/pkg/dbc/analysis" 8 | "go.einride.tech/can/pkg/dbc/analysis/analysistest" 9 | ) 10 | 11 | func TestAnalyzer(t *testing.T) { 12 | analysistest.Run(t, Analyzer(), []*analysistest.Case{ 13 | { 14 | Name: "correct order", 15 | Data: ` 16 | VERSION "foo" 17 | NS_ : 18 | BS_: 19 | BU_: 20 | `, 21 | }, 22 | 23 | { 24 | Name: "incorrect order", 25 | Data: ` 26 | VERSION "foo" 27 | NS_ : 28 | BU_: 29 | BS_: 30 | `, 31 | Diagnostics: []*analysis.Diagnostic{ 32 | { 33 | Pos: scanner.Position{Line: 3, Column: 1}, 34 | Message: "definition out of order", 35 | }, 36 | }, 37 | }, 38 | 39 | { 40 | Name: "unknown defs last", 41 | Data: ` 42 | VERSION "foo" 43 | NS_ : 44 | BS_: 45 | FOO "bar" 46 | BU_: 47 | `, 48 | Diagnostics: []*analysis.Diagnostic{ 49 | { 50 | Pos: scanner.Position{Line: 4, Column: 1}, 51 | Message: "definition out of order", 52 | }, 53 | }, 54 | }, 55 | }) 56 | } 57 | -------------------------------------------------------------------------------- /pkg/dbc/analysis/passes/intervals/analyzer.go: -------------------------------------------------------------------------------- 1 | package intervals 2 | 3 | import ( 4 | "go.einride.tech/can/pkg/dbc" 5 | "go.einride.tech/can/pkg/dbc/analysis" 6 | ) 7 | 8 | func Analyzer() *analysis.Analyzer { 9 | return &analysis.Analyzer{ 10 | Name: "intervals", 11 | Doc: "check that all intervals are valid (min <= max)", 12 | Run: run, 13 | } 14 | } 15 | 16 | func run(pass *analysis.Pass) error { 17 | for _, def := range pass.File.Defs { 18 | switch def := def.(type) { 19 | case *dbc.EnvironmentVariableDef: 20 | if def.Minimum > def.Maximum { 21 | pass.Reportf(def.Pos, "invalid interval: [%f, %f]", def.Minimum, def.Maximum) 22 | } 23 | case *dbc.MessageDef: 24 | for i := range def.Signals { 25 | signal := &def.Signals[i] 26 | if signal.Minimum > signal.Maximum { 27 | pass.Reportf(def.Pos, "invalid interval: [%f, %f]", signal.Minimum, signal.Maximum) 28 | } 29 | } 30 | case *dbc.AttributeDef: 31 | if def.MinimumInt > def.MaximumInt || def.MinimumFloat > def.MaximumFloat { 32 | pass.Reportf(def.Pos, "invalid interval: [%d, %d]", def.MinimumInt, def.MaximumInt) 33 | } 34 | if def.MinimumFloat > def.MaximumFloat { 35 | pass.Reportf(def.Pos, "invalid interval: [%f, %f]", def.MinimumFloat, def.MaximumFloat) 36 | } 37 | } 38 | } 39 | return nil 40 | } 41 | -------------------------------------------------------------------------------- /pkg/dbc/analysis/passes/intervals/analyzer_test.go: -------------------------------------------------------------------------------- 1 | package intervals 2 | 3 | import ( 4 | "testing" 5 | "text/scanner" 6 | 7 | "go.einride.tech/can/pkg/dbc/analysis" 8 | "go.einride.tech/can/pkg/dbc/analysis/analysistest" 9 | ) 10 | 11 | func TestAnalyzer(t *testing.T) { 12 | analysistest.Run(t, Analyzer(), []*analysistest.Case{ 13 | { 14 | Name: "attribute interval ok", 15 | Data: `BA_DEF_ "AttributeName" INT 0 10;`, 16 | }, 17 | 18 | { 19 | Name: "attribute interval bad", 20 | Data: `BA_DEF_ "AttributeName" INT 10 0;`, 21 | Diagnostics: []*analysis.Diagnostic{ 22 | { 23 | Pos: scanner.Position{Line: 1, Column: 1}, 24 | Message: "invalid interval: [10, 0]", 25 | }, 26 | }, 27 | }, 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /pkg/dbc/analysis/passes/lineendings/analyzer.go: -------------------------------------------------------------------------------- 1 | package lineendings 2 | 3 | import ( 4 | "bytes" 5 | "text/scanner" 6 | 7 | "go.einride.tech/can/pkg/dbc/analysis" 8 | ) 9 | 10 | func Analyzer() *analysis.Analyzer { 11 | return &analysis.Analyzer{ 12 | Name: "lineendings", 13 | Doc: `check that the file does not contain Windows line-endings (\r\n)`, 14 | Run: run, 15 | } 16 | } 17 | 18 | func run(pass *analysis.Pass) error { 19 | if bytes.Contains(pass.File.Data, []byte{'\r', '\n'}) { 20 | pass.Reportf( 21 | scanner.Position{Filename: pass.File.Name, Line: 1, Column: 1}, 22 | `file must not contain Windows line-endings (\r\n)`, 23 | ) 24 | } 25 | return nil 26 | } 27 | -------------------------------------------------------------------------------- /pkg/dbc/analysis/passes/lineendings/analyzer_test.go: -------------------------------------------------------------------------------- 1 | package lineendings 2 | 3 | import ( 4 | "testing" 5 | "text/scanner" 6 | 7 | "go.einride.tech/can/pkg/dbc/analysis" 8 | "go.einride.tech/can/pkg/dbc/analysis/analysistest" 9 | ) 10 | 11 | func TestAnalyzer(t *testing.T) { 12 | analysistest.Run(t, Analyzer(), []*analysistest.Case{ 13 | { 14 | Name: "ok", 15 | Data: `NS_ :`, 16 | }, 17 | 18 | { 19 | Name: "not ok", 20 | Data: "NS_ :\r\n", 21 | Diagnostics: []*analysis.Diagnostic{ 22 | { 23 | Pos: scanner.Position{Line: 1, Column: 1}, 24 | Message: `file must not contain Windows line-endings (\r\n)`, 25 | }, 26 | }, 27 | }, 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /pkg/dbc/analysis/passes/messagenames/analyzer.go: -------------------------------------------------------------------------------- 1 | package messagenames 2 | 3 | import ( 4 | "go.einride.tech/can/internal/identifiers" 5 | "go.einride.tech/can/pkg/dbc" 6 | "go.einride.tech/can/pkg/dbc/analysis" 7 | ) 8 | 9 | func Analyzer() *analysis.Analyzer { 10 | return &analysis.Analyzer{ 11 | Name: "messagenames", 12 | Doc: "check that message names are valid CamelCase identifiers", 13 | Run: run, 14 | } 15 | } 16 | 17 | func run(pass *analysis.Pass) error { 18 | for _, def := range pass.File.Defs { 19 | messageDef, ok := def.(*dbc.MessageDef) 20 | if !ok { 21 | continue // not a message 22 | } 23 | if !identifiers.IsCamelCase(string(messageDef.Name)) { 24 | pass.Reportf(messageDef.Pos, "message names must be CamelCase") 25 | } 26 | } 27 | return nil 28 | } 29 | -------------------------------------------------------------------------------- /pkg/dbc/analysis/passes/messagenames/analyzer_test.go: -------------------------------------------------------------------------------- 1 | package messagenames 2 | 3 | import ( 4 | "testing" 5 | "text/scanner" 6 | 7 | "go.einride.tech/can/pkg/dbc/analysis" 8 | "go.einride.tech/can/pkg/dbc/analysis/analysistest" 9 | ) 10 | 11 | func TestAnalyzer(t *testing.T) { 12 | analysistest.Run(t, Analyzer(), []*analysistest.Case{ 13 | { 14 | Name: "ok", 15 | Data: `BO_ 100 DriverHeartbeat: 1 DRIVER`, 16 | }, 17 | 18 | { 19 | Name: "not ok", 20 | Data: `BO_ 100 DRIVER_HEARTBEAT: 1 DRIVER`, 21 | Diagnostics: []*analysis.Diagnostic{ 22 | { 23 | Pos: scanner.Position{Line: 1, Column: 1}, 24 | Message: "message names must be CamelCase", 25 | }, 26 | }, 27 | }, 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /pkg/dbc/analysis/passes/multiplexedsignals/analyzer.go: -------------------------------------------------------------------------------- 1 | package multiplexedsignals 2 | 3 | import ( 4 | "go.einride.tech/can/pkg/dbc" 5 | "go.einride.tech/can/pkg/dbc/analysis" 6 | ) 7 | 8 | func Analyzer() *analysis.Analyzer { 9 | return &analysis.Analyzer{ 10 | Name: "multiplexedsignals", 11 | Doc: "check that multiplexed signals are valid", 12 | Run: run, 13 | } 14 | } 15 | 16 | func run(pass *analysis.Pass) error { 17 | for _, def := range pass.File.Defs { 18 | message, ok := def.(*dbc.MessageDef) 19 | if !ok { 20 | continue 21 | } 22 | // locate multiplexer switch 23 | var multiplexerSwitch *dbc.SignalDef 24 | for i := range message.Signals { 25 | if !message.Signals[i].IsMultiplexerSwitch { 26 | continue 27 | } 28 | if multiplexerSwitch != nil { 29 | pass.Reportf(message.Signals[i].Pos, "more than one multiplexer switch") 30 | continue 31 | } 32 | multiplexerSwitch = &message.Signals[i] 33 | if multiplexerSwitch.IsSigned { 34 | pass.Reportf(message.Signals[i].Pos, "signed multiplexer switch") 35 | continue 36 | } 37 | if multiplexerSwitch.IsMultiplexed { 38 | pass.Reportf(message.Signals[i].Pos, "can't be multiplexer and multiplexed") 39 | continue 40 | } 41 | } 42 | for i := range message.Signals { 43 | signal := &message.Signals[i] 44 | if !signal.IsMultiplexed { 45 | continue 46 | } 47 | if multiplexerSwitch == nil { 48 | pass.Reportf(message.Signals[i].Pos, "no multiplexer switch for multiplexed signal") 49 | continue 50 | } 51 | multiplexerSwitchMaxValue := uint64((1 << multiplexerSwitch.Size) - 1) 52 | if signal.MultiplexerSwitch > multiplexerSwitchMaxValue { 53 | pass.Reportf(signal.Pos, "multiplexer switch exceeds max value: %v", multiplexerSwitchMaxValue) 54 | continue 55 | } 56 | } 57 | } 58 | return nil 59 | } 60 | -------------------------------------------------------------------------------- /pkg/dbc/analysis/passes/multiplexedsignals/analyzer_test.go: -------------------------------------------------------------------------------- 1 | package multiplexedsignals 2 | 3 | import ( 4 | "testing" 5 | "text/scanner" 6 | 7 | "go.einride.tech/can/pkg/dbc/analysis" 8 | "go.einride.tech/can/pkg/dbc/analysis/analysistest" 9 | ) 10 | 11 | func TestAnalyzer(t *testing.T) { 12 | analysistest.Run(t, Analyzer(), []*analysistest.Case{ 13 | { 14 | Name: "valid", 15 | Data: ` 16 | BO_ 200 SENSOR_SONARS: 8 SENSOR 17 | SG_ SENSOR_SONARS_mux M : 0|4@1+ (1,0) [0|0] "" DRIVER,IO 18 | SG_ SENSOR_SONARS_err_count : 4|12@1+ (1,0) [0|0] "" DRIVER,IO 19 | SG_ SENSOR_SONARS_left m0 : 16|12@1+ (0.1,0) [0|0] "" DRIVER,IO 20 | SG_ SENSOR_SONARS_middle m0 : 28|12@1+ (0.1,0) [0|0] "" DRIVER,IO 21 | SG_ SENSOR_SONARS_right m0 : 40|12@1+ (0.1,0) [0|0] "" DRIVER,IO 22 | SG_ SENSOR_SONARS_rear m0 : 52|12@1+ (0.1,0) [0|0] "" DRIVER,IO 23 | SG_ SENSOR_SONARS_no_filt_left m1 : 16|12@1+ (0.1,0) [0|0] "" DBG 24 | SG_ SENSOR_SONARS_no_filt_middle m1 : 28|12@1+ (0.1,0) [0|0] "" DBG 25 | SG_ SENSOR_SONARS_no_filt_right m1 : 40|12@1+ (0.1,0) [0|0] "" DBG 26 | SG_ SENSOR_SONARS_no_filt_rear m1 : 52|12@1+ (0.1,0) [0|0] "" DBG 27 | `, 28 | }, 29 | 30 | { 31 | Name: "multiple multiplexer switches", 32 | Data: ` 33 | BO_ 200 SENSOR_SONARS: 8 SENSOR 34 | SG_ SENSOR_SONARS_mux M : 0|4@1+ (1,0) [0|0] "" DRIVER,IO 35 | SG_ SENSOR_SONARS_err_count M : 4|12@1+ (1,0) [0|0] "" DRIVER,IO 36 | SG_ SENSOR_SONARS_left m0 : 16|12@1+ (0.1,0) [0|0] "" DRIVER,IO 37 | SG_ SENSOR_SONARS_middle m0 : 28|12@1+ (0.1,0) [0|0] "" DRIVER,IO 38 | SG_ SENSOR_SONARS_right m0 : 40|12@1+ (0.1,0) [0|0] "" DRIVER,IO 39 | SG_ SENSOR_SONARS_rear m0 : 52|12@1+ (0.1,0) [0|0] "" DRIVER,IO 40 | SG_ SENSOR_SONARS_no_filt_left m1 : 16|12@1+ (0.1,0) [0|0] "" DBG 41 | SG_ SENSOR_SONARS_no_filt_middle m1 : 28|12@1+ (0.1,0) [0|0] "" DBG 42 | SG_ SENSOR_SONARS_no_filt_right m1 : 40|12@1+ (0.1,0) [0|0] "" DBG 43 | SG_ SENSOR_SONARS_no_filt_rear m1 : 52|12@1+ (0.1,0) [0|0] "" DBG 44 | `, 45 | Diagnostics: []*analysis.Diagnostic{ 46 | { 47 | Pos: scanner.Position{Line: 3, Column: 2}, 48 | Message: "more than one multiplexer switch", 49 | }, 50 | }, 51 | }, 52 | 53 | { 54 | Name: "signed multiplexer switch", 55 | Data: ` 56 | BO_ 200 SENSOR_SONARS: 8 SENSOR 57 | SG_ SENSOR_SONARS_mux M : 0|4@1- (1,0) [0|0] "" DRIVER,IO 58 | SG_ SENSOR_SONARS_err_count : 4|12@1+ (1,0) [0|0] "" DRIVER,IO 59 | SG_ SENSOR_SONARS_left m0 : 16|12@1+ (0.1,0) [0|0] "" DRIVER,IO 60 | SG_ SENSOR_SONARS_middle m0 : 28|12@1+ (0.1,0) [0|0] "" DRIVER,IO 61 | SG_ SENSOR_SONARS_right m0 : 40|12@1+ (0.1,0) [0|0] "" DRIVER,IO 62 | SG_ SENSOR_SONARS_rear m0 : 52|12@1+ (0.1,0) [0|0] "" DRIVER,IO 63 | SG_ SENSOR_SONARS_no_filt_left m1 : 16|12@1+ (0.1,0) [0|0] "" DBG 64 | SG_ SENSOR_SONARS_no_filt_middle m1 : 28|12@1+ (0.1,0) [0|0] "" DBG 65 | SG_ SENSOR_SONARS_no_filt_right m1 : 40|12@1+ (0.1,0) [0|0] "" DBG 66 | SG_ SENSOR_SONARS_no_filt_rear m1 : 52|12@1+ (0.1,0) [0|0] "" DBG 67 | `, 68 | Diagnostics: []*analysis.Diagnostic{ 69 | { 70 | Pos: scanner.Position{Line: 2, Column: 2}, 71 | Message: "signed multiplexer switch", 72 | }, 73 | }, 74 | }, 75 | 76 | { 77 | Name: "no multiplexer switch", 78 | Data: ` 79 | BO_ 200 SENSOR_SONARS: 8 SENSOR 80 | SG_ SENSOR_SONARS_err_count : 4|12@1+ (1,0) [0|0] "" DRIVER,IO 81 | SG_ SENSOR_SONARS_left m0 : 16|12@1+ (0.1,0) [0|0] "" DRIVER,IO 82 | `, 83 | Diagnostics: []*analysis.Diagnostic{ 84 | { 85 | Pos: scanner.Position{Line: 3, Column: 2}, 86 | Message: "no multiplexer switch for multiplexed signal", 87 | }, 88 | }, 89 | }, 90 | 91 | { 92 | Name: "too big multiplexer switch", 93 | Data: ` 94 | BO_ 200 SENSOR_SONARS: 8 SENSOR 95 | SG_ SENSOR_SONARS_mux M : 0|4@1+ (1,0) [0|0] "" DRIVER,IO 96 | SG_ SENSOR_SONARS_err_count : 4|12@1+ (1,0) [0|0] "" DRIVER,IO 97 | SG_ SENSOR_SONARS_left m16 : 16|12@1+ (0.1,0) [0|0] "" DRIVER,IO 98 | SG_ SENSOR_SONARS_middle m0 : 28|12@1+ (0.1,0) [0|0] "" DRIVER,IO 99 | SG_ SENSOR_SONARS_right m0 : 40|12@1+ (0.1,0) [0|0] "" DRIVER,IO 100 | SG_ SENSOR_SONARS_rear m0 : 52|12@1+ (0.1,0) [0|0] "" DRIVER,IO 101 | SG_ SENSOR_SONARS_no_filt_left m1 : 16|12@1+ (0.1,0) [0|0] "" DBG 102 | SG_ SENSOR_SONARS_no_filt_middle m1 : 28|12@1+ (0.1,0) [0|0] "" DBG 103 | SG_ SENSOR_SONARS_no_filt_right m1 : 40|12@1+ (0.1,0) [0|0] "" DBG 104 | SG_ SENSOR_SONARS_no_filt_rear m1 : 52|12@1+ (0.1,0) [0|0] "" DBG 105 | `, 106 | Diagnostics: []*analysis.Diagnostic{ 107 | { 108 | Pos: scanner.Position{Line: 4, Column: 2}, 109 | Message: "multiplexer switch exceeds max value: 15", 110 | }, 111 | }, 112 | }, 113 | }) 114 | } 115 | -------------------------------------------------------------------------------- /pkg/dbc/analysis/passes/newsymbols/analyzer.go: -------------------------------------------------------------------------------- 1 | package newsymbols 2 | 3 | import ( 4 | "go.einride.tech/can/pkg/dbc" 5 | "go.einride.tech/can/pkg/dbc/analysis" 6 | ) 7 | 8 | func Analyzer() *analysis.Analyzer { 9 | return &analysis.Analyzer{ 10 | Name: "newsymbols", 11 | Doc: "check that the new symbols definition is empty", 12 | Run: run, 13 | } 14 | } 15 | 16 | func run(pass *analysis.Pass) error { 17 | for _, def := range pass.File.Defs { 18 | newSymbolsDef, ok := def.(*dbc.NewSymbolsDef) 19 | if !ok { 20 | continue // not a new symbols definition 21 | } 22 | if len(newSymbolsDef.Symbols) > 0 { 23 | pass.Reportf(newSymbolsDef.Pos, "new symbols should be empty") 24 | } 25 | } 26 | return nil 27 | } 28 | -------------------------------------------------------------------------------- /pkg/dbc/analysis/passes/newsymbols/analyzer_test.go: -------------------------------------------------------------------------------- 1 | package newsymbols 2 | 3 | import ( 4 | "testing" 5 | "text/scanner" 6 | 7 | "go.einride.tech/can/pkg/dbc/analysis" 8 | "go.einride.tech/can/pkg/dbc/analysis/analysistest" 9 | ) 10 | 11 | func TestAnalyzer(t *testing.T) { 12 | analysistest.Run(t, Analyzer(), []*analysistest.Case{ 13 | { 14 | Name: "ok", 15 | Data: `NS_ :`, 16 | }, 17 | 18 | { 19 | Name: "not ok", 20 | Data: ` 21 | NS_ : 22 | BA_DEF_DEF_REL_ 23 | BA_DEF_SGTYPE_`, 24 | Diagnostics: []*analysis.Diagnostic{ 25 | { 26 | Pos: scanner.Position{Line: 1, Column: 1}, 27 | Message: "new symbols should be empty", 28 | }, 29 | }, 30 | }, 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /pkg/dbc/analysis/passes/nodereferences/analyzer.go: -------------------------------------------------------------------------------- 1 | package nodereferences 2 | 3 | import ( 4 | "go.einride.tech/can/pkg/dbc" 5 | "go.einride.tech/can/pkg/dbc/analysis" 6 | ) 7 | 8 | func Analyzer() *analysis.Analyzer { 9 | return &analysis.Analyzer{ 10 | Name: "nodereferences", 11 | Doc: "check that all node references refer to declared nodes", 12 | Run: run, 13 | } 14 | } 15 | 16 | func run(pass *analysis.Pass) error { 17 | declaredNodes := map[dbc.Identifier]struct{}{ 18 | dbc.NodePlaceholder: {}, // placeholder is implicitly declared 19 | } 20 | // collect declared nodes 21 | for _, def := range pass.File.Defs { 22 | if def, ok := def.(*dbc.NodesDef); ok { 23 | for _, nodeName := range def.NodeNames { 24 | declaredNodes[nodeName] = struct{}{} 25 | } 26 | } 27 | } 28 | // verify node references 29 | for _, def := range pass.File.Defs { 30 | switch def := def.(type) { 31 | case *dbc.MessageDef: 32 | if _, ok := declaredNodes[def.Transmitter]; !ok { 33 | pass.Reportf(def.Pos, "undeclared transmitter node: %v", def.Transmitter) 34 | } 35 | for i := range def.Signals { 36 | signal := &def.Signals[i] 37 | for _, receiver := range signal.Receivers { 38 | if _, ok := declaredNodes[receiver]; !ok { 39 | pass.Reportf(signal.Pos, "undeclared receiver node: %v", receiver) 40 | } 41 | } 42 | } 43 | case *dbc.EnvironmentVariableDef: 44 | for _, accessNode := range def.AccessNodes { 45 | if _, ok := declaredNodes[accessNode]; !ok { 46 | pass.Reportf(def.Pos, "undeclared access node: %v", accessNode) 47 | } 48 | } 49 | case *dbc.MessageTransmittersDef: 50 | for _, transmitter := range def.Transmitters { 51 | if _, ok := declaredNodes[transmitter]; !ok { 52 | pass.Reportf(def.Pos, "undeclared transmitter node: %v", transmitter) 53 | } 54 | } 55 | } 56 | } 57 | return nil 58 | } 59 | -------------------------------------------------------------------------------- /pkg/dbc/analysis/passes/nodereferences/analyzer_test.go: -------------------------------------------------------------------------------- 1 | package nodereferences 2 | 3 | import ( 4 | "testing" 5 | "text/scanner" 6 | 7 | "go.einride.tech/can/pkg/dbc/analysis" 8 | "go.einride.tech/can/pkg/dbc/analysis/analysistest" 9 | ) 10 | 11 | func TestAnalyzer(t *testing.T) { 12 | analysistest.Run(t, Analyzer(), []*analysistest.Case{ 13 | { 14 | Name: "valid", 15 | Data: ` 16 | BU_: ECU1 ECU2 17 | BO_ 42 TestMessage: 8 ECU2 18 | SG_ CellTempLowest : 32|8@0+ (1,-40) [-40|215] "C" ECU1 19 | `, 20 | }, 21 | 22 | { 23 | Name: "undeclared transmitter", 24 | Data: ` 25 | BU_: ECU1 ECU2 26 | BO_ 42 TestMessage: 8 ECU3 27 | SG_ CellTempLowest : 32|8@0+ (1,-40) [-40|215] "C" ECU1 28 | `, 29 | Diagnostics: []*analysis.Diagnostic{ 30 | { 31 | Pos: scanner.Position{Line: 2, Column: 1}, 32 | Message: "undeclared transmitter node: ECU3", 33 | }, 34 | }, 35 | }, 36 | 37 | { 38 | Name: "undeclared receiver", 39 | Data: ` 40 | BU_: ECU1 ECU2 41 | BO_ 42 TestMessage: 8 ECU2 42 | SG_ CellTempLowest : 32|8@0+ (1,-40) [-40|215] "C" ECU2,ECU3 43 | `, 44 | Diagnostics: []*analysis.Diagnostic{ 45 | { 46 | Pos: scanner.Position{Line: 3, Column: 2}, 47 | Message: "undeclared receiver node: ECU3", 48 | }, 49 | }, 50 | }, 51 | }) 52 | } 53 | -------------------------------------------------------------------------------- /pkg/dbc/analysis/passes/noreservedsignals/analyzer.go: -------------------------------------------------------------------------------- 1 | package noreservedsignals 2 | 3 | import ( 4 | "strings" 5 | 6 | "go.einride.tech/can/pkg/dbc" 7 | "go.einride.tech/can/pkg/dbc/analysis" 8 | ) 9 | 10 | func Analyzer() *analysis.Analyzer { 11 | return &analysis.Analyzer{ 12 | Name: "noreservedsignals", 13 | Doc: `checks that no signals have the prefix "Reserved"`, 14 | Run: run, 15 | } 16 | } 17 | 18 | func run(pass *analysis.Pass) error { 19 | for _, d := range pass.File.Defs { 20 | messageDef, ok := d.(*dbc.MessageDef) 21 | if !ok { 22 | continue 23 | } 24 | for _, signalDef := range messageDef.Signals { 25 | if strings.HasPrefix(string(signalDef.Name), "Reserved") { 26 | pass.Reportf(signalDef.Pos, "remove reserved signals") 27 | } 28 | } 29 | } 30 | return nil 31 | } 32 | -------------------------------------------------------------------------------- /pkg/dbc/analysis/passes/noreservedsignals/analyzer_test.go: -------------------------------------------------------------------------------- 1 | package noreservedsignals 2 | 3 | import ( 4 | "testing" 5 | "text/scanner" 6 | 7 | "go.einride.tech/can/pkg/dbc/analysis" 8 | "go.einride.tech/can/pkg/dbc/analysis/analysistest" 9 | ) 10 | 11 | func TestAnalyzer(t *testing.T) { 12 | analysistest.Run(t, Analyzer(), []*analysistest.Case{ 13 | { 14 | Name: "ok", 15 | Data: ` 16 | BO_ 400 MotorStatus: 3 MOTOR 17 | SG_ HasWheelError : 0|1@1+ (1,0) [0|0] "" DRIVER,IO 18 | `, 19 | }, 20 | 21 | { 22 | Name: "not ok", 23 | Data: ` 24 | BO_ 400 MotorStatus: 3 MOTOR 25 | SG_ Reserved1 : 0|1@1+ (1,0) [0|0] "" DRIVER,IO 26 | `, 27 | Diagnostics: []*analysis.Diagnostic{ 28 | { 29 | Pos: scanner.Position{Line: 2, Column: 2}, 30 | Message: "remove reserved signals", 31 | }, 32 | }, 33 | }, 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /pkg/dbc/analysis/passes/requireddefinitions/analyzer.go: -------------------------------------------------------------------------------- 1 | package requireddefinitions 2 | 3 | import ( 4 | "reflect" 5 | 6 | "go.einride.tech/can/pkg/dbc" 7 | "go.einride.tech/can/pkg/dbc/analysis" 8 | ) 9 | 10 | func Analyzer() *analysis.Analyzer { 11 | return &analysis.Analyzer{ 12 | Name: "requireddefinitions", 13 | Doc: "check that the file contains exactly one of all required definitions", 14 | Run: run, 15 | } 16 | } 17 | 18 | func requiredDefinitions() []dbc.Def { 19 | return []dbc.Def{ 20 | &dbc.BitTimingDef{}, 21 | &dbc.NodesDef{}, 22 | } 23 | } 24 | 25 | func run(pass *analysis.Pass) error { 26 | counts := make(map[reflect.Type]int) 27 | for _, def := range pass.File.Defs { 28 | counts[reflect.TypeOf(def)]++ 29 | } 30 | for _, requiredDef := range requiredDefinitions() { 31 | if counts[reflect.TypeOf(requiredDef)] == 0 { 32 | // we have no definition to return, so return the first 33 | pass.Reportf(pass.File.Defs[0].Position(), "missing required definition(s)") 34 | break 35 | } 36 | } 37 | return nil 38 | } 39 | -------------------------------------------------------------------------------- /pkg/dbc/analysis/passes/requireddefinitions/analyzer_test.go: -------------------------------------------------------------------------------- 1 | package requireddefinitions 2 | 3 | import ( 4 | "testing" 5 | "text/scanner" 6 | 7 | "go.einride.tech/can/pkg/dbc/analysis" 8 | "go.einride.tech/can/pkg/dbc/analysis/analysistest" 9 | ) 10 | 11 | func TestAnalyzer(t *testing.T) { 12 | analysistest.Run(t, Analyzer(), []*analysistest.Case{ 13 | { 14 | Name: "ok", 15 | Data: ` 16 | BS_: 17 | BU_: ECU1 18 | `, 19 | }, 20 | 21 | { 22 | Name: "missing bit timing", 23 | Data: ` 24 | BU_: ECU1 25 | `, 26 | Diagnostics: []*analysis.Diagnostic{ 27 | { 28 | Pos: scanner.Position{Line: 1, Column: 1}, 29 | Message: "missing required definition(s)", 30 | }, 31 | }, 32 | }, 33 | 34 | { 35 | Name: "missing nodes", 36 | Data: ` 37 | BS_: 38 | `, 39 | Diagnostics: []*analysis.Diagnostic{ 40 | { 41 | Pos: scanner.Position{Line: 1, Column: 1}, 42 | Message: "missing required definition(s)", 43 | }, 44 | }, 45 | }, 46 | }) 47 | } 48 | -------------------------------------------------------------------------------- /pkg/dbc/analysis/passes/signalbounds/analyzer.go: -------------------------------------------------------------------------------- 1 | package signalbounds 2 | 3 | import ( 4 | "go.einride.tech/can/pkg/dbc" 5 | "go.einride.tech/can/pkg/dbc/analysis" 6 | ) 7 | 8 | func Analyzer() *analysis.Analyzer { 9 | return &analysis.Analyzer{ 10 | Name: "signalbounds", 11 | Doc: "check that signal start and end bits are within bounds of the message size", 12 | Run: run, 13 | } 14 | } 15 | 16 | func run(pass *analysis.Pass) error { 17 | for _, def := range pass.File.Defs { 18 | message, ok := def.(*dbc.MessageDef) 19 | if !ok || dbc.IsIndependentSignalsMessage(message) { 20 | continue 21 | } 22 | for i := range message.Signals { 23 | signal := &message.Signals[i] 24 | if signal.StartBit >= 8*message.Size { 25 | pass.Reportf(signal.Pos, "start bit out of bounds") 26 | } 27 | // TODO: Check end bit 28 | } 29 | } 30 | return nil 31 | } 32 | -------------------------------------------------------------------------------- /pkg/dbc/analysis/passes/signalbounds/analyzer_test.go: -------------------------------------------------------------------------------- 1 | package signalbounds 2 | 3 | import ( 4 | "testing" 5 | "text/scanner" 6 | 7 | "go.einride.tech/can/pkg/dbc/analysis" 8 | "go.einride.tech/can/pkg/dbc/analysis/analysistest" 9 | ) 10 | 11 | func TestAnalyzer(t *testing.T) { 12 | analysistest.Run(t, Analyzer(), []*analysistest.Case{ 13 | { 14 | Name: "ok", 15 | Data: ` 16 | BO_ 500 IO_DEBUG: 4 IO 17 | SG_ IO_DEBUG_test_unsigned : 0|8@1+ (1,0) [0|0] "" DBG 18 | SG_ IO_DEBUG_test_enum : 8|8@1+ (1,0) [0|0] "" DBG 19 | SG_ IO_DEBUG_test_signed : 16|8@1- (1,0) [0|0] "" DBG 20 | SG_ IO_DEBUG_test_float : 24|8@1+ (0.5,0) [0|0] "" DBG 21 | `, 22 | }, 23 | 24 | { 25 | Name: "start bit out of bounds", 26 | Data: ` 27 | BO_ 500 IO_DEBUG: 4 IO 28 | SG_ IO_DEBUG_test_unsigned : 0|8@1+ (1,0) [0|0] "" DBG 29 | SG_ IO_DEBUG_test_enum : 8|8@1+ (1,0) [0|0] "" DBG 30 | SG_ IO_DEBUG_test_signed : 16|8@1- (1,0) [0|0] "" DBG 31 | SG_ IO_DEBUG_test_float : 32|8@1+ (0.5,0) [0|0] "" DBG 32 | `, 33 | Diagnostics: []*analysis.Diagnostic{ 34 | { 35 | Pos: scanner.Position{Line: 5, Column: 2}, 36 | Message: "start bit out of bounds", 37 | }, 38 | }, 39 | }, 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /pkg/dbc/analysis/passes/signalnames/analyzer.go: -------------------------------------------------------------------------------- 1 | package signalnames 2 | 3 | import ( 4 | "go.einride.tech/can/internal/identifiers" 5 | "go.einride.tech/can/pkg/dbc" 6 | "go.einride.tech/can/pkg/dbc/analysis" 7 | ) 8 | 9 | func Analyzer() *analysis.Analyzer { 10 | return &analysis.Analyzer{ 11 | Name: "signalnames", 12 | Doc: "check that signal names are valid CamelCase identifiers", 13 | Run: run, 14 | } 15 | } 16 | 17 | func run(pass *analysis.Pass) error { 18 | for _, d := range pass.File.Defs { 19 | messageDef, ok := d.(*dbc.MessageDef) 20 | if !ok { 21 | continue 22 | } 23 | for _, signalDef := range messageDef.Signals { 24 | if !identifiers.IsCamelCase(string(signalDef.Name)) { 25 | pass.Reportf(signalDef.Pos, "signal names must be CamelCase") 26 | } 27 | } 28 | } 29 | return nil 30 | } 31 | -------------------------------------------------------------------------------- /pkg/dbc/analysis/passes/signalnames/analyzer_test.go: -------------------------------------------------------------------------------- 1 | package signalnames 2 | 3 | import ( 4 | "testing" 5 | "text/scanner" 6 | 7 | "go.einride.tech/can/pkg/dbc/analysis" 8 | "go.einride.tech/can/pkg/dbc/analysis/analysistest" 9 | ) 10 | 11 | func TestAnalyzer(t *testing.T) { 12 | analysistest.Run(t, Analyzer(), []*analysistest.Case{ 13 | { 14 | Name: "ok", 15 | Data: ` 16 | BO_ 400 MotorStatus: 3 MOTOR 17 | SG_ HasWheelError : 0|1@1+ (1,0) [0|0] "" DRIVER,IO 18 | `, 19 | }, 20 | 21 | { 22 | Name: "not ok", 23 | Data: ` 24 | BO_ 400 MOTOR_STATUS: 3 MOTOR 25 | SG_ IS_OVERHEATED : 0|1@1+ (1,0) [0|0] "" DRIVER,IO 26 | `, 27 | Diagnostics: []*analysis.Diagnostic{ 28 | { 29 | Pos: scanner.Position{ 30 | Line: 2, 31 | Column: 2, 32 | }, 33 | Message: "signal names must be CamelCase", 34 | }, 35 | }, 36 | }, 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /pkg/dbc/analysis/passes/singletondefinitions/analyzer.go: -------------------------------------------------------------------------------- 1 | package singletondefinitions 2 | 3 | import ( 4 | "reflect" 5 | 6 | "go.einride.tech/can/pkg/dbc" 7 | "go.einride.tech/can/pkg/dbc/analysis" 8 | ) 9 | 10 | func Analyzer() *analysis.Analyzer { 11 | return &analysis.Analyzer{ 12 | Name: "singletondefinitions", 13 | Doc: "check that the file contains at most one of all singleton definitions", 14 | Run: run, 15 | } 16 | } 17 | 18 | func singletonDefinitions() []dbc.Def { 19 | return []dbc.Def{ 20 | &dbc.VersionDef{}, 21 | &dbc.NewSymbolsDef{}, 22 | &dbc.BitTimingDef{}, 23 | &dbc.NodesDef{}, 24 | } 25 | } 26 | 27 | func run(pass *analysis.Pass) error { 28 | defsByType := make(map[reflect.Type][]dbc.Def) 29 | for _, def := range pass.File.Defs { 30 | t := reflect.TypeOf(def) 31 | defsByType[t] = append(defsByType[t], def) 32 | } 33 | for _, singletonDef := range singletonDefinitions() { 34 | singletonDefs := defsByType[reflect.TypeOf(singletonDef)] 35 | if len(singletonDefs) > 1 { 36 | for i := 1; i < len(singletonDefs); i++ { 37 | pass.Reportf(singletonDefs[i].Position(), "more than one definition not allowed") 38 | } 39 | } 40 | } 41 | return nil 42 | } 43 | -------------------------------------------------------------------------------- /pkg/dbc/analysis/passes/singletondefinitions/analyzer_test.go: -------------------------------------------------------------------------------- 1 | package singletondefinitions 2 | 3 | import ( 4 | "testing" 5 | "text/scanner" 6 | 7 | "go.einride.tech/can/pkg/dbc/analysis" 8 | "go.einride.tech/can/pkg/dbc/analysis/analysistest" 9 | ) 10 | 11 | func TestAnalyzer(t *testing.T) { 12 | analysistest.Run(t, Analyzer(), []*analysistest.Case{ 13 | { 14 | Name: "ok", 15 | Data: ` 16 | VERSION "foo" 17 | NS_: 18 | BS_: 19 | BU_: ECU1 20 | `, 21 | }, 22 | 23 | { 24 | Name: "multiple versions", 25 | Data: ` 26 | VERSION "foo" 27 | VERSION "foo" 28 | NS_: 29 | BS_: 30 | BU_: ECU1 31 | `, 32 | Diagnostics: []*analysis.Diagnostic{ 33 | { 34 | Pos: scanner.Position{Line: 2, Column: 1}, 35 | Message: "more than one definition not allowed", 36 | }, 37 | }, 38 | }, 39 | 40 | { 41 | Name: "multiple new symbols", 42 | Data: ` 43 | VERSION "foo" 44 | NS_: 45 | NS_: 46 | BS_: 47 | BU_: ECU1 48 | `, 49 | Diagnostics: []*analysis.Diagnostic{ 50 | { 51 | Pos: scanner.Position{Line: 3, Column: 1}, 52 | Message: "more than one definition not allowed", 53 | }, 54 | }, 55 | }, 56 | 57 | { 58 | Name: "multiple bit timing", 59 | Data: ` 60 | VERSION "foo" 61 | NS_: 62 | BS_: 63 | BS_: 64 | BU_: ECU1 65 | `, 66 | Diagnostics: []*analysis.Diagnostic{ 67 | { 68 | Pos: scanner.Position{Line: 4, Column: 1}, 69 | Message: "more than one definition not allowed", 70 | }, 71 | }, 72 | }, 73 | 74 | { 75 | Name: "multiple nodes", 76 | Data: ` 77 | VERSION "foo" 78 | NS_: 79 | BS_: 80 | BU_: ECU1 81 | BU_: ECU2 82 | `, 83 | Diagnostics: []*analysis.Diagnostic{ 84 | { 85 | Pos: scanner.Position{Line: 5, Column: 1}, 86 | Message: "more than one definition not allowed", 87 | }, 88 | }, 89 | }, 90 | }) 91 | } 92 | -------------------------------------------------------------------------------- /pkg/dbc/analysis/passes/siunits/analyzer.go: -------------------------------------------------------------------------------- 1 | package siunits 2 | 3 | import ( 4 | "go.einride.tech/can/pkg/dbc" 5 | "go.einride.tech/can/pkg/dbc/analysis" 6 | ) 7 | 8 | func Analyzer() *analysis.Analyzer { 9 | return &analysis.Analyzer{ 10 | Name: "unitsuffixes", 11 | Doc: "check that signals with SI units have the correct symbols", 12 | Run: run, 13 | } 14 | } 15 | 16 | const ( 17 | metersPerSecond = "m/s" 18 | kilometersPerHour = "km/h" 19 | meters = "m" 20 | degrees = "°" 21 | radians = "rad" 22 | ) 23 | 24 | // symbolMap returns a map from non-standard unit symbols to SI unit symbols. 25 | func symbolMap() map[string]string { 26 | return map[string]string{ 27 | "kph": kilometersPerHour, 28 | "mps": metersPerSecond, 29 | "meters/sec": metersPerSecond, 30 | "meters": meters, 31 | "deg": degrees, 32 | "degrees": degrees, 33 | "radians": radians, 34 | } 35 | } 36 | 37 | func run(pass *analysis.Pass) error { 38 | symbols := symbolMap() 39 | for _, def := range pass.File.Defs { 40 | message, ok := def.(*dbc.MessageDef) 41 | if !ok { 42 | continue 43 | } 44 | for _, signal := range message.Signals { 45 | if symbol, ok := symbols[signal.Unit]; ok { 46 | pass.Reportf(signal.Pos, "signal with unit %s should have SI unit %s", signal.Unit, symbol) 47 | } 48 | } 49 | } 50 | return nil 51 | } 52 | -------------------------------------------------------------------------------- /pkg/dbc/analysis/passes/siunits/analyzer_test.go: -------------------------------------------------------------------------------- 1 | package siunits 2 | 3 | import ( 4 | "testing" 5 | "text/scanner" 6 | 7 | "go.einride.tech/can/pkg/dbc/analysis" 8 | "go.einride.tech/can/pkg/dbc/analysis/analysistest" 9 | ) 10 | 11 | func TestAnalyzer(t *testing.T) { 12 | analysistest.Run(t, Analyzer(), []*analysistest.Case{ 13 | { 14 | Name: "ok", 15 | Data: ` 16 | BO_ 400 TestMessage: 3 ECU1 17 | SG_ SpeedMps : 0|1@1+ (1,0) [0|0] "m/s" DRIVER,IO 18 | `, 19 | }, 20 | 21 | { 22 | Name: "not ok", 23 | Data: ` 24 | BO_ 400 TestMessage: 3 ECU1 25 | SG_ SpeedMps : 0|1@1+ (1,0) [0|0] "meters/sec" DRIVER,IO 26 | `, 27 | Diagnostics: []*analysis.Diagnostic{ 28 | { 29 | Pos: scanner.Position{Line: 2, Column: 2}, 30 | Message: "signal with unit meters/sec should have SI unit m/s", 31 | }, 32 | }, 33 | }, 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /pkg/dbc/analysis/passes/uniquemessageids/analyzer.go: -------------------------------------------------------------------------------- 1 | package uniquemessageids 2 | 3 | import ( 4 | "go.einride.tech/can/pkg/dbc" 5 | "go.einride.tech/can/pkg/dbc/analysis" 6 | ) 7 | 8 | func Analyzer() *analysis.Analyzer { 9 | return &analysis.Analyzer{ 10 | Name: "uniquemessageids", 11 | Doc: "check that all message IDs are unique", 12 | Run: run, 13 | } 14 | } 15 | 16 | func run(pass *analysis.Pass) error { 17 | messageIDs := make(map[dbc.MessageID]struct{}) 18 | for _, def := range pass.File.Defs { 19 | message, ok := def.(*dbc.MessageDef) 20 | if !ok || dbc.IsIndependentSignalsMessage(message) { 21 | continue 22 | } 23 | if _, ok := messageIDs[message.MessageID]; ok { 24 | pass.Reportf(message.Pos, "non-unique message ID") 25 | } else { 26 | messageIDs[message.MessageID] = struct{}{} 27 | } 28 | } 29 | return nil 30 | } 31 | -------------------------------------------------------------------------------- /pkg/dbc/analysis/passes/uniquemessageids/analyzer_test.go: -------------------------------------------------------------------------------- 1 | package uniquemessageids 2 | 3 | import ( 4 | "testing" 5 | "text/scanner" 6 | 7 | "go.einride.tech/can/pkg/dbc/analysis" 8 | "go.einride.tech/can/pkg/dbc/analysis/analysistest" 9 | ) 10 | 11 | func TestAnalyzer(t *testing.T) { 12 | analysistest.Run(t, Analyzer(), []*analysistest.Case{ 13 | { 14 | Name: "ok", 15 | Data: ` 16 | BO_ 101 MOTOR_CMD: 1 DRIVER 17 | SG_ MOTOR_CMD_steer : 0|4@1- (1,-5) [-5|5] "" MOTOR 18 | SG_ MOTOR_CMD_drive : 4|4@1+ (1,0) [0|9] "" MOTOR 19 | BO_ 102 MOTOR_CMD: 1 DRIVER 20 | SG_ MOTOR_CMD_steer : 0|4@1- (1,-5) [-5|5] "" MOTOR 21 | SG_ MOTOR_CMD_drive : 4|4@1+ (1,0) [0|9] "" MOTOR 22 | `, 23 | }, 24 | 25 | { 26 | Name: "duplicate", 27 | Data: ` 28 | BO_ 103 MOTOR_CMD: 1 DRIVER 29 | SG_ MOTOR_CMD_steer : 0|4@1- (1,-5) [-5|5] "" MOTOR 30 | SG_ MOTOR_CMD_steer : 4|4@1+ (1,0) [0|9] "" MOTOR 31 | BO_ 103 MOTOR_CMD: 1 DRIVER 32 | SG_ MOTOR_CMD_steer : 0|4@1- (1,-5) [-5|5] "" MOTOR 33 | SG_ MOTOR_CMD_steer : 4|4@1+ (1,0) [0|9] "" MOTOR 34 | `, 35 | Diagnostics: []*analysis.Diagnostic{ 36 | { 37 | Pos: scanner.Position{Line: 4, Column: 1}, 38 | Message: "non-unique message ID", 39 | }, 40 | }, 41 | }, 42 | }) 43 | } 44 | -------------------------------------------------------------------------------- /pkg/dbc/analysis/passes/uniquenodenames/analyzer.go: -------------------------------------------------------------------------------- 1 | package uniquenodenames 2 | 3 | import ( 4 | "go.einride.tech/can/pkg/dbc" 5 | "go.einride.tech/can/pkg/dbc/analysis" 6 | ) 7 | 8 | func Analyzer() *analysis.Analyzer { 9 | return &analysis.Analyzer{ 10 | Name: "uniquenodenames", 11 | Doc: "check that all declared node names are unique", 12 | Run: run, 13 | } 14 | } 15 | 16 | func run(pass *analysis.Pass) error { 17 | nodeNames := make(map[dbc.Identifier]struct{}) 18 | for _, def := range pass.File.Defs { 19 | if def, ok := def.(*dbc.NodesDef); ok { 20 | for _, nodeName := range def.NodeNames { 21 | if _, ok := nodeNames[nodeName]; ok { 22 | pass.Reportf(def.Pos, "non-unique node name") 23 | } 24 | nodeNames[nodeName] = struct{}{} 25 | } 26 | } 27 | } 28 | return nil 29 | } 30 | -------------------------------------------------------------------------------- /pkg/dbc/analysis/passes/uniquenodenames/analyzer_test.go: -------------------------------------------------------------------------------- 1 | package uniquenodenames 2 | 3 | import ( 4 | "testing" 5 | "text/scanner" 6 | 7 | "go.einride.tech/can/pkg/dbc/analysis" 8 | "go.einride.tech/can/pkg/dbc/analysis/analysistest" 9 | ) 10 | 11 | func TestAnalyzer(t *testing.T) { 12 | analysistest.Run(t, Analyzer(), []*analysistest.Case{ 13 | { 14 | Name: "ok", 15 | Data: `BU_: ECU1 ECU2 ECU3`, 16 | }, 17 | 18 | { 19 | Name: "duplicates", 20 | Data: `BU_: ECU1 ECU2 ECU3 ECU1`, 21 | Diagnostics: []*analysis.Diagnostic{ 22 | { 23 | Pos: scanner.Position{Line: 1, Column: 1}, 24 | Message: "non-unique node name", 25 | }, 26 | }, 27 | }, 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /pkg/dbc/analysis/passes/uniquesignalnames/analyzer.go: -------------------------------------------------------------------------------- 1 | package uniquesignalnames 2 | 3 | import ( 4 | "go.einride.tech/can/pkg/dbc" 5 | "go.einride.tech/can/pkg/dbc/analysis" 6 | ) 7 | 8 | func Analyzer() *analysis.Analyzer { 9 | return &analysis.Analyzer{ 10 | Name: "uniquesignalnames", 11 | Doc: "check that all signal names are unique", 12 | Run: run, 13 | } 14 | } 15 | 16 | func run(pass *analysis.Pass) error { 17 | for _, def := range pass.File.Defs { 18 | message, ok := def.(*dbc.MessageDef) 19 | if !ok || dbc.IsIndependentSignalsMessage(message) { 20 | continue 21 | } 22 | signalNames := make(map[dbc.Identifier]struct{}) 23 | for i := range message.Signals { 24 | signal := &message.Signals[i] 25 | if _, ok := signalNames[signal.Name]; ok { 26 | pass.Reportf(signal.Pos, "non-unique signal name") 27 | } else { 28 | signalNames[signal.Name] = struct{}{} 29 | } 30 | } 31 | } 32 | return nil 33 | } 34 | -------------------------------------------------------------------------------- /pkg/dbc/analysis/passes/uniquesignalnames/analyzer_test.go: -------------------------------------------------------------------------------- 1 | package uniquesignalnames 2 | 3 | import ( 4 | "testing" 5 | "text/scanner" 6 | 7 | "go.einride.tech/can/pkg/dbc/analysis" 8 | "go.einride.tech/can/pkg/dbc/analysis/analysistest" 9 | ) 10 | 11 | func TestAnalyzer(t *testing.T) { 12 | analysistest.Run(t, Analyzer(), []*analysistest.Case{ 13 | { 14 | Name: "ok", 15 | Data: ` 16 | BO_ 101 MOTOR_CMD: 1 DRIVER 17 | SG_ MOTOR_CMD_steer : 0|4@1- (1,-5) [-5|5] "" MOTOR 18 | SG_ MOTOR_CMD_drive : 4|4@1+ (1,0) [0|9] "" MOTOR 19 | `, 20 | }, 21 | 22 | { 23 | Name: "duplicate", 24 | Data: ` 25 | BO_ 101 MOTOR_CMD: 1 DRIVER 26 | SG_ MOTOR_CMD_steer : 0|4@1- (1,-5) [-5|5] "" MOTOR 27 | SG_ MOTOR_CMD_steer : 4|4@1+ (1,0) [0|9] "" MOTOR 28 | `, 29 | Diagnostics: []*analysis.Diagnostic{ 30 | { 31 | Pos: scanner.Position{Line: 3, Column: 2}, 32 | Message: "non-unique signal name", 33 | }, 34 | }, 35 | }, 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /pkg/dbc/analysis/passes/unitsuffixes/analyzer.go: -------------------------------------------------------------------------------- 1 | package unitsuffixes 2 | 3 | import ( 4 | "strings" 5 | 6 | "go.einride.tech/can/pkg/dbc" 7 | "go.einride.tech/can/pkg/dbc/analysis" 8 | ) 9 | 10 | func Analyzer() *analysis.Analyzer { 11 | return &analysis.Analyzer{ 12 | Name: "unitsuffixes", 13 | Doc: "check that signals with units have correct name suffixes", 14 | Run: run, 15 | } 16 | } 17 | 18 | func unitSuffixes() map[string]string { 19 | return map[string]string{ 20 | "°": "Degrees", 21 | "rad": "Radians", 22 | "%": "Percent", 23 | "km/h": "Kph", 24 | "m/s": "Mps", 25 | } 26 | } 27 | 28 | func run(pass *analysis.Pass) error { 29 | suffixes := unitSuffixes() 30 | for _, def := range pass.File.Defs { 31 | message, ok := def.(*dbc.MessageDef) 32 | if !ok { 33 | continue 34 | } 35 | for _, signal := range message.Signals { 36 | if suffix, ok := suffixes[signal.Unit]; ok { 37 | if !strings.HasSuffix(string(signal.Name), suffix) { 38 | pass.Reportf(signal.Pos, "signal with unit %s must have suffix %s", signal.Unit, suffix) 39 | } 40 | } 41 | } 42 | } 43 | return nil 44 | } 45 | -------------------------------------------------------------------------------- /pkg/dbc/analysis/passes/unitsuffixes/analyzer_test.go: -------------------------------------------------------------------------------- 1 | package unitsuffixes 2 | 3 | import ( 4 | "testing" 5 | "text/scanner" 6 | 7 | "go.einride.tech/can/pkg/dbc/analysis" 8 | "go.einride.tech/can/pkg/dbc/analysis/analysistest" 9 | ) 10 | 11 | func TestAnalyzer(t *testing.T) { 12 | analysistest.Run(t, Analyzer(), []*analysistest.Case{ 13 | { 14 | Name: "ok", 15 | Data: ` 16 | BO_ 400 TestMessage: 3 ECU1 17 | SG_ ValuePercent : 0|1@1+ (1,0) [0|0] "%" DRIVER,IO 18 | `, 19 | }, 20 | 21 | { 22 | Name: "not ok", 23 | Data: ` 24 | BO_ 400 TestMessage: 3 ECU1 25 | SG_ ValuePct : 0|1@1+ (1,0) [0|0] "%" DRIVER,IO 26 | `, 27 | Diagnostics: []*analysis.Diagnostic{ 28 | { 29 | Pos: scanner.Position{Line: 2, Column: 2}, 30 | Message: "signal with unit % must have suffix Percent", 31 | }, 32 | }, 33 | }, 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /pkg/dbc/analysis/passes/valuedescriptions/analyzer.go: -------------------------------------------------------------------------------- 1 | package valuedescriptions 2 | 3 | import ( 4 | "fmt" 5 | 6 | "go.einride.tech/can/internal/identifiers" 7 | "go.einride.tech/can/pkg/dbc" 8 | "go.einride.tech/can/pkg/dbc/analysis" 9 | ) 10 | 11 | func Analyzer() *analysis.Analyzer { 12 | return &analysis.Analyzer{ 13 | Name: "valuedescriptions", 14 | Doc: "check that value descriptions are valid CamelCase", 15 | Run: run, 16 | } 17 | } 18 | 19 | func run(pass *analysis.Pass) error { 20 | for _, def := range pass.File.Defs { 21 | var valueDescriptions []dbc.ValueDescriptionDef 22 | switch def := def.(type) { 23 | case *dbc.ValueTableDef: 24 | valueDescriptions = def.ValueDescriptions 25 | case *dbc.ValueDescriptionsDef: 26 | valueDescriptions = def.ValueDescriptions 27 | default: 28 | continue 29 | } 30 | for _, vd := range valueDescriptions { 31 | if !identifiers.IsCamelCase(vd.Description) { 32 | // Descriptor has format " " 33 | // 34 | // So we increase the column position by the size of value + 2 (space and quotes) so the lint 35 | // error marker is on the description and not on the value 36 | vd.Pos.Column += len(fmt.Sprintf("%d", int64(vd.Value))) + 2 37 | pass.Reportf(vd.Pos, "value description must be CamelCase (numbers ignored)") 38 | } 39 | } 40 | } 41 | return nil 42 | } 43 | -------------------------------------------------------------------------------- /pkg/dbc/analysis/passes/valuedescriptions/analyzer_test.go: -------------------------------------------------------------------------------- 1 | package valuedescriptions 2 | 3 | import ( 4 | "testing" 5 | "text/scanner" 6 | 7 | "go.einride.tech/can/pkg/dbc/analysis" 8 | "go.einride.tech/can/pkg/dbc/analysis/analysistest" 9 | ) 10 | 11 | func TestAnalyzer(t *testing.T) { 12 | analysistest.Run(t, Analyzer(), []*analysistest.Case{ 13 | { 14 | Name: "ok", 15 | Data: `VAL_ 100 Command 2 "Reboot" 1 "Sync" 0 "Noop";`, 16 | }, 17 | { 18 | Name: "ok", 19 | Data: `VAL_ 100 Command 2 "11Reboot" 1 "123" 0 "Noop";`, 20 | }, 21 | { 22 | Name: "underscore", 23 | Data: `VAL_ 100 Command 2 "Reboot_Command" 1 "Sync" 0 "Noop";`, 24 | Diagnostics: []*analysis.Diagnostic{ 25 | { 26 | Pos: scanner.Position{Line: 1, Column: 21}, 27 | Message: "value description must be CamelCase (numbers ignored)", 28 | }, 29 | }, 30 | }, 31 | { 32 | Name: "several digits value", 33 | Data: `VAL_ 100 Command 234 "Reboot_Command" 1 "Sync" 0 "Noop";`, 34 | Diagnostics: []*analysis.Diagnostic{ 35 | { 36 | Pos: scanner.Position{Line: 1, Column: 23}, 37 | Message: "value description must be CamelCase (numbers ignored)", 38 | }, 39 | }, 40 | }, 41 | }) 42 | } 43 | -------------------------------------------------------------------------------- /pkg/dbc/analysis/passes/version/analyzer.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "go.einride.tech/can/pkg/dbc" 5 | "go.einride.tech/can/pkg/dbc/analysis" 6 | ) 7 | 8 | func Analyzer() *analysis.Analyzer { 9 | return &analysis.Analyzer{ 10 | Name: "version", 11 | Doc: "check that the version definition is empty", 12 | Run: run, 13 | } 14 | } 15 | 16 | func run(pass *analysis.Pass) error { 17 | for _, def := range pass.File.Defs { 18 | versionDef, ok := def.(*dbc.VersionDef) 19 | if !ok { 20 | continue // not a version definition 21 | } 22 | if len(versionDef.Version) > 0 { 23 | pass.Reportf(versionDef.Pos, "version should be empty") 24 | } 25 | } 26 | return nil 27 | } 28 | -------------------------------------------------------------------------------- /pkg/dbc/analysis/passes/version/analyzer_test.go: -------------------------------------------------------------------------------- 1 | package version 2 | 3 | import ( 4 | "testing" 5 | "text/scanner" 6 | 7 | "go.einride.tech/can/pkg/dbc/analysis" 8 | "go.einride.tech/can/pkg/dbc/analysis/analysistest" 9 | ) 10 | 11 | func TestAnalyzer(t *testing.T) { 12 | analysistest.Run(t, Analyzer(), []*analysistest.Case{ 13 | { 14 | Name: "ok", 15 | Data: `VERSION ""`, 16 | }, 17 | 18 | { 19 | Name: "not ok", 20 | Data: `VERSION "foo"`, 21 | Diagnostics: []*analysis.Diagnostic{ 22 | { 23 | Pos: scanner.Position{Line: 1, Column: 1}, 24 | Message: "version should be empty", 25 | }, 26 | }, 27 | }, 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /pkg/dbc/attributevaluetype.go: -------------------------------------------------------------------------------- 1 | package dbc 2 | 3 | import "fmt" 4 | 5 | // AttributeValueType represents an attribute value type. 6 | type AttributeValueType string 7 | 8 | const ( 9 | AttributeValueTypeInt AttributeValueType = "INT" 10 | AttributeValueTypeHex AttributeValueType = "HEX" 11 | AttributeValueTypeFloat AttributeValueType = "FLOAT" 12 | AttributeValueTypeString AttributeValueType = "STRING" 13 | AttributeValueTypeEnum AttributeValueType = "ENUM" 14 | ) 15 | 16 | // Validate returns an error for invalid attribute value types. 17 | func (a AttributeValueType) Validate() error { 18 | switch a { 19 | case AttributeValueTypeInt: 20 | case AttributeValueTypeHex: 21 | case AttributeValueTypeFloat: 22 | case AttributeValueTypeString: 23 | case AttributeValueTypeEnum: 24 | default: 25 | return fmt.Errorf("invalid attribute value type: %v", a) 26 | } 27 | return nil 28 | } 29 | -------------------------------------------------------------------------------- /pkg/dbc/attributevaluetype_test.go: -------------------------------------------------------------------------------- 1 | package dbc 2 | 3 | import ( 4 | "testing" 5 | 6 | "gotest.tools/v3/assert" 7 | ) 8 | 9 | func TestAttributeValueType_Validate(t *testing.T) { 10 | for _, tt := range []AttributeValueType{ 11 | AttributeValueTypeInt, 12 | AttributeValueTypeHex, 13 | AttributeValueTypeFloat, 14 | AttributeValueTypeString, 15 | AttributeValueTypeEnum, 16 | } { 17 | assert.NilError(t, tt.Validate()) 18 | } 19 | } 20 | 21 | func TestAttributeValueType_Validate_Error(t *testing.T) { 22 | assert.ErrorContains(t, AttributeValueType("foo").Validate(), "invalid attribute value type") 23 | } 24 | -------------------------------------------------------------------------------- /pkg/dbc/doc.go: -------------------------------------------------------------------------------- 1 | // Package dbc provides primitives for parsing, formatting and linting DBC files. 2 | // 3 | // The implementation adheres to the "DBC File Format Documentation Version 01/2007" unless specified otherwise. 4 | package dbc 5 | -------------------------------------------------------------------------------- /pkg/dbc/envvartype.go: -------------------------------------------------------------------------------- 1 | package dbc 2 | 3 | import "fmt" 4 | 5 | // EnvironmentVariableType represents the type of an environment variable. 6 | type EnvironmentVariableType uint64 7 | 8 | const ( 9 | EnvironmentVariableTypeInteger EnvironmentVariableType = 0 10 | EnvironmentVariableTypeFloat EnvironmentVariableType = 1 11 | EnvironmentVariableTypeString EnvironmentVariableType = 2 12 | ) 13 | 14 | // Validate returns an error for invalid environment variable types. 15 | func (e EnvironmentVariableType) Validate() error { 16 | switch e { 17 | case EnvironmentVariableTypeInteger: 18 | case EnvironmentVariableTypeFloat: 19 | case EnvironmentVariableTypeString: 20 | default: 21 | return fmt.Errorf("invalid environment variable type: %v", e) 22 | } 23 | return nil 24 | } 25 | -------------------------------------------------------------------------------- /pkg/dbc/envvartype_test.go: -------------------------------------------------------------------------------- 1 | package dbc 2 | 3 | import ( 4 | "testing" 5 | 6 | "gotest.tools/v3/assert" 7 | ) 8 | 9 | func TestEnvironmentVariableType_Validate(t *testing.T) { 10 | for _, tt := range []EnvironmentVariableType{ 11 | EnvironmentVariableTypeInteger, 12 | EnvironmentVariableTypeFloat, 13 | EnvironmentVariableTypeString, 14 | } { 15 | assert.NilError(t, tt.Validate()) 16 | } 17 | } 18 | 19 | func TestEnvironmentVariableType_Validate_Error(t *testing.T) { 20 | assert.Error(t, EnvironmentVariableType(42).Validate(), "invalid environment variable type: 42") 21 | } 22 | -------------------------------------------------------------------------------- /pkg/dbc/error.go: -------------------------------------------------------------------------------- 1 | package dbc 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "text/scanner" 7 | ) 8 | 9 | // Error represents an error in a DBC file. 10 | type Error interface { 11 | error 12 | 13 | // Position of the error in the DBC file. 14 | Position() scanner.Position 15 | 16 | // Reason for the error. 17 | Reason() string 18 | } 19 | 20 | // validationError is an error resulting from an invalid DBC definition. 21 | type validationError struct { 22 | pos scanner.Position 23 | reason string 24 | cause error 25 | } 26 | 27 | func (e *validationError) Unwrap() error { 28 | return e.cause 29 | } 30 | 31 | var _ Error = &validationError{} 32 | 33 | func (e *validationError) Error() string { 34 | return fmt.Sprintf("%v: %s (validate)", e.Position(), e.reason) 35 | } 36 | 37 | // Reason returns the reason for the error. 38 | func (e *validationError) Reason() string { 39 | return e.reason 40 | } 41 | 42 | // Position returns the position of the validation error in the DBC file. 43 | // 44 | // When the validation error results from an invalid nested definition, the position of the nested definition is 45 | // returned. 46 | func (e *validationError) Position() scanner.Position { 47 | var errValidation *validationError 48 | if errors.As(e.cause, &errValidation) { 49 | return errValidation.Position() 50 | } 51 | return e.pos 52 | } 53 | 54 | // parseError is an error resulting from a failure to parse a DBC file. 55 | type parseError struct { 56 | pos scanner.Position 57 | reason string 58 | } 59 | 60 | var _ Error = &parseError{} 61 | 62 | func (e *parseError) Error() string { 63 | return fmt.Sprintf("%v: %s (parse)", e.pos, e.reason) 64 | } 65 | 66 | // Reason returns the reason for the error. 67 | func (e *parseError) Reason() string { 68 | return e.reason 69 | } 70 | 71 | // Position returns the position of the parse error in the DBC file. 72 | func (e *parseError) Position() scanner.Position { 73 | return e.pos 74 | } 75 | -------------------------------------------------------------------------------- /pkg/dbc/file.go: -------------------------------------------------------------------------------- 1 | package dbc 2 | 3 | // File is a parsed DBC source file. 4 | type File struct { 5 | // Name of the file. 6 | Name string 7 | // Data contains the raw file data. 8 | Data []byte 9 | // Defs in the file. 10 | Defs []Def 11 | } 12 | -------------------------------------------------------------------------------- /pkg/dbc/identifier.go: -------------------------------------------------------------------------------- 1 | package dbc 2 | 3 | import ( 4 | "fmt" 5 | 6 | "go.einride.tech/can/internal/identifiers" 7 | ) 8 | 9 | // Identifier represents a DBC identifier. 10 | type Identifier string 11 | 12 | // maxIdentifierLength is the length of the longest valid DBC identifier. 13 | const maxIdentifierLength = 128 14 | 15 | // Validate returns an error for invalid DBC identifiers. 16 | func (id Identifier) Validate() (err error) { 17 | defer func() { 18 | if err != nil { 19 | err = fmt.Errorf("invalid identifier '%s': %w", id, err) 20 | } 21 | }() 22 | if len(id) == 0 { 23 | return fmt.Errorf("zero-length") 24 | } 25 | if len(id) > maxIdentifierLength { 26 | return fmt.Errorf("length %v exceeds max length %v", len(id), maxIdentifierLength) 27 | } 28 | for i, r := range id { 29 | if i == 0 && r != '_' && !identifiers.IsAlphaChar(r) { // first char 30 | return fmt.Errorf("invalid first char: '%v'", r) 31 | } else if i > 0 && r != '_' && !identifiers.IsAlphaChar(r) && !identifiers.IsNumChar(r) { 32 | return fmt.Errorf("invalid char: '%v'", r) 33 | } 34 | } 35 | return nil 36 | } 37 | -------------------------------------------------------------------------------- /pkg/dbc/identifier_test.go: -------------------------------------------------------------------------------- 1 | package dbc 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "testing" 7 | 8 | "gotest.tools/v3/assert" 9 | ) 10 | 11 | func TestIdentifier_Validate(t *testing.T) { 12 | for _, tt := range []Identifier{ 13 | "_", 14 | "_foo", 15 | "foo", 16 | "foo32", 17 | "_43", 18 | Identifier(strings.Repeat("a", maxIdentifierLength)), 19 | } { 20 | t.Run(fmt.Sprintf("%v", tt), func(t *testing.T) { 21 | assert.NilError(t, tt.Validate()) 22 | }) 23 | } 24 | } 25 | 26 | func TestIdentifier_Validate_Error(t *testing.T) { 27 | for _, tt := range []Identifier{ 28 | "42", 29 | "", 30 | "42foo", 31 | "☃", 32 | "foo☃", 33 | "foo bar", 34 | Identifier(strings.Repeat("a", maxIdentifierLength+1)), 35 | } { 36 | t.Run(fmt.Sprintf("%v", tt), func(t *testing.T) { 37 | assert.ErrorContains(t, tt.Validate(), "invalid identifier") 38 | }) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /pkg/dbc/independent_signals.go: -------------------------------------------------------------------------------- 1 | package dbc 2 | 3 | // Independent signals constants. 4 | // 5 | // DBC files may contain a special message with the following message name and message ID. 6 | // 7 | // This message will have size 0 and may contain duplicate signal names. 8 | const ( 9 | // IndependentSignalsMessageName is the message name used by the special independent signals message. 10 | IndependentSignalsMessageName Identifier = "VECTOR__INDEPENDENT_SIG_MSG" 11 | // IndependentSignalsMessageName is the message ID used by the special independent signals message. 12 | IndependentSignalsMessageID MessageID = 0xc0000000 13 | // IndependentSignalsMessageSize is the size used by the special independent signals message. 14 | IndependentSignalsMessageSize = 0 15 | ) 16 | 17 | // IsIndependentSignalsMessage returns true if m is the special independent signals message. 18 | func IsIndependentSignalsMessage(m *MessageDef) bool { 19 | return m.Name == IndependentSignalsMessageName && 20 | m.MessageID == IndependentSignalsMessageID && 21 | m.Size == IndependentSignalsMessageSize 22 | } 23 | -------------------------------------------------------------------------------- /pkg/dbc/keyword.go: -------------------------------------------------------------------------------- 1 | package dbc 2 | 3 | // Keyword represents a DBC keyword. 4 | type Keyword string 5 | 6 | const ( 7 | KeywordAttribute Keyword = "BA_DEF_" 8 | KeywordAttributeDefault Keyword = "BA_DEF_DEF_" 9 | KeywordAttributeValue Keyword = "BA_" 10 | KeywordBitTiming Keyword = "BS_" 11 | KeywordComment Keyword = "CM_" 12 | KeywordEnvironmentVariable Keyword = "EV_" 13 | KeywordEnvironmentVariableData Keyword = "ENVVAR_DATA_" 14 | KeywordMessage Keyword = "BO_" 15 | KeywordMessageTransmitters Keyword = "BO_TX_BU_" 16 | KeywordNewSymbols Keyword = "NS_" 17 | KeywordNodes Keyword = "BU_" 18 | KeywordSignal Keyword = "SG_" 19 | KeywordSignalGroup Keyword = "SIG_GROUP_" 20 | KeywordSignalType Keyword = "SGTYPE_" 21 | KeywordSignalValueType Keyword = "SIG_VALTYPE_" 22 | KeywordValueDescriptions Keyword = "VAL_" 23 | KeywordValueTable Keyword = "VAL_TABLE_" 24 | KeywordVersion Keyword = "VERSION" 25 | ) 26 | -------------------------------------------------------------------------------- /pkg/dbc/messageid.go: -------------------------------------------------------------------------------- 1 | package dbc 2 | 3 | import "fmt" 4 | 5 | // MessageID represents a message ID. 6 | type MessageID uint32 7 | 8 | // ID constants. 9 | const ( 10 | // maxID is the largest valid standard CAN ID. 11 | maxID = 0x7ff 12 | // maxExtendedID is the largest valid extended CAN ID. 13 | maxExtendedID = 0x1fffffff 14 | ) 15 | 16 | // messageIDExtendedFlag is a bit flag that is set for extended message IDs. 17 | const messageIDExtendedFlag MessageID = 0x80000000 18 | 19 | // messageIDIndependentSignals is a special message ID used for the "independent signals" message. 20 | const messageIDIndependentSignals MessageID = 0xc0000000 21 | 22 | // IsExtended returns true if the message ID is an extended CAN ID. 23 | func (m MessageID) IsExtended() bool { 24 | return m != messageIDIndependentSignals && m&messageIDExtendedFlag > 0 25 | } 26 | 27 | // ToCAN returns the CAN id value of the message ID (i.e. with bit flags removed). 28 | func (m MessageID) ToCAN() uint32 { 29 | return uint32(m &^ messageIDExtendedFlag) 30 | } 31 | 32 | // Validate returns an error for invalid message IDs. 33 | func (m MessageID) Validate() error { 34 | if m == messageIDIndependentSignals { 35 | return nil 36 | } 37 | if m.IsExtended() && m.ToCAN() > maxExtendedID { 38 | return fmt.Errorf("invalid extended ID: %v", m) 39 | } 40 | if !m.IsExtended() && m.ToCAN() > maxID { 41 | return fmt.Errorf("invalid standard ID: %v", m) 42 | } 43 | return nil 44 | } 45 | -------------------------------------------------------------------------------- /pkg/dbc/messageid_test.go: -------------------------------------------------------------------------------- 1 | package dbc 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "gotest.tools/v3/assert" 8 | ) 9 | 10 | func TestMessageID_Validate(t *testing.T) { 11 | for _, tt := range []MessageID{ 12 | 0, 13 | 1, 14 | maxID, 15 | 0 | messageIDExtendedFlag, 16 | 1 | messageIDExtendedFlag, 17 | maxID | messageIDExtendedFlag, 18 | maxExtendedID | messageIDExtendedFlag, 19 | messageIDIndependentSignals, 20 | } { 21 | t.Run(fmt.Sprintf("%v", tt), func(t *testing.T) { 22 | assert.NilError(t, tt.Validate()) 23 | }) 24 | } 25 | } 26 | 27 | func TestMessageID_Validate_Error(t *testing.T) { 28 | for _, tt := range []MessageID{ 29 | maxID + 1, 30 | (maxExtendedID + 1) | messageIDExtendedFlag, 31 | 0xffffffff, 32 | } { 33 | t.Run(fmt.Sprintf("%v", tt), func(t *testing.T) { 34 | assert.ErrorContains(t, tt.Validate(), "invalid") 35 | }) 36 | } 37 | } 38 | 39 | func TestMessageID_ToCAN(t *testing.T) { 40 | for _, tt := range []struct { 41 | messageID MessageID 42 | expected uint32 43 | }{ 44 | {messageID: 1, expected: 1}, 45 | {messageID: messageIDIndependentSignals, expected: 0x40000000}, 46 | {messageID: 2566857156, expected: 419373508}, 47 | } { 48 | t.Run(fmt.Sprintf("%v", tt.messageID), func(t *testing.T) { 49 | assert.Equal(t, tt.expected, tt.messageID.ToCAN()) 50 | }) 51 | } 52 | } 53 | 54 | func TestMessageID_IsExtended(t *testing.T) { 55 | for _, tt := range []struct { 56 | messageID MessageID 57 | expected bool 58 | }{ 59 | {messageID: 1, expected: false}, 60 | {messageID: messageIDIndependentSignals, expected: false}, 61 | {messageID: 2566857156, expected: true}, 62 | } { 63 | t.Run(fmt.Sprintf("%v", tt.messageID), func(t *testing.T) { 64 | assert.Equal(t, tt.expected, tt.messageID.IsExtended()) 65 | }) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /pkg/dbc/objecttype.go: -------------------------------------------------------------------------------- 1 | package dbc 2 | 3 | import "fmt" 4 | 5 | // ObjectType identifies the type of a DBC object. 6 | type ObjectType string 7 | 8 | const ( 9 | ObjectTypeUnspecified ObjectType = "" 10 | ObjectTypeNetworkNode ObjectType = "BU_" 11 | ObjectTypeMessage ObjectType = "BO_" 12 | ObjectTypeSignal ObjectType = "SG_" 13 | ObjectTypeEnvironmentVariable ObjectType = "EV_" 14 | ) 15 | 16 | // Validate returns an error for invalid object types. 17 | func (o ObjectType) Validate() error { 18 | switch o { 19 | case ObjectTypeUnspecified: 20 | case ObjectTypeNetworkNode: 21 | case ObjectTypeMessage: 22 | case ObjectTypeSignal: 23 | case ObjectTypeEnvironmentVariable: 24 | default: 25 | return fmt.Errorf("invalid object type: %v", o) 26 | } 27 | return nil 28 | } 29 | -------------------------------------------------------------------------------- /pkg/dbc/objecttype_test.go: -------------------------------------------------------------------------------- 1 | package dbc 2 | 3 | import ( 4 | "testing" 5 | 6 | "gotest.tools/v3/assert" 7 | ) 8 | 9 | func TestObjectType_Validate(t *testing.T) { 10 | for _, tt := range []ObjectType{ 11 | ObjectTypeUnspecified, 12 | ObjectTypeNetworkNode, 13 | ObjectTypeMessage, 14 | ObjectTypeSignal, 15 | ObjectTypeEnvironmentVariable, 16 | } { 17 | assert.NilError(t, tt.Validate()) 18 | } 19 | } 20 | 21 | func TestObjectType_Validate_Error(t *testing.T) { 22 | assert.ErrorContains(t, ObjectType("foo").Validate(), "invalid object type") 23 | } 24 | -------------------------------------------------------------------------------- /pkg/dbc/placeholder.go: -------------------------------------------------------------------------------- 1 | package dbc 2 | 3 | // NodePlaceholder is the placeholder node name used when no actual node is specified. 4 | const NodePlaceholder Identifier = "Vector__XXX" 5 | -------------------------------------------------------------------------------- /pkg/dbc/signalvaluetype.go: -------------------------------------------------------------------------------- 1 | package dbc 2 | 3 | import "fmt" 4 | 5 | // SignalValueType represents an extended signal value type. 6 | type SignalValueType uint64 7 | 8 | const ( 9 | SignalValueTypeInt SignalValueType = 0 10 | SignalValueTypeFloat32 SignalValueType = 1 11 | SignalValueTypeFloat64 SignalValueType = 2 12 | ) 13 | 14 | // Validate returns an error for invalid signal value types. 15 | func (s SignalValueType) Validate() error { 16 | switch s { 17 | case SignalValueTypeInt: 18 | case SignalValueTypeFloat32: 19 | case SignalValueTypeFloat64: 20 | default: 21 | return fmt.Errorf("invalid signal value type: %v", s) 22 | } 23 | return nil 24 | } 25 | -------------------------------------------------------------------------------- /pkg/dbc/signalvaluetype_test.go: -------------------------------------------------------------------------------- 1 | package dbc 2 | 3 | import ( 4 | "testing" 5 | 6 | "gotest.tools/v3/assert" 7 | ) 8 | 9 | func TestSignalValueType_Validate(t *testing.T) { 10 | for _, tt := range []SignalValueType{ 11 | SignalValueTypeInt, 12 | SignalValueTypeFloat32, 13 | SignalValueTypeFloat64, 14 | } { 15 | assert.NilError(t, tt.Validate()) 16 | } 17 | } 18 | 19 | func TestSignalValueType_Validate_Error(t *testing.T) { 20 | assert.Error(t, SignalValueType(42).Validate(), "invalid signal value type: 42") 21 | } 22 | -------------------------------------------------------------------------------- /pkg/descriptor/database.go: -------------------------------------------------------------------------------- 1 | package descriptor 2 | 3 | import ( 4 | "path" 5 | "strings" 6 | ) 7 | 8 | // Database represents a CAN database. 9 | type Database struct { 10 | // SourceFile of the database. 11 | // 12 | // Example: 13 | // github.com/einride/can-databases/dbc/j1939.dbc 14 | SourceFile string 15 | // Version of the database. 16 | Version string 17 | // Messages in the database. 18 | Messages []*Message 19 | // Nodes in the database. 20 | Nodes []*Node 21 | } 22 | 23 | func (d *Database) Node(nodeName string) (*Node, bool) { 24 | for _, n := range d.Nodes { 25 | if n.Name == nodeName { 26 | return n, true 27 | } 28 | } 29 | return nil, false 30 | } 31 | 32 | func (d *Database) Message(id uint32) (*Message, bool) { 33 | for _, m := range d.Messages { 34 | if m.ID == id { 35 | return m, true 36 | } 37 | } 38 | return nil, false 39 | } 40 | 41 | func (d *Database) Signal(messageID uint32, signalName string) (*Signal, bool) { 42 | message, ok := d.Message(messageID) 43 | if !ok { 44 | return nil, false 45 | } 46 | for _, s := range message.Signals { 47 | if s.Name == signalName { 48 | return s, true 49 | } 50 | } 51 | return nil, false 52 | } 53 | 54 | // Description returns the name of the Database. 55 | func (d *Database) Name() string { 56 | return strings.TrimSuffix(path.Base(d.SourceFile), path.Ext(d.SourceFile)) 57 | } 58 | -------------------------------------------------------------------------------- /pkg/descriptor/message.go: -------------------------------------------------------------------------------- 1 | package descriptor 2 | 3 | import "time" 4 | 5 | // Message describes a CAN message. 6 | type Message struct { 7 | // Description of the message. 8 | Name string 9 | // ID of the message. 10 | ID uint32 11 | // IsExtended is true if the message is an extended CAN message. 12 | IsExtended bool 13 | // Length in bytes. 14 | Length uint8 15 | // SendType is the message's send type. 16 | SendType SendType 17 | // Description of the message. 18 | Description string 19 | // Signals in the message payload. 20 | Signals []*Signal 21 | // SenderNode is the name of the node sending the message. 22 | SenderNode string 23 | // CycleTime is the cycle time of a cyclic message. 24 | CycleTime time.Duration 25 | // DelayTime is the allowed delay between cyclic message sends. 26 | DelayTime time.Duration 27 | } 28 | 29 | // MultiplexerSignal returns the message's multiplexer signal. 30 | func (m *Message) MultiplexerSignal() (*Signal, bool) { 31 | for _, s := range m.Signals { 32 | if s.IsMultiplexer { 33 | return s, true 34 | } 35 | } 36 | return nil, false 37 | } 38 | -------------------------------------------------------------------------------- /pkg/descriptor/message_test.go: -------------------------------------------------------------------------------- 1 | package descriptor 2 | 3 | import ( 4 | "testing" 5 | 6 | "gotest.tools/v3/assert" 7 | is "gotest.tools/v3/assert/cmp" 8 | ) 9 | 10 | func TestMessage_MultiplexerSignal(t *testing.T) { 11 | mux := &Signal{ 12 | Name: "Mux", 13 | IsMultiplexer: true, 14 | } 15 | m := &Message{ 16 | Signals: []*Signal{ 17 | {Name: "NotMux"}, 18 | mux, 19 | {Name: "AlsoNotMux"}, 20 | }, 21 | } 22 | actualMux, ok := m.MultiplexerSignal() 23 | assert.Assert(t, ok) 24 | assert.DeepEqual(t, mux, actualMux) 25 | } 26 | 27 | func TestMessage_MultiplexerSignal_NotFound(t *testing.T) { 28 | m := &Message{ 29 | Signals: []*Signal{ 30 | {Name: "NotMux"}, 31 | {Name: "AlsoNotMux"}, 32 | }, 33 | } 34 | actualMux, ok := m.MultiplexerSignal() 35 | assert.Assert(t, !ok) 36 | assert.Assert(t, is.Nil(actualMux)) 37 | } 38 | -------------------------------------------------------------------------------- /pkg/descriptor/node.go: -------------------------------------------------------------------------------- 1 | package descriptor 2 | 3 | // Node describes a CAN node. 4 | type Node struct { 5 | // Description of the CAN node. 6 | Name string 7 | // Description of the CAN node. 8 | Description string 9 | } 10 | -------------------------------------------------------------------------------- /pkg/descriptor/sendtype.go: -------------------------------------------------------------------------------- 1 | package descriptor 2 | 3 | import "strings" 4 | 5 | // SendType represents the send type of a message. 6 | type SendType uint8 7 | 8 | //go:generate stringer -type SendType -trimprefix SendType 9 | 10 | const ( 11 | // SendTypeNone means the send type is unknown or not specified. 12 | SendTypeNone SendType = iota 13 | // SendTypeCyclic means the message is sent cyclically. 14 | SendTypeCyclic 15 | // SendTypeEvent means the message is only sent upon event or request. 16 | SendTypeEvent 17 | ) 18 | 19 | // UnmarshalString sets the value of *s from the provided string. 20 | func (s *SendType) UnmarshalString(str string) error { 21 | // TODO: Decide on conventions and make this more strict 22 | switch strings.ToLower(str) { 23 | case "cyclic", "cyclicifactive", "periodic", "fixedperiodic", "enabledperiodic", "eventperiodic": 24 | *s = SendTypeCyclic 25 | case "event", "onevent": 26 | *s = SendTypeEvent 27 | default: 28 | *s = SendTypeNone 29 | } 30 | return nil 31 | } 32 | -------------------------------------------------------------------------------- /pkg/descriptor/sendtype_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type SendType -trimprefix SendType"; DO NOT EDIT. 2 | 3 | package descriptor 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[SendTypeNone-0] 12 | _ = x[SendTypeCyclic-1] 13 | _ = x[SendTypeEvent-2] 14 | } 15 | 16 | const _SendType_name = "NoneCyclicEvent" 17 | 18 | var _SendType_index = [...]uint8{0, 4, 10, 15} 19 | 20 | func (i SendType) String() string { 21 | if i >= SendType(len(_SendType_index)-1) { 22 | return "SendType(" + strconv.FormatInt(int64(i), 10) + ")" 23 | } 24 | return _SendType_name[_SendType_index[i]:_SendType_index[i+1]] 25 | } 26 | -------------------------------------------------------------------------------- /pkg/descriptor/sendtype_test.go: -------------------------------------------------------------------------------- 1 | package descriptor 2 | 3 | import ( 4 | "testing" 5 | 6 | "gotest.tools/v3/assert" 7 | ) 8 | 9 | func TestSendType_UnmarshalString(t *testing.T) { 10 | for _, tt := range []struct { 11 | str string 12 | expected SendType 13 | }{ 14 | {str: "Cyclic", expected: SendTypeCyclic}, 15 | {str: "Periodic", expected: SendTypeCyclic}, 16 | {str: "OnEvent", expected: SendTypeEvent}, 17 | {str: "Event", expected: SendTypeEvent}, 18 | } { 19 | t.Run(tt.str, func(t *testing.T) { 20 | var actual SendType 21 | assert.NilError(t, actual.UnmarshalString(tt.str)) 22 | assert.Equal(t, tt.expected, actual) 23 | }) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /pkg/descriptor/signal_test.go: -------------------------------------------------------------------------------- 1 | package descriptor 2 | 3 | import ( 4 | "math" 5 | "testing" 6 | 7 | "go.einride.tech/can" 8 | "gotest.tools/v3/assert" 9 | ) 10 | 11 | func TestSignal_FromPhysical_SaturatedCast(t *testing.T) { 12 | s := &Signal{ 13 | Name: "TestSignal", 14 | Offset: -1, 15 | Scale: 3.0517578125e-05, 16 | Min: -1, 17 | Max: 1, 18 | Length: 16, 19 | } 20 | // without a saturated cast, the result would be math.MaxUint16 + 1, which would wrap around to 0 21 | assert.Equal(t, uint16(math.MaxUint16), uint16(s.FromPhysical(180))) 22 | } 23 | 24 | func TestSignal_SaturatedCastSigned(t *testing.T) { 25 | s := &Signal{ 26 | Name: "TestSignal", 27 | IsSigned: true, 28 | Length: 6, 29 | } 30 | assert.Equal(t, int64(31), s.SaturatedCastSigned(254)) 31 | assert.Equal(t, int64(-32), s.SaturatedCastSigned(-255)) 32 | } 33 | 34 | func TestSignal_SaturatedCastUnsigned(t *testing.T) { 35 | s := &Signal{ 36 | Name: "TestSignal", 37 | Length: 6, 38 | } 39 | assert.Equal(t, uint64(63), s.SaturatedCastUnsigned(255)) 40 | } 41 | 42 | func TestSignal_UnmarshalSigned_BigEndian(t *testing.T) { 43 | s := &Signal{ 44 | Name: "TestSignal", 45 | IsSigned: true, 46 | IsBigEndian: true, 47 | Length: 8, 48 | Start: 32, 49 | } 50 | const value int64 = -8 51 | var data can.Data 52 | data.SetSignedBitsBigEndian(s.Start, s.Length, value) 53 | assert.Equal(t, value, s.UnmarshalSigned(data)) 54 | } 55 | 56 | func TestSignal_MarshalUnsigned_BigEndian(t *testing.T) { 57 | s := &Signal{ 58 | Name: "TestSignal", 59 | IsBigEndian: true, 60 | Length: 8, 61 | Start: 32, 62 | } 63 | const value uint64 = 8 64 | var expected can.Data 65 | expected.SetUnsignedBitsBigEndian(s.Start, s.Length, value) 66 | var actual can.Data 67 | s.MarshalUnsigned(&actual, value) 68 | assert.DeepEqual(t, expected, actual) 69 | } 70 | 71 | func TestSignal_MarshalSigned_BigEndian(t *testing.T) { 72 | s := &Signal{ 73 | Name: "TestSignal", 74 | IsSigned: true, 75 | IsBigEndian: true, 76 | Length: 8, 77 | Start: 32, 78 | } 79 | const value int64 = -8 80 | var expected can.Data 81 | expected.SetSignedBitsBigEndian(s.Start, s.Length, value) 82 | var actual can.Data 83 | s.MarshalSigned(&actual, value) 84 | assert.DeepEqual(t, expected, actual) 85 | } 86 | -------------------------------------------------------------------------------- /pkg/descriptor/valuedescription.go: -------------------------------------------------------------------------------- 1 | package descriptor 2 | 3 | type ValueDescription struct { 4 | Value int64 5 | Description string 6 | } 7 | -------------------------------------------------------------------------------- /pkg/generated/message.go: -------------------------------------------------------------------------------- 1 | // Package generated provides primitives for working with code-generated CAN messages. 2 | package generated 3 | 4 | import ( 5 | "fmt" 6 | 7 | "go.einride.tech/can" 8 | "go.einride.tech/can/pkg/descriptor" 9 | ) 10 | 11 | // Message represents a code-generated CAN message. 12 | type Message interface { 13 | can.Message 14 | fmt.Stringer 15 | 16 | // Descriptor returns the message descriptor. 17 | Descriptor() *descriptor.Message 18 | 19 | // Reset the message signals to their default values. 20 | Reset() 21 | 22 | // Frame returns a CAN frame representing the message. 23 | // 24 | // A generated message ensures that its signals are valid and is always convertible to a CAN frame. 25 | Frame() can.Frame 26 | } 27 | -------------------------------------------------------------------------------- /pkg/socketcan/canrawaddr.go: -------------------------------------------------------------------------------- 1 | package socketcan 2 | 3 | import "net" 4 | 5 | const canRawNetwork = "can" 6 | 7 | // canRawAddr represents a CAN_RAW address. 8 | type canRawAddr struct { 9 | device string 10 | } 11 | 12 | var _ net.Addr = &canRawAddr{} 13 | 14 | func (a *canRawAddr) Network() string { 15 | return canRawNetwork 16 | } 17 | 18 | func (a *canRawAddr) String() string { 19 | return a.device 20 | } 21 | -------------------------------------------------------------------------------- /pkg/socketcan/canrawaddr_test.go: -------------------------------------------------------------------------------- 1 | package socketcan 2 | 3 | import ( 4 | "testing" 5 | 6 | "gotest.tools/v3/assert" 7 | ) 8 | 9 | func TestCanRawAddr_Network(t *testing.T) { 10 | addr := &canRawAddr{device: "can0"} 11 | assert.Equal(t, "can0", addr.String()) 12 | } 13 | 14 | func TestCanRawAddr_String(t *testing.T) { 15 | addr := &canRawAddr{device: "can0"} 16 | assert.Equal(t, "can", addr.Network()) 17 | } 18 | -------------------------------------------------------------------------------- /pkg/socketcan/controllererror.go: -------------------------------------------------------------------------------- 1 | package socketcan 2 | 3 | type ControllerError uint8 4 | 5 | //go:generate stringer -type ControllerError -trimprefix ControllerError 6 | 7 | const ( 8 | ControllerErrorUnspecified ControllerError = 0x00 9 | ControllerErrorRxBufferOverflow ControllerError = 0x01 10 | ControllerErrorTxBufferOverflow ControllerError = 0x02 11 | ControllerErrorRxWarning ControllerError = 0x04 12 | ControllerErrorTxWarning ControllerError = 0x08 13 | ControllerErrorRxPassive ControllerError = 0x10 14 | ControllerErrorTxPassive ControllerError = 0x20 // at least one error counter exceeds 127 15 | ControllerErrorActive ControllerError = 0x40 16 | ) 17 | -------------------------------------------------------------------------------- /pkg/socketcan/controllererror_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type ControllerError -trimprefix ControllerError"; DO NOT EDIT. 2 | 3 | package socketcan 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[ControllerErrorUnspecified-0] 12 | _ = x[ControllerErrorRxBufferOverflow-1] 13 | _ = x[ControllerErrorTxBufferOverflow-2] 14 | _ = x[ControllerErrorRxWarning-4] 15 | _ = x[ControllerErrorTxWarning-8] 16 | _ = x[ControllerErrorRxPassive-16] 17 | _ = x[ControllerErrorTxPassive-32] 18 | _ = x[ControllerErrorActive-64] 19 | } 20 | 21 | const ( 22 | _ControllerError_name_0 = "UnspecifiedRxBufferOverflowTxBufferOverflow" 23 | _ControllerError_name_1 = "RxWarning" 24 | _ControllerError_name_2 = "TxWarning" 25 | _ControllerError_name_3 = "RxPassive" 26 | _ControllerError_name_4 = "TxPassive" 27 | _ControllerError_name_5 = "Active" 28 | ) 29 | 30 | var ( 31 | _ControllerError_index_0 = [...]uint8{0, 11, 27, 43} 32 | ) 33 | 34 | func (i ControllerError) String() string { 35 | switch { 36 | case i <= 2: 37 | return _ControllerError_name_0[_ControllerError_index_0[i]:_ControllerError_index_0[i+1]] 38 | case i == 4: 39 | return _ControllerError_name_1 40 | case i == 8: 41 | return _ControllerError_name_2 42 | case i == 16: 43 | return _ControllerError_name_3 44 | case i == 32: 45 | return _ControllerError_name_4 46 | case i == 64: 47 | return _ControllerError_name_5 48 | default: 49 | return "ControllerError(" + strconv.FormatInt(int64(i), 10) + ")" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /pkg/socketcan/dial.go: -------------------------------------------------------------------------------- 1 | package socketcan 2 | 3 | import ( 4 | "context" 5 | "net" 6 | ) 7 | 8 | const udp = "udp" 9 | 10 | type DialOption func(*dialOpts) 11 | 12 | // Dial connects to the address on the named net. 13 | // 14 | // Linux only: If net is "can" it creates a SocketCAN connection to the device 15 | // (address is interpreted as a device name). 16 | // 17 | // If net is "udp" it assumes UDP multicast and sets up 2 connections, one for 18 | // receiving and one for transmitting. 19 | // See: https://golang.org/pkg/net/#Dial 20 | func Dial(network, address string, opt ...DialOption) (net.Conn, error) { 21 | switch network { 22 | case udp: 23 | return udpTransceiver(network, address) 24 | case canRawNetwork: 25 | return dialRaw(address, opt...) // platform-specific 26 | default: 27 | return net.Dial(network, address) 28 | } 29 | } 30 | 31 | // DialContext connects to the address on the named net using 32 | // the provided context. 33 | // 34 | // Linux only: If net is "can" it creates a SocketCAN connection to the device 35 | // (address is interpreted as a device name). 36 | // 37 | // See: https://golang.org/pkg/net/#Dialer.DialContext 38 | func DialContext(ctx context.Context, network, address string, opt ...DialOption) (net.Conn, error) { 39 | switch network { 40 | case canRawNetwork: 41 | return dialCtx(ctx, func() (net.Conn, error) { 42 | return dialRaw(address, opt...) 43 | }) 44 | case udp: 45 | return dialCtx(ctx, func() (net.Conn, error) { 46 | return udpTransceiver(network, address) 47 | }) 48 | default: 49 | var d net.Dialer 50 | return d.DialContext(ctx, network, address) 51 | } 52 | } 53 | 54 | func dialCtx(ctx context.Context, connProvider func() (net.Conn, error)) (net.Conn, error) { 55 | resultChan := make(chan struct { 56 | conn net.Conn 57 | err error 58 | }) 59 | go func() { 60 | conn, err := connProvider() 61 | resultChan <- struct { 62 | conn net.Conn 63 | err error 64 | }{conn: conn, err: err} 65 | }() 66 | // wait for connection or timeout 67 | select { 68 | case result := <-resultChan: 69 | return result.conn, result.err 70 | case <-ctx.Done(): 71 | // timeout - make sure we clean up the connection 72 | // error handling not possible since we've already returned 73 | go func() { 74 | result := <-resultChan 75 | if result.conn != nil { 76 | _ = result.conn.Close() 77 | } 78 | }() 79 | return nil, ctx.Err() 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /pkg/socketcan/dial_test.go: -------------------------------------------------------------------------------- 1 | package socketcan 2 | 3 | import ( 4 | "context" 5 | "net" 6 | "testing" 7 | "time" 8 | 9 | "go.einride.tech/can" 10 | "golang.org/x/sync/errgroup" 11 | "gotest.tools/v3/assert" 12 | ) 13 | 14 | func TestDial_TCP(t *testing.T) { 15 | lis, err := net.Listen("tcp", "localhost:0") 16 | assert.NilError(t, err) 17 | var g errgroup.Group 18 | g.Go(func() error { 19 | conn, err := lis.Accept() 20 | if err != nil { 21 | return err 22 | } 23 | return conn.Close() 24 | }) 25 | conn, err := Dial("tcp", lis.Addr().String()) 26 | assert.NilError(t, err) 27 | assert.NilError(t, conn.Close()) 28 | assert.NilError(t, g.Wait()) 29 | } 30 | 31 | func TestDialContext_TCP(t *testing.T) { 32 | lis, err := net.Listen("tcp", "localhost:0") 33 | assert.NilError(t, err) 34 | var g errgroup.Group 35 | g.Go(func() error { 36 | conn, err := lis.Accept() 37 | if err != nil { 38 | return err 39 | } 40 | return conn.Close() 41 | }) 42 | ctx, done := context.WithTimeout(context.Background(), time.Second) 43 | defer done() 44 | conn, err := DialContext(ctx, "tcp", lis.Addr().String()) 45 | assert.NilError(t, err) 46 | assert.NilError(t, conn.Close()) 47 | assert.NilError(t, g.Wait()) 48 | } 49 | 50 | func TestConn_TransmitReceiveTCP(t *testing.T) { 51 | // Given: A TCP listener that writes a frame on an accepted connection 52 | lis, err := net.Listen("tcp", "localhost:0") 53 | assert.NilError(t, err) 54 | var g errgroup.Group 55 | frame := can.Frame{ID: 42, Length: 5, Data: can.Data{'H', 'e', 'l', 'l', 'o'}} 56 | g.Go(func() error { 57 | conn, err := lis.Accept() 58 | if err != nil { 59 | return err 60 | } 61 | tr := NewTransmitter(conn) 62 | ctx, done := context.WithTimeout(context.Background(), time.Second) 63 | defer done() 64 | if err := tr.TransmitFrame(ctx, frame); err != nil { 65 | return err 66 | } 67 | return conn.Close() 68 | }) 69 | // When: We connect to the listener 70 | ctx, done := context.WithTimeout(context.Background(), time.Second) 71 | defer done() 72 | conn, err := DialContext(ctx, "tcp", lis.Addr().String()) 73 | assert.NilError(t, err) 74 | rec := NewReceiver(conn) 75 | assert.Assert(t, rec.Receive()) 76 | assert.Assert(t, !rec.HasErrorFrame()) 77 | assert.DeepEqual(t, frame, rec.Frame()) 78 | assert.NilError(t, conn.Close()) 79 | assert.NilError(t, g.Wait()) 80 | } 81 | -------------------------------------------------------------------------------- /pkg/socketcan/dialraw_linux.go: -------------------------------------------------------------------------------- 1 | //go:build linux && go1.12 2 | 3 | package socketcan 4 | 5 | import ( 6 | "fmt" 7 | "net" 8 | "os" 9 | 10 | "golang.org/x/sys/unix" 11 | ) 12 | 13 | type dialOpts struct { 14 | errorFrameMask *int 15 | } 16 | 17 | func dialRaw(device string, opt ...DialOption) (conn net.Conn, err error) { 18 | defer func() { 19 | if err != nil { 20 | err = &net.OpError{Op: "dial", Net: canRawNetwork, Addr: &canRawAddr{device: device}, Err: err} 21 | } 22 | }() 23 | opts := dialOpts{} 24 | for _, f := range opt { 25 | f(&opts) 26 | } 27 | ifi, err := net.InterfaceByName(device) 28 | if err != nil { 29 | return nil, fmt.Errorf("interface %s: %w", device, err) 30 | } 31 | fd, err := unix.Socket(unix.AF_CAN, unix.SOCK_RAW, unix.CAN_RAW) 32 | if err != nil { 33 | return nil, fmt.Errorf("socket: %w", err) 34 | } 35 | if opts.errorFrameMask != nil { 36 | if err := unix.SetsockoptInt(fd, unix.SOL_CAN_RAW, unix.CAN_RAW_ERR_FILTER, *opts.errorFrameMask); err != nil { 37 | return nil, fmt.Errorf("set error filter: %w", err) 38 | } 39 | } 40 | // put fd in non-blocking mode so the created file will be registered by the runtime poller (Go >= 1.12) 41 | if err := unix.SetNonblock(fd, true); err != nil { 42 | return nil, fmt.Errorf("set nonblock: %w", err) 43 | } 44 | if err := unix.Bind(fd, &unix.SockaddrCAN{Ifindex: ifi.Index}); err != nil { 45 | return nil, fmt.Errorf("bind: %w", err) 46 | } 47 | return &fileConn{ra: &canRawAddr{device: device}, f: os.NewFile(uintptr(fd), "can")}, nil 48 | } 49 | 50 | // WithReceiveErrorFrames returns a DialOption which enables 51 | // can error frame receiving on can port. 52 | func WithReceiveErrorFrames() DialOption { 53 | return func(o *dialOpts) { 54 | canErrMask := unix.CAN_ERR_MASK 55 | o.errorFrameMask = &canErrMask 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /pkg/socketcan/dialraw_others.go: -------------------------------------------------------------------------------- 1 | //go:build !linux || !go1.12 2 | 3 | package socketcan 4 | 5 | import ( 6 | "fmt" 7 | "net" 8 | "runtime" 9 | ) 10 | 11 | type dialOpts struct { 12 | } 13 | 14 | func dialRaw(interfaceName string, opt ...DialOption) (net.Conn, error) { 15 | return nil, fmt.Errorf("SocketCAN not supported on OS %s and runtime %s", runtime.GOOS, runtime.Version()) 16 | } 17 | 18 | func WithReceiveErrorFrames() DialOption { 19 | return func(o *dialOpts) { 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /pkg/socketcan/errorclass.go: -------------------------------------------------------------------------------- 1 | package socketcan 2 | 3 | type ErrorClass uint32 4 | 5 | //go:generate stringer -type ErrorClass -trimprefix ErrorClass 6 | 7 | const ( 8 | ErrorClassTxTimeout ErrorClass = 0x00000001 9 | ErrorClassLostArbitration ErrorClass = 0x00000002 10 | ErrorClassController ErrorClass = 0x00000004 11 | ErrorClassProtocolViolation ErrorClass = 0x00000008 12 | ErrorClassTransceiver ErrorClass = 0x00000010 13 | ErrorClassNoAck ErrorClass = 0x00000020 14 | ErrorClassBusOff ErrorClass = 0x00000040 15 | ErrorClassBusError ErrorClass = 0x00000080 16 | ErrorClassRestarted ErrorClass = 0x00000100 17 | ) 18 | -------------------------------------------------------------------------------- /pkg/socketcan/errorclass_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type ErrorClass -trimprefix ErrorClass"; DO NOT EDIT. 2 | 3 | package socketcan 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[ErrorClassTxTimeout-1] 12 | _ = x[ErrorClassLostArbitration-2] 13 | _ = x[ErrorClassController-4] 14 | _ = x[ErrorClassProtocolViolation-8] 15 | _ = x[ErrorClassTransceiver-16] 16 | _ = x[ErrorClassNoAck-32] 17 | _ = x[ErrorClassBusOff-64] 18 | _ = x[ErrorClassBusError-128] 19 | _ = x[ErrorClassRestarted-256] 20 | } 21 | 22 | const ( 23 | _ErrorClass_name_0 = "TxTimeoutLostArbitration" 24 | _ErrorClass_name_1 = "Controller" 25 | _ErrorClass_name_2 = "ProtocolViolation" 26 | _ErrorClass_name_3 = "Transceiver" 27 | _ErrorClass_name_4 = "NoAck" 28 | _ErrorClass_name_5 = "BusOff" 29 | _ErrorClass_name_6 = "BusError" 30 | _ErrorClass_name_7 = "Restarted" 31 | ) 32 | 33 | var ( 34 | _ErrorClass_index_0 = [...]uint8{0, 9, 24} 35 | ) 36 | 37 | func (i ErrorClass) String() string { 38 | switch { 39 | case 1 <= i && i <= 2: 40 | i -= 1 41 | return _ErrorClass_name_0[_ErrorClass_index_0[i]:_ErrorClass_index_0[i+1]] 42 | case i == 4: 43 | return _ErrorClass_name_1 44 | case i == 8: 45 | return _ErrorClass_name_2 46 | case i == 16: 47 | return _ErrorClass_name_3 48 | case i == 32: 49 | return _ErrorClass_name_4 50 | case i == 64: 51 | return _ErrorClass_name_5 52 | case i == 128: 53 | return _ErrorClass_name_6 54 | case i == 256: 55 | return _ErrorClass_name_7 56 | default: 57 | return "ErrorClass(" + strconv.FormatInt(int64(i), 10) + ")" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /pkg/socketcan/errorframe.go: -------------------------------------------------------------------------------- 1 | package socketcan 2 | 3 | import ( 4 | "encoding/hex" 5 | "fmt" 6 | ) 7 | 8 | type ErrorFrame struct { 9 | // Class is the error class 10 | ErrorClass ErrorClass 11 | // LostArbitrationBit contains the bit number when the error class is LostArbitration. 12 | LostArbitrationBit uint8 13 | // ControllerError contains error information when the error class is Controller. 14 | ControllerError ControllerError 15 | // ProtocolViolationError contains error information when the error class is Protocol. 16 | ProtocolError ProtocolViolationError 17 | // ProtocolViolationErrorLocation contains error location when the error class is Protocol. 18 | ProtocolViolationErrorLocation ProtocolViolationErrorLocation 19 | // TransceiverError contains error information when the error class is Transceiver. 20 | TransceiverError TransceiverError 21 | // ControllerSpecificInformation contains controller-specific additional error information. 22 | ControllerSpecificInformation [3]byte 23 | } 24 | 25 | func (e *ErrorFrame) String() string { 26 | switch e.ErrorClass { 27 | case ErrorClassLostArbitration: 28 | return fmt.Sprintf( 29 | "%s in bit %d (%s)", 30 | e.ErrorClass, 31 | e.LostArbitrationBit, 32 | hex.EncodeToString(e.ControllerSpecificInformation[:]), 33 | ) 34 | case ErrorClassController: 35 | return fmt.Sprintf( 36 | "%s: %s (%v)", 37 | e.ErrorClass, 38 | e.ControllerError, 39 | hex.EncodeToString(e.ControllerSpecificInformation[:]), 40 | ) 41 | case ErrorClassProtocolViolation: 42 | return fmt.Sprintf( 43 | "%s: %s: location %s (%v)", 44 | e.ErrorClass, 45 | e.ProtocolError, 46 | e.ProtocolViolationErrorLocation, 47 | hex.EncodeToString(e.ControllerSpecificInformation[:]), 48 | ) 49 | case ErrorClassTransceiver: 50 | return fmt.Sprintf( 51 | "%s: %s (%v)", 52 | e.ErrorClass, 53 | e.TransceiverError, 54 | hex.EncodeToString(e.ControllerSpecificInformation[:]), 55 | ) 56 | default: 57 | return fmt.Sprintf( 58 | "%s (%v)", 59 | e.ErrorClass, 60 | hex.EncodeToString(e.ControllerSpecificInformation[:]), 61 | ) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /pkg/socketcan/errorframe_test.go: -------------------------------------------------------------------------------- 1 | package socketcan 2 | 3 | import ( 4 | "testing" 5 | 6 | "gotest.tools/v3/assert" 7 | ) 8 | 9 | func TestErrorFrame_String(t *testing.T) { 10 | for _, tt := range []struct { 11 | msg string 12 | f ErrorFrame 13 | expected string 14 | }{ 15 | { 16 | msg: "lost arbitration", 17 | f: ErrorFrame{ 18 | ErrorClass: ErrorClassLostArbitration, 19 | LostArbitrationBit: 42, 20 | }, 21 | expected: "LostArbitration in bit 42 (000000)", 22 | }, 23 | { 24 | msg: "controller", 25 | f: ErrorFrame{ 26 | ErrorClass: ErrorClassController, 27 | ControllerError: ControllerErrorRxBufferOverflow, 28 | }, 29 | expected: "Controller: RxBufferOverflow (000000)", 30 | }, 31 | { 32 | msg: "protocol violation", 33 | f: ErrorFrame{ 34 | ErrorClass: ErrorClassProtocolViolation, 35 | ProtocolError: ProtocolViolationErrorFrameFormat, 36 | ProtocolViolationErrorLocation: ProtocolViolationErrorLocationID20To18, 37 | }, 38 | expected: "ProtocolViolation: FrameFormat: location ID20To18 (000000)", 39 | }, 40 | { 41 | msg: "transceiver", 42 | f: ErrorFrame{ 43 | ErrorClass: ErrorClassTransceiver, 44 | TransceiverError: TransceiverErrorCANHShortToGND, 45 | }, 46 | expected: "Transceiver: CANHShortToGND (000000)", 47 | }, 48 | { 49 | msg: "controller specific information", 50 | f: ErrorFrame{ 51 | ErrorClass: ErrorClassTxTimeout, 52 | ControllerSpecificInformation: [3]byte{0x12, 0x34, 0x56}, 53 | }, 54 | expected: "TxTimeout (123456)", 55 | }, 56 | } { 57 | t.Run(tt.msg, func(t *testing.T) { 58 | assert.Equal(t, tt.expected, tt.f.String()) 59 | }) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /pkg/socketcan/fileconn.go: -------------------------------------------------------------------------------- 1 | package socketcan 2 | 3 | import ( 4 | "errors" 5 | "net" 6 | "os" 7 | "time" 8 | ) 9 | 10 | // file is an interface for mocking file operations performed by fileConn. 11 | type file interface { 12 | Read([]byte) (int, error) 13 | Write([]byte) (int, error) 14 | SetDeadline(time.Time) error 15 | SetReadDeadline(time.Time) error 16 | SetWriteDeadline(time.Time) error 17 | Close() error 18 | } 19 | 20 | // fileConn provides a net.Conn API for file-like types. 21 | type fileConn struct { 22 | // f is the file to provide a net.Conn API for. 23 | f file 24 | // net is the connection's network. 25 | net string 26 | // la is the connection's local address, if any. 27 | la net.Addr 28 | // ra is the connection's remote address, if any. 29 | ra net.Addr 30 | } 31 | 32 | var _ net.Conn = &fileConn{} 33 | 34 | func (c *fileConn) Read(b []byte) (int, error) { 35 | n, err := c.f.Read(b) 36 | if err != nil { 37 | return n, &net.OpError{Op: "read", Net: c.net, Source: c.la, Addr: c.ra, Err: unwrapPathError(err)} 38 | } 39 | return n, nil 40 | } 41 | 42 | func (c *fileConn) Write(b []byte) (int, error) { 43 | n, err := c.f.Write(b) 44 | if err != nil { 45 | return n, &net.OpError{Op: "write", Net: c.net, Source: c.la, Addr: c.ra, Err: unwrapPathError(err)} 46 | } 47 | return n, nil 48 | } 49 | 50 | func (c *fileConn) LocalAddr() net.Addr { 51 | return c.la 52 | } 53 | 54 | func (c *fileConn) RemoteAddr() net.Addr { 55 | return c.ra 56 | } 57 | 58 | func (c *fileConn) SetDeadline(t time.Time) error { 59 | if err := c.f.SetDeadline(t); err != nil { 60 | return &net.OpError{Op: "set deadline", Net: c.net, Source: c.la, Addr: c.ra, Err: unwrapPathError(err)} 61 | } 62 | return nil 63 | } 64 | 65 | func (c *fileConn) SetReadDeadline(t time.Time) error { 66 | if err := c.f.SetReadDeadline(t); err != nil { 67 | return &net.OpError{Op: "set read deadline", Net: c.net, Source: c.la, Addr: c.ra, Err: unwrapPathError(err)} 68 | } 69 | return nil 70 | } 71 | 72 | func (c *fileConn) SetWriteDeadline(t time.Time) error { 73 | if err := c.f.SetWriteDeadline(t); err != nil { 74 | return &net.OpError{Op: "set write deadline", Net: c.net, Source: c.la, Addr: c.ra, Err: unwrapPathError(err)} 75 | } 76 | return nil 77 | } 78 | 79 | func (c *fileConn) Close() error { 80 | if err := c.f.Close(); err != nil { 81 | return &net.OpError{Op: "close", Net: c.net, Source: c.la, Addr: c.ra, Err: unwrapPathError(err)} 82 | } 83 | return nil 84 | } 85 | 86 | // unwrapPathError unwraps one level of *os.PathError from the provided error. 87 | func unwrapPathError(err error) error { 88 | var pe *os.PathError 89 | if errors.As(err, &pe) { 90 | return pe.Err 91 | } 92 | return err 93 | } 94 | -------------------------------------------------------------------------------- /pkg/socketcan/fileconn_test.go: -------------------------------------------------------------------------------- 1 | package socketcan 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "os" 7 | "testing" 8 | "time" 9 | 10 | "github.com/golang/mock/gomock" 11 | "go.einride.tech/can/internal/mocks/gen/mocksocketcan" 12 | "gotest.tools/v3/assert" 13 | ) 14 | 15 | func TestUnwrapPathError(t *testing.T) { 16 | innerErr := fmt.Errorf("inner error") 17 | for _, tt := range []struct { 18 | msg string 19 | err error 20 | expected error 21 | }{ 22 | { 23 | msg: "no path error", 24 | err: innerErr, 25 | expected: innerErr, 26 | }, 27 | { 28 | msg: "single path error", 29 | err: &os.PathError{Op: "read", Err: innerErr}, 30 | expected: innerErr, 31 | }, 32 | { 33 | msg: "double path error", 34 | err: &os.PathError{Op: "read", Err: &os.PathError{Op: "read", Err: innerErr}}, 35 | expected: &os.PathError{Op: "read", Err: innerErr}, 36 | }, 37 | } { 38 | t.Run(tt.msg, func(t *testing.T) { 39 | assert.Error(t, unwrapPathError(tt.err), tt.expected.Error()) 40 | }) 41 | } 42 | } 43 | 44 | func TestFileConn_ReadWrite(t *testing.T) { 45 | for _, tt := range []struct { 46 | op string 47 | fn func(file, []byte) (int, error) 48 | mockFn func(*mocksocketcan.MockfileMockRecorder, interface{}) *gomock.Call 49 | }{ 50 | { 51 | op: "read", 52 | fn: file.Read, 53 | mockFn: (*mocksocketcan.MockfileMockRecorder).Read, 54 | }, 55 | { 56 | op: "write", 57 | fn: file.Write, 58 | mockFn: (*mocksocketcan.MockfileMockRecorder).Write, 59 | }, 60 | } { 61 | t.Run(tt.op, func(t *testing.T) { 62 | ctrl := gomock.NewController(t) 63 | defer ctrl.Finish() 64 | f := mocksocketcan.NewMockfile(ctrl) 65 | fc := &fileConn{f: f, net: "can", ra: &canRawAddr{device: "can0"}} 66 | t.Run("no error", func(t *testing.T) { 67 | var data []byte 68 | tt.mockFn(f.EXPECT(), data).Return(42, nil) 69 | n, err := tt.fn(fc, data) 70 | assert.Equal(t, 42, n) 71 | assert.NilError(t, err) 72 | }) 73 | t.Run("error", func(t *testing.T) { 74 | var data []byte 75 | cause := fmt.Errorf("boom") 76 | tt.mockFn(f.EXPECT(), data).Return(0, &os.PathError{Err: cause}) 77 | n, err := tt.fn(fc, data) 78 | assert.Equal(t, 0, n) 79 | assert.ErrorContains(t, &net.OpError{Op: tt.op, Net: fc.net, Addr: fc.RemoteAddr(), Err: err}, "boom") 80 | }) 81 | }) 82 | } 83 | } 84 | 85 | func TestFileConn_Addr(t *testing.T) { 86 | fc := &fileConn{la: &canRawAddr{device: "can0"}, ra: &canRawAddr{device: "can1"}} 87 | t.Run("local", func(t *testing.T) { 88 | assert.Equal(t, fc.la, fc.LocalAddr()) 89 | }) 90 | t.Run("remote", func(t *testing.T) { 91 | assert.Equal(t, fc.ra, fc.RemoteAddr()) 92 | }) 93 | } 94 | 95 | func TestFileConn_SetDeadlines(t *testing.T) { 96 | for _, tt := range []struct { 97 | op string 98 | fn func(file, time.Time) error 99 | mockFn func(*mocksocketcan.MockfileMockRecorder, interface{}) *gomock.Call 100 | }{ 101 | { 102 | op: "set deadline", 103 | fn: file.SetDeadline, 104 | mockFn: (*mocksocketcan.MockfileMockRecorder).SetDeadline, 105 | }, 106 | { 107 | op: "set read deadline", 108 | fn: file.SetReadDeadline, 109 | mockFn: (*mocksocketcan.MockfileMockRecorder).SetReadDeadline, 110 | }, 111 | { 112 | op: "set write deadline", 113 | fn: file.SetWriteDeadline, 114 | mockFn: (*mocksocketcan.MockfileMockRecorder).SetWriteDeadline, 115 | }, 116 | } { 117 | t.Run(tt.op, func(t *testing.T) { 118 | ctrl := gomock.NewController(t) 119 | defer ctrl.Finish() 120 | f := mocksocketcan.NewMockfile(ctrl) 121 | fc := &fileConn{f: f, net: "can", ra: &canRawAddr{device: "can0"}} 122 | t.Run("no error", func(t *testing.T) { 123 | tt.mockFn(f.EXPECT(), time.Unix(0, 1)).Return(nil) 124 | assert.NilError(t, tt.fn(fc, time.Unix(0, 1))) 125 | }) 126 | t.Run("error", func(t *testing.T) { 127 | cause := fmt.Errorf("boom") 128 | tt.mockFn(f.EXPECT(), time.Unix(0, 1)).Return(&os.PathError{Err: cause}) 129 | err := tt.fn(fc, time.Unix(0, 1)) 130 | assert.Error(t, err, (&net.OpError{Op: tt.op, Net: fc.net, Addr: fc.RemoteAddr(), Err: cause}).Error()) 131 | }) 132 | }) 133 | } 134 | } 135 | 136 | func TestFileConn_Close(t *testing.T) { 137 | ctrl := gomock.NewController(t) 138 | defer ctrl.Finish() 139 | f := mocksocketcan.NewMockfile(ctrl) 140 | fc := &fileConn{f: f, net: "can", ra: &canRawAddr{device: "can0"}} 141 | t.Run("no error", func(t *testing.T) { 142 | f.EXPECT().Close().Return(nil) 143 | assert.NilError(t, fc.Close()) 144 | }) 145 | t.Run("error", func(t *testing.T) { 146 | cause := fmt.Errorf("boom") 147 | f.EXPECT().Close().Return(&os.PathError{Err: cause}) 148 | err := fc.Close() 149 | assert.Error(t, err, (&net.OpError{Op: "close", Net: fc.net, Addr: fc.RemoteAddr(), Err: cause}).Error()) 150 | }) 151 | } 152 | -------------------------------------------------------------------------------- /pkg/socketcan/frame_test.go: -------------------------------------------------------------------------------- 1 | package socketcan 2 | 3 | import ( 4 | "testing" 5 | "testing/quick" 6 | 7 | "go.einride.tech/can" 8 | "gotest.tools/v3/assert" 9 | ) 10 | 11 | func TestFrame_MarshalUnmarshalBinary_Property_Idempotent(t *testing.T) { 12 | f := func(data [lengthOfFrame]byte) [lengthOfFrame]byte { 13 | data[5], data[6], data[7] = 0, 0, 0 // padding+reserved fields 14 | return data 15 | } 16 | g := func(data [lengthOfFrame]byte) [lengthOfFrame]byte { 17 | var f frame 18 | f.unmarshalBinary(data[:]) 19 | var newData [lengthOfFrame]byte 20 | f.marshalBinary(newData[:]) 21 | return newData 22 | } 23 | assert.NilError(t, quick.CheckEqual(f, g, nil)) 24 | } 25 | 26 | func TestFrame_EncodeDecode(t *testing.T) { 27 | for _, tt := range []struct { 28 | msg string 29 | frame can.Frame 30 | socketCANFrame frame 31 | }{ 32 | { 33 | msg: "data", 34 | frame: can.Frame{ 35 | ID: 0x00000001, 36 | Length: 8, 37 | Data: can.Data{1, 2, 3, 4, 5, 6, 7, 8}, 38 | }, 39 | socketCANFrame: frame{ 40 | idAndFlags: 0x00000001, 41 | dataLengthCode: 8, 42 | data: [8]byte{1, 2, 3, 4, 5, 6, 7, 8}, 43 | }, 44 | }, 45 | { 46 | msg: "extended", 47 | frame: can.Frame{ 48 | ID: 0x00000001, 49 | IsExtended: true, 50 | }, 51 | socketCANFrame: frame{ 52 | idAndFlags: 0x80000001, 53 | }, 54 | }, 55 | { 56 | msg: "remote", 57 | frame: can.Frame{ 58 | ID: 0x00000001, 59 | IsRemote: true, 60 | }, 61 | socketCANFrame: frame{ 62 | idAndFlags: 0x40000001, 63 | }, 64 | }, 65 | { 66 | msg: "extended and remote", 67 | frame: can.Frame{ 68 | ID: 0x00000001, 69 | IsExtended: true, 70 | IsRemote: true, 71 | }, 72 | socketCANFrame: frame{ 73 | idAndFlags: 0xc0000001, 74 | }, 75 | }, 76 | } { 77 | t.Run(tt.msg, func(t *testing.T) { 78 | t.Run("encode", func(t *testing.T) { 79 | var actual frame 80 | actual.encodeFrame(tt.frame) 81 | assert.Equal(t, tt.socketCANFrame, actual) 82 | }) 83 | t.Run("decode", func(t *testing.T) { 84 | assert.Equal(t, tt.frame, tt.socketCANFrame.decodeFrame()) 85 | }) 86 | }) 87 | } 88 | } 89 | 90 | func TestFrame_IsError(t *testing.T) { 91 | assert.Assert(t, (&frame{idAndFlags: 0x20000001}).isError()) 92 | assert.Assert(t, !(&frame{idAndFlags: 0x00000001}).isError()) 93 | } 94 | 95 | func TestFrame_DecodeErrorFrame(t *testing.T) { 96 | for _, tt := range []struct { 97 | msg string 98 | f frame 99 | expected ErrorFrame 100 | }{ 101 | { 102 | msg: "lost arbitration", 103 | f: frame{ 104 | idAndFlags: 0x20000002, 105 | dataLengthCode: 8, 106 | data: [8]byte{ 107 | 42, 108 | }, 109 | }, 110 | expected: ErrorFrame{ 111 | ErrorClass: ErrorClassLostArbitration, 112 | LostArbitrationBit: 42, 113 | }, 114 | }, 115 | { 116 | msg: "controller", 117 | f: frame{ 118 | idAndFlags: 0x20000004, 119 | dataLengthCode: 8, 120 | data: [8]byte{ 121 | 0, 122 | 0x04, 123 | }, 124 | }, 125 | expected: ErrorFrame{ 126 | ErrorClass: ErrorClassController, 127 | ControllerError: ControllerErrorRxWarning, 128 | }, 129 | }, 130 | { 131 | msg: "protocol violation", 132 | f: frame{ 133 | idAndFlags: 0x20000008, 134 | dataLengthCode: 8, 135 | data: [8]byte{ 136 | 0, 137 | 0, 138 | 0x10, 139 | 0x02, 140 | }, 141 | }, 142 | expected: ErrorFrame{ 143 | ErrorClass: ErrorClassProtocolViolation, 144 | ProtocolError: ProtocolViolationErrorBit1, 145 | ProtocolViolationErrorLocation: ProtocolViolationErrorLocationID28To21, 146 | }, 147 | }, 148 | { 149 | msg: "transceiver", 150 | f: frame{ 151 | idAndFlags: 0x20000010, 152 | dataLengthCode: 8, 153 | data: [8]byte{ 154 | 0, 155 | 0, 156 | 0, 157 | 0, 158 | 0x07, 159 | }, 160 | }, 161 | expected: ErrorFrame{ 162 | ErrorClass: ErrorClassTransceiver, 163 | TransceiverError: TransceiverErrorCANHShortToGND, 164 | }, 165 | }, 166 | { 167 | msg: "controller-specific information", 168 | f: frame{ 169 | idAndFlags: 0x20000001, 170 | dataLengthCode: 8, 171 | data: [8]byte{ 172 | 0, 173 | 0, 174 | 0, 175 | 0, 176 | 0, 177 | 1, 178 | 2, 179 | 3, 180 | }, 181 | }, 182 | expected: ErrorFrame{ 183 | ErrorClass: ErrorClassTxTimeout, 184 | ControllerSpecificInformation: [3]byte{1, 2, 3}, 185 | }, 186 | }, 187 | } { 188 | t.Run(tt.msg, func(t *testing.T) { 189 | assert.Equal(t, tt.expected, tt.f.decodeErrorFrame()) 190 | }) 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /pkg/socketcan/main_test.go: -------------------------------------------------------------------------------- 1 | package socketcan 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "go.uber.org/goleak" 8 | ) 9 | 10 | func TestMain(m *testing.M) { 11 | goleak.VerifyTestMain(m) 12 | os.Exit(m.Run()) 13 | } 14 | -------------------------------------------------------------------------------- /pkg/socketcan/protocolviolationerror.go: -------------------------------------------------------------------------------- 1 | package socketcan 2 | 3 | type ProtocolViolationError uint8 4 | 5 | //go:generate stringer -type ProtocolViolationError -trimprefix ProtocolViolationError 6 | 7 | const ( 8 | ProtocolViolationErrorUnspecified ProtocolViolationError = 0x00 9 | ProtocolViolationErrorSingleBit ProtocolViolationError = 0x01 10 | ProtocolViolationErrorFrameFormat ProtocolViolationError = 0x02 11 | ProtocolViolationErrorBitStuffing ProtocolViolationError = 0x04 12 | ProtocolViolationErrorBit0 ProtocolViolationError = 0x08 // unable to send dominant bit 13 | ProtocolViolationErrorBit1 ProtocolViolationError = 0x10 // unable to send recessive bit 14 | ProtocolViolationErrorBusOverload ProtocolViolationError = 0x20 15 | ProtocolViolationErrorActive ProtocolViolationError = 0x40 // active error announcement 16 | ProtocolViolationErrorTx ProtocolViolationError = 0x80 // error occurred on transmission 17 | ) 18 | -------------------------------------------------------------------------------- /pkg/socketcan/protocolviolationerror_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type ProtocolViolationError -trimprefix ProtocolViolationError"; DO NOT EDIT. 2 | 3 | package socketcan 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[ProtocolViolationErrorUnspecified-0] 12 | _ = x[ProtocolViolationErrorSingleBit-1] 13 | _ = x[ProtocolViolationErrorFrameFormat-2] 14 | _ = x[ProtocolViolationErrorBitStuffing-4] 15 | _ = x[ProtocolViolationErrorBit0-8] 16 | _ = x[ProtocolViolationErrorBit1-16] 17 | _ = x[ProtocolViolationErrorBusOverload-32] 18 | _ = x[ProtocolViolationErrorActive-64] 19 | _ = x[ProtocolViolationErrorTx-128] 20 | } 21 | 22 | const ( 23 | _ProtocolViolationError_name_0 = "UnspecifiedSingleBitFrameFormat" 24 | _ProtocolViolationError_name_1 = "BitStuffing" 25 | _ProtocolViolationError_name_2 = "Bit0" 26 | _ProtocolViolationError_name_3 = "Bit1" 27 | _ProtocolViolationError_name_4 = "BusOverload" 28 | _ProtocolViolationError_name_5 = "Active" 29 | _ProtocolViolationError_name_6 = "Tx" 30 | ) 31 | 32 | var ( 33 | _ProtocolViolationError_index_0 = [...]uint8{0, 11, 20, 31} 34 | ) 35 | 36 | func (i ProtocolViolationError) String() string { 37 | switch { 38 | case i <= 2: 39 | return _ProtocolViolationError_name_0[_ProtocolViolationError_index_0[i]:_ProtocolViolationError_index_0[i+1]] 40 | case i == 4: 41 | return _ProtocolViolationError_name_1 42 | case i == 8: 43 | return _ProtocolViolationError_name_2 44 | case i == 16: 45 | return _ProtocolViolationError_name_3 46 | case i == 32: 47 | return _ProtocolViolationError_name_4 48 | case i == 64: 49 | return _ProtocolViolationError_name_5 50 | case i == 128: 51 | return _ProtocolViolationError_name_6 52 | default: 53 | return "ProtocolViolationError(" + strconv.FormatInt(int64(i), 10) + ")" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /pkg/socketcan/protocolviolationerrorlocation.go: -------------------------------------------------------------------------------- 1 | package socketcan 2 | 3 | type ProtocolViolationErrorLocation uint8 4 | 5 | //go:generate stringer -type ProtocolViolationErrorLocation -trimprefix ProtocolViolationErrorLocation 6 | 7 | const ( 8 | ProtocolViolationErrorLocationUnspecified ProtocolViolationErrorLocation = 0x00 9 | ProtocolViolationErrorLocationStartOfFrame ProtocolViolationErrorLocation = 0x03 10 | ProtocolViolationErrorLocationID28To21 ProtocolViolationErrorLocation = 0x02 // standard frames: 10 - 3 11 | ProtocolViolationErrorLocationID20To18 ProtocolViolationErrorLocation = 0x06 // standard frames: 2 - 0 12 | ProtocolViolationErrorLocationSubstituteRTR ProtocolViolationErrorLocation = 0x04 // standard frames: RTR 13 | ProtocolViolationErrorLocationIDExtension ProtocolViolationErrorLocation = 0x05 14 | ProtocolViolationErrorLocationIDBits17To13 ProtocolViolationErrorLocation = 0x07 15 | ProtocolViolationErrorLocationIDBits12To05 ProtocolViolationErrorLocation = 0x0F 16 | ProtocolViolationErrorLocationIDBits04To00 ProtocolViolationErrorLocation = 0x0E 17 | ProtocolViolationErrorLocationRTR ProtocolViolationErrorLocation = 0x0C 18 | ProtocolViolationErrorLocationReservedBit1 ProtocolViolationErrorLocation = 0x0D 19 | ProtocolViolationErrorLocationReservedBit0 ProtocolViolationErrorLocation = 0x09 20 | ProtocolViolationErrorLocationDataLengthCode ProtocolViolationErrorLocation = 0x0B 21 | ProtocolViolationErrorLocationData ProtocolViolationErrorLocation = 0x0A 22 | ProtocolViolationErrorLocationCRCSequence ProtocolViolationErrorLocation = 0x08 23 | ProtocolViolationErrorLocationCRCDelimiter ProtocolViolationErrorLocation = 0x18 24 | ProtocolViolationErrorLocationACKSlot ProtocolViolationErrorLocation = 0x19 25 | ProtocolViolationErrorLocationACKDelimiter ProtocolViolationErrorLocation = 0x1B 26 | ProtocolViolationErrorLocationEndOfFrame ProtocolViolationErrorLocation = 0x1A 27 | ProtocolViolationErrorLocationIntermission ProtocolViolationErrorLocation = 0x12 28 | ) 29 | -------------------------------------------------------------------------------- /pkg/socketcan/protocolviolationerrorlocation_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type ProtocolViolationErrorLocation -trimprefix ProtocolViolationErrorLocation"; DO NOT EDIT. 2 | 3 | package socketcan 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[ProtocolViolationErrorLocationUnspecified-0] 12 | _ = x[ProtocolViolationErrorLocationStartOfFrame-3] 13 | _ = x[ProtocolViolationErrorLocationID28To21-2] 14 | _ = x[ProtocolViolationErrorLocationID20To18-6] 15 | _ = x[ProtocolViolationErrorLocationSubstituteRTR-4] 16 | _ = x[ProtocolViolationErrorLocationIDExtension-5] 17 | _ = x[ProtocolViolationErrorLocationIDBits17To13-7] 18 | _ = x[ProtocolViolationErrorLocationIDBits12To05-15] 19 | _ = x[ProtocolViolationErrorLocationIDBits04To00-14] 20 | _ = x[ProtocolViolationErrorLocationRTR-12] 21 | _ = x[ProtocolViolationErrorLocationReservedBit1-13] 22 | _ = x[ProtocolViolationErrorLocationReservedBit0-9] 23 | _ = x[ProtocolViolationErrorLocationDataLengthCode-11] 24 | _ = x[ProtocolViolationErrorLocationData-10] 25 | _ = x[ProtocolViolationErrorLocationCRCSequence-8] 26 | _ = x[ProtocolViolationErrorLocationCRCDelimiter-24] 27 | _ = x[ProtocolViolationErrorLocationACKSlot-25] 28 | _ = x[ProtocolViolationErrorLocationACKDelimiter-27] 29 | _ = x[ProtocolViolationErrorLocationEndOfFrame-26] 30 | _ = x[ProtocolViolationErrorLocationIntermission-18] 31 | } 32 | 33 | const ( 34 | _ProtocolViolationErrorLocation_name_0 = "Unspecified" 35 | _ProtocolViolationErrorLocation_name_1 = "ID28To21StartOfFrameSubstituteRTRIDExtensionID20To18IDBits17To13CRCSequenceReservedBit0DataDataLengthCodeRTRReservedBit1IDBits04To00IDBits12To05" 36 | _ProtocolViolationErrorLocation_name_2 = "Intermission" 37 | _ProtocolViolationErrorLocation_name_3 = "CRCDelimiterACKSlotEndOfFrameACKDelimiter" 38 | ) 39 | 40 | var ( 41 | _ProtocolViolationErrorLocation_index_1 = [...]uint8{0, 8, 20, 33, 44, 52, 64, 75, 87, 91, 105, 108, 120, 132, 144} 42 | _ProtocolViolationErrorLocation_index_3 = [...]uint8{0, 12, 19, 29, 41} 43 | ) 44 | 45 | func (i ProtocolViolationErrorLocation) String() string { 46 | switch { 47 | case i == 0: 48 | return _ProtocolViolationErrorLocation_name_0 49 | case 2 <= i && i <= 15: 50 | i -= 2 51 | return _ProtocolViolationErrorLocation_name_1[_ProtocolViolationErrorLocation_index_1[i]:_ProtocolViolationErrorLocation_index_1[i+1]] 52 | case i == 18: 53 | return _ProtocolViolationErrorLocation_name_2 54 | case 24 <= i && i <= 27: 55 | i -= 24 56 | return _ProtocolViolationErrorLocation_name_3[_ProtocolViolationErrorLocation_index_3[i]:_ProtocolViolationErrorLocation_index_3[i+1]] 57 | default: 58 | return "ProtocolViolationErrorLocation(" + strconv.FormatInt(int64(i), 10) + ")" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /pkg/socketcan/receiver.go: -------------------------------------------------------------------------------- 1 | package socketcan 2 | 3 | import ( 4 | "bufio" 5 | "io" 6 | 7 | "go.einride.tech/can" 8 | ) 9 | 10 | type ReceiverOption func(*receiverOpts) 11 | 12 | type receiverOpts struct { 13 | frameInterceptor FrameInterceptor 14 | } 15 | 16 | type Receiver struct { 17 | opts receiverOpts 18 | rc io.ReadCloser 19 | sc *bufio.Scanner 20 | frame frame 21 | } 22 | 23 | func NewReceiver(rc io.ReadCloser, opt ...ReceiverOption) *Receiver { 24 | opts := receiverOpts{} 25 | for _, f := range opt { 26 | f(&opts) 27 | } 28 | sc := bufio.NewScanner(rc) 29 | sc.Split(scanFrames) 30 | return &Receiver{ 31 | rc: rc, 32 | opts: opts, 33 | sc: sc, 34 | } 35 | } 36 | 37 | func scanFrames(data []byte, _ bool) (int, []byte, error) { 38 | if len(data) < lengthOfFrame { 39 | // not enough data for a full frame 40 | return 0, nil, nil 41 | } 42 | return lengthOfFrame, data[0:lengthOfFrame], nil 43 | } 44 | 45 | func (r *Receiver) Receive() bool { 46 | ok := r.sc.Scan() 47 | r.frame = frame{} 48 | if ok { 49 | r.frame.unmarshalBinary(r.sc.Bytes()) 50 | if r.opts.frameInterceptor != nil { 51 | r.opts.frameInterceptor(r.frame.decodeFrame()) 52 | } 53 | } 54 | return ok 55 | } 56 | 57 | func (r *Receiver) HasErrorFrame() bool { 58 | return r.frame.isError() 59 | } 60 | 61 | func (r *Receiver) Frame() can.Frame { 62 | return r.frame.decodeFrame() 63 | } 64 | 65 | func (r *Receiver) ErrorFrame() ErrorFrame { 66 | return r.frame.decodeErrorFrame() 67 | } 68 | 69 | func (r *Receiver) Err() error { 70 | return r.sc.Err() 71 | } 72 | 73 | func (r *Receiver) Close() error { 74 | return r.rc.Close() 75 | } 76 | 77 | // ReceiverFrameInterceptor returns a ReceiverOption that sets the FrameInterceptor for the 78 | // receiver. Only one frame interceptor can be installed. 79 | func ReceiverFrameInterceptor(i FrameInterceptor) ReceiverOption { 80 | return func(o *receiverOpts) { 81 | o.frameInterceptor = i 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /pkg/socketcan/receiver_test.go: -------------------------------------------------------------------------------- 1 | package socketcan 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "testing" 7 | 8 | "go.einride.tech/can" 9 | "gotest.tools/v3/assert" 10 | ) 11 | 12 | func TestReceiver_ReceiveFrames_Options(t *testing.T) { 13 | testReceive := func(opt ReceiverOption) { 14 | input := []byte{ 15 | // id---------------> | dlc | padding-------> | data----------------------------------------> | 16 | 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x12, 0x34, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 17 | } 18 | expected := can.Frame{ID: 0x01, Length: 2, Data: can.Data{0x12, 0x34}} 19 | receiver := NewReceiver(io.NopCloser(bytes.NewReader(input)), opt) 20 | assert.Assert(t, receiver.Receive(), "expecting 1 CAN frames") 21 | assert.NilError(t, receiver.Err()) 22 | assert.Assert(t, !receiver.HasErrorFrame()) 23 | assert.DeepEqual(t, expected, receiver.Frame()) 24 | assert.Assert(t, !receiver.Receive(), "expecting exactly 1 CAN frames") 25 | assert.NilError(t, receiver.Err()) 26 | } 27 | 28 | // no options 29 | testReceive(func(*receiverOpts) {}) 30 | 31 | // frame interceptor 32 | run := false 33 | intFunc := func(can.Frame) { 34 | run = true 35 | } 36 | testReceive(ReceiverFrameInterceptor(intFunc)) 37 | assert.Assert(t, run) 38 | } 39 | 40 | func TestReceiver_ReceiveFrames(t *testing.T) { 41 | for _, tt := range []struct { 42 | msg string 43 | input []byte 44 | expectedFrames []can.Frame 45 | }{ 46 | { 47 | msg: "no data", 48 | input: []byte{}, 49 | expectedFrames: []can.Frame{}, 50 | }, 51 | { 52 | msg: "incomplete frame", 53 | input: []byte{ 54 | 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 55 | }, 56 | expectedFrames: []can.Frame{}, 57 | }, 58 | { 59 | msg: "whole single frame", 60 | input: []byte{ 61 | // id---------------> | dlc | padding-------> | data----------------------------------------> | 62 | 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x12, 0x34, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 63 | }, 64 | expectedFrames: []can.Frame{ 65 | {ID: 0x01, Length: 2, Data: can.Data{0x12, 0x34}}, 66 | }, 67 | }, 68 | { 69 | msg: "one whole one incomplete", 70 | input: []byte{ 71 | // id---------------> | dlc | padding-------> | data----------------------------------------> | 72 | 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x12, 0x34, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 73 | 0x00, 74 | }, 75 | expectedFrames: []can.Frame{ 76 | {ID: 0x01, Length: 2, Data: can.Data{0x12, 0x34}}, 77 | }, 78 | }, 79 | { 80 | msg: "two whole frames", 81 | input: []byte{ 82 | // id---------------> | dlc | padding-------> | data----------------------------------------> | 83 | 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x12, 0x34, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 84 | // id---------------> | dlc | padding-------> | data----------------------------------------> | 85 | 0x02, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x56, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 86 | }, 87 | expectedFrames: []can.Frame{ 88 | {ID: 0x01, Length: 2, Data: can.Data{0x12, 0x34}}, 89 | {ID: 0x02, Length: 2, Data: can.Data{0x56, 0x78}}, 90 | }, 91 | }, 92 | } { 93 | t.Run(tt.msg, func(t *testing.T) { 94 | receiver := NewReceiver(io.NopCloser(bytes.NewReader(tt.input))) 95 | for i, expected := range tt.expectedFrames { 96 | assert.Assert(t, receiver.Receive(), "expecting %d CAN frames", i+1) 97 | assert.NilError(t, receiver.Err()) 98 | assert.Assert(t, !receiver.HasErrorFrame()) 99 | assert.DeepEqual(t, expected, receiver.Frame()) 100 | } 101 | assert.Assert(t, !receiver.Receive(), "expecting exactly %d CAN frames", len(tt.expectedFrames)) 102 | assert.NilError(t, receiver.Err()) 103 | }) 104 | } 105 | } 106 | 107 | func TestReceiver_ReceiveErrorFrame(t *testing.T) { 108 | input := []byte{ 109 | // frame 110 | // id---------------> | dlc | padding-------> | data----------------------------------------> | 111 | 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x12, 0x34, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 112 | // error frame 113 | // id---------------> | dlc | padding-------> | data----------------------------------------> | 114 | 0x01, 0x00, 0x00, 0x20, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 115 | // frame 116 | // id---------------> | dlc | padding-------> | data----------------------------------------> | 117 | 0x02, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x12, 0x34, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 118 | } 119 | receiver := NewReceiver(io.NopCloser(bytes.NewReader(input))) 120 | // expect frame 121 | assert.Assert(t, receiver.Receive()) 122 | assert.Assert(t, !receiver.HasErrorFrame()) 123 | assert.Equal(t, can.Frame{ID: 0x01, Length: 2, Data: can.Data{0x12, 0x34}}, receiver.Frame()) 124 | // expect error frame 125 | assert.Assert(t, receiver.Receive()) 126 | assert.Assert(t, receiver.HasErrorFrame()) 127 | assert.Equal(t, ErrorFrame{ErrorClass: ErrorClassTxTimeout}, receiver.ErrorFrame()) 128 | // expect frame 129 | assert.Assert(t, receiver.Receive()) 130 | assert.Assert(t, !receiver.HasErrorFrame()) 131 | assert.Equal(t, can.Frame{ID: 0x02, Length: 2, Data: can.Data{0x12, 0x34}}, receiver.Frame()) 132 | // expect end of stream 133 | assert.Assert(t, !receiver.Receive()) 134 | assert.NilError(t, receiver.Err()) 135 | } 136 | -------------------------------------------------------------------------------- /pkg/socketcan/transceivererror.go: -------------------------------------------------------------------------------- 1 | package socketcan 2 | 3 | type TransceiverError uint8 4 | 5 | //go:generate stringer -type TransceiverError -trimprefix TransceiverError 6 | 7 | const ( 8 | TransceiverErrorUnspecified TransceiverError = 0x00 9 | TransceiverErrorCANHNoWire TransceiverError = 0x04 10 | TransceiverErrorCANHShortToBat TransceiverError = 0x05 11 | TransceiverErrorCANHShortToVCC TransceiverError = 0x06 12 | TransceiverErrorCANHShortToGND TransceiverError = 0x07 13 | TransceiverErrorCANLNoWire TransceiverError = 0x40 14 | TransceiverErrorCANLShortToBat TransceiverError = 0x50 15 | TransceiverErrorCANLShortToVcc TransceiverError = 0x60 16 | TransceiverErrorCANLShortToGND TransceiverError = 0x70 17 | TransceiverErrorCANLShortToCANH TransceiverError = 0x80 18 | ) 19 | -------------------------------------------------------------------------------- /pkg/socketcan/transceivererror_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type TransceiverError -trimprefix TransceiverError"; DO NOT EDIT. 2 | 3 | package socketcan 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[TransceiverErrorUnspecified-0] 12 | _ = x[TransceiverErrorCANHNoWire-4] 13 | _ = x[TransceiverErrorCANHShortToBat-5] 14 | _ = x[TransceiverErrorCANHShortToVCC-6] 15 | _ = x[TransceiverErrorCANHShortToGND-7] 16 | _ = x[TransceiverErrorCANLNoWire-64] 17 | _ = x[TransceiverErrorCANLShortToBat-80] 18 | _ = x[TransceiverErrorCANLShortToVcc-96] 19 | _ = x[TransceiverErrorCANLShortToGND-112] 20 | _ = x[TransceiverErrorCANLShortToCANH-128] 21 | } 22 | 23 | const ( 24 | _TransceiverError_name_0 = "Unspecified" 25 | _TransceiverError_name_1 = "CANHNoWireCANHShortToBatCANHShortToVCCCANHShortToGND" 26 | _TransceiverError_name_2 = "CANLNoWire" 27 | _TransceiverError_name_3 = "CANLShortToBat" 28 | _TransceiverError_name_4 = "CANLShortToVcc" 29 | _TransceiverError_name_5 = "CANLShortToGND" 30 | _TransceiverError_name_6 = "CANLShortToCANH" 31 | ) 32 | 33 | var ( 34 | _TransceiverError_index_1 = [...]uint8{0, 10, 24, 38, 52} 35 | ) 36 | 37 | func (i TransceiverError) String() string { 38 | switch { 39 | case i == 0: 40 | return _TransceiverError_name_0 41 | case 4 <= i && i <= 7: 42 | i -= 4 43 | return _TransceiverError_name_1[_TransceiverError_index_1[i]:_TransceiverError_index_1[i+1]] 44 | case i == 64: 45 | return _TransceiverError_name_2 46 | case i == 80: 47 | return _TransceiverError_name_3 48 | case i == 96: 49 | return _TransceiverError_name_4 50 | case i == 112: 51 | return _TransceiverError_name_5 52 | case i == 128: 53 | return _TransceiverError_name_6 54 | default: 55 | return "TransceiverError(" + strconv.FormatInt(int64(i), 10) + ")" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /pkg/socketcan/transmitter.go: -------------------------------------------------------------------------------- 1 | package socketcan 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | 8 | "go.einride.tech/can" 9 | ) 10 | 11 | type TransmitterOption func(*transmitterOpts) 12 | 13 | type transmitterOpts struct { 14 | frameInterceptor FrameInterceptor 15 | } 16 | 17 | // Transmitter transmits CAN frames. 18 | type Transmitter struct { 19 | opts transmitterOpts 20 | conn net.Conn 21 | } 22 | 23 | // NewTransmitter creates a new transmitter that transmits CAN frames to the provided io.Writer. 24 | func NewTransmitter(conn net.Conn, opt ...TransmitterOption) *Transmitter { 25 | opts := transmitterOpts{} 26 | for _, f := range opt { 27 | f(&opts) 28 | } 29 | return &Transmitter{ 30 | conn: conn, 31 | opts: opts, 32 | } 33 | } 34 | 35 | // TransmitMessage transmits a CAN message. 36 | func (t *Transmitter) TransmitMessage(ctx context.Context, m can.Message) error { 37 | f, err := m.MarshalFrame() 38 | if err != nil { 39 | return fmt.Errorf("transmit message: %w", err) 40 | } 41 | return t.TransmitFrame(ctx, f) 42 | } 43 | 44 | // TransmitFrame transmits a CAN frame. 45 | func (t *Transmitter) TransmitFrame(ctx context.Context, f can.Frame) error { 46 | var scf frame 47 | scf.encodeFrame(f) 48 | data := make([]byte, lengthOfFrame) 49 | scf.marshalBinary(data) 50 | if deadline, ok := ctx.Deadline(); ok { 51 | if err := t.conn.SetWriteDeadline(deadline); err != nil { 52 | return fmt.Errorf("transmit frame: %w", err) 53 | } 54 | } 55 | if _, err := t.conn.Write(data); err != nil { 56 | return fmt.Errorf("transmit frame: %w", err) 57 | } 58 | if t.opts.frameInterceptor != nil { 59 | t.opts.frameInterceptor(f) 60 | } 61 | return nil 62 | } 63 | 64 | // Close the transmitter's underlying connection. 65 | func (t *Transmitter) Close() error { 66 | return t.conn.Close() 67 | } 68 | 69 | // TransmitterFrameInterceptor returns a TransmitterOption that sets the FrameInterceptor for the 70 | // transmitter. Only one frame interceptor can be installed. 71 | func TransmitterFrameInterceptor(i FrameInterceptor) TransmitterOption { 72 | return func(o *transmitterOpts) { 73 | o.frameInterceptor = i 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /pkg/socketcan/transmitter_test.go: -------------------------------------------------------------------------------- 1 | package socketcan 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "net" 9 | "testing" 10 | "time" 11 | 12 | "go.einride.tech/can" 13 | "golang.org/x/sync/errgroup" 14 | "gotest.tools/v3/assert" 15 | ) 16 | 17 | func TestTransmitter_TransmitMessage(t *testing.T) { 18 | testTransmit := func(opt TransmitterOption) { 19 | w, r := net.Pipe() 20 | f := can.Frame{ 21 | ID: 0x12, 22 | Length: 8, 23 | Data: can.Data{0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0}, 24 | } 25 | msg := &testMessage{frame: f} 26 | expected := []byte{ 27 | // id---------------> | dlc | padding-------> | data----------------------------------------> | 28 | 0x12, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x12, 0x34, 0x56, 0x78, 0x9a, 0xbc, 0xde, 0xf0, 29 | } 30 | // write 31 | var g errgroup.Group 32 | g.Go(func() error { 33 | tr := NewTransmitter(w, opt) 34 | ctx, done := context.WithTimeout(context.Background(), time.Second) 35 | defer done() 36 | if err := tr.TransmitMessage(ctx, msg); err != nil { 37 | return err 38 | } 39 | return w.Close() 40 | }) 41 | // read 42 | actual := make([]byte, len(expected)) 43 | _, err := io.ReadFull(r, actual) 44 | assert.NilError(t, err) 45 | assert.NilError(t, r.Close()) 46 | // assert 47 | assert.DeepEqual(t, expected, actual) 48 | assert.NilError(t, g.Wait()) 49 | } 50 | 51 | // No opts 52 | testTransmit(func(*transmitterOpts) {}) 53 | 54 | // Frame Interceptor 55 | run := false 56 | intFunc := func(_ can.Frame) { 57 | run = true 58 | } 59 | testTransmit(TransmitterFrameInterceptor(intFunc)) 60 | assert.Assert(t, run) 61 | } 62 | 63 | func TestTransmitter_TransmitMessage_Error(t *testing.T) { 64 | cause := fmt.Errorf("boom") 65 | msg := &testMessage{err: cause} 66 | tr := NewTransmitter(nil) 67 | ctx, done := context.WithTimeout(context.Background(), time.Second) 68 | defer done() 69 | err := tr.TransmitMessage(ctx, msg) 70 | assert.Error(t, err, "transmit message: boom") 71 | assert.Equal(t, cause, errors.Unwrap(err)) 72 | } 73 | 74 | func TestTransmitter_TransmitFrame_Error(t *testing.T) { 75 | t.Run("set deadline", func(t *testing.T) { 76 | cause := fmt.Errorf("boom") 77 | w := &errCon{deadlineErr: cause} 78 | tr := NewTransmitter(w) 79 | ctx, done := context.WithTimeout(context.Background(), time.Second) 80 | defer done() 81 | err := tr.TransmitFrame(ctx, can.Frame{}) 82 | assert.ErrorContains(t, err, "boom") 83 | assert.Equal(t, cause, errors.Unwrap(err)) 84 | }) 85 | t.Run("write", func(t *testing.T) { 86 | cause := fmt.Errorf("boom") 87 | w := &errCon{writeErr: cause} 88 | tr := NewTransmitter(w) 89 | ctx, done := context.WithTimeout(context.Background(), time.Second) 90 | defer done() 91 | err := tr.TransmitFrame(ctx, can.Frame{}) 92 | assert.ErrorContains(t, err, "boom") 93 | assert.Equal(t, cause, errors.Unwrap(err)) 94 | }) 95 | } 96 | 97 | type testMessage struct { 98 | frame can.Frame 99 | err error 100 | } 101 | 102 | func (t *testMessage) MarshalFrame() (can.Frame, error) { 103 | return t.frame, t.err 104 | } 105 | 106 | func (t *testMessage) UnmarshalFrame(can.Frame) error { 107 | panic("should not be called") 108 | } 109 | 110 | type errCon struct { 111 | deadlineErr error 112 | writeErr error 113 | } 114 | 115 | func (e *errCon) Write([]byte) (n int, err error) { 116 | return 0, e.writeErr 117 | } 118 | 119 | func (e *errCon) SetWriteDeadline(time.Time) error { 120 | return e.deadlineErr 121 | } 122 | 123 | func (e *errCon) Read([]byte) (n int, err error) { 124 | panic("should not be called") 125 | } 126 | 127 | func (e *errCon) Close() error { 128 | panic("should not be called") 129 | } 130 | 131 | func (e *errCon) LocalAddr() net.Addr { 132 | panic("should not be called") 133 | } 134 | 135 | func (e *errCon) RemoteAddr() net.Addr { 136 | panic("should not be called") 137 | } 138 | 139 | func (e *errCon) SetDeadline(time.Time) error { 140 | panic("should not be called") 141 | } 142 | 143 | func (e *errCon) SetReadDeadline(time.Time) error { 144 | panic("should not be called") 145 | } 146 | -------------------------------------------------------------------------------- /pkg/socketcan/udp.go: -------------------------------------------------------------------------------- 1 | package socketcan 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "strconv" 7 | "time" 8 | 9 | "golang.org/x/net/ipv4" 10 | "golang.org/x/net/nettest" 11 | ) 12 | 13 | // udpTxRx emulates a single `net.Conn` that can be used for both transmitting 14 | // and receiving UDP multicast packets. 15 | type udpTxRx struct { 16 | tx *ipv4.PacketConn 17 | rx *ipv4.PacketConn 18 | groupAddr *net.UDPAddr 19 | } 20 | 21 | func (utr *udpTxRx) Close() error { 22 | if err := utr.tx.Close(); err != nil { 23 | _ = utr.rx.Close() 24 | return err 25 | } 26 | return utr.rx.Close() 27 | } 28 | 29 | func (utr *udpTxRx) LocalAddr() net.Addr { 30 | return utr.rx.LocalAddr() 31 | } 32 | 33 | func (utr *udpTxRx) SetDeadline(t time.Time) error { 34 | if err := utr.rx.SetReadDeadline(t); err != nil { 35 | return err 36 | } 37 | return utr.tx.SetWriteDeadline(t) 38 | } 39 | 40 | func (utr *udpTxRx) SetReadDeadline(t time.Time) error { 41 | return utr.rx.SetReadDeadline(t) 42 | } 43 | 44 | func (utr *udpTxRx) SetWriteDeadline(t time.Time) error { 45 | return utr.tx.SetWriteDeadline(t) 46 | } 47 | 48 | func (utr *udpTxRx) Read(b []byte) (n int, err error) { 49 | n, _, _, err = utr.rx.ReadFrom(b) 50 | return 51 | } 52 | 53 | func (utr *udpTxRx) Write(b []byte) (n int, err error) { 54 | return utr.tx.WriteTo(b, nil, nil) 55 | } 56 | 57 | func (utr *udpTxRx) RemoteAddr() net.Addr { 58 | return utr.groupAddr 59 | } 60 | 61 | func udpTransceiver(network, address string) (*udpTxRx, error) { 62 | if network != udp { 63 | return nil, fmt.Errorf("[%v] is not a udp network", network) 64 | } 65 | ifi, err := getMulticastInterface() 66 | if err != nil { 67 | return nil, fmt.Errorf("new UDP transceiver: %w", err) 68 | } 69 | rx, groupAddr, err := udpReceiver(address, ifi) 70 | if err != nil { 71 | return nil, fmt.Errorf("new UDP transceiver: %w", err) 72 | } 73 | tx, err := udpTransmitter(groupAddr, ifi) 74 | if err != nil { 75 | return nil, fmt.Errorf("new UDP transceiver: %w", err) 76 | } 77 | return &udpTxRx{rx: rx, tx: tx, groupAddr: groupAddr}, nil 78 | } 79 | 80 | func getMulticastInterface() (*net.Interface, error) { 81 | ifi, err := nettest.RoutedInterface("ip4", net.FlagUp|net.FlagMulticast|net.FlagLoopback) 82 | if err == nil { 83 | return ifi, nil 84 | } 85 | return nettest.RoutedInterface("ip4", net.FlagUp|net.FlagMulticast) 86 | } 87 | 88 | func hostPortToUDPAddr(hostport string) (*net.UDPAddr, error) { 89 | host, portStr, err := net.SplitHostPort(hostport) 90 | if err != nil { 91 | return nil, fmt.Errorf("convert hostport to udp addr: %w", err) 92 | } 93 | port, err := strconv.Atoi(portStr) 94 | if err != nil { 95 | return nil, fmt.Errorf("convert hostport to udp addr: %w", err) 96 | } 97 | ip := net.ParseIP(host) 98 | return &net.UDPAddr{Port: port, IP: ip}, nil 99 | } 100 | 101 | func setMulticastOpts(p *ipv4.PacketConn, ifi *net.Interface, groupAddr net.Addr) error { 102 | if err := p.JoinGroup(ifi, groupAddr); err != nil { 103 | return err 104 | } 105 | if err := p.SetMulticastInterface(ifi); err != nil { 106 | return err 107 | } 108 | if err := p.SetMulticastLoopback(true); err != nil { 109 | return err 110 | } 111 | if err := p.SetMulticastTTL(0); err != nil { 112 | return err 113 | } 114 | return p.SetTOS(0x0) 115 | } 116 | 117 | func udpReceiver(address string, ifi *net.Interface) (*ipv4.PacketConn, *net.UDPAddr, error) { 118 | c, err := net.ListenPacket("udp4", address) 119 | if err != nil { 120 | return nil, nil, fmt.Errorf("create udp receiver: %w", err) 121 | } 122 | groupAddr, err := hostPortToUDPAddr(address) 123 | if err != nil { 124 | return nil, nil, fmt.Errorf("create udp receiver: %w", err) 125 | } 126 | // If requested port is 0, one is provided when creating the packet listener 127 | if groupAddr.Port == 0 { 128 | localAddr, err := hostPortToUDPAddr(c.LocalAddr().String()) 129 | if err != nil { 130 | return nil, nil, fmt.Errorf("create udp receiver: %w", err) 131 | } 132 | groupAddr.Port = localAddr.Port 133 | } 134 | rx := ipv4.NewPacketConn(c) 135 | if err := setMulticastOpts(rx, ifi, groupAddr); err != nil { 136 | return nil, nil, fmt.Errorf("new UDP transceiver: %w", err) 137 | } 138 | return rx, groupAddr, nil 139 | } 140 | 141 | func udpTransmitter(groupAddr *net.UDPAddr, ifi *net.Interface) (*ipv4.PacketConn, error) { 142 | c, err := net.DialUDP("udp4", nil, groupAddr) 143 | if err != nil { 144 | return nil, fmt.Errorf("new UDP transmitter: %w", err) 145 | } 146 | tx := ipv4.NewPacketConn(c) 147 | if err := tx.SetMulticastInterface(ifi); err != nil { 148 | return nil, fmt.Errorf("new UDP transmitter: %w", err) 149 | } 150 | return tx, nil 151 | } 152 | -------------------------------------------------------------------------------- /testdata/dbc-invalid/example/example_float32_invalid_signal_length.dbc: -------------------------------------------------------------------------------- 1 | VERSION "" 2 | 3 | NS_ : 4 | 5 | BS_: 6 | 7 | BU_: DBG IO 8 | 9 | BO_ 42 IOFloat32: 8 IO 10 | SG_ Float32Signal : 0|16@1- (1,0) [0|0] "" DBG 11 | 12 | SIG_VALTYPE_ 42 Float32Signal: 1; 13 | -------------------------------------------------------------------------------- /testdata/dbc-invalid/example/example_float32_invalid_signal_name.dbc: -------------------------------------------------------------------------------- 1 | VERSION "" 2 | 3 | NS_ : 4 | 5 | BS_: 6 | 7 | BU_: DBG IO 8 | 9 | BO_ 42 IOFloat32: 8 IO 10 | SG_ Float32Signal : 0|32@1- (1,0) [0|0] "" DBG 11 | 12 | SIG_VALTYPE_ 42 SomeOtherSignal: 1; 13 | -------------------------------------------------------------------------------- /testdata/dbc-invalid/example/example_float64_signal.dbc: -------------------------------------------------------------------------------- 1 | VERSION "" 2 | 3 | NS_ : 4 | 5 | BS_: 6 | 7 | BU_: DBG IO 8 | 9 | BO_ 42 IOFloat64: 8 IO 10 | SG_ Float64Signal : 0|64@1- (1,0) [0|0] "" DBG 11 | 12 | SIG_VALTYPE_ 42 Float64Signal: 2; 13 | -------------------------------------------------------------------------------- /testdata/dbc-invalid/example/example_metadata_invalid_signal_reference.dbc: -------------------------------------------------------------------------------- 1 | VERSION "" 2 | 3 | NS_ : 4 | 5 | BS_: 6 | 7 | BU_: DBG IO 8 | 9 | BO_ 42 AMessage: 8 Vector__XXX 10 | SG_ ASignal : 7|16@0- (1,0) [-32768|32767] "%" Vector__XXX 11 | 12 | BA_DEF_ SG_ "GenSigStartValue" FLOAT -3.4E+038 3.4E+038; 13 | BA_DEF_DEF_ "GenSigStartValue" 0; 14 | BA_ "GenSigStartValue" SG_ 42 AnotherSignalName 0; 15 | -------------------------------------------------------------------------------- /testdata/dbc/example/example.dbc: -------------------------------------------------------------------------------- 1 | VERSION "" 2 | 3 | NS_ : 4 | 5 | BS_: 6 | 7 | BU_: DBG DRIVER IO MOTOR SENSOR 8 | 9 | BO_ 1 EmptyMessage: 0 DBG 10 | 11 | BO_ 100 DriverHeartbeat: 1 DRIVER 12 | SG_ Command : 0|8@1+ (1,0) [0|0] "" SENSOR,MOTOR 13 | 14 | BO_ 101 MotorCommand: 1 DRIVER 15 | SG_ Steer : 0|4@1- (1,-5) [-5|5] "" MOTOR 16 | SG_ Drive : 4|4@1+ (1,0) [0|9] "" MOTOR 17 | 18 | BO_ 400 MotorStatus: 3 MOTOR 19 | SG_ WheelError : 0|1@1+ (1,0) [0|0] "" DRIVER,IO 20 | SG_ SpeedKph : 8|16@1+ (0.001,0) [0|0] "km/h" DRIVER,IO 21 | 22 | BO_ 200 SensorSonars: 8 SENSOR 23 | SG_ Mux M : 0|4@1+ (1,0) [0|0] "" DRIVER,IO 24 | SG_ ErrCount : 4|12@1+ (1,0) [0|0] "" DRIVER,IO 25 | SG_ Left m0 : 16|12@1+ (0.1,0) [0|0] "" DRIVER,IO 26 | SG_ Middle m0 : 28|12@1+ (0.1,0) [0|0] "" DRIVER,IO 27 | SG_ Right m0 : 40|12@1+ (0.1,0) [0|0] "" DRIVER,IO 28 | SG_ Rear m0 : 52|12@1+ (0.1,0) [0|0] "" DRIVER,IO 29 | SG_ NoFiltLeft m1 : 16|12@1+ (0.1,0) [0|0] "" DBG 30 | SG_ NoFiltMiddle m1 : 28|12@1+ (0.1,0) [0|0] "" DBG 31 | SG_ NoFiltRight m1 : 40|12@1+ (0.1,0) [0|0] "" DBG 32 | SG_ NoFiltRear m1 : 52|12@1+ (0.1,0) [0|0] "" DBG 33 | 34 | BO_ 500 IODebug: 6 IO 35 | SG_ TestUnsigned : 0|8@1+ (1,0) [0|0] "" DBG 36 | SG_ TestEnum : 8|6@1+ (1,0) [0|0] "" DBG 37 | SG_ TestSigned : 16|8@1- (1,0) [0|0] "" DBG 38 | SG_ TestFloat : 24|8@1+ (0.5,0) [0|0] "" DBG 39 | SG_ TestBoolEnum : 32|1@1+ (1,0) [0|0] "" DBG 40 | SG_ TestScaledEnum : 40|2@1+ (2,0) [0|6] "" DBG 41 | 42 | BO_ 600 IOFloat32: 8 IO 43 | SG_ Float32ValueNoRange : 0|32@1- (1,0) [0|0] "" DBG 44 | SG_ Float32WithRange : 32|32@1- (1,0) [-100|100] "" DBG 45 | 46 | BO_ 700 SignalNameFormatting: 8 IO 47 | SG_ non_capitalized_signal : 0|8@1- (1,0) [0|0] "" DBG 48 | 49 | EV_ BrakeEngaged: 0 [0|1] "" 0 10 DUMMY_NODE_VECTOR0 Vector__XXX; 50 | EV_ Torque: 1 [0|30000] "mNm" 500 16 DUMMY_NODE_VECTOR0 Vector__XXX; 51 | 52 | CM_ EV_ BrakeEngaged "Brake fully engaged"; 53 | CM_ BU_ DRIVER "The driver controller driving the car"; 54 | CM_ BU_ MOTOR "The motor controller of the car"; 55 | CM_ BU_ SENSOR "The sensor controller of the car"; 56 | CM_ BO_ 100 "Sync message used to synchronize the controllers"; 57 | 58 | BA_DEF_ "BusType" STRING ; 59 | BA_DEF_ BO_ "GenMsgSendType" ENUM "None","Cyclic","OnEvent"; 60 | BA_DEF_ BO_ "GenMsgCycleTime" INT 0 0; 61 | BA_DEF_ SG_ "FieldType" STRING ; 62 | BA_DEF_ SG_ "GenSigStartValue" INT 0 10000; 63 | BA_DEF_ SG_ "SPN" INT -3.4E+038 3.4E+038; 64 | BA_DEF_DEF_ "BusType" "CAN"; 65 | BA_DEF_DEF_ "FieldType" ""; 66 | BA_DEF_DEF_ "GenMsgCycleTime" 0; 67 | BA_DEF_DEF_ "GenSigStartValue" 0; 68 | 69 | BA_ "GenMsgSendType" BO_ 1 0; 70 | BA_ "GenMsgSendType" BO_ 100 1; 71 | BA_ "GenMsgCycleTime" BO_ 100 1000; 72 | BA_ "GenMsgSendType" BO_ 101 1; 73 | BA_ "GenMsgCycleTime" BO_ 101 100; 74 | BA_ "GenMsgSendType" BO_ 200 1; 75 | BA_ "GenMsgCycleTime" BO_ 200 100; 76 | BA_ "GenMsgSendType" BO_ 400 1; 77 | BA_ "GenMsgCycleTime" BO_ 400 100; 78 | BA_ "GenMsgSendType" BO_ 500 2; 79 | BA_ "FieldType" SG_ 100 Command "Command"; 80 | BA_ "FieldType" SG_ 500 TestEnum "TestEnum"; 81 | BA_ "GenSigStartValue" SG_ 500 TestEnum 2; 82 | 83 | VAL_ 100 Command 3 "Headlights On" 2 "Reboot" 1 "Sync" 0 "None" ; 84 | VAL_ 500 TestEnum 2 "Two" 1 "One" ; 85 | VAL_ 500 TestScaledEnum 3 "Six" 2 "Four" 1 "Two" 0 "Zero" ; 86 | VAL_ 500 TestBoolEnum 1 "One" 0 "Zero" ; 87 | 88 | SIG_VALTYPE_ 600 Float32ValueNoRange: 1; 89 | SIG_VALTYPE_ 600 Float32WithRange: 1; 90 | -------------------------------------------------------------------------------- /tools.go: -------------------------------------------------------------------------------- 1 | //go:build tools 2 | 3 | package tools 4 | 5 | import ( 6 | _ "github.com/golang/mock/mockgen" 7 | _ "golang.org/x/tools/cmd/stringer" 8 | ) 9 | --------------------------------------------------------------------------------