Refactor kafka to pure Go (franz-go), fix DBC stubs, update Dockerfile

This commit is contained in:
Chris Rai
2026-01-31 00:05:47 -05:00
parent fbb820d7b3
commit b5bec57dfa
776 changed files with 18945 additions and 2052 deletions

3
pkg/can-go/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
# generated DBC files
*.dbc.go
*.dbc

View File

@@ -0,0 +1,129 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
- Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
- The use of sexualized language or imagery, and sexual attention or
advances of any kind
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email
address, without their explicit permission
- Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
<open-source@einride.tech>.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.

21
pkg/can-go/LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2019 Einride AB
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

70
pkg/can-go/Makefile Normal file
View File

@@ -0,0 +1,70 @@
SHELL := /bin/bash
all: \
commitlint \
stringer-generate \
mockgen-generate \
testdata \
go-lint \
go-review \
go-test \
go-mod-tidy \
git-verify-nodiff
include tools/commitlint/rules.mk
include tools/git-verify-nodiff/rules.mk
include tools/golangci-lint/rules.mk
include tools/goreview/rules.mk
include tools/semantic-release/rules.mk
include tools/stringer/rules.mk
.PHONY: clean
clean:
$(info [$@] removing build files...)
@rm -rf tools/*/*/ build
.PHONY: mockgen-generate
mockgen-generate: \
internal/gen/mock/mockcanrunner/mocks.go \
internal/gen/mock/mockclock/mocks.go \
internal/gen/mock/mocksocketcan/mocks.go
internal/gen/mock/mockcanrunner/mocks.go: pkg/canrunner/run.go go.mod
go run github.com/golang/mock/mockgen \
-destination $@ -package mockcanrunner github.com/Fisker-Inc/project-ai-can-go/pkg/canrunner \
Node,TransmittedMessage,ReceivedMessage,FrameTransmitter,FrameReceiver
internal/gen/mock/mockclock/mocks.go: internal/clock/clock.go go.mod
go run github.com/golang/mock/mockgen \
-destination $@ -package mockclock github.com/Fisker-Inc/project-ai-can-go/internal/clock \
Clock,Ticker
internal/gen/mock/mocksocketcan/mocks.go: pkg/socketcan/fileconn.go go.mod
go run github.com/golang/mock/mockgen \
-destination $@ -package mocksocketcan -source $<
.PHONY: stringer-generate
stringer-generate: \
pkg/descriptor/sendtype_string.go \
pkg/socketcan/errorclass_string.go \
pkg/socketcan/protocolviolationerrorlocation_string.go \
pkg/socketcan/protocolviolationerror_string.go \
pkg/socketcan/controllererror_string.go \
pkg/socketcan/transceivererror_string.go
%_string.go: %.go $(stringer)
go generate $<
.PHONY: testdata
testdata:
go run cmd/cantool/main.go generate testdata/dbc testdata/gen/go
.PHONY: go-test
go-test:
$(info [$@] running Go tests...)
@mkdir -p build/coverage
@go test -short -race -coverprofile=build/coverage/$@.txt -covermode=atomic ./...
.PHONY: go-mod-tidy
go-mod-tidy:
go mod tidy -v

168
pkg/can-go/README.md Normal file
View File

@@ -0,0 +1,168 @@
# :electric_plug: CAN Go
[![PkgGoDev][pkg-badge]][pkg]
[![GoReportCard][report-badge]][report]
[![Codecov][codecov-badge]][codecov]
[pkg-badge]: https://pkg.go.dev/badge/github.com/Fisker-Inc/project-ai-can-go
[pkg]: https://pkg.go.dev/github.com/Fisker-Inc/project-ai-can-go
[report-badge]: https://goreportcard.com/badge/github.com/Fisker-Inc/project-ai-can-go
[report]: https://goreportcard.com/report/github.com/Fisker-Inc/project-ai-can-go
[codecov-badge]: https://codecov.io/gh/einride/can-go/branch/master/graph/badge.svg
[codecov]: https://codecov.io/gh/einride/can-go
CAN toolkit for Go programmers.
can-go makes use of the Linux SocketCAN abstraction for CAN communication.
(See the [SocketCAN][socketcan] documentation for more details).
[socketcan]: https://www.kernel.org/doc/Documentation/networking/can.txt
## Modifications to the Original Repo
Original repo: `https://github.com/jshiv/can-go/tree/623b1140d54a845026249a5bbbaf23f44c614173`.
Comment out the value descriptor generation code within `can-go/internal/generate/file.go`, lines 128-141. This code creates compile errors due to duplicate values within the `.dbc` file provided.
## DBC Go Code Library Generation
1. Place `.dbc` file into `orig/` folder.
2. Run `scripts/main.go`:
```
go run scripts/main.go
```
This will translate all Chinese variable values within the file to English. Any non-alphanumeric characters are also removed. **NOTE** The default read file is labelled `orig/121-N60AB_ADASBUS_Matrix_CANFD_V2.3.dbc` and the default write file is labelled `dbc/n60.dbc`.
3. There will still be leftover Chinese characters in the `.dbc` file, I chose to just remove them.
4. Generate Go code from cleaned `.dbc` file:
```
can-go> go run cmd/cantool/main.go generate data/dbc dbc
```
## Examples
### Decoding CAN messages
Decoding CAN messages from byte arrays can be done using `can.Payload`
```go
func main() {
// DBC file
var dbcFile = []byte(`
VERSION ""
NS_ :
BS_:
BU_: DBG DRIVER IO MOTOR SENSOR
BO_ 1530 DisconnectState: 14 MOTOR
SG_ LockCountRearRight : 91|20@0+ (1,0) [0|1048575] "" IO
SG_ DisconnectStateRearRight : 95|4@0+ (1,0) [0|5] "" IO
SG_ CurrentRearRight : 79|16@0+ (1,0) [0|65535] "" IO
SG_ DisconnectStateRearRightTarget : 64|1@0+ (1,0) [0|1] "" IO
SG_ TargetSpeedRearRight : 63|15@0+ (0.125,-2048) [-2048|2047.875] "rad/s" IO
SG_ LockCountRearLeft : 35|20@0+ (1,0) [0|1048575] "" IO
SG_ DisconnectStateRearLeft : 39|4@0+ (1,0) [0|5] "" IO
SG_ CurrentRearLeft : 23|16@0+ (1,0) [0|65535] "" IO
SG_ DisconnectStateRearLeftTarget : 8|1@0+ (1,0) [0|1] "" IO
SG_ TargetSpeedRearLeft : 7|15@0+ (0.125,-2048) [-2048|2047.875] "rad/s" IO
VAL_ 1530 DisconnectStateRearRight 0 "Undefined" 1 "Locked" 2 "Unlocked" 3 "Locking" 4 "Unlocking" 5 "Faulted" ;
VAL_ 1530 DisconnectStateRearLeft 0 "Undefined" 1 "Locked" 2 "Unlocked" 3 "Locking" 4 "Unlocking" 5 "Faulted" ;
`)
// Create payload from hex string
byteStringHex := "8000000420061880000005200600"
p, _ := can.PayloadFromHex(byteStringHex)
// Load example dbc file
c, _ := generate.Compile("test.dbc", dbcFile)
db := *c.Database
// Decode message frame ID 1530
message, _ := db.Message(uint32(1530))
decodedSignals := message.Decode(&p)
for _, signal := range decodedSignals {
fmt.Printf("Signal: %s, Value: %f, Description: %s\n", signal.Signal.Name, signal.Value, signal.Description)
}
}
```
```
Signal: TargetSpeedRearLeft, Value: 0.000000, Description:
Signal: DisconnectStateRearLeftTarget, Value: 0.000000, Description:
Signal: CurrentRearLeft, Value: 4.000000, Description:
Signal: LockCountRearLeft, Value: 1560.000000, Description:
Signal: DisconnectStateRearLeft, Value: 2.000000, Description: Unlocked
Signal: TargetSpeedRearRight, Value: 0.000000, Description:
Signal: DisconnectStateRearRightTarget, Value: 0.000000, Description:
Signal: CurrentRearRight, Value: 5.000000, Description:
Signal: LockCountRearRight, Value: 1536.000000, Description:
Signal: DisconnectStateRearRight, Value: 2.000000, Description: Unlocked
```
### Receiving CAN frames
Receiving CAN frames from a socketcan interface.
```go
func main() {
// Error handling omitted to keep example simple
conn, _ := socketcan.DialContext(context.Background(), "can", "can0")
recv := socketcan.NewReceiver(conn)
for recv.Receive() {
frame := recv.Frame()
fmt.Println(frame.String())
}
}
```
### Sending CAN frames/messages
Sending CAN frames to a socketcan interface.
```go
func main() {
// Error handling omitted to keep example simple
conn, _ := socketcan.DialContext(context.Background(), "can", "can0")
frame := can.Frame{}
tx := socketcan.NewTransmitter(conn)
_ = tx.TransmitFrame(context.Background(), frame)
}
```
### Generating Go code from a DBC file
It is possible to generate Go code from a `.dbc` file.
```
$ go run github.com/Fisker-Inc/project-ai-can-go/cmd/cantool generate <dbc file root folder> <output folder>
```
In order to generate Go code that makes sense, we currently perform some
validations when parsing the DBC file so there may need to be some changes
on the DBC file to make it work
After generating Go code we can marshal a message to a frame:
```go
// import etruckcan "github.com/myproject/myrepo/gen"
auxMsg := etruckcan.NewAuxiliary().SetHeadLights(etruckcan.Auxiliary_HeadLights_LowBeam)
frame := auxMsg.Frame()
```
Or unmarshal a frame to a message:
```go
// import etruckcan "github.com/myproject/myrepo/gen"
// Error handling omitted for simplicity
_ := recv.Receive()
frame := recv.Frame()
var auxMsg *etruckcan.Auxiliary
_ = auxMsg.UnmarshalFrame(frame)
```

4
pkg/can-go/can.go Normal file
View File

@@ -0,0 +1,4 @@
// Package can provides primitives for working with CAN.
//
// See: https://en.wikipedia.org/wiki/CAN_bus
package can // import "github.com/fiskerinc/cloud-services/pkg/can-go"

View File

@@ -0,0 +1,212 @@
package main
import (
"errors"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"text/scanner"
"github.com/fiskerinc/cloud-services/pkg/can-go/internal/generate"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis/passes/definitiontypeorder"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis/passes/intervals"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis/passes/lineendings"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis/passes/messagenames"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis/passes/multiplexedsignals"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis/passes/newsymbols"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis/passes/nodereferences"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis/passes/noreservedsignals"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis/passes/requireddefinitions"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis/passes/signalbounds"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis/passes/signalnames"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis/passes/singletondefinitions"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis/passes/siunits"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis/passes/uniquenodenames"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis/passes/uniquesignalnames"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis/passes/unitsuffixes"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis/passes/valuedescriptions"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis/passes/version"
"github.com/fatih/color"
"gopkg.in/alecthomas/kingpin.v2"
)
func main() {
app := kingpin.New("cantool", "CAN tool for Go programmers")
generateCommand(app)
lintCommand(app)
kingpin.MustParse(app.Parse(os.Args[1:]))
}
func generateCommand(app *kingpin.Application) {
command := app.Command("generate", "generate CAN messages")
inputDir := command.
Arg("input-dir", "input directory").
Required().
ExistingDir()
outputDir := command.
Arg("output-dir", "output directory").
Required().
String()
command.Action(func(c *kingpin.ParseContext) error {
return filepath.Walk(*inputDir, func(p string, i os.FileInfo, err error) error {
if err != nil {
return err
}
if i.IsDir() || filepath.Ext(p) != ".dbc" {
return nil
}
relPath, err := filepath.Rel(*inputDir, p)
if err != nil {
return err
}
outputFile := relPath + ".go"
outputPath := filepath.Join(*outputDir, outputFile)
return genGo(p, outputPath)
})
})
}
func lintCommand(app *kingpin.Application) {
command := app.Command("lint", "lint DBC files")
fileOrDir := command.
Arg("file-or-dir", "DBC file or directory").
Required().
ExistingFileOrDir()
command.Action(func(context *kingpin.ParseContext) error {
filesToLint, err := resolveFileOrDirectory(*fileOrDir)
if err != nil {
return err
}
var hasFailed bool
for _, lintFile := range filesToLint {
f, err := os.Open(lintFile)
if err != nil {
return err
}
source, err := ioutil.ReadAll(f)
if err != nil {
return err
}
p := dbc.NewParser(f.Name(), source)
if err := p.Parse(); err != nil {
printError(source, err.Position(), err.Reason(), "parse")
continue
}
for _, a := range analyzers() {
pass := &analysis.Pass{
Analyzer: a,
File: p.File(),
}
if err := a.Run(pass); err != nil {
return err
}
hasFailed = hasFailed || len(pass.Diagnostics) > 0
for _, d := range pass.Diagnostics {
printError(source, d.Pos, d.Message, a.Name)
}
}
}
if hasFailed {
return errors.New("one or more lint errors")
}
return nil
})
}
func analyzers() []*analysis.Analyzer {
return []*analysis.Analyzer{
// TODO: Re-evaluate if we want boolprefix.Analyzer(), since it creates a lot of churn in vendor schemas
definitiontypeorder.Analyzer(),
intervals.Analyzer(),
lineendings.Analyzer(),
messagenames.Analyzer(),
multiplexedsignals.Analyzer(),
newsymbols.Analyzer(),
nodereferences.Analyzer(),
noreservedsignals.Analyzer(),
requireddefinitions.Analyzer(),
signalbounds.Analyzer(),
signalnames.Analyzer(),
singletondefinitions.Analyzer(),
siunits.Analyzer(),
uniquenodenames.Analyzer(),
uniquesignalnames.Analyzer(),
unitsuffixes.Analyzer(),
valuedescriptions.Analyzer(),
version.Analyzer(),
}
}
func genGo(inputFile, outputFile string) error {
if err := os.MkdirAll(filepath.Dir(outputFile), 0o755); err != nil {
return err
}
input, err := ioutil.ReadFile(inputFile)
if err != nil {
return err
}
result, err := generate.Compile(inputFile, input)
if err != nil {
return err
}
for _, warning := range result.Warnings {
return warning
}
output, err := generate.Database(result.Hash, result.Database)
if err != nil {
return err
}
if err := ioutil.WriteFile(outputFile, output, 0o600); err != nil {
return err
}
fmt.Println("hash:", result.Hash)
fmt.Println("version:", result.Database.Version)
fmt.Println("wrote:", outputFile)
return nil
}
func resolveFileOrDirectory(fileOrDirectory string) ([]string, error) {
fileInfo, err := os.Stat(fileOrDirectory)
if err != nil {
return nil, err
}
if !fileInfo.IsDir() {
return []string{fileOrDirectory}, nil
}
var files []string
if err := filepath.Walk(fileOrDirectory, func(path string, info os.FileInfo, err error) error {
if !info.IsDir() && filepath.Ext(path) == ".dbc" {
files = append(files, path)
}
return nil
}); err != nil {
return nil, err
}
return files, nil
}
func printError(source []byte, pos scanner.Position, msg, name string) {
fmt.Printf("\n%s: %s (%s)\n", pos, color.RedString("%s", msg), name)
fmt.Printf("%s\n", getSourceLine(source, pos))
fmt.Printf("%s\n", caretAtPosition(pos))
}
func getSourceLine(source []byte, pos scanner.Position) []byte {
lineStart := pos.Offset
for lineStart > 0 && source[lineStart-1] != '\n' {
lineStart--
}
lineEnd := pos.Offset
for lineEnd < len(source) && source[lineEnd] != '\n' {
lineEnd++
}
return source[lineStart:lineEnd]
}
func caretAtPosition(pos scanner.Position) string {
return strings.Repeat(" ", pos.Column-1) + color.YellowString("^")
}

309
pkg/can-go/data.go Normal file
View File

@@ -0,0 +1,309 @@
package can
import (
"fmt"
"github.com/fiskerinc/cloud-services/pkg/can-go/internal/reinterpret"
)
const MaxDataLength = 8
// Data holds the data in a CAN frame.
//
// Layout
//
// Individual bits in the data are numbered according to the following scheme:
//
// BIT
// NUMBER
// +------+------+------+------+------+------+------+------+
// | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
// BYTE +------+------+------+------+------+------+------+------+
// NUMBER
// +-----+ +------+------+------+------+------+------+------+------+
// | 0 | | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
// +-----+ +------+------+------+------+------+------+------+------+
// | 1 | | 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 |
// +-----+ +------+------+------+------+------+------+------+------+
// | 2 | | 23 | 22 | 21 | 20 | 19 | 18 | 17 | 16 |
// +-----+ +------+------+------+------+------+------+------+------+
// | 3 | | 31 | 30 | 29 | 28 | 27 | 26 | 25 | 24 |
// +-----+ +------+------+------+------+------+------+------+------+
// | 4 | | 39 | 38 | 37 | 36 | 35 | 34 | 33 | 32 |
// +-----+ +------+------+------+------+------+------+------+------+
// | 5 | | 47 | 46 | 45 | 44 | 43 | 42 | 41 | 40 |
// +-----+ +------+------+------+------+------+------+------+------+
// | 6 | | 55 | 54 | 53 | 52 | 51 | 50 | 49 | 48 |
// +-----+ +------+------+------+------+------+------+------+------+
// | 7 | | 63 | 62 | 61 | 60 | 59 | 58 | 57 | 56 |
// +-----+ +------+------+------+------+------+------+------+------+
//
// Bit ranges can be manipulated using little-endian and big-endian bit ordering.
//
// Little-endian bit ranges
//
// Example range of length 32 starting at bit 29:
//
// BIT
// NUMBER
// +------+------+------+------+------+------+------+------+
// | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
// BYTE +------+------+------+------+------+------+------+------+
// NUMBER
// +-----+ +------+------+------+------+------+------+------+------+
// | 0 | | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
// +-----+ +------+------+------+------+------+------+------+------+
// | 1 | | 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 |
// +-----+ +------+------+------+------+------+------+------+------+
// | 2 | | 23 | 22 | 21 | 20 | 19 | 18 | 17 | 16 |
// +-----+ +------+------+------+------+------+------+------+------+
// | 3 | | <-------------LSb | 28 | 27 | 26 | 25 | 24 |
// +-----+ +------+------+------+------+------+------+------+------+
// | 4 | | <-------------------------------------------------- |
// +-----+ +------+------+------+------+------+------+------+------+
// | 5 | | <-------------------------------------------------- |
// +-----+ +------+------+------+------+------+------+------+------+
// | 6 | | <-------------------------------------------------- |
// +-----+ +------+------+------+------+------+------+------+------+
// | 7 | | 63 | 62 | 61 | <-MSb--------------------------- |
// +-----+ +------+------+------+------+------+------+------+------+
//
// Big-endian bit ranges
//
// Example range of length 32 starting at bit 29:
//
// BIT
// NUMBER
// +------+------+------+------+------+------+------+------+
// | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
// BYTE +------+------+------+------+------+------+------+------+
// NUMBER
// +-----+ +------+------+------+------+------+------+------+------+
// | 0 | | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
// +-----+ +------+------+------+------+------+------+------+------+
// | 1 | | 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 |
// +-----+ +------+------+------+------+------+------+------+------+
// | 2 | | 23 | 22 | 21 | 20 | 19 | 18 | 17 | 16 |
// +-----+ +------+------+------+------+------+------+------+------+
// | 3 | | 31 | 30 | <-MSb--------------------------------- |
// +-----+ +------+------+------+------+------+------+------+------+
// | 4 | | <-------------------------------------------------- |
// +-----+ +------+------+------+------+------+------+------+------+
// | 5 | | <-------------------------------------------------- |
// +-----+ +------+------+------+------+------+------+------+------+
// | 6 | | <-------------------------------------------------- |
// +-----+ +------+------+------+------+------+------+------+------+
// | 7 | | <------LSb | 61 | 60 | 59 | 58 | 57 | 56 |
// +-----+ +------+------+------+------+------+------+------+------+
type Data [MaxDataLength]byte
// UnsignedBitsLittleEndian returns the little-endian bit range [start, start+length) as an unsigned value.
func (d *Data) UnsignedBitsLittleEndian(start, length uint8) uint64 {
// pack bits into one continuous value
packed := d.PackLittleEndian()
// lsb index in the packed value is the start bit
lsbIndex := start
// shift away lower bits
shifted := packed >> lsbIndex
// mask away higher bits
masked := shifted & ((1 << length) - 1)
// done
return masked
}
// UnsignedBitsBigEndian returns the big-endian bit range [start, start+length) as an unsigned value.
func (d *Data) UnsignedBitsBigEndian(start, length uint8) uint64 {
// pack bits into one continuous value
packed := d.PackBigEndian()
// calculate msb index in the packed value
msbIndex := invertEndian(start)
// calculate lsb index in the packed value
lsbIndex := msbIndex - length + 1
// shift away lower bits
shifted := packed >> lsbIndex
// mask away higher bits
masked := shifted & ((1 << length) - 1)
// done
return masked
}
// SignedBitsLittleEndian returns little-endian bit range [start, start+length) as a signed value.
func (d *Data) SignedBitsLittleEndian(start, length uint8) int64 {
unsigned := d.UnsignedBitsLittleEndian(start, length)
return reinterpret.AsSigned(unsigned, length)
}
// SignedBitsBigEndian returns little-endian bit range [start, start+length) as a signed value.
func (d *Data) SignedBitsBigEndian(start, length uint8) int64 {
unsigned := d.UnsignedBitsBigEndian(start, length)
return reinterpret.AsSigned(unsigned, length)
}
// SetUnsignedBitsBigEndian sets the little-endian bit range [start, start+length) to the provided unsigned value.
func (d *Data) SetUnsignedBitsLittleEndian(start, length uint8, value uint64) {
// pack bits into one continuous value
packed := d.PackLittleEndian()
// lsb index in the packed value is the start bit
lsbIndex := start
// calculate bit mask for zeroing the bit range to set
unsetMask := ^uint64(((1 << length) - 1) << lsbIndex)
// calculate bit mask for setting the new value
setMask := value << lsbIndex
// calculate the new packed value
newPacked := packed&unsetMask | setMask
// unpack the new packed value into the data
d.UnpackLittleEndian(newPacked)
}
// SetUnsignedBitsBigEndian sets the big-endian bit range [start, start+length) to the provided unsigned value.
func (d *Data) SetUnsignedBitsBigEndian(start, length uint8, value uint64) {
// pack bits into one continuous value
packed := d.PackBigEndian()
// calculate msb index in the packed value
msbIndex := invertEndian(start)
// calculate lsb index in the packed value
lsbIndex := msbIndex - length + 1
// calculate bit mask for zeroing the bit range to set
unsetMask := ^uint64(((1 << length) - 1) << lsbIndex)
// calculate bit mask for setting the new value
setMask := value << lsbIndex
// calculate the new packed value
newPacked := packed&unsetMask | setMask
// unpack the new packed value into the data
d.UnpackBigEndian(newPacked)
}
// SetSignedBitsLittleEndian sets the little-endian bit range [start, start+length) to the provided signed value.
func (d *Data) SetSignedBitsLittleEndian(start, length uint8, value int64) {
d.SetUnsignedBitsLittleEndian(start, length, reinterpret.AsUnsigned(value, length))
}
// SetSignedBitsBigEndian sets the big-endian bit range [start, start+length) to the provided signed value.
func (d *Data) SetSignedBitsBigEndian(start, length uint8, value int64) {
d.SetUnsignedBitsBigEndian(start, length, reinterpret.AsUnsigned(value, length))
}
// Bit returns the value of the i:th bit in the data as a bool.
func (d *Data) Bit(i uint8) bool {
if i > 63 {
return false
}
// calculate which byte the bit belongs to
byteIndex := i / 8
// calculate bit mask for extracting the bit
bitMask := uint8(1 << (i % 8))
// mocks the bit
bit := d[byteIndex]&bitMask > 0
// done
return bit
}
// SetBit sets the value of the i:th bit in the data.
func (d *Data) SetBit(i uint8, value bool) {
if i > 63 {
return
}
byteIndex := i / 8
bitIndex := i % 8
if value {
d[byteIndex] |= uint8(1 << bitIndex)
} else {
d[byteIndex] &= ^uint8(1 << bitIndex)
}
}
// PackLittleEndian packs the data into a contiguous uint64 value for little-endian signals.
func (d *Data) PackLittleEndian() uint64 {
var packed uint64
packed |= uint64(d[0]) << (0 * 8)
packed |= uint64(d[1]) << (1 * 8)
packed |= uint64(d[2]) << (2 * 8)
packed |= uint64(d[3]) << (3 * 8)
packed |= uint64(d[4]) << (4 * 8)
packed |= uint64(d[5]) << (5 * 8)
packed |= uint64(d[6]) << (6 * 8)
packed |= uint64(d[7]) << (7 * 8)
return packed
}
// PackBigEndian packs the data into a contiguous uint64 value for big-endian signals.
func (d *Data) PackBigEndian() uint64 {
var packed uint64
packed |= uint64(d[0]) << (7 * 8)
packed |= uint64(d[1]) << (6 * 8)
packed |= uint64(d[2]) << (5 * 8)
packed |= uint64(d[3]) << (4 * 8)
packed |= uint64(d[4]) << (3 * 8)
packed |= uint64(d[5]) << (2 * 8)
packed |= uint64(d[6]) << (1 * 8)
packed |= uint64(d[7]) << (0 * 8)
return packed
}
// UnpackLittleEndian sets the value of d.Bytes by unpacking the provided value as sequential little-endian bits.
func (d *Data) UnpackLittleEndian(packed uint64) {
d[0] = uint8(packed >> (0 * 8))
d[1] = uint8(packed >> (1 * 8))
d[2] = uint8(packed >> (2 * 8))
d[3] = uint8(packed >> (3 * 8))
d[4] = uint8(packed >> (4 * 8))
d[5] = uint8(packed >> (5 * 8))
d[6] = uint8(packed >> (6 * 8))
d[7] = uint8(packed >> (7 * 8))
}
// UnpackBigEndian sets the value of d.Bytes by unpacking the provided value as sequential big-endian bits.
func (d *Data) UnpackBigEndian(packed uint64) {
d[0] = uint8(packed >> (7 * 8))
d[1] = uint8(packed >> (6 * 8))
d[2] = uint8(packed >> (5 * 8))
d[3] = uint8(packed >> (4 * 8))
d[4] = uint8(packed >> (3 * 8))
d[5] = uint8(packed >> (2 * 8))
d[6] = uint8(packed >> (1 * 8))
d[7] = uint8(packed >> (0 * 8))
}
// invertEndian converts from big-endian to little-endian bit indexing and vice versa.
func invertEndian(i uint8) uint8 {
row := i / 8
col := i % 8
oppositeRow := 7 - row
bitIndex := (oppositeRow * 8) + col
return bitIndex
}
// CheckBitRangeLittleEndian checks that a little-endian bit range fits in the data.
func CheckBitRangeLittleEndian(frameLength, rangeStart, rangeLength uint8) error {
lsbIndex := rangeStart
msbIndex := rangeStart + rangeLength - 1
upperBound := frameLength * 8
if msbIndex >= upperBound {
return fmt.Errorf("bit range out of bounds [0, %v): [%v, %v]", upperBound, lsbIndex, msbIndex)
}
return nil
}
// CheckBitRangeBigEndian checks that a big-endian bit range fits in the data.
func CheckBitRangeBigEndian(frameLength, rangeStart, rangeLength uint8) error {
upperBound := frameLength * 8
if rangeStart >= upperBound {
return fmt.Errorf("bit range starts out of bounds [0, %v): %v", upperBound, rangeStart)
}
msbIndex := invertEndian(rangeStart)
lsbIndex := msbIndex - rangeLength + 1
end := invertEndian(lsbIndex)
if end >= upperBound {
return fmt.Errorf("bit range ends out of bounds [0, %v): %v", upperBound, end)
}
return nil
}
// CheckValue checks that a value fits in a number of bits.
func CheckValue(value uint64, bits uint8) error {
upperBound := uint64(1 << bits)
if value >= upperBound {
return fmt.Errorf("value out of bounds [0, %v): %v", upperBound, value)
}
return nil
}

347
pkg/can-go/data_test.go Normal file
View File

@@ -0,0 +1,347 @@
package can
import (
"fmt"
"testing"
"testing/quick"
"gotest.tools/v3/assert"
)
func TestData_Bit(t *testing.T) {
for i, tt := range []struct {
data Data
bits []struct {
i uint8
bit bool
}
}{
{
data: Data{0x01, 0x23},
bits: []struct {
i uint8
bit bool
}{
// nibble 1: 0x1
{bit: true, i: 0},
{bit: false, i: 1},
{bit: false, i: 2},
{bit: false, i: 3},
// nibble 2: 0x0
{bit: false, i: 4},
{bit: false, i: 5},
{bit: false, i: 6},
{bit: false, i: 7},
// nibble 3: 0x3
{bit: true, i: 8},
{bit: true, i: 9},
{bit: false, i: 10},
{bit: false, i: 11},
// nibble 4: 0x2
{bit: false, i: 12},
{bit: true, i: 13},
{bit: false, i: 14},
{bit: false, i: 15},
},
},
} {
i, tt := i, tt
t.Run("Get", func(t *testing.T) {
i, tt := i, tt
for j, ttBit := range tt.bits {
j, ttBit := j, ttBit
t.Run(fmt.Sprintf("tt=%v,bit=%v", i, j), func(t *testing.T) {
bit := tt.data.Bit(ttBit.i)
assert.Equal(t, ttBit.bit, bit)
})
}
})
t.Run("Set", func(t *testing.T) {
i, tt := i, tt
t.Run(fmt.Sprintf("data=%v", i), func(t *testing.T) {
var data Data
for _, tBit := range tt.bits {
data.SetBit(tBit.i, tBit.bit)
}
assert.DeepEqual(t, tt.data, data)
})
})
}
}
func TestData_Property_SetGetBit(t *testing.T) {
f := func(_ Data, _ uint8, bit bool) bool {
return bit
}
g := func(data Data, i uint8, bit bool) bool {
i %= 64
data.SetBit(i, bit)
return data.Bit(i)
}
assert.NilError(t, quick.CheckEqual(f, g, nil))
}
func TestData_LittleEndian(t *testing.T) {
for i, tt := range []struct {
data Data
signals []struct {
start uint8
length uint8
unsigned uint64
signed int64
}
}{
{
data: Data{0x80, 0x01},
signals: []struct {
start uint8
length uint8
unsigned uint64
signed int64
}{
{start: 7, length: 2, unsigned: 0x3, signed: -1},
},
},
{
data: Data{0x01, 0x02, 0x03},
signals: []struct {
start uint8
length uint8
unsigned uint64
signed int64
}{
{start: 0, length: 24, unsigned: 0x030201, signed: 197121},
},
},
{
data: Data{0x40, 0x23, 0x01, 0x12},
signals: []struct {
start uint8
length uint8
unsigned uint64
signed int64
}{
{start: 24, length: 8, unsigned: 0x12, signed: 18},
{start: 8, length: 8, unsigned: 0x23, signed: 35},
{start: 4, length: 16, unsigned: 0x1234, signed: 4660},
},
},
} {
i, tt := i, tt
t.Run(fmt.Sprintf("UnsignedBits:%v", i), func(t *testing.T) {
for j, signal := range tt.signals {
j, signal := j, signal
t.Run(fmt.Sprintf("signal:%v", j), func(t *testing.T) {
actual := tt.data.UnsignedBitsLittleEndian(signal.start, signal.length)
assert.Equal(t, signal.unsigned, actual)
})
}
})
t.Run(fmt.Sprintf("SignedBits:%v", i), func(t *testing.T) {
for j, signal := range tt.signals {
j, signal := j, signal
t.Run(fmt.Sprintf("signal:%v", j), func(t *testing.T) {
actual := tt.data.SignedBitsLittleEndian(signal.start, signal.length)
assert.Equal(t, signal.signed, actual)
})
}
})
t.Run(fmt.Sprintf("SetUnsignedBits:%v", i), func(t *testing.T) {
var data Data
for j, signal := range tt.signals {
j, signal := j, signal
t.Run(fmt.Sprintf("data:%v", j), func(t *testing.T) {
data.SetUnsignedBitsLittleEndian(signal.start, signal.length, signal.unsigned)
})
}
assert.DeepEqual(t, tt.data, data)
})
t.Run(fmt.Sprintf("SetSignedBits:%v", i), func(t *testing.T) {
var data Data
for j, signal := range tt.signals {
j, signal := j, signal
t.Run(fmt.Sprintf("data:%v", j), func(t *testing.T) {
data.SetSignedBitsLittleEndian(signal.start, signal.length, signal.signed)
})
}
assert.DeepEqual(t, tt.data, data)
})
}
}
func TestData_BigEndian(t *testing.T) {
for i, tt := range []struct {
data Data
signals []struct {
start uint8
length uint8
unsigned uint64
signed int64
}
}{
{
data: Data{0x3f, 0xf7, 0x0d, 0xc4, 0x0c, 0x93, 0xff, 0xff},
signals: []struct {
start uint8
length uint8
unsigned uint64
signed int64
}{
{start: 7, length: 3, unsigned: 0x1, signed: 1},
{start: 4, length: 1, unsigned: 0x1, signed: -1},
{start: 55, length: 16, unsigned: 0xffff, signed: -1},
{start: 39, length: 16, unsigned: 0xc93, signed: 3219},
{start: 23, length: 16, unsigned: 0xdc4, signed: 3524},
{start: 3, length: 12, unsigned: 0xff7, signed: -9},
},
},
{
data: Data{0x3f, 0xe4, 0x0e, 0xb6, 0x0c, 0xba, 0x00, 0x05},
signals: []struct {
start uint8
length uint8
unsigned uint64
signed int64
}{
{start: 7, length: 3, unsigned: 0x1, signed: 1},
{start: 4, length: 1, unsigned: 0x1, signed: -1},
{start: 55, length: 16, unsigned: 0x5, signed: 5},
{start: 39, length: 16, unsigned: 0xcba, signed: 3258},
{start: 23, length: 16, unsigned: 0xeb6, signed: 3766},
{start: 3, length: 12, unsigned: 0xfe4, signed: -28},
},
},
{
data: Data{0x30, 0x53, 0x23, 0xe5, 0x0e, 0x11, 0xff, 0xff},
signals: []struct {
start uint8
length uint8
unsigned uint64
signed int64
}{
{start: 7, length: 3, unsigned: 0x1, signed: 1},
{start: 4, length: 1, unsigned: 0x1, signed: -1},
{start: 55, length: 16, unsigned: 0xffff, signed: -1},
{start: 39, length: 16, unsigned: 0xe11, signed: 3601},
{start: 23, length: 16, unsigned: 0x23e5, signed: 9189},
{start: 3, length: 12, unsigned: 0x53, signed: 83},
},
},
} {
i, tt := i, tt
t.Run(fmt.Sprintf("UnsignedBits:%v", i), func(t *testing.T) {
for j, signal := range tt.signals {
j, signal := j, signal
t.Run(fmt.Sprintf("signal:%v", j), func(t *testing.T) {
actual := tt.data.UnsignedBitsBigEndian(signal.start, signal.length)
assert.Equal(t, signal.unsigned, actual)
})
}
})
t.Run(fmt.Sprintf("SignedBits:%v", i), func(t *testing.T) {
for j, signal := range tt.signals {
j, signal := j, signal
t.Run(fmt.Sprintf("signal:%v", j), func(t *testing.T) {
actual := tt.data.SignedBitsBigEndian(signal.start, signal.length)
assert.Equal(t, signal.signed, actual)
})
}
})
t.Run(fmt.Sprintf("SetUnsignedBits:%v", i), func(t *testing.T) {
var data Data
for j, signal := range tt.signals {
j, signal := j, signal
t.Run(fmt.Sprintf("data:%v", j), func(t *testing.T) {
data.SetUnsignedBitsBigEndian(signal.start, signal.length, signal.unsigned)
})
}
assert.DeepEqual(t, tt.data, data)
})
t.Run(fmt.Sprintf("SetSignedBits:%v", i), func(t *testing.T) {
var data Data
for j, signal := range tt.signals {
j, signal := j, signal
t.Run(fmt.Sprintf("data:%v", j), func(t *testing.T) {
data.SetSignedBitsBigEndian(signal.start, signal.length, signal.signed)
})
}
assert.DeepEqual(t, tt.data, data)
})
}
}
func TestInvertEndian_Property_Idempotent(t *testing.T) {
for i := uint8(0); i < 64; i++ {
assert.Equal(t, i, invertEndian(invertEndian(i)))
}
}
func TestPackUnpackBigEndian(t *testing.T) {
f := func(data Data) Data {
return data
}
g := func(data Data) Data {
data.UnpackBigEndian(data.PackBigEndian())
return data
}
assert.NilError(t, quick.CheckEqual(f, g, nil))
}
func TestPackUnpackLittleEndian(t *testing.T) {
f := func(data Data) Data {
return data
}
g := func(data Data) Data {
data.UnpackLittleEndian(data.PackLittleEndian())
return data
}
assert.NilError(t, quick.CheckEqual(f, g, nil))
}
func TestData_CheckBitRange(t *testing.T) {
// example case that big-endian signals and little-endian signals use different indexing
assert.NilError(t, CheckBitRangeBigEndian(8, 55, 16))
assert.ErrorContains(t, CheckBitRangeLittleEndian(8, 55, 16), "bit range out of bounds")
}
func BenchmarkData_UnpackLittleEndian(b *testing.B) {
var data Data
for i := 0; i < b.N; i++ {
data.UnpackLittleEndian(0)
}
}
func BenchmarkData_UnpackBigEndian(b *testing.B) {
var data Data
for i := 0; i < b.N; i++ {
data.UnpackBigEndian(0)
}
}
func BenchmarkData_PackBigEndian(b *testing.B) {
var data Data
for i := 0; i < b.N; i++ {
_ = data.PackBigEndian()
}
}
func BenchmarkData_PackLittleEndian(b *testing.B) {
var data Data
for i := 0; i < b.N; i++ {
_ = data.PackLittleEndian()
}
}
func BenchmarkData_UnsignedBitsBigEndian(b *testing.B) {
var data Data
for i := 0; i < b.N; i++ {
_ = data.UnsignedBitsBigEndian(0, 16)
}
}
func BenchmarkData_UnsignedBitsLittleEndian(b *testing.B) {
var data Data
for i := 0; i < b.N; i++ {
_ = data.UnsignedBitsLittleEndian(0, 16)
}
}

135
pkg/can-go/frame.go Normal file
View File

@@ -0,0 +1,135 @@
package can
import (
"encoding/hex"
"fmt"
"strconv"
"strings"
)
const (
idBits = 11
extendedIDBits = 29
)
// CAN format constants.
const (
MaxID = 0x7ff
MaxExtendedID = 0x1fffffff
)
// Frame represents a CAN frame.
//
// A Frame is intentionally designed to fit into 16 bytes on common architectures
// and is therefore amenable to pass-by-value and judicious copying.
type Frame struct {
// ID is the CAN ID
ID uint32
// Length is the number of bytes of data in the frame.
Length uint16
// Data is the frame data.
Data Data
// IsRemote is true for remote frames.
IsRemote bool
// IsExtended is true for extended frames, i.e. frames with 29-bit IDs.
IsExtended bool
}
// Validate returns an error if the Frame is not a valid CAN frame.
func (f *Frame) Validate() error {
// Validate: ID
if f.IsExtended && f.ID > MaxExtendedID {
return fmt.Errorf(
"invalid extended CAN id: %v does not fit in %v bits",
f.ID,
extendedIDBits,
)
} else if !f.IsExtended && f.ID > MaxID {
return fmt.Errorf(
"invalid standard CAN id: %v does not fit in %v bits",
f.ID,
idBits,
)
}
// Validate: Data
if f.Length > MaxDataLength {
return fmt.Errorf("invalid data length: %v", f.Length)
}
return nil
}
// String returns an ASCII representation the CAN frame.
//
// Format:
//
// ([0-9A-F]{3}|[0-9A-F]{3})#(R[0-8]?|[0-9A-F]{0,16})
//
// The format is compatible with the candump(1) log file format.
func (f Frame) String() string {
var id string
if f.IsExtended {
id = fmt.Sprintf("%08X", f.ID)
} else {
id = fmt.Sprintf("%03X", f.ID)
}
if f.IsRemote && f.Length == 0 {
return id + "#R"
} else if f.IsRemote {
return id + "#R" + strconv.Itoa(int(f.Length))
}
return id + "#" + strings.ToUpper(hex.EncodeToString(f.Data[:f.Length]))
}
// UnmarshalString sets *f using the provided ASCII representation of a Frame.
func (f *Frame) UnmarshalString(s string) error {
// Split split into parts
parts := strings.Split(s, "#")
if len(parts) != 2 {
return fmt.Errorf("invalid frame format: %v", s)
}
idPart, dataPart := parts[0], parts[1]
var frame Frame
// Parse: IsExtended
if len(idPart) != 3 && len(idPart) != 8 {
return fmt.Errorf("invalid ID length: %v", s)
}
frame.IsExtended = len(idPart) == 8
// Parse: ID
id, err := strconv.ParseUint(idPart, 16, 32)
if err != nil {
return fmt.Errorf("invalid frame ID: %v", s)
}
frame.ID = uint32(id)
if len(dataPart) == 0 {
*f = frame
return nil
}
// Parse: IsRemote
if dataPart[0] == 'R' {
frame.IsRemote = true
if len(dataPart) > 2 {
return fmt.Errorf("invalid remote length: %v", s)
} else if len(dataPart) == 2 {
dataLength, err := strconv.Atoi(dataPart[1:2])
if err != nil {
return fmt.Errorf("invalid remote length: %v: %w", s, err)
}
frame.Length = uint16(dataLength)
}
*f = frame
return nil
}
// Parse: Length
if len(dataPart) > 16 || len(dataPart)%2 != 0 {
return fmt.Errorf("invalid data length: %v", s)
}
frame.Length = uint16(len(dataPart) / 2)
// Parse: Data
decodedData, err := hex.DecodeString(dataPart)
if err != nil {
return fmt.Errorf("invalid data: %v: %w", s, err)
}
copy(frame.Data[:], decodedData)
*f = frame
return nil
}

95
pkg/can-go/frame_json.go Normal file
View File

@@ -0,0 +1,95 @@
package can
import (
"encoding/hex"
"encoding/json"
"fmt"
"strconv"
)
type jsonFrame struct {
ID uint32 `json:"id"`
Data *string `json:"data"`
Length *uint16 `json:"length"`
Extended *bool `json:"extended"`
Remote *bool `json:"remote"`
}
// JSON returns the JSON-encoding of f, using hex-encoding for the data.
//
// Examples:
//
// {"id":32,"data":"0102030405060708"}
// {"id":32,"extended":true,"remote":true,"length":4}
func (f Frame) JSON() string {
switch {
case f.IsRemote && f.IsExtended:
return `{"id":` + strconv.Itoa(int(f.ID)) +
`,"extended":true,"remote":true,"length":` +
strconv.Itoa(int(f.Length)) + `}`
case f.IsRemote:
return `{"id":` + strconv.Itoa(int(f.ID)) +
`,"remote":true,"length":` +
strconv.Itoa(int(f.Length)) + `}`
case f.IsExtended && f.Length == 0:
return `{"id":` + strconv.Itoa(int(f.ID)) + `,"extended":true}`
case f.IsExtended:
return `{"id":` + strconv.Itoa(int(f.ID)) +
`,"data":"` + hex.EncodeToString(f.Data[:f.Length]) + `"` +
`,"extended":true}`
case f.Length == 0:
return `{"id":` + strconv.Itoa(int(f.ID)) + `}`
default:
return `{"id":` + strconv.Itoa(int(f.ID)) +
`,"data":"` + hex.EncodeToString(f.Data[:f.Length]) + `"}`
}
}
// MarshalJSON returns the JSON-encoding of f, using hex-encoding for the data.
//
// See JSON for an example of the JSON schema.
func (f Frame) MarshalJSON() ([]byte, error) {
return []byte(f.JSON()), nil
}
// UnmarshalJSON sets *f using the provided JSON-encoded values.
//
// See MarshalJSON for an example of the expected JSON schema.
//
// The result should be checked with Validate to guard against invalid JSON data.
func (f *Frame) UnmarshalJSON(jsonData []byte) error {
jf := jsonFrame{}
if err := json.Unmarshal(jsonData, &jf); err != nil {
return err
}
if jf.Data != nil {
data, err := hex.DecodeString(*jf.Data)
if err != nil {
return fmt.Errorf("failed to hex-decode CAN data: %v: %w", string(jsonData), err)
}
f.Data = Data{}
copy(f.Data[:], data)
f.Length = uint16(len(data))
} else {
f.Data = Data{}
f.Length = 0
}
f.ID = jf.ID
if jf.Remote != nil {
f.IsRemote = *jf.Remote
} else {
f.IsRemote = false
}
if f.IsRemote {
if jf.Length == nil {
return fmt.Errorf("missing length field for remote JSON frame: %v", string(jsonData))
}
f.Length = *jf.Length
}
if jf.Extended != nil {
f.IsExtended = *jf.Extended
} else {
f.IsExtended = false
}
return nil
}

View File

@@ -0,0 +1,126 @@
package can
import (
"encoding/json"
"fmt"
"math/rand"
"reflect"
"testing"
"testing/quick"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
func TestFrame_JSON(t *testing.T) {
for _, tt := range []struct {
jsonFrame string
frame Frame
}{
{
// Standard frame
jsonFrame: `{"id":42,"data":"00010203"}`,
frame: Frame{
ID: 42,
Length: 4,
Data: Data{0x00, 0x01, 0x02, 0x03},
},
},
{
// Standard frame, no data
jsonFrame: `{"id":42}`,
frame: Frame{ID: 42},
},
{
// Standard remote frame
jsonFrame: `{"id":42,"remote":true,"length":4}`,
frame: Frame{
ID: 42,
IsRemote: true,
Length: 4,
},
},
{
// Extended frame
jsonFrame: `{"id":42,"data":"0001020304050607","extended":true}`,
frame: Frame{
ID: 42,
IsExtended: true,
Length: 8,
Data: Data{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07},
},
},
{
// Extended frame, no data
jsonFrame: `{"id":42,"extended":true}`,
frame: Frame{ID: 42, IsExtended: true},
},
{
// Extended remote frame
jsonFrame: `{"id":42,"extended":true,"remote":true,"length":8}`,
frame: Frame{
ID: 42,
IsExtended: true,
IsRemote: true,
Length: 8,
},
},
} {
tt := tt
t.Run(fmt.Sprintf("JSON|frame=%v", tt.frame), func(t *testing.T) {
assert.Check(t, is.Equal(tt.jsonFrame, tt.frame.JSON()))
})
t.Run(fmt.Sprintf("UnmarshalJSON|frame=%v", tt.frame), func(t *testing.T) {
var frame Frame
if err := json.Unmarshal([]byte(tt.jsonFrame), &frame); err != nil {
t.Fatal(err)
}
assert.Check(t, is.DeepEqual(tt.frame, frame))
})
}
}
func TestFrame_UnmarshalJSON_Invalid(t *testing.T) {
var f Frame
t.Run("invalid JSON", func(t *testing.T) {
data := `foobar`
assert.Check(t, f.UnmarshalJSON([]uint8(data)) != nil)
})
t.Run("invalid payload", func(t *testing.T) {
data := `{"id":1,"data":"foobar","extended":false,"remote":false}`
assert.Check(t, f.UnmarshalJSON([]uint8(data)) != nil)
})
}
func (Frame) Generate(rand *rand.Rand, size int) reflect.Value {
f := Frame{
IsExtended: rand.Intn(2) == 0,
IsRemote: rand.Intn(2) == 0,
}
if f.IsExtended {
f.ID = rand.Uint32() & MaxExtendedID
} else {
f.ID = rand.Uint32() & MaxID
}
f.Length = uint16(rand.Intn(9))
if !f.IsRemote {
_, _ = rand.Read(f.Data[:f.Length])
}
return reflect.ValueOf(f)
}
func TestPropertyFrame_MarshalUnmarshalJSON(t *testing.T) {
f := func(f Frame) Frame {
return f
}
g := func(f Frame) Frame {
f2 := Frame{}
if err := json.Unmarshal([]uint8(f.JSON()), &f2); err != nil {
t.Fatal(err)
}
return f2
}
if err := quick.CheckEqual(f, g, nil); err != nil {
t.Fatal(err)
}
}

View File

@@ -0,0 +1,86 @@
package can
import (
"fmt"
"testing"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
func TestFrame_String(t *testing.T) {
for _, tt := range []struct {
frame Frame
str string
}{
{
frame: Frame{
ID: 0x62e,
Length: 2,
Data: Data{0x10, 0x44},
},
str: "62E#1044",
},
{
frame: Frame{
ID: 0x410,
IsRemote: true,
Length: 3,
},
str: "410#R3",
},
{
frame: Frame{
ID: 0xd2,
Length: 2,
Data: Data{0xf0, 0x31},
},
str: "0D2#F031",
},
{
frame: Frame{ID: 0xee},
str: "0EE#",
},
{
frame: Frame{ID: 0},
str: "000#",
},
{
frame: Frame{ID: 0, IsExtended: true},
str: "00000000#",
},
{
frame: Frame{ID: 0x1234abcd, IsExtended: true},
str: "1234ABCD#",
},
} {
tt := tt
t.Run(fmt.Sprintf("String|frame=%v,str=%v", tt.frame, tt.str), func(t *testing.T) {
assert.Check(t, is.Equal(tt.str, tt.frame.String()))
})
t.Run(fmt.Sprintf("UnmarshalString|frame=%v,str=%v", tt.frame, tt.str), func(t *testing.T) {
var actual Frame
if err := actual.UnmarshalString(tt.str); err != nil {
t.Fatal(err)
}
assert.Check(t, is.DeepEqual(actual, tt.frame))
})
}
}
func TestParseFrame_Errors(t *testing.T) {
for _, tt := range []string{
"foo", // invalid
"foo#", // invalid ID
"0D23#F031", // invalid ID length
"62E#104400000000000000", // invalid data length
} {
tt := tt
t.Run(fmt.Sprintf("str=%v", tt), func(t *testing.T) {
var frame Frame
err := frame.UnmarshalString(tt)
assert.ErrorContains(t, err, "invalid")
assert.Check(t, is.DeepEqual(Frame{}, frame))
})
}
}

27
pkg/can-go/frame_test.go Normal file
View File

@@ -0,0 +1,27 @@
package can
import (
"fmt"
"testing"
"unsafe"
"gotest.tools/v3/assert"
)
// If this mocks ever starts failing, the documentation needs to be updated
// to prefer pass-by-pointer over pass-by-value.
func TestFrame_Size(t *testing.T) {
assert.Assert(t, unsafe.Sizeof(Frame{}) <= 16, "Frame size is <= 16 bytes")
}
func TestFrame_Validate_Error(t *testing.T) {
for _, tt := range []Frame{
{ID: MaxID + 1},
{ID: MaxExtendedID + 1, IsExtended: true},
} {
tt := tt
t.Run(fmt.Sprintf("%v", tt), func(t *testing.T) {
assert.Check(t, tt.Validate() != nil, "should return validation error")
})
}
}

29
pkg/can-go/go.mod Normal file
View File

@@ -0,0 +1,29 @@
module github.com/fiskerinc/cloud-services/pkg/can-go
go 1.24.0
require (
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
github.com/fatih/color v1.15.0
github.com/golang/mock v1.7.0-rc.1
github.com/shurcooL/go-goon v0.0.0-20210110234559-7585751d9a17
go.uber.org/goleak v1.3.0
golang.org/x/net v0.47.0
golang.org/x/sync v0.18.0
golang.org/x/sys v0.38.0
golang.org/x/tools v0.38.0
gopkg.in/alecthomas/kingpin.v2 v2.2.6
gotest.tools/v3 v3.5.1
)
require (
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect
github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/shurcooL/go v0.0.0-20200502201357-93f07166e636 // indirect
github.com/stretchr/testify v1.10.0 // indirect
golang.org/x/mod v0.29.0 // indirect
)

45
pkg/can-go/go.sum Normal file
View File

@@ -0,0 +1,45 @@
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15 h1:AUNCr9CiJuwrRYS3XieqF+Z9B9gNxo/eANAJCF2eiN4=
github.com/alecthomas/units v0.0.0-20210208195552-ff826a37aa15/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs=
github.com/golang/mock v1.7.0-rc.1 h1:YojYx61/OLFsiv6Rw1Z96LpldJIy31o+UHmwAUMJ6/U=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/shurcooL/go v0.0.0-20200502201357-93f07166e636 h1:aSISeOcal5irEhJd1M+IrApc0PdcN7e7Aj4yuEnOrfQ=
github.com/shurcooL/go v0.0.0-20200502201357-93f07166e636/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
github.com/shurcooL/go-goon v0.0.0-20210110234559-7585751d9a17 h1:lRAUE0dIvigSSFAmaM2dfg7OH8T+a8zJ5smEh09a/GI=
github.com/shurcooL/go-goon v0.0.0-20210110234559-7585751d9a17/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=

View File

@@ -0,0 +1,27 @@
// Package clock provides primitives for mocking time.
package clock
import (
"time"
)
// Clock provides capabilities from the time standard library package.
type Clock interface {
// After waits for the duration to elapse and then sends the current time on the returned channel.
After(duration time.Duration) <-chan time.Time
// NewTicker returns a new Ticker.
NewTicker(d time.Duration) Ticker
// Now returns the current local time.
Now() time.Time
}
// Ticker wraps the time.Ticker class.
type Ticker interface {
// C returns the channel on which the ticks are delivered.
C() <-chan time.Time
// Stop the Ticker.
Stop()
}

View File

@@ -0,0 +1,34 @@
package clock
import (
"time"
)
// System returns a Clock implementation that delegate to the time package.
func System() Clock {
return &systemClock{}
}
type systemClock struct{}
var _ Clock = &systemClock{}
func (c systemClock) After(d time.Duration) <-chan time.Time {
return time.After(d)
}
func (c systemClock) NewTicker(d time.Duration) Ticker {
return &systemTicker{Ticker: *time.NewTicker(d)}
}
func (c systemClock) Now() time.Time {
return time.Now()
}
type systemTicker struct {
time.Ticker
}
func (t systemTicker) C() <-chan time.Time {
return t.Ticker.C
}

View File

@@ -0,0 +1,529 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/fiskerinc/cloud-services/pkg/can-go/pkg/canrunner (interfaces: Node,TransmittedMessage,ReceivedMessage,FrameTransmitter,FrameReceiver)
// Package mockcanrunner is a generated GoMock package.
package mockcanrunner
import (
context "context"
can "github.com/fiskerinc/cloud-services/pkg/can-go"
canrunner "github.com/fiskerinc/cloud-services/pkg/can-go/pkg/canrunner"
descriptor "github.com/fiskerinc/cloud-services/pkg/can-go/pkg/descriptor"
gomock "github.com/golang/mock/gomock"
net "net"
reflect "reflect"
time "time"
)
// MockNode is a mock of Node interface.
type MockNode struct {
ctrl *gomock.Controller
recorder *MockNodeMockRecorder
}
// MockNodeMockRecorder is the mock recorder for MockNode.
type MockNodeMockRecorder struct {
mock *MockNode
}
// NewMockNode creates a new mock instance.
func NewMockNode(ctrl *gomock.Controller) *MockNode {
mock := &MockNode{ctrl: ctrl}
mock.recorder = &MockNodeMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockNode) EXPECT() *MockNodeMockRecorder {
return m.recorder
}
// Connect mocks base method.
func (m *MockNode) Connect() (net.Conn, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Connect")
ret0, _ := ret[0].(net.Conn)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Connect indicates an expected call of Connect.
func (mr *MockNodeMockRecorder) Connect() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Connect", reflect.TypeOf((*MockNode)(nil).Connect))
}
// Descriptor mocks base method.
func (m *MockNode) Descriptor() *descriptor.Node {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Descriptor")
ret0, _ := ret[0].(*descriptor.Node)
return ret0
}
// Descriptor indicates an expected call of Descriptor.
func (mr *MockNodeMockRecorder) Descriptor() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Descriptor", reflect.TypeOf((*MockNode)(nil).Descriptor))
}
// Lock mocks base method.
func (m *MockNode) Lock() {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Lock")
}
// Lock indicates an expected call of Lock.
func (mr *MockNodeMockRecorder) Lock() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Lock", reflect.TypeOf((*MockNode)(nil).Lock))
}
// ReceivedMessage mocks base method.
func (m *MockNode) ReceivedMessage(arg0 uint32) (canrunner.ReceivedMessage, bool) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "ReceivedMessage", arg0)
ret0, _ := ret[0].(canrunner.ReceivedMessage)
ret1, _ := ret[1].(bool)
return ret0, ret1
}
// ReceivedMessage indicates an expected call of ReceivedMessage.
func (mr *MockNodeMockRecorder) ReceivedMessage(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ReceivedMessage", reflect.TypeOf((*MockNode)(nil).ReceivedMessage), arg0)
}
// TransmittedMessages mocks base method.
func (m *MockNode) TransmittedMessages() []canrunner.TransmittedMessage {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "TransmittedMessages")
ret0, _ := ret[0].([]canrunner.TransmittedMessage)
return ret0
}
// TransmittedMessages indicates an expected call of TransmittedMessages.
func (mr *MockNodeMockRecorder) TransmittedMessages() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TransmittedMessages", reflect.TypeOf((*MockNode)(nil).TransmittedMessages))
}
// Unlock mocks base method.
func (m *MockNode) Unlock() {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Unlock")
}
// Unlock indicates an expected call of Unlock.
func (mr *MockNodeMockRecorder) Unlock() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Unlock", reflect.TypeOf((*MockNode)(nil).Unlock))
}
// MockTransmittedMessage is a mock of TransmittedMessage interface.
type MockTransmittedMessage struct {
ctrl *gomock.Controller
recorder *MockTransmittedMessageMockRecorder
}
// MockTransmittedMessageMockRecorder is the mock recorder for MockTransmittedMessage.
type MockTransmittedMessageMockRecorder struct {
mock *MockTransmittedMessage
}
// NewMockTransmittedMessage creates a new mock instance.
func NewMockTransmittedMessage(ctrl *gomock.Controller) *MockTransmittedMessage {
mock := &MockTransmittedMessage{ctrl: ctrl}
mock.recorder = &MockTransmittedMessageMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockTransmittedMessage) EXPECT() *MockTransmittedMessageMockRecorder {
return m.recorder
}
// BeforeTransmitHook mocks base method.
func (m *MockTransmittedMessage) BeforeTransmitHook() func(context.Context) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "BeforeTransmitHook")
ret0, _ := ret[0].(func(context.Context) error)
return ret0
}
// BeforeTransmitHook indicates an expected call of BeforeTransmitHook.
func (mr *MockTransmittedMessageMockRecorder) BeforeTransmitHook() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BeforeTransmitHook", reflect.TypeOf((*MockTransmittedMessage)(nil).BeforeTransmitHook))
}
// Descriptor mocks base method.
func (m *MockTransmittedMessage) Descriptor() *descriptor.Message {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Descriptor")
ret0, _ := ret[0].(*descriptor.Message)
return ret0
}
// Descriptor indicates an expected call of Descriptor.
func (mr *MockTransmittedMessageMockRecorder) Descriptor() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Descriptor", reflect.TypeOf((*MockTransmittedMessage)(nil).Descriptor))
}
// Frame mocks base method.
func (m *MockTransmittedMessage) Frame() can.Frame {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Frame")
ret0, _ := ret[0].(can.Frame)
return ret0
}
// Frame indicates an expected call of Frame.
func (mr *MockTransmittedMessageMockRecorder) Frame() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Frame", reflect.TypeOf((*MockTransmittedMessage)(nil).Frame))
}
// IsCyclicTransmissionEnabled mocks base method.
func (m *MockTransmittedMessage) IsCyclicTransmissionEnabled() bool {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "IsCyclicTransmissionEnabled")
ret0, _ := ret[0].(bool)
return ret0
}
// IsCyclicTransmissionEnabled indicates an expected call of IsCyclicTransmissionEnabled.
func (mr *MockTransmittedMessageMockRecorder) IsCyclicTransmissionEnabled() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsCyclicTransmissionEnabled", reflect.TypeOf((*MockTransmittedMessage)(nil).IsCyclicTransmissionEnabled))
}
// MarshalFrame mocks base method.
func (m *MockTransmittedMessage) MarshalFrame() (can.Frame, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "MarshalFrame")
ret0, _ := ret[0].(can.Frame)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// MarshalFrame indicates an expected call of MarshalFrame.
func (mr *MockTransmittedMessageMockRecorder) MarshalFrame() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MarshalFrame", reflect.TypeOf((*MockTransmittedMessage)(nil).MarshalFrame))
}
// Reset mocks base method.
func (m *MockTransmittedMessage) Reset() {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Reset")
}
// Reset indicates an expected call of Reset.
func (mr *MockTransmittedMessageMockRecorder) Reset() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Reset", reflect.TypeOf((*MockTransmittedMessage)(nil).Reset))
}
// SetTransmitTime mocks base method.
func (m *MockTransmittedMessage) SetTransmitTime(arg0 time.Time) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "SetTransmitTime", arg0)
}
// SetTransmitTime indicates an expected call of SetTransmitTime.
func (mr *MockTransmittedMessageMockRecorder) SetTransmitTime(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetTransmitTime", reflect.TypeOf((*MockTransmittedMessage)(nil).SetTransmitTime), arg0)
}
// String mocks base method.
func (m *MockTransmittedMessage) String() string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "String")
ret0, _ := ret[0].(string)
return ret0
}
// String indicates an expected call of String.
func (mr *MockTransmittedMessageMockRecorder) String() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "String", reflect.TypeOf((*MockTransmittedMessage)(nil).String))
}
// TransmitEventChan mocks base method.
func (m *MockTransmittedMessage) TransmitEventChan() <-chan struct{} {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "TransmitEventChan")
ret0, _ := ret[0].(<-chan struct{})
return ret0
}
// TransmitEventChan indicates an expected call of TransmitEventChan.
func (mr *MockTransmittedMessageMockRecorder) TransmitEventChan() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TransmitEventChan", reflect.TypeOf((*MockTransmittedMessage)(nil).TransmitEventChan))
}
// UnmarshalFrame mocks base method.
func (m *MockTransmittedMessage) UnmarshalFrame(arg0 can.Frame) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UnmarshalFrame", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// UnmarshalFrame indicates an expected call of UnmarshalFrame.
func (mr *MockTransmittedMessageMockRecorder) UnmarshalFrame(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UnmarshalFrame", reflect.TypeOf((*MockTransmittedMessage)(nil).UnmarshalFrame), arg0)
}
// WakeUpChan mocks base method.
func (m *MockTransmittedMessage) WakeUpChan() <-chan struct{} {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "WakeUpChan")
ret0, _ := ret[0].(<-chan struct{})
return ret0
}
// WakeUpChan indicates an expected call of WakeUpChan.
func (mr *MockTransmittedMessageMockRecorder) WakeUpChan() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WakeUpChan", reflect.TypeOf((*MockTransmittedMessage)(nil).WakeUpChan))
}
// MockReceivedMessage is a mock of ReceivedMessage interface.
type MockReceivedMessage struct {
ctrl *gomock.Controller
recorder *MockReceivedMessageMockRecorder
}
// MockReceivedMessageMockRecorder is the mock recorder for MockReceivedMessage.
type MockReceivedMessageMockRecorder struct {
mock *MockReceivedMessage
}
// NewMockReceivedMessage creates a new mock instance.
func NewMockReceivedMessage(ctrl *gomock.Controller) *MockReceivedMessage {
mock := &MockReceivedMessage{ctrl: ctrl}
mock.recorder = &MockReceivedMessageMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockReceivedMessage) EXPECT() *MockReceivedMessageMockRecorder {
return m.recorder
}
// AfterReceiveHook mocks base method.
func (m *MockReceivedMessage) AfterReceiveHook() func(context.Context) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "AfterReceiveHook")
ret0, _ := ret[0].(func(context.Context) error)
return ret0
}
// AfterReceiveHook indicates an expected call of AfterReceiveHook.
func (mr *MockReceivedMessageMockRecorder) AfterReceiveHook() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AfterReceiveHook", reflect.TypeOf((*MockReceivedMessage)(nil).AfterReceiveHook))
}
// Descriptor mocks base method.
func (m *MockReceivedMessage) Descriptor() *descriptor.Message {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Descriptor")
ret0, _ := ret[0].(*descriptor.Message)
return ret0
}
// Descriptor indicates an expected call of Descriptor.
func (mr *MockReceivedMessageMockRecorder) Descriptor() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Descriptor", reflect.TypeOf((*MockReceivedMessage)(nil).Descriptor))
}
// Frame mocks base method.
func (m *MockReceivedMessage) Frame() can.Frame {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Frame")
ret0, _ := ret[0].(can.Frame)
return ret0
}
// Frame indicates an expected call of Frame.
func (mr *MockReceivedMessageMockRecorder) Frame() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Frame", reflect.TypeOf((*MockReceivedMessage)(nil).Frame))
}
// MarshalFrame mocks base method.
func (m *MockReceivedMessage) MarshalFrame() (can.Frame, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "MarshalFrame")
ret0, _ := ret[0].(can.Frame)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// MarshalFrame indicates an expected call of MarshalFrame.
func (mr *MockReceivedMessageMockRecorder) MarshalFrame() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MarshalFrame", reflect.TypeOf((*MockReceivedMessage)(nil).MarshalFrame))
}
// Reset mocks base method.
func (m *MockReceivedMessage) Reset() {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Reset")
}
// Reset indicates an expected call of Reset.
func (mr *MockReceivedMessageMockRecorder) Reset() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Reset", reflect.TypeOf((*MockReceivedMessage)(nil).Reset))
}
// SetReceiveTime mocks base method.
func (m *MockReceivedMessage) SetReceiveTime(arg0 time.Time) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "SetReceiveTime", arg0)
}
// SetReceiveTime indicates an expected call of SetReceiveTime.
func (mr *MockReceivedMessageMockRecorder) SetReceiveTime(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetReceiveTime", reflect.TypeOf((*MockReceivedMessage)(nil).SetReceiveTime), arg0)
}
// String mocks base method.
func (m *MockReceivedMessage) String() string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "String")
ret0, _ := ret[0].(string)
return ret0
}
// String indicates an expected call of String.
func (mr *MockReceivedMessageMockRecorder) String() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "String", reflect.TypeOf((*MockReceivedMessage)(nil).String))
}
// UnmarshalFrame mocks base method.
func (m *MockReceivedMessage) UnmarshalFrame(arg0 can.Frame) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UnmarshalFrame", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// UnmarshalFrame indicates an expected call of UnmarshalFrame.
func (mr *MockReceivedMessageMockRecorder) UnmarshalFrame(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UnmarshalFrame", reflect.TypeOf((*MockReceivedMessage)(nil).UnmarshalFrame), arg0)
}
// MockFrameTransmitter is a mock of FrameTransmitter interface.
type MockFrameTransmitter struct {
ctrl *gomock.Controller
recorder *MockFrameTransmitterMockRecorder
}
// MockFrameTransmitterMockRecorder is the mock recorder for MockFrameTransmitter.
type MockFrameTransmitterMockRecorder struct {
mock *MockFrameTransmitter
}
// NewMockFrameTransmitter creates a new mock instance.
func NewMockFrameTransmitter(ctrl *gomock.Controller) *MockFrameTransmitter {
mock := &MockFrameTransmitter{ctrl: ctrl}
mock.recorder = &MockFrameTransmitterMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockFrameTransmitter) EXPECT() *MockFrameTransmitterMockRecorder {
return m.recorder
}
// TransmitFrame mocks base method.
func (m *MockFrameTransmitter) TransmitFrame(arg0 context.Context, arg1 can.Frame) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "TransmitFrame", arg0, arg1)
ret0, _ := ret[0].(error)
return ret0
}
// TransmitFrame indicates an expected call of TransmitFrame.
func (mr *MockFrameTransmitterMockRecorder) TransmitFrame(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TransmitFrame", reflect.TypeOf((*MockFrameTransmitter)(nil).TransmitFrame), arg0, arg1)
}
// MockFrameReceiver is a mock of FrameReceiver interface.
type MockFrameReceiver struct {
ctrl *gomock.Controller
recorder *MockFrameReceiverMockRecorder
}
// MockFrameReceiverMockRecorder is the mock recorder for MockFrameReceiver.
type MockFrameReceiverMockRecorder struct {
mock *MockFrameReceiver
}
// NewMockFrameReceiver creates a new mock instance.
func NewMockFrameReceiver(ctrl *gomock.Controller) *MockFrameReceiver {
mock := &MockFrameReceiver{ctrl: ctrl}
mock.recorder = &MockFrameReceiverMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockFrameReceiver) EXPECT() *MockFrameReceiverMockRecorder {
return m.recorder
}
// Err mocks base method.
func (m *MockFrameReceiver) Err() error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Err")
ret0, _ := ret[0].(error)
return ret0
}
// Err indicates an expected call of Err.
func (mr *MockFrameReceiverMockRecorder) Err() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Err", reflect.TypeOf((*MockFrameReceiver)(nil).Err))
}
// Frame mocks base method.
func (m *MockFrameReceiver) Frame() can.Frame {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Frame")
ret0, _ := ret[0].(can.Frame)
return ret0
}
// Frame indicates an expected call of Frame.
func (mr *MockFrameReceiverMockRecorder) Frame() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Frame", reflect.TypeOf((*MockFrameReceiver)(nil).Frame))
}
// Receive mocks base method.
func (m *MockFrameReceiver) Receive() bool {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Receive")
ret0, _ := ret[0].(bool)
return ret0
}
// Receive indicates an expected call of Receive.
func (mr *MockFrameReceiverMockRecorder) Receive() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Receive", reflect.TypeOf((*MockFrameReceiver)(nil).Receive))
}

View File

@@ -0,0 +1,126 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/fiskerinc/cloud-services/pkg/can-go/internal/clock (interfaces: Clock,Ticker)
// Package mockclock is a generated GoMock package.
package mockclock
import (
clock "github.com/fiskerinc/cloud-services/pkg/can-go/internal/clock"
gomock "github.com/golang/mock/gomock"
reflect "reflect"
time "time"
)
// MockClock is a mock of Clock interface.
type MockClock struct {
ctrl *gomock.Controller
recorder *MockClockMockRecorder
}
// MockClockMockRecorder is the mock recorder for MockClock.
type MockClockMockRecorder struct {
mock *MockClock
}
// NewMockClock creates a new mock instance.
func NewMockClock(ctrl *gomock.Controller) *MockClock {
mock := &MockClock{ctrl: ctrl}
mock.recorder = &MockClockMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockClock) EXPECT() *MockClockMockRecorder {
return m.recorder
}
// After mocks base method.
func (m *MockClock) After(arg0 time.Duration) <-chan time.Time {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "After", arg0)
ret0, _ := ret[0].(<-chan time.Time)
return ret0
}
// After indicates an expected call of After.
func (mr *MockClockMockRecorder) After(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "After", reflect.TypeOf((*MockClock)(nil).After), arg0)
}
// NewTicker mocks base method.
func (m *MockClock) NewTicker(arg0 time.Duration) clock.Ticker {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "NewTicker", arg0)
ret0, _ := ret[0].(clock.Ticker)
return ret0
}
// NewTicker indicates an expected call of NewTicker.
func (mr *MockClockMockRecorder) NewTicker(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewTicker", reflect.TypeOf((*MockClock)(nil).NewTicker), arg0)
}
// Now mocks base method.
func (m *MockClock) Now() time.Time {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Now")
ret0, _ := ret[0].(time.Time)
return ret0
}
// Now indicates an expected call of Now.
func (mr *MockClockMockRecorder) Now() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Now", reflect.TypeOf((*MockClock)(nil).Now))
}
// MockTicker is a mock of Ticker interface.
type MockTicker struct {
ctrl *gomock.Controller
recorder *MockTickerMockRecorder
}
// MockTickerMockRecorder is the mock recorder for MockTicker.
type MockTickerMockRecorder struct {
mock *MockTicker
}
// NewMockTicker creates a new mock instance.
func NewMockTicker(ctrl *gomock.Controller) *MockTicker {
mock := &MockTicker{ctrl: ctrl}
mock.recorder = &MockTickerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockTicker) EXPECT() *MockTickerMockRecorder {
return m.recorder
}
// C mocks base method.
func (m *MockTicker) C() <-chan time.Time {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "C")
ret0, _ := ret[0].(<-chan time.Time)
return ret0
}
// C indicates an expected call of C.
func (mr *MockTickerMockRecorder) C() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "C", reflect.TypeOf((*MockTicker)(nil).C))
}
// Stop mocks base method.
func (m *MockTicker) Stop() {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Stop")
}
// Stop indicates an expected call of Stop.
func (mr *MockTickerMockRecorder) Stop() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Stop", reflect.TypeOf((*MockTicker)(nil).Stop))
}

View File

@@ -0,0 +1,120 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: pkg/socketcan/fileconn.go
// Package mocksocketcan is a generated GoMock package.
package mocksocketcan
import (
gomock "github.com/golang/mock/gomock"
reflect "reflect"
time "time"
)
// Mockfile is a mock of file interface.
type Mockfile struct {
ctrl *gomock.Controller
recorder *MockfileMockRecorder
}
// MockfileMockRecorder is the mock recorder for Mockfile.
type MockfileMockRecorder struct {
mock *Mockfile
}
// NewMockfile creates a new mock instance.
func NewMockfile(ctrl *gomock.Controller) *Mockfile {
mock := &Mockfile{ctrl: ctrl}
mock.recorder = &MockfileMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *Mockfile) EXPECT() *MockfileMockRecorder {
return m.recorder
}
// Read mocks base method.
func (m *Mockfile) Read(arg0 []byte) (int, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Read", arg0)
ret0, _ := ret[0].(int)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Read indicates an expected call of Read.
func (mr *MockfileMockRecorder) Read(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*Mockfile)(nil).Read), arg0)
}
// Write mocks base method.
func (m *Mockfile) Write(arg0 []byte) (int, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Write", arg0)
ret0, _ := ret[0].(int)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Write indicates an expected call of Write.
func (mr *MockfileMockRecorder) Write(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Write", reflect.TypeOf((*Mockfile)(nil).Write), arg0)
}
// SetDeadline mocks base method.
func (m *Mockfile) SetDeadline(arg0 time.Time) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetDeadline", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// SetDeadline indicates an expected call of SetDeadline.
func (mr *MockfileMockRecorder) SetDeadline(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetDeadline", reflect.TypeOf((*Mockfile)(nil).SetDeadline), arg0)
}
// SetReadDeadline mocks base method.
func (m *Mockfile) SetReadDeadline(arg0 time.Time) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetReadDeadline", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// SetReadDeadline indicates an expected call of SetReadDeadline.
func (mr *MockfileMockRecorder) SetReadDeadline(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetReadDeadline", reflect.TypeOf((*Mockfile)(nil).SetReadDeadline), arg0)
}
// SetWriteDeadline mocks base method.
func (m *Mockfile) SetWriteDeadline(arg0 time.Time) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "SetWriteDeadline", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// SetWriteDeadline indicates an expected call of SetWriteDeadline.
func (mr *MockfileMockRecorder) SetWriteDeadline(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetWriteDeadline", reflect.TypeOf((*Mockfile)(nil).SetWriteDeadline), arg0)
}
// Close mocks base method.
func (m *Mockfile) Close() error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Close")
ret0, _ := ret[0].(error)
return ret0
}
// Close indicates an expected call of Close.
func (mr *MockfileMockRecorder) Close() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*Mockfile)(nil).Close))
}

View File

@@ -0,0 +1,231 @@
package generate
import (
"crypto/sha256"
"fmt"
"regexp"
"sort"
"time"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/descriptor"
)
type CompileResult struct {
Hash string
Database *descriptor.Database
Warnings []error
}
func Compile(sourceFile string, data []byte) (result *CompileResult, err error) {
p := dbc.NewParser(sourceFile, data)
if err := p.Parse(); err != nil {
return nil, fmt.Errorf("failed to parse DBC source file: %w", err)
}
defs := p.Defs()
c := &compiler{
db: &descriptor.Database{SourceFile: sourceFile, ECUs: make(map[string]bool)},
defs: defs,
}
c.collectDescriptors()
c.addMetadata()
c.sortDescriptors()
hash := ComputeHash(data)
return &CompileResult{Hash: hash, Database: c.db, Warnings: c.warnings}, nil
}
type compileError struct {
def dbc.Def
reason string
}
func (e *compileError) Error() string {
return fmt.Sprintf("failed to compile: %v (%v)", e.reason, e.def)
}
type compiler struct {
db *descriptor.Database
defs []dbc.Def
warnings []error
}
func (c *compiler) addWarning(warning error) {
c.warnings = append(c.warnings, warning)
}
func (c *compiler) collectDescriptors() {
// find ECU names from messages
re := regexp.MustCompile(`^[0-9A-Za-z]+`)
for _, def := range c.defs {
switch def := def.(type) {
case *dbc.VersionDef:
c.db.Version = def.Version
case *dbc.MessageDef:
if def.MessageID == dbc.IndependentSignalsMessageID {
continue // don't compile
}
// add ECU names to set
ecu := re.FindString(string(def.Name))
if ecu != "" {
c.db.ECUs[ecu] = true
}
message := &descriptor.Message{
Name: string(def.Name),
ID: def.MessageID.ToCAN(),
IsExtended: def.MessageID.IsExtended(),
Length: uint16(def.Size),
SenderNode: string(def.Transmitter),
}
for _, signalDef := range def.Signals {
signal := &descriptor.Signal{
Name: string(signalDef.Name),
IsBigEndian: signalDef.IsBigEndian,
IsSigned: signalDef.IsSigned,
IsMultiplexer: signalDef.IsMultiplexerSwitch,
IsMultiplexed: signalDef.IsMultiplexed,
MultiplexerValue: uint(signalDef.MultiplexerSwitch),
Start: uint16(signalDef.StartBit),
Length: uint16(signalDef.Size),
Scale: signalDef.Factor,
Offset: signalDef.Offset,
Min: signalDef.Minimum,
Max: signalDef.Maximum,
Unit: signalDef.Unit,
}
for _, receiver := range signalDef.Receivers {
signal.ReceiverNodes = append(signal.ReceiverNodes, string(receiver))
}
message.Signals = append(message.Signals, signal)
}
c.db.Messages[message.ID] = message
case *dbc.NodesDef:
for _, node := range def.NodeNames {
c.db.Nodes = append(c.db.Nodes, &descriptor.Node{Name: string(node)})
}
}
}
}
func (c *compiler) addMetadata() {
for _, def := range c.defs {
switch def := def.(type) {
case *dbc.CommentDef:
switch def.ObjectType {
case dbc.ObjectTypeMessage:
if def.MessageID == dbc.IndependentSignalsMessageID {
continue // don't compile
}
message, ok := c.db.Message(def.MessageID.ToCAN())
if !ok {
c.addWarning(&compileError{def: def, reason: "no declared message"})
continue
}
message.Description = def.Comment
case dbc.ObjectTypeSignal:
if def.MessageID == dbc.IndependentSignalsMessageID {
continue // don't compile
}
signal, ok := c.db.Signal(def.MessageID.ToCAN(), string(def.SignalName))
if !ok {
c.addWarning(&compileError{def: def, reason: "no declared signal"})
continue
}
signal.Description = def.Comment
case dbc.ObjectTypeNetworkNode:
node, ok := c.db.Node(string(def.NodeName))
if !ok {
c.addWarning(&compileError{def: def, reason: "no declared node"})
continue
}
node.Description = def.Comment
}
case *dbc.ValueDescriptionsDef:
if def.MessageID == dbc.IndependentSignalsMessageID {
continue // don't compile
}
if def.ObjectType != dbc.ObjectTypeSignal {
continue // don't compile
}
signal, ok := c.db.Signal(def.MessageID.ToCAN(), string(def.SignalName))
if !ok {
c.addWarning(&compileError{def: def, reason: "no declared signal"})
continue
}
for _, valueDescription := range def.ValueDescriptions {
signal.ValueDescriptions = append(signal.ValueDescriptions, &descriptor.ValueDescription{
Description: valueDescription.Description,
Value: int(valueDescription.Value),
})
}
case *dbc.AttributeValueForObjectDef:
switch def.ObjectType {
case dbc.ObjectTypeMessage:
msg, ok := c.db.Message(def.MessageID.ToCAN())
if !ok {
c.addWarning(&compileError{def: def, reason: "no declared message"})
continue
}
switch def.AttributeName {
case "GenMsgSendType":
if err := msg.SendType.UnmarshalString(def.StringValue); err != nil {
c.addWarning(&compileError{def: def, reason: err.Error()})
continue
}
case "GenMsgCycleTime":
msg.CycleTime = time.Duration(def.IntValue) * time.Millisecond
case "GenMsgDelayTime":
msg.DelayTime = time.Duration(def.IntValue) * time.Millisecond
}
case dbc.ObjectTypeSignal:
sig, ok := c.db.Signal(def.MessageID.ToCAN(), string(def.SignalName))
if !ok {
c.addWarning(&compileError{def: def, reason: "no declared signal"})
}
if def.AttributeName == "GenSigStartValue" {
sig.DefaultValue = int(def.IntValue)
}
}
}
}
}
func (c *compiler) sortDescriptors() {
// Sort nodes by name
sort.Slice(c.db.Nodes, func(i, j int) bool {
return c.db.Nodes[i].Name < c.db.Nodes[j].Name
})
// Sort messages by ID
// sort.Slice(c.db.Messages, func(i, j int) bool {
// return c.db.Messages[i].ID < c.db.Messages[j].ID
// })
for _, m := range c.db.Messages {
if m == nil {
continue
}
m := m
// Sort signals by start (and multiplexer value)
sort.Slice(m.Signals, func(j, k int) bool {
if m.Signals[j].MultiplexerValue < m.Signals[k].MultiplexerValue {
return true
}
return m.Signals[j].Start < m.Signals[k].Start
})
// Sort value descriptions by value
for _, s := range m.Signals {
s := s
sort.Slice(s.ValueDescriptions, func(k, l int) bool {
return s.ValueDescriptions[k].Value < s.ValueDescriptions[l].Value
})
}
}
}
func ComputeHash(input []byte) string {
h := sha256.New()
h.Write(input)
return fmt.Sprintf("%x", h.Sum(nil))
}

View File

@@ -0,0 +1,306 @@
package generate
import (
"io/ioutil"
"testing"
"time"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/descriptor"
examplecan "github.com/fiskerinc/cloud-services/pkg/can-go/testdata/gen/go/example"
"gotest.tools/v3/assert"
)
func TestCompile_ExampleDBC(t *testing.T) {
finish := runTestInDir(t, "../..")
defer finish()
const exampleDBCFile = "testdata/dbc/example/example.dbc"
exampleDatabase := &descriptor.Database{
SourceFile: exampleDBCFile,
Version: "",
Nodes: []*descriptor.Node{
{
Name: "DBG",
},
{
Name: "DRIVER",
Description: "The driver controller driving the car",
},
{
Name: "IO",
},
{
Name: "MOTOR",
Description: "The motor controller of the car",
},
{
Name: "SENSOR",
Description: "The sensor controller of the car",
},
},
}
exampleDatabase.Messages[1] = &descriptor.Message{
ID: 1,
Name: "EmptyMessage",
SenderNode: "DBG",
}
exampleDatabase.Messages[100] = &descriptor.Message{
ID: 100,
Name: "DriverHeartbeat",
Length: 1,
SenderNode: "DRIVER",
Description: "Sync message used to synchronize the controllers",
SendType: descriptor.SendTypeCyclic,
CycleTime: time.Second,
Signals: []*descriptor.Signal{
{
Name: "Command",
Start: 0,
Length: 8,
Scale: 1,
ReceiverNodes: []string{"SENSOR", "MOTOR"},
ValueDescriptions: []*descriptor.ValueDescription{
{Value: 0, Description: "None"},
{Value: 1, Description: "Sync"},
{Value: 2, Description: "Reboot"},
},
},
},
}
exampleDatabase.Messages[101] = &descriptor.Message{
ID: 101,
Name: "MotorCommand",
Length: 1,
SenderNode: "DRIVER",
SendType: descriptor.SendTypeCyclic,
CycleTime: 100 * time.Millisecond,
Signals: []*descriptor.Signal{
{
Name: "Steer",
Start: 0,
Length: 4,
IsSigned: true,
Scale: 1,
Offset: -5,
Min: -5,
Max: 5,
ReceiverNodes: []string{"MOTOR"},
},
{
Name: "Drive",
Start: 4,
Length: 4,
Scale: 1,
Max: 9,
ReceiverNodes: []string{"MOTOR"},
},
},
}
exampleDatabase.Messages[200] = &descriptor.Message{
ID: 200,
Name: "SensorSonars",
Length: 8,
SenderNode: "SENSOR",
SendType: descriptor.SendTypeCyclic,
CycleTime: 100 * time.Millisecond,
Signals: []*descriptor.Signal{
{
Name: "Mux",
IsMultiplexer: true,
Start: 0,
Length: 4,
Scale: 1,
ReceiverNodes: []string{"DRIVER", "IO"},
},
{
Name: "ErrCount",
Start: 4,
Length: 12,
Scale: 1,
ReceiverNodes: []string{"DRIVER", "IO"},
},
{
Name: "Left",
IsMultiplexed: true,
MultiplexerValue: 0,
Start: 16,
Length: 12,
Scale: 0.1,
ReceiverNodes: []string{"DRIVER", "IO"},
},
{
Name: "NoFiltLeft",
IsMultiplexed: true,
MultiplexerValue: 1,
Start: 16,
Length: 12,
Scale: 0.1,
ReceiverNodes: []string{"DBG"},
},
{
Name: "Middle",
IsMultiplexed: true,
MultiplexerValue: 0,
Start: 28,
Length: 12,
Scale: 0.1,
ReceiverNodes: []string{"DRIVER", "IO"},
},
{
Name: "NoFiltMiddle",
IsMultiplexed: true,
MultiplexerValue: 1,
Start: 28,
Length: 12,
Scale: 0.1,
ReceiverNodes: []string{"DBG"},
},
{
Name: "Right",
IsMultiplexed: true,
MultiplexerValue: 0,
Start: 40,
Length: 12,
Scale: 0.1,
ReceiverNodes: []string{"DRIVER", "IO"},
},
{
Name: "NoFiltRight",
IsMultiplexed: true,
MultiplexerValue: 1,
Start: 40,
Length: 12,
Scale: 0.1,
ReceiverNodes: []string{"DBG"},
},
{
Name: "Rear",
IsMultiplexed: true,
MultiplexerValue: 0,
Start: 52,
Length: 12,
Scale: 0.1,
ReceiverNodes: []string{"DRIVER", "IO"},
},
{
Name: "NoFiltRear",
IsMultiplexed: true,
MultiplexerValue: 1,
Start: 52,
Length: 12,
Scale: 0.1,
ReceiverNodes: []string{"DBG"},
},
},
}
exampleDatabase.Messages[400] = &descriptor.Message{
ID: 400,
Name: "MotorStatus",
Length: 3,
SenderNode: "MOTOR",
SendType: descriptor.SendTypeCyclic,
CycleTime: 100 * time.Millisecond,
Signals: []*descriptor.Signal{
{
Name: "WheelError",
Start: 0,
Length: 1,
Scale: 1,
ReceiverNodes: []string{"DRIVER", "IO"},
},
{
Name: "SpeedKph",
Start: 8,
Length: 16,
Scale: 0.001,
Unit: "km/h",
ReceiverNodes: []string{"DRIVER", "IO"},
},
},
}
exampleDatabase.Messages[500] = &descriptor.Message{
ID: 500,
Name: "IODebug",
Length: 6,
SenderNode: "IO",
SendType: descriptor.SendTypeEvent,
Signals: []*descriptor.Signal{
{
Name: "TestUnsigned",
Start: 0,
Length: 8,
Scale: 1,
ReceiverNodes: []string{"DBG"},
},
{
Name: "TestEnum",
Start: 8,
Length: 6,
Scale: 1,
ReceiverNodes: []string{"DBG"},
DefaultValue: int(examplecan.IODebug_TestEnum_Two),
ValueDescriptions: []*descriptor.ValueDescription{
{Value: 1, Description: "One"},
{Value: 2, Description: "Two"},
},
},
{
Name: "TestSigned",
Start: 16,
Length: 8,
IsSigned: true,
Scale: 1,
ReceiverNodes: []string{"DBG"},
},
{
Name: "TestFloat",
Start: 24,
Length: 8,
Scale: 0.5,
ReceiverNodes: []string{"DBG"},
},
{
Name: "TestBoolEnum",
Start: 32,
Length: 1,
Scale: 1,
ReceiverNodes: []string{"DBG"},
ValueDescriptions: []*descriptor.ValueDescription{
{Value: 0, Description: "Zero"},
{Value: 1, Description: "One"},
},
},
{
Name: "TestScaledEnum",
Start: 40,
Length: 2,
Scale: 2,
Min: 0,
Max: 6,
ReceiverNodes: []string{"DBG"},
ValueDescriptions: []*descriptor.ValueDescription{
{Value: 0, Description: "Zero"},
{Value: 1, Description: "Two"},
{Value: 2, Description: "Four"},
{Value: 3, Description: "Six"},
},
},
},
}
input, err := ioutil.ReadFile(exampleDBCFile)
assert.NilError(t, err)
result, err := Compile(exampleDBCFile, input)
if err != nil {
t.Fatal(err)
}
if len(result.Warnings) > 0 {
t.Fatal(result.Warnings)
}
assert.DeepEqual(t, exampleDatabase, result.Database)
}

View File

@@ -0,0 +1,338 @@
package generate
import (
"context"
"fmt"
"net"
"reflect"
"testing"
"time"
can "github.com/fiskerinc/cloud-services/pkg/can-go"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/generated"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/socketcan"
examplecan "github.com/fiskerinc/cloud-services/pkg/can-go/testdata/gen/go/example"
"golang.org/x/sync/errgroup"
"gotest.tools/v3/assert"
)
func TestExampleDatabase_MarshalUnmarshal(t *testing.T) {
for _, tt := range []struct {
name string
m can.Message
f can.Frame
}{
{
name: "IODebug",
m: examplecan.NewIODebug().
SetTestUnsigned(5).
SetTestEnum(examplecan.IODebug_TestEnum_Two).
SetTestSigned(-42).
SetTestFloat(61.5).
SetTestBoolEnum(examplecan.IODebug_TestBoolEnum_One).
SetRawTestScaledEnum(examplecan.IODebug_TestScaledEnum_Four),
f: can.Frame{
ID: 500,
Length: 6,
Data: can.Data{5, 2, 214, 123, 1, 2},
},
},
{
name: "MotorStatus1",
m: examplecan.NewMotorStatus().
SetSpeedKph(0.423).
SetWheelError(true),
f: can.Frame{
ID: 400,
Length: 3,
Data: can.Data{0x1, 0xa7, 0x1},
},
},
{
name: "MotorStatus2",
m: examplecan.NewMotorStatus().
SetSpeedKph(12),
f: can.Frame{
ID: 400,
Length: 3,
Data: can.Data{0x00, 0xe0, 0x2e},
},
},
} {
tt := tt
t.Run(tt.name, func(t *testing.T) {
f, err := tt.m.MarshalFrame()
assert.NilError(t, err)
assert.Equal(t, tt.f, f)
// allocate new message of same type as tt.m
msg := reflect.New(reflect.ValueOf(tt.m).Elem().Type()).Interface().(generated.Message)
assert.NilError(t, msg.UnmarshalFrame(f))
assert.Assert(t, reflect.DeepEqual(tt.m, msg))
})
}
}
func TestExampleDatabase_UnmarshalFrame_Error(t *testing.T) {
for _, tt := range []struct {
name string
f can.Frame
m generated.Message
err string
}{
{
name: "wrong ID",
f: can.Frame{ID: 11, Length: 8},
m: examplecan.NewSensorSonars(),
err: "unmarshal SensorSonars: expects ID 200 (got 00B#0000000000000000 with ID 11)",
},
{
name: "wrong length",
f: can.Frame{ID: 200, Length: 4},
m: examplecan.NewSensorSonars(),
err: "unmarshal SensorSonars: expects length 8 (got 0C8#00000000 with length 4)",
},
{
name: "remote frame",
f: can.Frame{ID: 200, Length: 8, IsRemote: true},
m: examplecan.NewSensorSonars(),
err: "unmarshal SensorSonars: expects non-remote frame (got remote frame 0C8#R8)",
},
{
name: "extended ID",
f: can.Frame{ID: 200, Length: 8, IsExtended: true},
m: examplecan.NewSensorSonars(),
err: "unmarshal SensorSonars: expects standard ID (got 000000C8#0000000000000000 with extended ID)",
},
} {
tt := tt
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.err, tt.m.UnmarshalFrame(tt.f).Error())
})
}
}
func TestExampleDatabase_TestEnum_String(t *testing.T) {
assert.Equal(t, "One", examplecan.IODebug_TestEnum_One.String())
assert.Equal(t, "Two", examplecan.IODebug_TestEnum_Two.String())
assert.Equal(t, "IODebug_TestEnum(3)", examplecan.IODebug_TestEnum(3).String())
}
func TestExampleDatabase_Message_String(t *testing.T) {
const expected = "{WheelError: true, SpeedKph: 42km/h}"
msg := examplecan.NewMotorStatus().
SetSpeedKph(42).
SetWheelError(true)
assert.Equal(t, expected, msg.String())
assert.Equal(t, expected, fmt.Sprintf("%v", msg))
}
func TestExampleDatabase_OutOfBoundsValue(t *testing.T) {
const expected = examplecan.IODebug_TestEnum(63)
actual := examplecan.NewIODebug().SetTestEnum(255).TestEnum()
assert.Equal(t, expected, actual)
}
func TestExampleDatabase_MultiplexedSignals(t *testing.T) {
// Given a message with multiplexed signals
msg := examplecan.NewSensorSonars().
SetErrCount(1).
SetMux(1).
SetLeft(20).
SetMiddle(30).
SetRight(40).
SetRear(50).
SetNoFiltLeft(60).
SetNoFiltMiddle(70).
SetNoFiltRight(80).
SetNoFiltRear(90)
for _, tt := range []struct {
expectedMux uint8
expectedErrCount uint16
expectedLeft float64
expectedMiddle float64
expectedRight float64
expectedRear float64
expectedNoFiltLeft float64
expectedNoFiltMiddle float64
expectedNoFiltRight float64
expectedNoFiltRear float64
}{
{
expectedMux: 0,
expectedErrCount: 1,
expectedLeft: 20,
expectedMiddle: 30,
expectedRight: 40,
expectedRear: 50,
expectedNoFiltLeft: 0,
expectedNoFiltMiddle: 0,
expectedNoFiltRight: 0,
expectedNoFiltRear: 0,
},
{
expectedMux: 1,
expectedErrCount: 1,
expectedLeft: 0,
expectedMiddle: 0,
expectedRight: 0,
expectedRear: 0,
expectedNoFiltLeft: 60,
expectedNoFiltMiddle: 70,
expectedNoFiltRight: 80,
expectedNoFiltRear: 90,
},
} {
tt := tt
t.Run(fmt.Sprintf("mux=%v", tt.expectedMux), func(t *testing.T) {
unmarshal1 := examplecan.NewSensorSonars()
// When the multiplexer signal is 0 and we marshal the message
// to a CAN frame
msg.SetMux(tt.expectedMux)
f1, err := msg.MarshalFrame()
assert.NilError(t, err)
// When we unmarshal the CAN frame back to a message
assert.NilError(t, unmarshal1.UnmarshalFrame(f1))
// Then only the multiplexed signals with multiplexer value 0
// should be unmarshaled
assert.Equal(t, tt.expectedMux, unmarshal1.Mux(), "Mux")
assert.Equal(t, tt.expectedErrCount, unmarshal1.ErrCount(), "ErrCount")
assert.Equal(t, tt.expectedLeft, unmarshal1.Left(), "Left")
assert.Equal(t, tt.expectedMiddle, unmarshal1.Middle(), "Middle")
assert.Equal(t, tt.expectedRight, unmarshal1.Right(), "Right")
assert.Equal(t, tt.expectedRear, unmarshal1.Rear(), "Rear")
assert.Equal(t, tt.expectedNoFiltLeft, unmarshal1.NoFiltLeft(), "NoFiltLeft")
assert.Equal(t, tt.expectedNoFiltMiddle, unmarshal1.NoFiltMiddle(), "NoFiltMiddle")
assert.Equal(t, tt.expectedNoFiltRight, unmarshal1.NoFiltRight(), "NoFiltRight")
assert.Equal(t, tt.expectedNoFiltRear, unmarshal1.NoFiltRear(), "NoFiltRear")
})
}
}
func TestExampleDatabase_CopyFrom(t *testing.T) {
// Given: an original message
from := examplecan.NewIODebug().
SetRawTestScaledEnum(examplecan.IODebug_TestScaledEnum_Four).
SetTestBoolEnum(true).
SetTestFloat(0.1).
SetTestSigned(-10).
SetTestUnsigned(10)
// When: another message copies from the original message
to := examplecan.NewIODebug().CopyFrom(from)
// Then:
// all fields should be equal...
assert.Equal(t, from.String(), to.String())
assert.Equal(t, from.TestScaledEnum(), to.TestScaledEnum())
assert.Equal(t, from.TestBoolEnum(), to.TestBoolEnum())
assert.Equal(t, from.TestFloat(), to.TestFloat())
assert.Equal(t, from.TestSigned(), to.TestSigned())
assert.Equal(t, from.TestUnsigned(), to.TestUnsigned())
// ...and changes to the original should not affect the new message
from.SetTestUnsigned(100)
assert.Equal(t, uint8(10), to.TestUnsigned())
}
func TestExample_Nodes(t *testing.T) {
const testTimeout = 2 * time.Second
requireVCAN0(t)
// given a DRIVER node and a MOTOR node
motor := examplecan.NewMOTOR("can", "vcan0")
driver := examplecan.NewDRIVER("can", "vcan0")
// when starting them
ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
g, ctx := errgroup.WithContext(ctx)
g.Go(func() error {
return motor.Run(ctx)
})
g.Go(func() error {
return driver.Run(ctx)
})
// and the MOTOR node is configured to send a speed report
const expectedSpeedKph = 42
motor.Lock()
motor.Tx().MotorStatus().SetSpeedKph(expectedSpeedKph)
motor.Tx().MotorStatus().SetCyclicTransmissionEnabled(true)
motor.Unlock()
// and the DRIVER node is configured to send a steering command
const expectedSteer = -4
driver.Lock()
driver.Tx().MotorCommand().SetSteer(expectedSteer)
driver.Tx().MotorCommand().SetCyclicTransmissionEnabled(true)
driver.Unlock()
// and the MOTOR node is listening for the steering command
expectedSteerReceivedChan := make(chan struct{})
motor.Lock()
motor.Rx().MotorCommand().SetAfterReceiveHook(func(context.Context) error {
motor.Lock()
if motor.Rx().MotorCommand().Steer() == expectedSteer {
close(expectedSteerReceivedChan)
motor.Rx().MotorCommand().SetAfterReceiveHook(func(context.Context) error { return nil })
}
motor.Unlock()
return nil
})
motor.Unlock()
// and the DRIVER node is listening for the speed report
expectedSpeedReceivedChan := make(chan struct{})
driver.Lock()
driver.Rx().MotorStatus().SetAfterReceiveHook(func(context.Context) error {
driver.Lock()
if driver.Rx().MotorStatus().SpeedKph() == expectedSpeedKph {
close(expectedSpeedReceivedChan)
driver.Rx().MotorStatus().SetAfterReceiveHook(func(context.Context) error { return nil })
}
driver.Unlock()
return nil
})
driver.Unlock()
// then the steer command transmitted by DRIVER should be received by MOTOR
select {
case <-expectedSteerReceivedChan:
case <-ctx.Done():
t.Fatalf("expected steer not received: %v", expectedSteer)
}
// and the speed report transmitted by MOTOR should be received by DRIVER
select {
case <-expectedSpeedReceivedChan:
case <-ctx.Done():
t.Fatalf("expected speed not received: %v", expectedSpeedKph)
}
cancel()
assert.NilError(t, g.Wait())
}
func TestExample_Node_NoEmptyMessages(t *testing.T) {
const testTimeout = 2 * time.Second
requireVCAN0(t)
// given a DRIVER node and a MOTOR node
motor := examplecan.NewMOTOR("can", "vcan0")
// when starting them
ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
handler := func(ctx context.Context) error {
motor.Lock()
motor.Tx().MotorStatus().SetSpeedKph(100).SetWheelError(true)
motor.Unlock()
return nil
}
motor.Tx().MotorStatus().SetBeforeTransmitHook(handler)
motor.Tx().MotorStatus().SetCyclicTransmissionEnabled(true)
c, err := socketcan.Dial("can", "vcan0")
r := socketcan.NewReceiver(c)
assert.NilError(t, err)
g := errgroup.Group{}
g.Go(func() error {
return motor.Run(ctx)
})
assert.Assert(t, r.Receive())
assert.Equal(t, examplecan.NewMotorStatus().SetSpeedKph(100).SetWheelError(true).Frame(), r.Frame())
cancel()
assert.NilError(t, g.Wait())
}
func requireVCAN0(t *testing.T) {
t.Helper()
if _, err := net.InterfaceByName("vcan0"); err != nil {
t.Skip("interface vcan0 does not exist")
}
}

View File

@@ -0,0 +1,976 @@
package generate
import (
"bytes"
"fmt"
"go/format"
"go/types"
"path"
"strings"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/descriptor"
"github.com/shurcooL/go-goon"
)
type File struct {
buf bytes.Buffer
err error
}
func NewFile() *File {
f := &File{}
f.buf.Grow(1e5) // 100K
return f
}
func (f *File) Write(p []byte) (int, error) {
if f.err != nil {
return 0, f.err
}
n, err := f.buf.Write(p)
f.err = err
return n, err
}
func (f *File) P(v ...interface{}) {
for _, x := range v {
_, _ = fmt.Fprint(f, x)
}
_, _ = fmt.Fprintln(f)
}
func (f *File) Dump(v interface{}) {
_, _ = goon.Fdump(f, v)
}
func (f *File) Content() ([]byte, error) {
if f.err != nil {
return nil, fmt.Errorf("file content: %w", f.err)
}
formatted, err := format.Source(f.buf.Bytes())
if err != nil {
return nil, fmt.Errorf("file content: %s: %w", f.buf.String(), err)
}
return formatted, nil
}
func Database(h string, d *descriptor.Database) ([]byte, error) {
f := NewFile()
Package(f, d)
Imports(f)
Version(f, h, d.Version)
ListECUs(f, d)
for _, m := range d.Messages {
if m == nil {
continue
}
MessageType(f, m)
for _, s := range m.Signals {
if hasCustomType(s) {
SignalCustomType(f, m, s)
}
}
MarshalFrame(f, m)
UnmarshalFrame(f, m)
}
if hasSendType(d) { // only code-generate nodes for schemas with send types specified
for _, n := range d.Nodes {
Node(f, d, n)
}
}
Descriptors(f, d)
return f.Content()
}
func Package(f *File, d *descriptor.Database) {
packageName := strings.TrimSuffix(path.Base(d.SourceFile), path.Ext(d.SourceFile)) + "can"
f.P("// Package ", packageName, " provides primitives for encoding and decoding ", d.Name(), " CAN messages.")
f.P("//")
f.P("// Source: ", d.SourceFile)
f.P("package ", packageName)
f.P()
}
func Imports(f *File) {
f.P("import (")
f.P(`"context"`)
f.P(`"fmt"`)
f.P(`"net"`)
f.P(`"net/http"`)
f.P(`"sync"`)
f.P(`"time"`)
f.P()
f.P(`"github.com/fiskerinc/cloud-services/pkg/can-go"`)
f.P(`"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/socketcan"`)
f.P(`"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/candebug"`)
f.P(`"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/canrunner"`)
f.P(`"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/descriptor"`)
f.P(`"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/generated"`)
f.P(`"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/cantext"`)
f.P(")")
f.P()
// we could use goimports for this, but it significantly slows down code generation
f.P("// prevent unused imports")
f.P("var (")
f.P("_ = context.Background")
f.P("_ = fmt.Print")
f.P("_ = net.Dial")
f.P("_ = http.Error")
f.P("_ = sync.Mutex{}")
f.P("_ = time.Now")
f.P("_ = socketcan.Dial")
f.P("_ = candebug.ServeMessagesHTTP")
f.P("_ = canrunner.Run")
f.P(")")
f.P()
f.P("// Generated code. DO NOT EDIT.")
}
func Version(f *File, h string, v string) {
f.P()
f.P("// Hash used as versioning control for DBC")
f.P(`const Hash string = "`, h, `"`)
f.P("// Version is the version listed in the DBC")
f.P(`const Version string = "`, v, `"`)
f.P()
}
func ListECUs(f *File, d *descriptor.Database) {
f.P()
f.P("// ECUs parsed from DBC")
ecuList := "var ECUs = []string{"
i := 0
for ecu := range d.ECUs {
ecuList += fmt.Sprintf(`"%s"`, ecu)
i++
if i != len(d.ECUs) {
ecuList += ", "
}
}
ecuList += "}"
f.P(ecuList)
f.P()
}
func SignalCustomType(f *File, m *descriptor.Message, s *descriptor.Signal) {
f.P("// ", signalType(m, s), " models the ", s.Name, " signal of the ", m.Name, " message.")
f.P("type ", signalType(m, s), " ", signalPrimitiveType(s))
f.P()
// dtaylor@fiskerinc.com EDIT
// f.P("// Value descriptions for the ", s.Name, " signal of the ", m.Name, " message.")
// f.P("const (")
// for _, vd := range s.ValueDescriptions {
// switch {
// case s.Length == 1 && vd.Value == 1:
// f.P(signalType(m, s), "_", vd.Description, " ", signalType(m, s), " = true")
// case s.Length == 1 && vd.Value == 0:
// f.P(signalType(m, s), "_", vd.Description, " ", signalType(m, s), " = false")
// default:
// f.P(signalType(m, s), "_", vd.Description, " ", signalType(m, s), " = ", vd.Value)
// }
// }
// f.P(")")
// f.P()
f.P("func (v ", signalType(m, s), ") String() string {")
if s.Length == 1 {
f.P("switch bool(v) {")
for _, vd := range s.ValueDescriptions {
if vd.Value == 1 {
f.P("case true:")
} else {
f.P("case false:")
}
f.P(`return "`, vd.Description, `"`)
}
f.P("}")
f.P(`return fmt.Sprintf("`, signalType(m, s), `(%t)", v)`)
} else {
f.P("switch v {")
for _, vd := range s.ValueDescriptions {
f.P("case ", vd.Value, ":")
f.P(`return "`, vd.Description, `"`)
}
f.P("default:")
f.P(`return fmt.Sprintf("`, signalType(m, s), `(%d)", v)`)
f.P("}")
}
f.P("}")
}
func MessageType(f *File, m *descriptor.Message) {
f.P("// ", messageReaderInterface(m), " provides read access to a ", m.Name, " message.")
f.P("type ", messageReaderInterface(m), " interface {")
for _, s := range m.Signals {
if hasPhysicalRepresentation(s) {
f.P("// ", s.Name, " returns the physical value of the ", s.Name, " signal.")
f.P(s.Name, "() float64")
if len(s.ValueDescriptions) > 0 {
f.P()
f.P("// ", s.Name, " returns the raw (encoded) value of the ", s.Name, " signal.")
f.P("Raw", s.Name, "() ", signalType(m, s))
}
} else {
f.P("// ", s.Name, " returns the value of the ", s.Name, " signal.")
f.P(s.Name, "()", signalType(m, s))
}
}
f.P("}")
f.P()
f.P("// ", messageWriterInterface(m), " provides write access to a ", m.Name, " message.")
f.P("type ", messageWriterInterface(m), " interface {")
f.P("// CopyFrom copies all values from ", messageReaderInterface(m), ".")
f.P("CopyFrom(", messageReaderInterface(m), ") *", messageStruct(m))
for _, s := range m.Signals {
if hasPhysicalRepresentation(s) {
f.P("// Set", s.Name, " sets the physical value of the ", s.Name, " signal.")
f.P("Set", s.Name, "(float64) *", messageStruct(m))
if len(s.ValueDescriptions) > 0 {
f.P()
f.P("// SetRaw", s.Name, " sets the raw (encoded) value of the ", s.Name, " signal.")
f.P("SetRaw", s.Name, "(", signalType(m, s), ") *", messageStruct(m))
}
} else {
f.P("// Set", s.Name, " sets the value of the ", s.Name, " signal.")
f.P("Set", s.Name, "(", signalType(m, s), ") *", messageStruct(m))
}
}
f.P("}")
f.P()
f.P("type ", messageStruct(m), " struct {")
for _, s := range m.Signals {
f.P(signalField(s), " ", signalType(m, s))
}
f.P("}")
f.P()
f.P("func New", messageStruct(m), "() *", messageStruct(m), " {")
f.P("m := &", messageStruct(m), "{}")
f.P("m.Reset()")
f.P("return m")
f.P("}")
f.P()
f.P("func (m *", messageStruct(m), ") Reset() {")
for _, s := range m.Signals {
switch {
case s.Length == 1 && s.DefaultValue == 1:
f.P("m.", signalField(s), " = true")
case s.Length == 1:
f.P("m.", signalField(s), " = false")
default:
f.P("m.", signalField(s), " = ", s.DefaultValue)
}
}
f.P("}")
f.P()
f.P("func (m *", messageStruct(m), ") CopyFrom(o ", messageReaderInterface(m), ") *", messageStruct(m), "{")
for _, s := range m.Signals {
if hasPhysicalRepresentation(s) {
f.P("m.Set", s.Name, "(o.", s.Name, "())")
} else {
f.P("m.", signalField(s), " = o.", s.Name, "()")
}
}
f.P("return m")
f.P("}")
f.P()
f.P("// Descriptor returns the ", m.Name, " descriptor.")
f.P("func (m *", messageStruct(m), ") Descriptor() *descriptor.Message {")
f.P("return ", messageDescriptor(m), ".Message")
f.P("}")
f.P()
f.P("// String returns a compact string representation of the message.")
f.P("func(m *", messageStruct(m), ") String() string {")
f.P("return cantext.MessageString(m)")
f.P("}")
f.P()
for _, s := range m.Signals {
if !hasPhysicalRepresentation(s) {
f.P("func (m *", messageStruct(m), ") ", s.Name, "() ", signalType(m, s), " {")
f.P("return m.", signalField(s))
f.P("}")
f.P()
f.P("func (m *", messageStruct(m), ") Set", s.Name, "(v ", signalType(m, s), ") *", messageStruct(m), " {")
if s.Length == 1 {
f.P("m.", signalField(s), " = v")
} else {
f.P(
"m.", signalField(s), " = ", signalType(m, s), "(",
signalDescriptor(m, s), ".SaturatedCast", signalSuperType(s), "(",
signalPrimitiveSuperType(s), "(v)))",
)
}
f.P("return m")
f.P("}")
f.P()
continue
}
f.P("func (m *", messageStruct(m), ") ", s.Name, "() float64 {")
f.P("return ", signalDescriptor(m, s), ".ToPhysical(float64(m.", signalField(s), "))")
f.P("}")
f.P()
f.P("func (m *", messageStruct(m), ") Set", s.Name, "(v float64) *", messageStruct(m), " {")
f.P("m.", signalField(s), " = ", signalType(m, s), "(", signalDescriptor(m, s), ".FromPhysical(v))")
f.P("return m")
f.P("}")
f.P()
if len(s.ValueDescriptions) > 0 {
f.P("func (m *", messageStruct(m), ") Raw", s.Name, "() ", signalType(m, s), " {")
f.P("return m.", signalField(s))
f.P("}")
f.P()
f.P("func (m *", messageStruct(m), ") SetRaw", s.Name, "(v ", signalType(m, s), ") *", messageStruct(m), "{")
f.P(
"m.", signalField(s), " = ", signalType(m, s), "(",
signalDescriptor(m, s), ".SaturatedCast", signalSuperType(s), "(",
signalPrimitiveSuperType(s), "(v)))",
)
f.P("return m")
f.P("}")
f.P()
}
}
}
func Descriptors(f *File, d *descriptor.Database) {
f.P("// Nodes returns the ", d.Name(), " node descriptors.")
f.P("func Nodes() *NodesDescriptor {")
f.P("return nd")
f.P("}")
f.P()
f.P("// NodesDescriptor contains all ", d.Name(), " node descriptors.")
f.P("type NodesDescriptor struct{")
for _, n := range d.Nodes {
f.P(n.Name, " *descriptor.Node")
}
f.P("}")
f.P()
f.P("// Messages returns the ", d.Name(), " message descriptors.")
f.P("func Messages() *MessagesDescriptor {")
f.P("return md")
f.P("}")
f.P()
f.P("// MessagesDescriptor contains all ", d.Name(), " message descriptors.")
f.P("type MessagesDescriptor struct{")
for _, m := range d.Messages {
if m == nil {
continue
}
f.P(m.Name, " *", m.Name, "Descriptor")
}
f.P("}")
f.P()
f.P("// UnmarshalFrame unmarshals the provided ", d.Name(), " CAN frame.")
f.P("func (md *MessagesDescriptor) UnmarshalFrame(f can.Frame) (generated.Message, error) {")
f.P("switch f.ID {")
for _, m := range d.Messages {
if m == nil {
continue
}
f.P("case md.", m.Name, ".ID:")
f.P("var msg ", messageStruct(m))
f.P("if err := msg.UnmarshalFrame(f); err != nil {")
f.P(`return nil, fmt.Errorf("unmarshal `, d.Name(), ` frame: %w", err)`)
f.P("}")
f.P("return &msg, nil")
}
f.P("default:")
f.P(`return nil, fmt.Errorf("unmarshal `, d.Name(), ` frame: ID not in database: %d", f.ID)`)
f.P("}")
f.P("}")
f.P()
for _, m := range d.Messages {
if m == nil {
continue
}
f.P("type ", m.Name, "Descriptor struct{")
f.P("*descriptor.Message")
for _, s := range m.Signals {
f.P(s.Name, " *descriptor.Signal")
}
f.P("}")
f.P()
}
f.P("// Database returns the ", d.Name(), " database descriptor.")
f.P("func (md *MessagesDescriptor) Database() *descriptor.Database {")
f.P("return d")
f.P("}")
f.P()
f.P("var nd = &NodesDescriptor{")
for ni, n := range d.Nodes {
f.P(n.Name, ": d.Nodes[", ni, "],")
}
f.P("}")
f.P()
f.P("var md = &MessagesDescriptor{")
for mi, m := range d.Messages {
if m == nil {
continue
}
f.P(m.Name, ": &", m.Name, "Descriptor{")
f.P("Message: d.Messages[", mi, "],")
for si, s := range m.Signals {
f.P(s.Name, ": d.Messages[", mi, "].Signals[", si, "],")
}
f.P("},")
}
f.P("}")
f.P()
f.P("var d = ")
f.Dump(d)
f.P()
}
func MarshalFrame(f *File, m *descriptor.Message) {
f.P("// Frame returns a CAN frame representing the message.")
f.P("func (m *", messageStruct(m), ") Frame() can.Frame {")
f.P("md := ", messageDescriptor(m))
f.P("f := can.Frame{ID: md.ID, IsExtended: md.IsExtended, Length: md.Length}")
for _, s := range m.Signals {
if s.IsMultiplexed {
continue
}
f.P(
"md.", s.Name, ".Marshal", signalSuperType(s),
"(&f.Data, ", signalPrimitiveSuperType(s), "(m.", signalField(s), "))",
)
}
if mux, ok := m.MultiplexerSignal(); ok {
for _, s := range m.Signals {
if !s.IsMultiplexed {
continue
}
f.P("if m.", signalField(mux), " == ", s.MultiplexerValue, " {")
f.P(
"md.", s.Name, ".Marshal", signalSuperType(s), "(&f.Data, ", signalPrimitiveSuperType(s),
"(m.", signalField(s), "))",
)
f.P("}")
}
}
f.P("return f")
f.P("}")
f.P()
f.P("// MarshalFrame encodes the message as a CAN frame.")
f.P("func (m *", messageStruct(m), ") MarshalFrame() (can.Frame, error) {")
f.P("return m.Frame(), nil")
f.P("}")
f.P()
}
func UnmarshalFrame(f *File, m *descriptor.Message) {
f.P("// UnmarshalFrame decodes the message from a CAN frame.")
f.P("func (m *", messageStruct(m), ") UnmarshalFrame(f can.Frame) error {")
f.P("md := ", messageDescriptor(m))
// generate frame checks
id := func(isExtended bool) string {
if isExtended {
return "extended ID"
}
return "standard ID"
}
f.P("switch {")
f.P("case f.ID != md.ID:")
f.P(`return fmt.Errorf(`)
f.P(`"unmarshal `, m.Name, `: expects ID `, m.ID, ` (got %s with ID %d)", f.String(), f.ID,`)
f.P(`)`)
f.P("case f.Length != md.Length:")
f.P(`return fmt.Errorf(`)
f.P(`"unmarshal `, m.Name, `: expects length `, m.Length, ` (got %s with length %d)", f.String(), f.Length,`)
f.P(`)`)
f.P("case f.IsRemote:")
f.P(`return fmt.Errorf(`)
f.P(`"unmarshal `, m.Name, `: expects non-remote frame (got remote frame %s)", f.String(),`)
f.P(`)`)
f.P("case f.IsExtended != md.IsExtended:")
f.P(`return fmt.Errorf(`)
f.P(`"unmarshal `, m.Name, `: expects `, id(m.IsExtended), ` (got %s with `, id(!m.IsExtended), `)", f.String(),`)
f.P(`)`)
f.P("}")
if len(m.Signals) == 0 {
f.P("return nil")
f.P("}")
return
}
// generate non-multiplexed signal unmarshaling
for _, s := range m.Signals {
if s.IsMultiplexed {
continue
}
f.P("m.", signalField(s), " = ", signalType(m, s), "(md.", s.Name, ".Unmarshal", signalSuperType(s), "(f.Data))")
}
// generate multiplexed signal unmarshaling
if mux, ok := m.MultiplexerSignal(); ok {
for _, s := range m.Signals {
if !s.IsMultiplexed {
continue
}
f.P("if m.", signalField(mux), " == ", s.MultiplexerValue, " {")
f.P("m.", signalField(s), " = ", signalType(m, s), "(md.", s.Name, ".Unmarshal", signalSuperType(s), "(f.Data))")
f.P("}")
}
}
f.P("return nil")
f.P("}")
f.P()
}
func Node(f *File, d *descriptor.Database, n *descriptor.Node) {
rxMessages := collectRxMessages(d, n)
txMessages := collectTxMessages(d, n)
f.P("type ", nodeInterface(n), " interface {")
f.P("sync.Locker")
f.P("Tx() ", txGroupInterface(n))
f.P("Rx() ", rxGroupInterface(n))
f.P("Run(ctx context.Context) error")
f.P("}")
f.P()
f.P("type ", rxGroupInterface(n), " interface {")
f.P("http.Handler // for debugging")
for _, m := range rxMessages {
f.P(m.Name, "() ", rxMessageInterface(n, m))
}
f.P("}")
f.P()
f.P("type ", txGroupInterface(n), " interface {")
f.P("http.Handler // for debugging")
for _, m := range txMessages {
f.P(m.Name, "() ", txMessageInterface(n, m))
}
f.P("}")
f.P()
for _, m := range rxMessages {
f.P("type ", rxMessageInterface(n, m), " interface {")
f.P(messageReaderInterface(m))
f.P("ReceiveTime() time.Time")
f.P("SetAfterReceiveHook(h func(context.Context) error)")
f.P("}")
f.P()
}
for _, m := range txMessages {
f.P("type ", txMessageInterface(n, m), " interface {")
f.P(messageReaderInterface(m))
f.P(messageWriterInterface(m))
f.P("TransmitTime() time.Time")
f.P("Transmit(ctx context.Context) error")
f.P("SetBeforeTransmitHook(h func(context.Context) error)")
if m.SendType == descriptor.SendTypeCyclic {
f.P("// SetCyclicTransmissionEnabled enables/disables cyclic transmission.")
f.P("SetCyclicTransmissionEnabled(bool)")
f.P("// IsCyclicTransmissionEnabled returns whether cyclic transmission is enabled/disabled.")
f.P("IsCyclicTransmissionEnabled() bool")
}
f.P("}")
f.P()
}
f.P("type ", nodeStruct(n), " struct {")
f.P("sync.Mutex // protects all node state")
f.P("network string")
f.P("address string")
f.P("rx ", rxGroupStruct(n))
f.P("tx ", txGroupStruct(n))
f.P("}")
f.P()
f.P("var _ ", nodeInterface(n), " = &", nodeStruct(n), "{}")
f.P("var _ canrunner.Node = &", nodeStruct(n), "{}")
f.P()
f.P("func New", nodeInterface(n), "(network, address string) ", nodeInterface(n), " {")
f.P("n := &", nodeStruct(n), "{network: network, address: address}")
f.P("n.rx.parentMutex = &n.Mutex")
f.P("n.tx.parentMutex = &n.Mutex")
for _, m := range rxMessages {
f.P("n.rx.", messageField(m), ".init()")
f.P("n.rx.", messageField(m), ".Reset()")
}
for _, m := range txMessages {
f.P("n.tx.", messageField(m), ".init()")
f.P("n.tx.", messageField(m), ".Reset()")
}
f.P("return n")
f.P("}")
f.P()
f.P("func (n *", nodeStruct(n), ") Run(ctx context.Context) error {")
f.P("return canrunner.Run(ctx, n)")
f.P("}")
f.P()
f.P("func (n *", nodeStruct(n), ") Rx() ", rxGroupInterface(n), " {")
f.P("return &n.rx")
f.P("}")
f.P()
f.P("func (n *", nodeStruct(n), ") Tx() ", txGroupInterface(n), " {")
f.P("return &n.tx")
f.P("}")
f.P()
f.P("type ", rxGroupStruct(n), " struct {")
f.P("parentMutex *sync.Mutex")
for _, m := range rxMessages {
f.P(messageField(m), " ", rxMessageStruct(n, m))
}
f.P("}")
f.P()
f.P("var _ ", rxGroupInterface(n), " = &", rxGroupStruct(n), "{}")
f.P()
f.P("func (rx *", rxGroupStruct(n), ") ServeHTTP(w http.ResponseWriter, r *http.Request) {")
f.P("rx.parentMutex.Lock()")
f.P("defer rx.parentMutex.Unlock()")
f.P("candebug.ServeMessagesHTTP(w, r, []generated.Message{")
for _, m := range rxMessages {
f.P("&rx.", messageField(m), ",")
}
f.P("})")
f.P("}")
f.P()
for _, m := range rxMessages {
f.P("func (rx *", rxGroupStruct(n), ") ", m.Name, "() ", rxMessageInterface(n, m), " {")
f.P("return &rx.", messageField(m))
f.P("}")
f.P()
}
f.P()
f.P("type ", txGroupStruct(n), " struct {")
f.P("parentMutex *sync.Mutex")
for _, m := range txMessages {
f.P(messageField(m), " ", txMessageStruct(n, m))
}
f.P("}")
f.P()
f.P("var _ ", txGroupInterface(n), " = &", txGroupStruct(n), "{}")
f.P()
f.P("func (tx *", txGroupStruct(n), ") ServeHTTP(w http.ResponseWriter, r *http.Request) {")
f.P("tx.parentMutex.Lock()")
f.P("defer tx.parentMutex.Unlock()")
f.P("candebug.ServeMessagesHTTP(w, r, []generated.Message{")
for _, m := range txMessages {
f.P("&tx.", messageField(m), ",")
}
f.P("})")
f.P("}")
f.P()
for _, m := range txMessages {
f.P("func (tx *", txGroupStruct(n), ") ", m.Name, "() ", txMessageInterface(n, m), " {")
f.P("return &tx.", messageField(m))
f.P("}")
f.P()
}
f.P()
f.P("func (n *", nodeStruct(n), ") Descriptor() *descriptor.Node {")
f.P("return ", nodeDescriptor(n))
f.P("}")
f.P()
f.P("func (n *", nodeStruct(n), ") Connect() (net.Conn, error) {")
f.P("return socketcan.Dial(n.network, n.address)")
f.P("}")
f.P()
f.P("func (n *", nodeStruct(n), ") ReceivedMessage(id uint32) (canrunner.ReceivedMessage, bool) {")
f.P("switch id {")
for _, m := range rxMessages {
f.P("case ", m.ID, ":")
f.P("return &n.rx.", messageField(m), ", true")
}
f.P("default:")
f.P("return nil, false")
f.P("}")
f.P("}")
f.P()
f.P("func (n *", nodeStruct(n), ") TransmittedMessages() []canrunner.TransmittedMessage {")
f.P("return []canrunner.TransmittedMessage{")
for _, m := range txMessages {
f.P("&n.tx.", messageField(m), ",")
}
f.P("}")
f.P("}")
f.P()
for _, m := range rxMessages {
f.P("type ", rxMessageStruct(n, m), " struct {")
f.P(messageStruct(m))
f.P("receiveTime time.Time")
f.P("afterReceiveHook func(context.Context) error")
f.P("}")
f.P()
f.P("func (m *", rxMessageStruct(n, m), ") init() {")
f.P("m.afterReceiveHook = func(context.Context) error { return nil }")
f.P("}")
f.P()
f.P("func (m *", rxMessageStruct(n, m), ") SetAfterReceiveHook(h func(context.Context) error) {")
f.P("m.afterReceiveHook = h")
f.P("}")
f.P()
f.P("func (m *", rxMessageStruct(n, m), ") AfterReceiveHook() func(context.Context) error {")
f.P("return m.afterReceiveHook")
f.P("}")
f.P()
f.P("func (m *", rxMessageStruct(n, m), ") ReceiveTime() time.Time {")
f.P("return m.receiveTime")
f.P("}")
f.P()
f.P("func (m *", rxMessageStruct(n, m), ") SetReceiveTime(t time.Time) {")
f.P("m.receiveTime = t")
f.P("}")
f.P()
f.P("var _ canrunner.ReceivedMessage = &", rxMessageStruct(n, m), "{}")
f.P()
}
for _, m := range txMessages {
f.P("type ", txMessageStruct(n, m), " struct {")
f.P(messageStruct(m))
f.P("transmitTime time.Time")
f.P("beforeTransmitHook func(context.Context) error")
f.P("isCyclicEnabled bool")
f.P("wakeUpChan chan struct{}")
f.P("transmitEventChan chan struct{}")
f.P("}")
f.P()
f.P("var _ ", txMessageInterface(n, m), " = &", txMessageStruct(n, m), "{}")
f.P("var _ canrunner.TransmittedMessage = &", txMessageStruct(n, m), "{}")
f.P()
f.P("func (m *", txMessageStruct(n, m), ") init() {")
f.P("m.beforeTransmitHook = func(context.Context) error { return nil }")
f.P("m.wakeUpChan = make(chan struct{}, 1)")
f.P("m.transmitEventChan = make(chan struct{})")
f.P("}")
f.P()
f.P("func (m *", txMessageStruct(n, m), ") SetBeforeTransmitHook(h func(context.Context) error) {")
f.P("m.beforeTransmitHook = h")
f.P("}")
f.P()
f.P("func (m *", txMessageStruct(n, m), ") BeforeTransmitHook() func(context.Context) error {")
f.P("return m.beforeTransmitHook")
f.P("}")
f.P()
f.P("func (m *", txMessageStruct(n, m), ") TransmitTime() time.Time {")
f.P("return m.transmitTime")
f.P("}")
f.P()
f.P("func (m *", txMessageStruct(n, m), ") SetTransmitTime(t time.Time) {")
f.P("m.transmitTime = t")
f.P("}")
f.P()
f.P("func (m *", txMessageStruct(n, m), ") IsCyclicTransmissionEnabled() bool {")
f.P("return m.isCyclicEnabled")
f.P("}")
f.P()
f.P("func (m *", txMessageStruct(n, m), ") SetCyclicTransmissionEnabled(b bool) {")
f.P("m.isCyclicEnabled = b")
f.P("select {")
f.P("case m.wakeUpChan <-struct{}{}:")
f.P("default:")
f.P("}")
f.P("}")
f.P()
f.P("func (m *", txMessageStruct(n, m), ") WakeUpChan() <-chan struct{} {")
f.P("return m.wakeUpChan")
f.P("}")
f.P()
f.P("func (m *", txMessageStruct(n, m), ") Transmit(ctx context.Context) error {")
f.P("select {")
f.P("case m.transmitEventChan <- struct{}{}:")
f.P("return nil")
f.P("case <-ctx.Done():")
f.P(`return fmt.Errorf("event-triggered transmit of `, m.Name, `: %w", ctx.Err())`)
f.P("}")
f.P("}")
f.P()
f.P("func (m *", txMessageStruct(n, m), ") TransmitEventChan() <-chan struct{} {")
f.P("return m.transmitEventChan")
f.P("}")
f.P()
f.P("var _ canrunner.TransmittedMessage = &", txMessageStruct(n, m), "{}")
f.P()
}
}
func txGroupInterface(n *descriptor.Node) string {
return n.Name + "_Tx"
}
func txGroupStruct(n *descriptor.Node) string {
return "xxx_" + n.Name + "_Tx"
}
func rxGroupInterface(n *descriptor.Node) string {
return n.Name + "_Rx"
}
func rxGroupStruct(n *descriptor.Node) string {
return "xxx_" + n.Name + "_Rx"
}
func rxMessageInterface(n *descriptor.Node, m *descriptor.Message) string {
return n.Name + "_Rx_" + m.Name
}
func rxMessageStruct(n *descriptor.Node, m *descriptor.Message) string {
return "xxx_" + n.Name + "_Rx_" + m.Name
}
func txMessageInterface(n *descriptor.Node, m *descriptor.Message) string {
return n.Name + "_Tx_" + m.Name
}
func txMessageStruct(n *descriptor.Node, m *descriptor.Message) string {
return "xxx_" + n.Name + "_Tx_" + m.Name
}
func collectTxMessages(d *descriptor.Database, n *descriptor.Node) []*descriptor.Message {
tx := make([]*descriptor.Message, 0, len(d.Messages))
for _, m := range d.Messages {
if m == nil {
continue
}
if m.SenderNode == n.Name && m.SendType != descriptor.SendTypeNone {
tx = append(tx, m)
}
}
return tx
}
func collectRxMessages(d *descriptor.Database, n *descriptor.Node) []*descriptor.Message {
rx := make([]*descriptor.Message, 0, len(d.Messages))
Loop:
for _, m := range d.Messages {
if m == nil {
continue
}
for _, s := range m.Signals {
for _, node := range s.ReceiverNodes {
if node != n.Name {
continue
}
rx = append(rx, m)
continue Loop
}
}
}
return rx
}
func hasPhysicalRepresentation(s *descriptor.Signal) bool {
hasScale := s.Scale != 0 && s.Scale != 1
hasOffset := s.Offset != 0
hasRange := s.Min != 0 || s.Max != 0
var hasConstrainedRange bool
if s.IsSigned {
hasConstrainedRange = s.Min > float64(s.MinSigned()) || s.Max < float64(s.MaxSigned())
} else {
hasConstrainedRange = s.Min > 0 || s.Max < float64(s.MaxUnsigned())
}
return hasScale || hasOffset || hasRange && hasConstrainedRange
}
func hasCustomType(s *descriptor.Signal) bool {
return len(s.ValueDescriptions) > 0
}
func hasSendType(d *descriptor.Database) bool {
for _, m := range d.Messages {
if m == nil {
continue
}
if m.SendType != descriptor.SendTypeNone {
return true
}
}
return false
}
func signalType(m *descriptor.Message, s *descriptor.Signal) string {
if hasCustomType(s) {
return m.Name + "_" + s.Name
}
return signalPrimitiveType(s).String()
}
func signalPrimitiveType(s *descriptor.Signal) types.Type {
var t types.BasicKind
switch {
case s.Length == 1:
t = types.Bool
case s.Length <= 8 && s.IsSigned:
t = types.Int8
case s.Length <= 8:
t = types.Uint8
case s.Length <= 16 && s.IsSigned:
t = types.Int16
case s.Length <= 16:
t = types.Uint16
case s.Length <= 32 && s.IsSigned:
t = types.Int32
case s.Length <= 32:
t = types.Uint32
case s.Length <= 64 && s.IsSigned:
t = types.Int64
default:
t = types.Uint64
}
return types.Typ[t]
}
func signalPrimitiveSuperType(s *descriptor.Signal) types.Type {
var t types.BasicKind
switch {
case s.Length == 1:
t = types.Bool
case s.IsSigned:
t = types.Int64
default:
t = types.Uint64
}
return types.Typ[t]
}
func signalSuperType(s *descriptor.Signal) string {
switch {
case s.Length == 1:
return "Bool"
case s.IsSigned:
return "Signed"
default:
return "Unsigned"
}
}
func nodeInterface(n *descriptor.Node) string {
return n.Name
}
func nodeStruct(n *descriptor.Node) string {
return "xxx_" + n.Name
}
func messageStruct(m *descriptor.Message) string {
return m.Name
}
func messageReaderInterface(m *descriptor.Message) string {
return m.Name + "Reader"
}
func messageWriterInterface(m *descriptor.Message) string {
return m.Name + "Writer"
}
func messageField(m *descriptor.Message) string {
return "xxx_" + m.Name
}
func signalField(s *descriptor.Signal) string {
return "xxx_" + s.Name
}
func nodeDescriptor(n *descriptor.Node) string {
return "Nodes()." + n.Name
}
func messageDescriptor(m *descriptor.Message) string {
return "Messages()." + m.Name
}
func signalDescriptor(m *descriptor.Message, s *descriptor.Signal) string {
return messageDescriptor(m) + "." + s.Name
}

View File

@@ -0,0 +1,18 @@
package generate
import (
"os"
"testing"
"gotest.tools/v3/assert"
)
func runTestInDir(t *testing.T, dir string) func() {
// change working directory to project root
wd, err := os.Getwd()
assert.NilError(t, err)
assert.NilError(t, os.Chdir(dir))
return func() {
assert.NilError(t, os.Chdir(wd))
}
}

View File

@@ -0,0 +1,17 @@
package identifiers
import "unicode"
func IsCamelCase(s string) bool {
i := 0
for _, r := range s {
if unicode.IsDigit(r) {
continue
}
if i == 0 && !unicode.IsUpper(r) || !IsAlphaChar(r) && !IsNumChar(r) {
return false
}
i++
}
return true
}

View File

@@ -0,0 +1,18 @@
package identifiers
import (
"testing"
"gotest.tools/v3/assert"
)
func TestIsCamelCase(t *testing.T) {
assert.Assert(t, IsCamelCase("SOC"))
assert.Assert(t, IsCamelCase("Camel"))
assert.Assert(t, IsCamelCase("CamelCase"))
assert.Assert(t, IsCamelCase("111CamelCaseNr"))
assert.Assert(t, !IsCamelCase("camelCase"))
assert.Assert(t, !IsCamelCase("snake_case"))
assert.Assert(t, !IsCamelCase("kebab-case"))
assert.Assert(t, !IsCamelCase("111camelCaseNr"))
}

View File

@@ -0,0 +1,9 @@
package identifiers
func IsAlphaChar(r rune) bool {
return ('A' <= r && r <= 'Z') || ('a' <= r && r <= 'z')
}
func IsNumChar(r rune) bool {
return '0' <= r && r <= '9'
}

View File

@@ -0,0 +1,23 @@
package identifiers
import (
"testing"
"gotest.tools/v3/assert"
)
func TestIsAlphaChar(t *testing.T) {
assert.Assert(t, IsAlphaChar('b'))
assert.Assert(t, IsAlphaChar('C'))
assert.Assert(t, !IsAlphaChar('Ö'))
assert.Assert(t, !IsAlphaChar('_'))
}
func TestIsNumChar(t *testing.T) {
assert.Assert(t, IsNumChar('0'))
assert.Assert(t, IsNumChar('1'))
assert.Assert(t, IsNumChar('2'))
assert.Assert(t, IsNumChar('9'))
assert.Assert(t, !IsNumChar('/'))
assert.Assert(t, !IsNumChar('a'))
}

View File

@@ -0,0 +1,50 @@
// Package reinterpret provides primitives for reinterpreting arbitrary-length values as signed or unsigned.
package reinterpret
// AsSigned reinterprets the provided unsigned value as a signed value.
func AsSigned(unsigned uint64, bits uint8) int64 {
switch bits {
case 8:
return int64(int8(uint8(unsigned)))
case 16:
return int64(int16(uint16(unsigned)))
case 32:
return int64(int32(uint32(unsigned)))
case 64:
return int64(unsigned)
default:
// calculate bit mask for sign bit
signBitMask := uint64(1 << (bits - 1))
// check if sign bit is set
isNegative := unsigned&signBitMask > 0
if !isNegative {
// sign bit not set means we can reinterpret the value as-is
return int64(unsigned)
}
// calculate bit mask for extracting value bits (all bits except the sign bit)
valueBitMask := signBitMask - 1
// calculate two's complement of the value bits
value := ((^unsigned) & valueBitMask) + 1
// result is the negative value of the two's complement
return -1 * int64(value)
}
}
// AsUnsigned reinterprets the provided signed value as an unsigned value.
func AsUnsigned(signed int64, bits uint8) uint64 {
switch bits {
case 8:
return uint64(uint8(int8(signed)))
case 16:
return uint64(uint16(int16(signed)))
case 32:
return uint64(uint32(int32(signed)))
case 64:
return uint64(signed)
default:
// calculate bit mask for extracting relevant bits
valueBitMask := uint64(1<<bits) - 1
// extract relevant bits
return uint64(signed) & valueBitMask
}
}

View File

@@ -0,0 +1,68 @@
package reinterpret
import (
"fmt"
"testing"
"gotest.tools/v3/assert"
)
func TestReinterpretSign(t *testing.T) {
for _, tt := range []struct {
unsigned uint64
length uint8
signed int64
}{
// -1, byte aligned
{unsigned: 0xf, length: 4, signed: -1},
{unsigned: 0xff, length: 8, signed: -1},
{unsigned: 0xfff, length: 12, signed: -1},
{unsigned: 0xffff, length: 16, signed: -1},
{unsigned: 0xfffff, length: 20, signed: -1},
{unsigned: 0xffffff, length: 24, signed: -1},
{unsigned: 0xfffffff, length: 28, signed: -1},
{unsigned: 0xffffffff, length: 32, signed: -1},
{unsigned: 0xfffffffff, length: 36, signed: -1},
{unsigned: 0xffffffffff, length: 40, signed: -1},
{unsigned: 0xfffffffffff, length: 44, signed: -1},
{unsigned: 0xffffffffffff, length: 48, signed: -1},
{unsigned: 0xfffffffffffff, length: 52, signed: -1},
{unsigned: 0xffffffffffffff, length: 56, signed: -1},
{unsigned: 0xfffffffffffffff, length: 60, signed: -1},
{unsigned: 0xffffffffffffffff, length: 64, signed: -1},
// 3 bits
{unsigned: 0x0, length: 3, signed: 0},
{unsigned: 0x1, length: 3, signed: 1},
{unsigned: 0x2, length: 3, signed: 2},
{unsigned: 0x3, length: 3, signed: 3},
{unsigned: 0x4, length: 3, signed: -4},
{unsigned: 0x5, length: 3, signed: -3},
{unsigned: 0x6, length: 3, signed: -2},
{unsigned: 0x7, length: 3, signed: -1},
// 4 bits
{unsigned: 0x0, length: 4, signed: 0},
{unsigned: 0x1, length: 4, signed: 1},
{unsigned: 0x2, length: 4, signed: 2},
{unsigned: 0x3, length: 4, signed: 3},
{unsigned: 0x4, length: 4, signed: 4},
{unsigned: 0x5, length: 4, signed: 5},
{unsigned: 0x6, length: 4, signed: 6},
{unsigned: 0x7, length: 4, signed: 7},
{unsigned: 0x8, length: 4, signed: -8},
{unsigned: 0x9, length: 4, signed: -7},
{unsigned: 0xa, length: 4, signed: -6},
{unsigned: 0xb, length: 4, signed: -5},
{unsigned: 0xc, length: 4, signed: -4},
{unsigned: 0xd, length: 4, signed: -3},
{unsigned: 0xe, length: 4, signed: -2},
{unsigned: 0xf, length: 4, signed: -1},
} {
tt := tt
t.Run(fmt.Sprintf("%+v", tt), func(t *testing.T) {
assert.Equal(t, tt.signed, AsSigned(tt.unsigned, tt.length))
assert.Equal(t, tt.unsigned, AsUnsigned(tt.signed, tt.length))
assert.Equal(t, tt.signed, AsSigned(AsUnsigned(tt.signed, tt.length), tt.length))
assert.Equal(t, tt.unsigned, AsUnsigned(AsSigned(tt.unsigned, tt.length), tt.length))
})
}
}

17
pkg/can-go/message.go Normal file
View File

@@ -0,0 +1,17 @@
package can
// Message is anything that can marshal and unmarshal itself to/from a CAN frame.
type Message interface {
FrameMarshaler
FrameUnmarshaler
}
// FrameMarshaler can marshal itself to a CAN frame.
type FrameMarshaler interface {
MarshalFrame() (Frame, error)
}
// FrameUnmarshaler can unmarshal itself from a CAN frame.
type FrameUnmarshaler interface {
UnmarshalFrame(Frame) error
}

332
pkg/can-go/payload.go Normal file
View File

@@ -0,0 +1,332 @@
package can
import (
"encoding/hex"
"math/big"
)
// Data holds the data in a CAN frame.
//
// Layout
//
// Individual bits in the data are numbered according to the following scheme:
//
// BIT
// NUMBER
// +------+------+------+------+------+------+------+------+
// | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
// BYTE +------+------+------+------+------+------+------+------+
// NUMBER
// +-----+ +------+------+------+------+------+------+------+------+
// | 0 | | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
// +-----+ +------+------+------+------+------+------+------+------+
// | 1 | | 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 |
// +-----+ +------+------+------+------+------+------+------+------+
// | 2 | | 23 | 22 | 21 | 20 | 19 | 18 | 17 | 16 |
// +-----+ +------+------+------+------+------+------+------+------+
// | 3 | | 31 | 30 | 29 | 28 | 27 | 26 | 25 | 24 |
// +-----+ +------+------+------+------+------+------+------+------+
// | 4 | | 39 | 38 | 37 | 36 | 35 | 34 | 33 | 32 |
// +-----+ +------+------+------+------+------+------+------+------+
// | 5 | | 47 | 46 | 45 | 44 | 43 | 42 | 41 | 40 |
// +-----+ +------+------+------+------+------+------+------+------+
// | 6 | | 55 | 54 | 53 | 52 | 51 | 50 | 49 | 48 |
// +-----+ +------+------+------+------+------+------+------+------+
// | 7 | | 63 | 62 | 61 | 60 | 59 | 58 | 57 | 56 |
// +-----+ +------+------+------+------+------+------+------+------+
//
// Bit ranges can be manipulated using little-endian and big-endian bit ordering.
//
// Little-endian bit ranges
//
// Example range of length 32 starting at bit 29:
//
// BIT
// NUMBER
// +------+------+------+------+------+------+------+------+
// | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
// BYTE +------+------+------+------+------+------+------+------+
// NUMBER
// +-----+ +------+------+------+------+------+------+------+------+
// | 0 | | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
// +-----+ +------+------+------+------+------+------+------+------+
// | 1 | | 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 |
// +-----+ +------+------+------+------+------+------+------+------+
// | 2 | | 23 | 22 | 21 | 20 | 19 | 18 | 17 | 16 |
// +-----+ +------+------+------+------+------+------+------+------+
// | 3 | | <-------------LSb | 28 | 27 | 26 | 25 | 24 |
// +-----+ +------+------+------+------+------+------+------+------+
// | 4 | | <-------------------------------------------------- |
// +-----+ +------+------+------+------+------+------+------+------+
// | 5 | | <-------------------------------------------------- |
// +-----+ +------+------+------+------+------+------+------+------+
// | 6 | | <-------------------------------------------------- |
// +-----+ +------+------+------+------+------+------+------+------+
// | 7 | | 63 | 62 | 61 | <-MSb--------------------------- |
// +-----+ +------+------+------+------+------+------+------+------+
//
// Big-endian bit ranges
//
// Example range of length 32 starting at bit 29:
//
// BIT
// NUMBER
// +------+------+------+------+------+------+------+------+
// | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
// BYTE +------+------+------+------+------+------+------+------+
// NUMBER
// +-----+ +------+------+------+------+------+------+------+------+
// | 0 | | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
// +-----+ +------+------+------+------+------+------+------+------+
// | 1 | | 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 |
// +-----+ +------+------+------+------+------+------+------+------+
// | 2 | | 23 | 22 | 21 | 20 | 19 | 18 | 17 | 16 |
// +-----+ +------+------+------+------+------+------+------+------+
// | 3 | | 31 | 30 | <-MSb--------------------------------- |
// +-----+ +------+------+------+------+------+------+------+------+
// | 4 | | <-------------------------------------------------- |
// +-----+ +------+------+------+------+------+------+------+------+
// | 5 | | <-------------------------------------------------- |
// +-----+ +------+------+------+------+------+------+------+------+
// | 6 | | <-------------------------------------------------- |
// +-----+ +------+------+------+------+------+------+------+------+
// | 7 | | <------LSb | 61 | 60 | 59 | 58 | 57 | 56 |
// +-----+ +------+------+------+------+------+------+------+------+
type Payload struct {
// Binary data
Data []byte
// Packed little endian
PackedLittleEndian *big.Int
// Packed big endian
PackedBigEndian *big.Int
}
// Hex returns the hexadecimal representation of the byte array in a Payload.
func (p *Payload) Hex() string {
h := hex.EncodeToString(p.Data)
return h
}
// PayloadFromHex generates a Payload from a hexadecimal string.
func PayloadFromHex(hexString string) (Payload, error) {
b, err := hex.DecodeString(hexString)
var p Payload
if err != nil {
return p, err
}
p = Payload{Data: b}
return p, nil
}
// UnsignedBitsLittleEndian returns the little-endian bit range [start, start+length) as an unsigned value.
func (p *Payload) UnsignedBitsLittleEndian(start, length uint16) uint64 {
// pack bits into one continuous value
packed := p.PackLittleEndian()
// lsb index in the packed value is the start bit
lsbIndex := uint(start)
// shift away lower bits
shifted := packed.Rsh(packed, lsbIndex)
// mask away higher bits
masked := shifted.And(shifted, big.NewInt((1<<length)-1))
// done
return masked.Uint64()
}
// UnsignedBitsBigEndian returns the big-endian bit range [start, start+length) as an unsigned value.
func (p *Payload) UnsignedBitsBigEndian(start, length uint16) uint64 {
// pack bits into one continuous value
packed := p.PackBigEndian()
// calculate msb index in the packed value
msbIndex := p.invertEndian(start)
// calculate lsb index in the packed value
lsbIndex := uint(msbIndex - length + 1)
// shift away lower bits
shifted := packed.Rsh(packed, lsbIndex)
// mask away higher bits
masked := shifted.And(shifted, big.NewInt((1<<length)-1))
// done
return masked.Uint64()
}
// SignedBitsLittleEndian returns little-endian bit range [start, start+length) as a signed value.
func (p *Payload) SignedBitsLittleEndian(start, length uint16) int64 {
unsigned := p.UnsignedBitsLittleEndian(start, length)
return AsSigned(unsigned, length)
}
// SignedBitsBigEndian returns little-endian bit range [start, start+length) as a signed value.
func (p *Payload) SignedBitsBigEndian(start, length uint16) int64 {
unsigned := p.UnsignedBitsBigEndian(start, length)
return AsSigned(unsigned, length)
}
// TODO: Implement SetUnsignedBitsLittleEndian for Payload.
// SetUnsignedBitsLittleEndian sets the little-endian bit range [start, start+length) to the provided unsigned value.
// func (d *Data) SetUnsignedBitsLittleEndian(start, length uint8, value uint64) {
// // pack bits into one continuous value
// packed := d.PackLittleEndian()
// // lsb index in the packed value is the start bit
// lsbIndex := start
// // calculate bit mask for zeroing the bit range to set
// unsetMask := ^uint64(((1 << length) - 1) << lsbIndex)
// // calculate bit mask for setting the new value
// setMask := value << lsbIndex
// // calculate the new packed value
// newPacked := packed&unsetMask | setMask
// // unpack the new packed value into the data
// d.UnpackLittleEndian(newPacked)
// }
// TODO: Implement SetUnsignedBitsBigEndian for Payload.
// SetUnsignedBitsBigEndian sets the big-endian bit range [start, start+length) to the provided unsigned value.
// func (d *Data) SetUnsignedBitsBigEndian(start, length uint8, value uint64) {
// // pack bits into one continuous value
// packed := d.PackBigEndian()
// // calculate msb index in the packed value
// msbIndex := invertEndian(start)
// // calculate lsb index in the packed value
// lsbIndex := msbIndex - length + 1
// // calculate bit mask for zeroing the bit range to set
// unsetMask := ^uint64(((1 << length) - 1) << lsbIndex)
// // calculate bit mask for setting the new value
// setMask := value << lsbIndex
// // calculate the new packed value
// newPacked := packed&unsetMask | setMask
// // unpack the new packed value into the data
// d.UnpackBigEndian(newPacked)
// }
// TODO: Implement SetSignedBitsLittleEndian for Payload.
// SetSignedBitsLittleEndian sets the little-endian bit range [start, start+length) to the provided signed value.
// func (d *Data) SetSignedBitsLittleEndian(start, length uint8, value int64) {
// d.SetUnsignedBitsLittleEndian(start, length, reinterpret.AsUnsigned(value, length))
// }
// TODO: Implement SetSignedBitsBigEndian for Payload.
// SetSignedBitsBigEndian sets the big-endian bit range [start, start+length) to the provided signed value.
// func (d *Data) SetSignedBitsBigEndian(start, length uint8, value int64) {
// d.SetUnsignedBitsBigEndian(start, length, reinterpret.AsUnsigned(value, length))
// }
// Bit returns the value of the i:th bit in the data as a bool.
func (p *Payload) Bit(i uint16) bool {
if int(i) > 8*len(p.Data)-1 {
return false
}
// calculate which byte the bit belongs to
byteIndex := i / 8
// calculate bit mask for extracting the bit
bitMask := uint8(1 << (i % 8))
// mocks the bit
bit := p.Data[byteIndex]&bitMask > 0
// done
return bit
}
// SetBit sets the value of the i:th bit in the data.
func (p *Payload) SetBit(i uint16, value bool) {
if int(i) > 8*len(p.Data)-1 {
return
}
byteIndex := i / 8
bitIndex := i % 8
if value {
p.Data[byteIndex] |= uint8(1 << bitIndex)
} else {
p.Data[byteIndex] &= ^uint8(1 << bitIndex)
}
}
// PackLittleEndian packs the byte array into a continuous little endian big.Int.
func (p *Payload) PackLittleEndian() *big.Int {
if p.PackedLittleEndian == nil {
packed := new(big.Int).SetBytes(reverse(p.Data))
p.PackedLittleEndian = packed
}
return new(big.Int).Set(p.PackedLittleEndian)
}
// Reverse byte array for little endian signals.
func reverse(data []byte) []byte {
reversedArray := make([]byte, len(data))
for i, j := 0, len(data)-1; i < j; i, j = i+1, j-1 {
reversedArray[i], reversedArray[j] = data[j], data[i]
}
return reversedArray
}
// PackBigEndian packs the byte array into a continuous big endian big.Int.
func (p *Payload) PackBigEndian() *big.Int {
if p.PackedBigEndian == nil {
packed := new(big.Int).SetBytes(p.Data)
p.PackedBigEndian = packed
}
return new(big.Int).Set(p.PackedBigEndian)
}
// TODO: Implement UnpackLittleEndian for Payload.
// UnpackLittleEndian sets the value of d.Bytes by unpacking the provided value as sequential little-endian bits.
// func (d *Data) UnpackLittleEndian(packed uint64) {
// d[0] = uint8(packed >> (0 * 8))
// d[1] = uint8(packed >> (1 * 8))
// d[2] = uint8(packed >> (2 * 8))
// d[3] = uint8(packed >> (3 * 8))
// d[4] = uint8(packed >> (4 * 8))
// d[5] = uint8(packed >> (5 * 8))
// d[6] = uint8(packed >> (6 * 8))
// d[7] = uint8(packed >> (7 * 8))
// }
// TODO: Implement UnpackBigEndian for Payload.
// UnpackBigEndian sets the value of d.Bytes by unpacking the provided value as sequential big-endian bits.
// func (d *Data) UnpackBigEndian(packed uint64) {
// d[0] = uint8(packed >> (7 * 8))
// d[1] = uint8(packed >> (6 * 8))
// d[2] = uint8(packed >> (5 * 8))
// d[3] = uint8(packed >> (4 * 8))
// d[4] = uint8(packed >> (3 * 8))
// d[5] = uint8(packed >> (2 * 8))
// d[6] = uint8(packed >> (1 * 8))
// d[7] = uint8(packed >> (0 * 8))
// }
// invertEndian converts from big-endian to little-endian bit indexing and vice versa.
func (p *Payload) invertEndian(i uint16) uint16 {
row := i / 8
col := i % 8
oppositeRow := uint16(len(p.Data)) - row - 1
bitIndex := (oppositeRow * 8) + col
return bitIndex
}
// AsSigned reinterprets the provided unsigned value as a signed value.
func AsSigned(unsigned uint64, bits uint16) int64 {
switch bits {
case 8:
return int64(int8(uint8(unsigned)))
case 16:
return int64(int16(uint16(unsigned)))
case 32:
return int64(int32(uint32(unsigned)))
case 64:
return int64(unsigned)
default:
// calculate bit mask for sign bit
signBitMask := uint64(1 << (bits - 1))
// check if sign bit is set
isNegative := unsigned&signBitMask > 0
if !isNegative {
// sign bit not set means we can reinterpret the value as-is
return int64(unsigned)
}
// calculate bit mask for extracting value bits (all bits except the sign bit)
valueBitMask := signBitMask - 1
// calculate two's complement of the value bits
value := ((^unsigned) & valueBitMask) + 1
// result is the negative value of the two's complement
return -1 * int64(value)
}
}

View File

@@ -0,0 +1,99 @@
package can
import (
"fmt"
"testing"
)
type signals struct {
start uint16
length uint16
unsigned uint64
signed int64
}
func TestPackLittleEndian(t *testing.T) {
// 302064448
// 10010000000010010001101000000
data := []byte{0x40, 0x23, 0x01, 0x12}
payload := Payload{Data: data}
dataLittleEndian := payload.PackLittleEndian()
fmt.Println(dataLittleEndian)
fmt.Printf("%b\n", dataLittleEndian)
}
func TestPackBigEndian(t *testing.T) {
// 4621538819433299968
// 100000000100011000000010001001000000000000000000000000000000000
data := []byte{0x40, 0x23, 0x01, 0x12}
payload := Payload{Data: data}
dataBigEndian := payload.PackBigEndian()
fmt.Println(dataBigEndian)
fmt.Printf("%b\n", dataBigEndian)
}
func TestUnsignedLittleEndian(t *testing.T) {
// 18
data := []byte{0x40, 0x23, 0x01, 0x12}
payload := Payload{Data: data}
signal := signals{start: 24, length: 8, unsigned: 0x12, signed: 18}
fmt.Println(payload.UnsignedBitsLittleEndian(signal.start, signal.length))
}
func TestUnsignedBigEndian(t *testing.T) {
// 3219
data := []byte{0x3f, 0xf7, 0x0d, 0xc4, 0x0c, 0x93, 0xff, 0xff}
payload := Payload{Data: data}
signal := signals{start: 39, length: 16, unsigned: 0xc93, signed: 3219}
fmt.Println(payload.UnsignedBitsBigEndian(signal.start, signal.length))
}
func TestSignedLittleEndian(t *testing.T) {
// -1
data := []byte{0x80, 0x01}
payload := Payload{Data: data}
signal := signals{start: 7, length: 2, unsigned: 0x3, signed: -1}
fmt.Println(payload.SignedBitsLittleEndian(signal.start, signal.length))
}
func TestSignedBigEndian(t *testing.T) {
// -9
data := []byte{0x3f, 0xf7, 0x0d, 0xc4, 0x0c, 0x93, 0xff, 0xff}
payload := Payload{Data: data}
signal := signals{start: 3, length: 12, unsigned: 0xff7, signed: -9}
fmt.Println(payload.SignedBitsBigEndian(signal.start, signal.length))
}
func Benchmark4BytesPayload_PackLittleEndian(b *testing.B) {
data := []byte{0x40, 0x23, 0x01, 0x12}
payload := Payload{Data: data}
for i := 0; i < b.N; i++ {
_ = payload.PackLittleEndian()
}
}
func Benchmark4BytesPayload_PackBigEndian(b *testing.B) {
data := []byte{0x40, 0x23, 0x01, 0x12}
payload := Payload{Data: data}
for i := 0; i < b.N; i++ {
_ = payload.PackBigEndian()
}
}
func Benchmark4BytesPayload_UnsignedBitsLittleEndian(b *testing.B) {
data := []byte{0x40, 0x23, 0x01, 0x12}
payload := Payload{Data: data}
for i := 0; i < b.N; i++ {
_ = payload.UnsignedBitsLittleEndian(0, 16)
}
}
func Benchmark4BytesPayload_UnsignedBitsBigEndian(b *testing.B) {
data := []byte{0x40, 0x23, 0x01, 0x12}
payload := Payload{Data: data}
for i := 0; i < b.N; i++ {
_ = payload.UnsignedBitsBigEndian(0, 16)
}
}

View File

@@ -0,0 +1,98 @@
package candebug
import (
"bytes"
"net/http"
"path"
"strconv"
"time"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/cantext"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/descriptor"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/generated"
)
func ServeMessagesHTTP(w http.ResponseWriter, r *http.Request, msgs []generated.Message) {
base := path.Base(r.URL.Path)
// if path ends with a message name, serve only that message
for _, m := range msgs {
if m.Descriptor().Name == base {
serveMessagesHTTP(w, r, []generated.Message{m})
return
}
}
serveMessagesHTTP(w, r, msgs)
}
func serveMessagesHTTP(w http.ResponseWriter, _ *http.Request, msgs []generated.Message) {
var buf []byte
for i, m := range msgs {
buf = appendMessage(buf, m)
if i != len(msgs)-1 {
buf = append(buf, "\n\n\n"...)
}
}
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusOK)
_, _ = w.Write(buf)
}
func appendMessage(buf []byte, m generated.Message) []byte {
name := m.Descriptor().Name
sep := append(bytes.Repeat([]byte{'='}, len(name)), '\n')
buf = append(buf, name...)
buf = append(buf, '\n')
buf = append(buf, sep...)
buf = cantext.AppendID(buf, m.Descriptor())
buf = append(buf, '\n')
buf = cantext.AppendSender(buf, m.Descriptor())
buf = append(buf, '\n')
buf = cantext.AppendSendType(buf, m.Descriptor())
buf = append(buf, '\n')
if m.Descriptor().SendType == descriptor.SendTypeCyclic {
if enabler, ok := m.(interface{ IsCyclicTransmissionEnabled() bool }); ok {
buf = append(buf, "Enabled: "...)
buf = strconv.AppendBool(buf, enabler.IsCyclicTransmissionEnabled())
buf = append(buf, '\n')
}
buf = cantext.AppendCycleTime(buf, m.Descriptor())
buf = append(buf, '\n')
}
if m.Descriptor().DelayTime != 0 {
buf = cantext.AppendDelayTime(buf, m.Descriptor())
buf = append(buf, '\n')
}
buf = append(buf, sep...)
if timer, ok := m.(interface{ ReceiveTime() time.Time }); ok {
buf = append(buf, "Received: "...)
buf = appendTime(buf, timer.ReceiveTime())
buf = append(buf, '\n')
buf = append(buf, sep...)
}
if timer, ok := m.(interface{ TransmitTime() time.Time }); ok {
buf = append(buf, "Transmitted: "...)
buf = appendTime(buf, timer.TransmitTime())
buf = append(buf, '\n')
buf = append(buf, sep...)
}
f := m.Frame()
for i, s := range m.Descriptor().Signals {
buf = cantext.AppendSignal(buf, s, f.Data)
if i < len(m.Descriptor().Signals)-1 {
buf = append(buf, '\n')
}
}
return buf
}
func appendTime(buf []byte, t time.Time) []byte {
if t.IsZero() {
buf = append(buf, "never"...)
return buf
}
buf = append(buf, time.Since(t).String()...)
buf = append(buf, " ago ("...)
buf = t.AppendFormat(buf, "15:04:05.000000000")
buf = append(buf, ")"...)
return buf
}

View File

@@ -0,0 +1,142 @@
package candebug
import (
"io/ioutil"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
can "github.com/fiskerinc/cloud-services/pkg/can-go"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/descriptor"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/generated"
"gotest.tools/v3/assert"
)
func TestServeMessagesHTTP_Single(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ServeMessagesHTTP(w, r, []generated.Message{
&testMessage{
frame: can.Frame{ID: 100, Length: 1},
descriptor: newDriverHeartbeatDescriptor(),
},
})
}))
res, err := http.Get(ts.URL)
assert.NilError(t, err)
response, err := ioutil.ReadAll(res.Body)
assert.NilError(t, err)
assert.NilError(t, res.Body.Close())
const expected = `
DriverHeartbeat
===============
ID: 100 (0x64)
Sender: DRIVER
SendType: Cyclic
CycleTime: 100ms
DelayTime: 2s
===============
Command: 0 (0x0) None
`
assert.Equal(t, strings.TrimSpace(expected), string(response))
}
func TestServeMessagesHTTP_Multi(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ServeMessagesHTTP(w, r, []generated.Message{
&testMessage{
frame: can.Frame{ID: 100, Length: 1},
descriptor: newDriverHeartbeatDescriptor(),
},
&testMessage{
frame: can.Frame{ID: 100, Length: 1, Data: can.Data{0x01}},
descriptor: newDriverHeartbeatDescriptor(),
},
})
}))
res, err := http.Get(ts.URL)
assert.NilError(t, err)
response, err := ioutil.ReadAll(res.Body)
assert.NilError(t, err)
assert.NilError(t, res.Body.Close())
const expected = `
DriverHeartbeat
===============
ID: 100 (0x64)
Sender: DRIVER
SendType: Cyclic
CycleTime: 100ms
DelayTime: 2s
===============
Command: 0 (0x0) None
DriverHeartbeat
===============
ID: 100 (0x64)
Sender: DRIVER
SendType: Cyclic
CycleTime: 100ms
DelayTime: 2s
===============
Command: 1 (0x1) Sync
`
assert.Equal(t, strings.TrimSpace(expected), string(response))
}
type testMessage struct {
frame can.Frame
descriptor *descriptor.Message
}
func (m *testMessage) Frame() can.Frame {
return m.frame
}
func (m *testMessage) Descriptor() *descriptor.Message {
return m.descriptor
}
func (m *testMessage) MarshalFrame() (can.Frame, error) {
panic("should not be called")
}
func (testMessage) Reset() {
panic("should not be called")
}
func (testMessage) String() string {
panic("should not be called")
}
func (testMessage) UnmarshalFrame(can.Frame) error {
panic("should not be called")
}
func newDriverHeartbeatDescriptor() *descriptor.Message {
return &descriptor.Message{
Name: "DriverHeartbeat",
SenderNode: "DRIVER",
ID: 100,
Length: 1,
Description: "Sync message used to synchronize the controllers",
SendType: descriptor.SendTypeCyclic,
CycleTime: 100 * time.Millisecond,
DelayTime: 2 * time.Second,
Signals: []*descriptor.Signal{
{
Name: "Command",
Start: 0,
Length: 8,
Scale: 1,
ValueDescriptions: []*descriptor.ValueDescription{
{Value: 0, Description: "None"},
{Value: 1, Description: "Sync"},
{Value: 2, Description: "Reboot"},
},
ReceiverNodes: []string{"SENSOR", "MOTOR"},
},
},
}
}

View File

@@ -0,0 +1,105 @@
package canjson
import (
"encoding/json"
"fmt"
"strconv"
can "github.com/fiskerinc/cloud-services/pkg/can-go"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/descriptor"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/generated"
)
// preAllocatedBytesPerSignal is an estimate of how many bytes each signal needs.
const preAllocatedBytesPerSignal = 40
// Marshal a CAN message to JSON.
func Marshal(m generated.Message) ([]byte, error) {
f := m.Frame()
bytes := make([]byte, 0, len(m.Descriptor().Signals)*preAllocatedBytesPerSignal)
bytes = append(bytes, '{')
for i, s := range m.Descriptor().Signals {
s := s
bytes = append(bytes, '"')
bytes = append(bytes, s.Name...)
bytes = append(bytes, `":`...)
sig := &signal{}
sig.set(s, f)
jsonSig, err := json.Marshal(sig)
if err != nil {
return nil, fmt.Errorf("marshal json: %w", err)
}
bytes = append(bytes, jsonSig...)
if i < len(m.Descriptor().Signals)-1 {
bytes = append(bytes, ',')
}
}
bytes = append(bytes, '}')
return bytes, nil
}
type signal struct {
Raw json.Number
Physical json.Number
Unit string `json:",omitempty"`
Description string `json:",omitempty"`
}
func (s *signal) set(desc *descriptor.Signal, f can.Frame) {
switch {
case desc.Length == 1: // bool
s.setBoolValue(desc.UnmarshalBool(f.Data), desc)
case desc.IsSigned: // signed
s.setSignedValue(desc.UnmarshalSigned(f.Data), desc)
default: // unsigned
s.setUnsignedValue(desc.UnmarshalUnsigned(f.Data), desc)
}
}
func (s *signal) setUnsignedValue(value uint64, desc *descriptor.Signal) {
s.Raw = uintToJSON(value)
s.Physical = floatToJSON(desc.ToPhysical(float64(value)))
s.Unit = desc.Unit
if value, ok := desc.ValueDescription(int(value)); ok {
s.Description = value
}
}
func (s *signal) setSignedValue(value int64, desc *descriptor.Signal) {
s.Raw = intToJSON(value)
s.Physical = floatToJSON(desc.ToPhysical(float64(value)))
s.Unit = desc.Unit
if value, ok := desc.ValueDescription(int(value)); ok {
s.Description = value
}
}
func (s *signal) setBoolValue(value bool, desc *descriptor.Signal) {
if value {
s.Raw = "1"
s.Physical = floatToJSON(desc.ToPhysical(1))
} else {
s.Raw = "0"
s.Physical = floatToJSON(desc.ToPhysical(0))
}
s.Unit = desc.Unit
intValue := 0
if value {
intValue = 1
}
if value, ok := desc.ValueDescription(intValue); ok {
s.Description = value
}
}
func floatToJSON(f float64) json.Number {
return json.Number(strconv.FormatFloat(f, 'f', -1, 64))
}
func intToJSON(i int64) json.Number {
return json.Number(strconv.Itoa(int(i)))
}
func uintToJSON(i uint64) json.Number {
return json.Number(strconv.Itoa(int(i)))
}

View File

@@ -0,0 +1,19 @@
package canjson
import (
"strings"
"testing"
examplecan "github.com/fiskerinc/cloud-services/pkg/can-go/testdata/gen/go/example"
"gotest.tools/v3/assert"
)
func TestMarshal(t *testing.T) {
driverHeartbeat := examplecan.NewDriverHeartbeat().SetCommand(examplecan.DriverHeartbeat_Command_Reboot)
js, err := Marshal(driverHeartbeat)
assert.NilError(t, err)
expected := strings.TrimSpace(`
{"Command":{"Raw":2,"Physical":2,"Description":"Reboot"}}
`)
assert.Equal(t, expected, string(js))
}

View File

@@ -0,0 +1,204 @@
package canrunner
import (
"context"
"fmt"
"net"
"strings"
"sync"
"time"
can "github.com/fiskerinc/cloud-services/pkg/can-go"
"github.com/fiskerinc/cloud-services/pkg/can-go/internal/clock"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/descriptor"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/generated"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/socketcan"
"golang.org/x/sync/errgroup"
)
// defaultSendTimeout is the send timeout used for messages without a cycle time.
const defaultSendTimeout = time.Second
// Node is an interface for a CAN node to be run by the runner.
type Node interface {
sync.Locker
Connect() (net.Conn, error)
Descriptor() *descriptor.Node
TransmittedMessages() []TransmittedMessage
ReceivedMessage(id uint32) (ReceivedMessage, bool)
}
// TransmittedMessage is an interface for a message to be transmitted by the runner.
type TransmittedMessage interface {
generated.Message
// SetTransmitTime sets the time the message was last transmitted.
SetTransmitTime(time.Time)
// IsCyclicTransmissionEnabled returns true when cyclic transmission is enabled.
IsCyclicTransmissionEnabled() bool
// WakeUpChan returns a channel for waking up and checking if cyclic transmission is enabled.
WakeUpChan() <-chan struct{}
// TransmitEventChan returns channel for event-based transmission of the message.
TransmitEventChan() <-chan struct{}
// BeforeTransmitHook returns a function to be called before the message is transmitted.
//
// If the hook returns an error, the transmitter halt.
BeforeTransmitHook() func(context.Context) error
}
// ReceivedMessage is an interface for a message to be received by the runner.
type ReceivedMessage interface {
generated.Message
// SetReceiveTime sets the time the message was last received.
SetReceiveTime(time.Time)
// AfterReceiveHook returns a function to be called after the message has been received.
//
// If the hook returns an error, the receiver will halt.
AfterReceiveHook() func(context.Context) error
}
// FrameTransmitter is an interface for the the CAN frame transmitter used by the runner.
type FrameTransmitter interface {
TransmitFrame(context.Context, can.Frame) error
}
// FrameReceiver is an interface for the CAN frame receiver used by the runner.
type FrameReceiver interface {
Receive() bool
Frame() can.Frame
Err() error
}
func Run(ctx context.Context, n Node) error {
conn, err := n.Connect()
if err != nil {
return fmt.Errorf("run %s node: %w", n.Descriptor().Name, err)
}
g, ctx := errgroup.WithContext(ctx)
g.Go(func() error {
<-ctx.Done()
return conn.Close()
})
g.Go(func() error {
rx := socketcan.NewReceiver(conn)
return RunMessageReceiver(ctx, rx, n, clock.System())
})
for _, m := range n.TransmittedMessages() {
m := m
g.Go(func() error {
tx := socketcan.NewTransmitter(conn)
return RunMessageTransmitter(ctx, tx, n, m, clock.System())
})
}
if err := g.Wait(); err != nil {
if strings.Contains(err.Error(), "closed") {
return nil
}
return fmt.Errorf("run %s node: %w", n.Descriptor().Name, err)
}
return nil
}
func RunMessageReceiver(ctx context.Context, rx FrameReceiver, n Node, c clock.Clock) error {
for rx.Receive() {
f := rx.Frame()
m, ok := n.ReceivedMessage(f.ID)
if !ok {
continue
}
n.Lock()
hook := m.AfterReceiveHook()
m.SetReceiveTime(c.Now())
err := m.UnmarshalFrame(f)
n.Unlock()
if err != nil {
return fmt.Errorf("receiver: %w", err)
}
if err := hook(ctx); err != nil {
return fmt.Errorf("receiver: %w", err)
}
}
if err := rx.Err(); err != nil {
return fmt.Errorf("receiver: %w", err)
}
return nil
}
func RunMessageTransmitter(
ctx context.Context,
tx FrameTransmitter,
l sync.Locker,
m TransmittedMessage,
c clock.Clock,
) error {
sendTimeout := m.Descriptor().CycleTime
if sendTimeout == 0 {
sendTimeout = defaultSendTimeout
}
var cyclicTransmissionTicker *time.Ticker
var cyclicTransmissionTickChan <-chan time.Time
enableCyclicTransmission := func() {
isCyclic := m.Descriptor().SendType == descriptor.SendTypeCyclic
hasCycleTime := m.Descriptor().CycleTime > 0
if !isCyclic || !hasCycleTime || cyclicTransmissionTicker != nil {
return
}
cyclicTransmissionTicker = time.NewTicker(m.Descriptor().CycleTime)
cyclicTransmissionTickChan = cyclicTransmissionTicker.C
}
disableCyclicTransmission := func() {
if cyclicTransmissionTicker == nil {
return
}
cyclicTransmissionTicker.Stop()
cyclicTransmissionTicker = nil
}
setCyclicTransmission := func() {
l.Lock()
isCyclicTransmissionEnabled := m.IsCyclicTransmissionEnabled()
l.Unlock()
if isCyclicTransmissionEnabled {
enableCyclicTransmission()
} else {
disableCyclicTransmission()
}
}
transmit := func() error {
l.Lock()
hook := m.BeforeTransmitHook()
m.SetTransmitTime(c.Now())
l.Unlock()
if err := hook(ctx); err != nil {
return fmt.Errorf("%s transmitter: %w", m.Descriptor().Name, err)
}
l.Lock()
f := m.Frame()
l.Unlock()
ctx, cancel := context.WithTimeout(ctx, sendTimeout)
err := tx.TransmitFrame(ctx, f)
cancel()
if err != nil {
return fmt.Errorf("%s transmitter: %w", m.Descriptor().Name, err)
}
return nil
}
ctxDone := ctx.Done()
transmitEventChan := m.TransmitEventChan()
setCyclicTransmission()
wakeUpChan := m.WakeUpChan()
for {
select {
case <-ctxDone:
return nil
case <-wakeUpChan:
setCyclicTransmission()
case <-transmitEventChan:
if err := transmit(); err != nil {
return err
}
case <-cyclicTransmissionTickChan:
if err := transmit(); err != nil {
return err
}
}
}
}

View File

@@ -0,0 +1,125 @@
package canrunner_test
import (
"context"
"errors"
"os"
"testing"
"time"
can "github.com/fiskerinc/cloud-services/pkg/can-go"
"github.com/fiskerinc/cloud-services/pkg/can-go/internal/gen/mock/mockcanrunner"
"github.com/fiskerinc/cloud-services/pkg/can-go/internal/gen/mock/mockclock"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/canrunner"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/descriptor"
"github.com/golang/mock/gomock"
"golang.org/x/sync/errgroup"
"gotest.tools/v3/assert"
)
func TestRunMessageReceiver_NoMessages(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
rx := mockcanrunner.NewMockFrameReceiver(ctrl)
node := mockcanrunner.NewMockNode(ctrl)
clock := mockclock.NewMockClock(ctrl)
ctx := context.Background()
// when the first receive fails
rx.EXPECT().Receive().Return(false)
rx.EXPECT().Err().Return(os.ErrClosed)
// then an error is returned
assert.Assert(t, errors.Is(canrunner.RunMessageReceiver(ctx, rx, node, clock), os.ErrClosed))
}
func TestRunMessageReceiver_ReceiveMessage(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
rx := mockcanrunner.NewMockFrameReceiver(ctrl)
node := mockcanrunner.NewMockNode(ctrl)
clock := mockclock.NewMockClock(ctrl)
msg := mockcanrunner.NewMockReceivedMessage(ctrl)
ctx := context.Background()
// when the first receive succeeds
frame := can.Frame{ID: 42}
rx.EXPECT().Receive().Return(true)
rx.EXPECT().Frame().Return(frame)
// then the receiver should do a message lookup
node.EXPECT().ReceivedMessage(frame.ID).Return(msg, true)
// and the node should be locked
node.EXPECT().Lock()
// and the message should be queried for a hook with the same context
afterReceiveHook := func(c context.Context) error {
assert.DeepEqual(t, ctx, c)
return nil
}
msg.EXPECT().AfterReceiveHook().Return(afterReceiveHook)
// and the receive time should be set
now := time.Unix(0, 1)
clock.EXPECT().Now().Return(now)
msg.EXPECT().SetReceiveTime(now)
// and the message should be called to unmarshal the frame
msg.EXPECT().UnmarshalFrame(frame)
// and the node should be unlocked
node.EXPECT().Unlock()
// when the next receive fails
rx.EXPECT().Receive().Return(false)
rx.EXPECT().Err().Return(nil)
// then the receiver should return
assert.NilError(t, canrunner.RunMessageReceiver(ctx, rx, node, clock))
}
func TestRunMessageTransmitter_TransmitEventMessage(t *testing.T) {
t.Skip() // TODO: fix deadlock flakynes.
ctrl := gomock.NewController(t)
defer ctrl.Finish()
tx := mockcanrunner.NewMockFrameTransmitter(ctrl)
node := mockcanrunner.NewMockNode(ctrl)
msg := mockcanrunner.NewMockTransmittedMessage(ctrl)
clock := mockclock.NewMockClock(ctrl)
desc := &descriptor.Message{
Name: "TestMessage",
SendType: descriptor.SendTypeEvent,
}
transmitEventChan := make(chan struct{})
wakeUpChan := make(chan struct{})
ctx := context.Background()
msg.EXPECT().Descriptor().AnyTimes().Return(desc)
msg.EXPECT().TransmitEventChan().Return(transmitEventChan)
msg.EXPECT().WakeUpChan().Return(wakeUpChan)
// given a running transmitter
ctx, cancel := context.WithCancel(context.Background())
var g errgroup.Group
g.Go(func() error {
return canrunner.RunMessageTransmitter(ctx, tx, node, msg, clock)
})
// then message should be queried for if it has cyclic transmission enabled
node.EXPECT().Lock()
msg.EXPECT().IsCyclicTransmissionEnabled()
node.EXPECT().Unlock()
// then the node should be locked
node.EXPECT().Lock()
// and the time should be queried
now := time.Unix(0, 1)
clock.EXPECT().Now().Return(now)
// and the transmit hook should be queried with the same context
hook := func(c context.Context) error {
assert.Equal(t, ctx, c)
return nil
}
msg.EXPECT().BeforeTransmitHook().Return(hook)
// and the message should be marshaled to a CAN frame
frame := can.Frame{ID: 42}
// and the transmit time should be set
msg.EXPECT().SetTransmitTime(now)
// and the node should be unlocked
node.EXPECT().Unlock()
node.EXPECT().Lock()
msg.EXPECT().Frame().Return(frame)
node.EXPECT().Unlock()
// and the CAN frame should be transmitted
tx.EXPECT().TransmitFrame(gomock.Any(), frame)
// when the transmitter receives a transmit event
transmitEventChan <- struct{}{}
cancel()
assert.NilError(t, g.Wait())
}

View File

@@ -0,0 +1,127 @@
package cantext
import (
"strconv"
can "github.com/fiskerinc/cloud-services/pkg/can-go"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/descriptor"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/generated"
)
// preAllocatedBytesPerSignal is an estimate of how many bytes each signal needs.
const preAllocatedBytesPerSignal = 40
func MessageString(m generated.Message) string {
return string(MarshalCompact(m))
}
func MarshalCompact(m generated.Message) []byte {
f := m.Frame()
buf := make([]byte, 0, len(m.Descriptor().Signals)*preAllocatedBytesPerSignal)
buf = append(buf, "{"...)
for i, s := range m.Descriptor().Signals {
buf = AppendSignalCompact(buf, s, f.Data)
if i != len(m.Descriptor().Signals)-1 {
buf = append(buf, ", "...)
}
}
buf = append(buf, "}"...)
return buf
}
func Marshal(m generated.Message) []byte {
f := m.Frame()
// allocate space for one "extra" signal to account for message header
buf := make([]byte, 0, (len(m.Descriptor().Signals)+1)*preAllocatedBytesPerSignal)
buf = append(buf, m.Descriptor().Name...)
for _, s := range m.Descriptor().Signals {
buf = append(buf, "\n\t"...)
buf = AppendSignal(buf, s, f.Data)
}
return buf
}
func AppendSignal(buf []byte, s *descriptor.Signal, d can.Data) []byte {
buf = append(buf, s.Name...)
buf = append(buf, ": "...)
switch {
case s.Length == 1: // bool
val := s.UnmarshalBool(d)
buf = strconv.AppendBool(buf, val)
case s.IsSigned: // signed
buf = strconv.AppendFloat(buf, s.UnmarshalPhysical(d), 'g', -1, 64)
buf = append(buf, s.Unit...)
buf = append(buf, " ("...)
buf = append(buf, "0x"...)
buf = strconv.AppendUint(buf, uint64(s.UnmarshalSigned(d)), 16)
buf = append(buf, ')')
default: // unsigned
buf = strconv.AppendFloat(buf, s.UnmarshalPhysical(d), 'g', -1, 64)
buf = append(buf, s.Unit...)
buf = append(buf, " ("...)
buf = append(buf, "0x"...)
buf = strconv.AppendUint(buf, s.UnmarshalUnsigned(d), 16)
buf = append(buf, ")"...)
}
if vd, ok := s.UnmarshalValueDescription(d); ok {
buf = append(buf, ' ')
buf = append(buf, vd...)
}
return buf
}
func AppendSignalCompact(buf []byte, s *descriptor.Signal, d can.Data) []byte {
buf = append(buf, s.Name...)
buf = append(buf, ": "...)
valueDescription, hasValueDescription := s.UnmarshalValueDescription(d)
switch {
case hasValueDescription:
buf = append(buf, valueDescription...)
case s.Length == 1: // bool
val := s.UnmarshalBool(d)
buf = strconv.AppendBool(buf, val)
case s.IsSigned: // signed
buf = strconv.AppendFloat(buf, s.UnmarshalPhysical(d), 'g', -1, 64)
buf = append(buf, s.Unit...)
default: // unsigned
buf = strconv.AppendFloat(buf, s.UnmarshalPhysical(d), 'g', -1, 64)
buf = append(buf, s.Unit...)
}
return buf
}
func AppendID(buf []byte, m *descriptor.Message) []byte {
buf = append(buf, "ID: "...)
buf = strconv.AppendUint(buf, uint64(m.ID), 10)
buf = append(buf, " (0x"...)
buf = strconv.AppendUint(buf, uint64(m.ID), 16)
buf = append(buf, ")"...)
return buf
}
func AppendSender(buf []byte, m *descriptor.Message) []byte {
return appendAttributeString(buf, "Sender", m.SenderNode)
}
func AppendSendType(buf []byte, m *descriptor.Message) []byte {
return appendAttributeString(buf, "SendType", m.SendType.String())
}
func AppendCycleTime(buf []byte, m *descriptor.Message) []byte {
return appendAttributeString(buf, "CycleTime", m.CycleTime.String())
}
func AppendDelayTime(buf []byte, m *descriptor.Message) []byte {
return appendAttributeString(buf, "DelayTime", m.DelayTime.String())
}
func AppendFrame(buf []byte, f can.Frame) []byte {
return appendAttributeString(buf, "Frame", f.String())
}
func appendAttributeString(buf []byte, name string, s string) []byte {
buf = append(buf, name...)
buf = append(buf, ": "...)
buf = append(buf, s...)
return buf
}

View File

@@ -0,0 +1,234 @@
package cantext
import (
"strings"
"testing"
"time"
can "github.com/fiskerinc/cloud-services/pkg/can-go"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/descriptor"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/generated"
"gotest.tools/v3/assert"
)
func TestMarshal(t *testing.T) {
for _, tt := range []struct {
name string
msg generated.Message
expected string
expectedCompact string
}{
{
name: "with enum",
msg: &testMessage{
frame: can.Frame{ID: 100, Length: 1, Data: can.Data{2}},
descriptor: newDriverHeartbeatDescriptor(),
},
expected: `
DriverHeartbeat
Command: 2 (0x2) Reboot
`,
expectedCompact: `{Command: Reboot}`,
},
{
name: "with unit",
msg: &testMessage{
frame: can.Frame{ID: 100, Length: 3, Data: can.Data{1, 0x7b}},
descriptor: newMotorStatusDescriptor(),
},
expected: `
MotorStatus
WheelError: true
SpeedKph: 0.123km/h (0x7b)
`,
expectedCompact: `{WheelError: true, SpeedKph: 0.123km/h}`,
},
} {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Run("standard", func(t *testing.T) {
txt := Marshal(tt.msg)
assert.Equal(t, strings.TrimSpace(tt.expected), string(txt))
})
t.Run("compact", func(t *testing.T) {
txt := MarshalCompact(tt.msg)
assert.Equal(t, strings.TrimSpace(tt.expectedCompact), string(txt))
})
})
}
}
func TestAppendID(t *testing.T) {
const expected = "ID: 100 (0x64)"
actual := string(AppendID([]byte{}, newDriverHeartbeatDescriptor()))
assert.Equal(t, expected, actual)
}
func TestAppendSender(t *testing.T) {
const expected = "Sender: DRIVER"
actual := string(AppendSender([]byte{}, newDriverHeartbeatDescriptor()))
assert.Equal(t, expected, actual)
}
func TestAppendSendType(t *testing.T) {
const expected = "SendType: Cyclic"
actual := string(AppendSendType([]byte{}, newDriverHeartbeatDescriptor()))
assert.Equal(t, expected, actual)
}
func TestAppendCycleTime(t *testing.T) {
const expected = "CycleTime: 100ms"
actual := string(AppendCycleTime([]byte{}, newDriverHeartbeatDescriptor()))
assert.Equal(t, expected, actual)
}
func TestAppendDelayTime(t *testing.T) {
const expected = "DelayTime: 2s"
actual := string(AppendDelayTime([]byte{}, newDriverHeartbeatDescriptor()))
assert.Equal(t, expected, actual)
}
func TestAppendFrame(t *testing.T) {
const expected = "Frame: 042#123456"
actual := string(AppendFrame([]byte{}, can.Frame{ID: 0x42, Length: 3, Data: can.Data{0x12, 0x34, 0x56}}))
assert.Equal(t, expected, actual)
}
func newDriverHeartbeatDescriptor() *descriptor.Message {
return &descriptor.Message{
Name: (string)("DriverHeartbeat"),
ID: (uint32)(100),
IsExtended: (bool)(false),
Length: (uint16)(1),
Description: (string)("Sync message used to synchronize the controllers"),
SendType: descriptor.SendTypeCyclic,
CycleTime: 100 * time.Millisecond,
DelayTime: 2 * time.Second,
Signals: []*descriptor.Signal{
{
Name: (string)("Command"),
Start: (uint16)(0),
Length: (uint16)(8),
IsBigEndian: (bool)(false),
IsSigned: (bool)(false),
IsMultiplexer: (bool)(false),
IsMultiplexed: (bool)(false),
MultiplexerValue: (uint)(0),
Offset: (float64)(0),
Scale: (float64)(1),
Min: (float64)(0),
Max: (float64)(0),
Unit: (string)(""),
Description: (string)(""),
ValueDescriptions: []*descriptor.ValueDescription{
{
Value: (int)(0),
Description: (string)("None"),
},
{
Value: (int)(1),
Description: (string)("Sync"),
},
{
Value: (int)(2),
Description: (string)("Reboot"),
},
},
ReceiverNodes: []string{
(string)("SENSOR"),
(string)("MOTOR"),
},
DefaultValue: (int)(0),
},
},
SenderNode: (string)("DRIVER"),
}
}
func newMotorStatusDescriptor() *descriptor.Message {
return &descriptor.Message{
Name: (string)("MotorStatus"),
ID: (uint32)(400),
IsExtended: (bool)(false),
Length: (uint16)(3),
Description: (string)(""),
Signals: []*descriptor.Signal{
{
Name: (string)("WheelError"),
Start: (uint16)(0),
Length: (uint16)(1),
IsBigEndian: (bool)(false),
IsSigned: (bool)(false),
IsMultiplexer: (bool)(false),
IsMultiplexed: (bool)(false),
MultiplexerValue: (uint)(0),
Offset: (float64)(0),
Scale: (float64)(1),
Min: (float64)(0),
Max: (float64)(0),
Unit: (string)(""),
Description: (string)(""),
ValueDescriptions: ([]*descriptor.ValueDescription)(nil),
ReceiverNodes: []string{
(string)("DRIVER"),
(string)("IO"),
},
DefaultValue: (int)(0),
},
{
Name: (string)("SpeedKph"),
Start: (uint16)(8),
Length: (uint16)(16),
IsBigEndian: (bool)(false),
IsSigned: (bool)(false),
IsMultiplexer: (bool)(false),
IsMultiplexed: (bool)(false),
MultiplexerValue: (uint)(0),
Offset: (float64)(0),
Scale: (float64)(0.001),
Min: (float64)(0),
Max: (float64)(0),
Unit: (string)("km/h"),
Description: (string)(""),
ValueDescriptions: ([]*descriptor.ValueDescription)(nil),
ReceiverNodes: []string{
(string)("DRIVER"),
(string)("IO"),
},
DefaultValue: (int)(0),
},
},
SenderNode: (string)("MOTOR"),
CycleTime: (time.Duration)(100000000),
DelayTime: (time.Duration)(0),
}
}
type testMessage struct {
frame can.Frame
descriptor *descriptor.Message
}
func (m *testMessage) Frame() can.Frame {
return m.frame
}
func (m *testMessage) Descriptor() *descriptor.Message {
return m.descriptor
}
func (m *testMessage) MarshalFrame() (can.Frame, error) {
panic("should not be called")
}
func (testMessage) Reset() {
panic("should not be called")
}
func (testMessage) String() string {
panic("should not be called")
}
func (testMessage) UnmarshalFrame(can.Frame) error {
panic("should not be called")
}

View File

@@ -0,0 +1,26 @@
package dbc
import "fmt"
// AccessType represents the access type of an environment variable.
type AccessType string
const (
AccessTypeUnrestricted AccessType = "DUMMY_NODE_VECTOR0"
AccessTypeRead AccessType = "DUMMY_NODE_VECTOR1"
AccessTypeWrite AccessType = "DUMMY_NODE_VECTOR2"
AccessTypeReadWrite AccessType = "DUMMY_NODE_VECTOR3"
)
// Validate returns an error for invalid access types.
func (a AccessType) Validate() error {
switch a {
case AccessTypeUnrestricted:
case AccessTypeRead:
case AccessTypeWrite:
case AccessTypeReadWrite:
default:
return fmt.Errorf("invalid access type: %v", a)
}
return nil
}

View File

@@ -0,0 +1,22 @@
package dbc
import (
"testing"
"gotest.tools/v3/assert"
)
func TestAccessType_Validate(t *testing.T) {
for _, tt := range []AccessType{
AccessTypeUnrestricted,
AccessTypeRead,
AccessTypeWrite,
AccessTypeReadWrite,
} {
assert.NilError(t, tt.Validate())
}
}
func TestAccessType_Validate_Error(t *testing.T) {
assert.ErrorContains(t, AccessType("foo").Validate(), "invalid access type")
}

View File

@@ -0,0 +1,53 @@
package analysis
import (
"fmt"
"strings"
"text/scanner"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc"
)
// An Analyzer describes an analysis function and its options.
type Analyzer struct {
// Name of the analyzer.
Name string
// Doc is the documentation for the analyzer.
Doc string
// Run the analyzer.
Run func(*Pass) error
}
// Title is the part before the first "\n\n" of the documentation.
func (a *Analyzer) Title() string {
return strings.SplitN(a.Doc, "\n\n", 2)[0]
}
// Validate the analyzer metadata.
func (a *Analyzer) Validate() error {
if a.Doc == "" {
return fmt.Errorf("missing doc")
}
return nil
}
// A Diagnostic is a message associated with a source location.
type Diagnostic struct {
Pos scanner.Position
Message string
}
// Pass is the interface to the run function that analyzes DBC definitions.
type Pass struct {
Analyzer *Analyzer
File *dbc.File
Diagnostics []*Diagnostic
}
// Reportf reports a diagnostic by building a message from the provided format and args.
func (pass *Pass) Reportf(pos scanner.Position, format string, args ...interface{}) {
msg := fmt.Sprintf(format, args...)
pass.Diagnostics = append(pass.Diagnostics, &Diagnostic{Pos: pos, Message: msg})
}

View File

@@ -0,0 +1,38 @@
package analysistest
import (
"strings"
"testing"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis"
"gotest.tools/v3/assert"
)
type Case struct {
Name string
Data string
Diagnostics []*analysis.Diagnostic
}
func Run(t *testing.T, a *analysis.Analyzer, cs []*Case) {
t.Helper()
for _, c := range cs {
p := dbc.NewParser(c.Name, []byte(strings.TrimLeft(c.Data, "\n")))
assert.NilError(t, p.Parse())
pass := &analysis.Pass{
Analyzer: a,
File: p.File(),
}
assert.NilError(t, a.Run(pass))
// allow omitting byte offsets and file names
for _, d := range c.Diagnostics {
d.Pos.Offset = 0
d.Pos.Filename = c.Name
}
for _, d := range pass.Diagnostics {
d.Pos.Offset = 0
}
assert.DeepEqual(t, c.Diagnostics, pass.Diagnostics)
}
}

View File

@@ -0,0 +1,60 @@
package boolprefix
import (
"strings"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis"
)
func Analyzer() *analysis.Analyzer {
return &analysis.Analyzer{
Name: "boolprefix",
Doc: "check that bools (1-bit signals) have a correct prefix",
Run: run,
}
}
func allowedPrefixes() []string {
return []string{
"Is",
"Has",
}
}
func run(pass *analysis.Pass) error {
for _, d := range pass.File.Defs {
messageDef, ok := d.(*dbc.MessageDef)
if !ok {
continue
}
SignalLoop:
for _, signalDef := range messageDef.Signals {
if signalDef.Size != 1 {
continue // skip all non-bool signals
}
for _, allowedPrefix := range allowedPrefixes() {
if strings.HasPrefix(string(signalDef.Name), allowedPrefix) {
continue SignalLoop // has allowed prefix
}
}
// edge-case: allow non-prefixed 1-bit signals with value descriptions
for _, d := range pass.File.Defs {
valueDescriptionsDef, ok := d.(*dbc.ValueDescriptionsDef)
if !ok {
continue // not value descriptions
}
if valueDescriptionsDef.MessageID == messageDef.MessageID &&
valueDescriptionsDef.SignalName == signalDef.Name {
continue SignalLoop // has value descriptions
}
}
pass.Reportf(
signalDef.Pos,
"bool signals (1-bit) must have prefix %s",
strings.Join(allowedPrefixes(), " or "),
)
}
}
return nil
}

View File

@@ -0,0 +1,53 @@
package boolprefix
import (
"testing"
"text/scanner"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis/analysistest"
)
func TestAnalyzer(t *testing.T) {
analysistest.Run(t, Analyzer(), []*analysistest.Case{
{
Name: "prefix has",
Data: `
BO_ 400 MOTOR_STATUS: 3 MOTOR
SG_ HasWheelError : 0|1@1+ (1,0) [0|0] "" DRIVER,IO
`,
},
{
Name: "prefix is",
Data: `
BO_ 400 MOTOR_STATUS: 3 MOTOR
SG_ IsOverheated : 0|1@1+ (1,0) [0|0] "" DRIVER,IO
`,
},
{
Name: "missing prefix",
Data: `
BO_ 400 MOTOR_STATUS: 3 MOTOR
SG_ WheelError : 0|1@1+ (1,0) [0|0] "" DRIVER,IO
`,
Diagnostics: []*analysis.Diagnostic{
{
Pos: scanner.Position{Line: 2, Column: 2},
Message: "bool signals (1-bit) must have prefix Is or Has",
},
},
},
{
Name: "missing prefix with value descriptions",
Data: `
BO_ 400 MOTOR_STATUS: 3 MOTOR
SG_ Status : 0|1@1+ (1,0) [0|0] "" DRIVER,IO
VAL_ 400 Status 1 "ValidDataPresent" 0 "NoData" ;
`,
},
})
}

View File

@@ -0,0 +1,56 @@
package definitiontypeorder
import (
"math"
"reflect"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis"
)
func Analyzer() *analysis.Analyzer {
return &analysis.Analyzer{
Name: "definitiontypeorder",
Doc: "check that definitions are in the correct order",
Run: run,
}
}
func orderOf(def dbc.Def) uint64 {
for i, orderDef := range []dbc.Def{
&dbc.VersionDef{},
&dbc.NewSymbolsDef{},
&dbc.BitTimingDef{},
&dbc.NodesDef{},
&dbc.ValueTableDef{},
&dbc.MessageDef{},
&dbc.MessageTransmittersDef{},
&dbc.EnvironmentVariableDef{},
&dbc.EnvironmentVariableDataDef{},
&dbc.CommentDef{},
&dbc.AttributeDef{},
&dbc.AttributeDefaultValueDef{},
&dbc.AttributeValueForObjectDef{},
&dbc.ValueDescriptionsDef{},
} {
if reflect.TypeOf(def) == reflect.TypeOf(orderDef) {
return uint64(i)
}
}
return math.MaxUint64
}
func run(pass *analysis.Pass) error {
minOrder := uint64(math.MaxUint64)
for i := range pass.File.Defs {
// diagnostics make more sense when going backwards
def := pass.File.Defs[len(pass.File.Defs)-i-1]
currOrder := orderOf(def)
if currOrder > minOrder {
pass.Reportf(def.Position(), "definition out of order")
} else {
minOrder = currOrder
}
}
return nil
}

View File

@@ -0,0 +1,56 @@
package definitiontypeorder
import (
"testing"
"text/scanner"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis/analysistest"
)
func TestAnalyzer(t *testing.T) {
analysistest.Run(t, Analyzer(), []*analysistest.Case{
{
Name: "correct order",
Data: `
VERSION "foo"
NS_ :
BS_:
BU_:
`,
},
{
Name: "incorrect order",
Data: `
VERSION "foo"
NS_ :
BU_:
BS_:
`,
Diagnostics: []*analysis.Diagnostic{
{
Pos: scanner.Position{Line: 3, Column: 1},
Message: "definition out of order",
},
},
},
{
Name: "unknown defs last",
Data: `
VERSION "foo"
NS_ :
BS_:
FOO "bar"
BU_:
`,
Diagnostics: []*analysis.Diagnostic{
{
Pos: scanner.Position{Line: 4, Column: 1},
Message: "definition out of order",
},
},
},
})
}

View File

@@ -0,0 +1,40 @@
package intervals
import (
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis"
)
func Analyzer() *analysis.Analyzer {
return &analysis.Analyzer{
Name: "intervals",
Doc: "check that all intervals are valid (min <= max)",
Run: run,
}
}
func run(pass *analysis.Pass) error {
for _, def := range pass.File.Defs {
switch def := def.(type) {
case *dbc.EnvironmentVariableDef:
if def.Minimum > def.Maximum {
pass.Reportf(def.Pos, "invalid interval: [%f, %f]", def.Minimum, def.Maximum)
}
case *dbc.MessageDef:
for i := range def.Signals {
signal := &def.Signals[i]
if signal.Minimum > signal.Maximum {
pass.Reportf(def.Pos, "invalid interval: [%f, %f]", signal.Minimum, signal.Maximum)
}
}
case *dbc.AttributeDef:
if def.MinimumInt > def.MaximumInt || def.MinimumFloat > def.MaximumFloat {
pass.Reportf(def.Pos, "invalid interval: [%d, %d]", def.MinimumInt, def.MaximumInt)
}
if def.MinimumFloat > def.MaximumFloat {
pass.Reportf(def.Pos, "invalid interval: [%f, %f]", def.MinimumFloat, def.MaximumFloat)
}
}
}
return nil
}

View File

@@ -0,0 +1,29 @@
package intervals
import (
"testing"
"text/scanner"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis/analysistest"
)
func TestAnalyzer(t *testing.T) {
analysistest.Run(t, Analyzer(), []*analysistest.Case{
{
Name: "attribute interval ok",
Data: `BA_DEF_ "AttributeName" INT 0 10;`,
},
{
Name: "attribute interval bad",
Data: `BA_DEF_ "AttributeName" INT 10 0;`,
Diagnostics: []*analysis.Diagnostic{
{
Pos: scanner.Position{Line: 1, Column: 1},
Message: "invalid interval: [10, 0]",
},
},
},
})
}

View File

@@ -0,0 +1,26 @@
package lineendings
import (
"bytes"
"text/scanner"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis"
)
func Analyzer() *analysis.Analyzer {
return &analysis.Analyzer{
Name: "lineendings",
Doc: `check that the file does not contain Windows line-endings (\r\n)`,
Run: run,
}
}
func run(pass *analysis.Pass) error {
if bytes.Contains(pass.File.Data, []byte{'\r', '\n'}) {
pass.Reportf(
scanner.Position{Filename: pass.File.Name, Line: 1, Column: 1},
`file must not contain Windows line-endings (\r\n)`,
)
}
return nil
}

View File

@@ -0,0 +1,29 @@
package lineendings
import (
"testing"
"text/scanner"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis/analysistest"
)
func TestAnalyzer(t *testing.T) {
analysistest.Run(t, Analyzer(), []*analysistest.Case{
{
Name: "ok",
Data: `NS_ :`,
},
{
Name: "not ok",
Data: "NS_ :\r\n",
Diagnostics: []*analysis.Diagnostic{
{
Pos: scanner.Position{Line: 1, Column: 1},
Message: `file must not contain Windows line-endings (\r\n)`,
},
},
},
})
}

View File

@@ -0,0 +1,28 @@
package messagenames
import (
"github.com/fiskerinc/cloud-services/pkg/can-go/internal/identifiers"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis"
)
func Analyzer() *analysis.Analyzer {
return &analysis.Analyzer{
Name: "messagenames",
Doc: "check that message names are valid CamelCase identifiers",
Run: run,
}
}
func run(pass *analysis.Pass) error {
for _, def := range pass.File.Defs {
messageDef, ok := def.(*dbc.MessageDef)
if !ok {
continue // not a message
}
if !identifiers.IsCamelCase(string(messageDef.Name)) {
pass.Reportf(messageDef.Pos, "message names must be CamelCase")
}
}
return nil
}

View File

@@ -0,0 +1,29 @@
package messagenames
import (
"testing"
"text/scanner"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis/analysistest"
)
func TestAnalyzer(t *testing.T) {
analysistest.Run(t, Analyzer(), []*analysistest.Case{
{
Name: "ok",
Data: `BO_ 100 DriverHeartbeat: 1 DRIVER`,
},
{
Name: "not ok",
Data: `BO_ 100 DRIVER_HEARTBEAT: 1 DRIVER`,
Diagnostics: []*analysis.Diagnostic{
{
Pos: scanner.Position{Line: 1, Column: 1},
Message: "message names must be CamelCase",
},
},
},
})
}

View File

@@ -0,0 +1,59 @@
package multiplexedsignals
import (
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis"
)
func Analyzer() *analysis.Analyzer {
return &analysis.Analyzer{
Name: "multiplexedsignals",
Doc: "check that multiplexed signals are valid",
Run: run,
}
}
func run(pass *analysis.Pass) error {
for _, def := range pass.File.Defs {
message, ok := def.(*dbc.MessageDef)
if !ok {
continue
}
// locate multiplexer switch
var multiplexerSwitch *dbc.SignalDef
for i := range message.Signals {
if !message.Signals[i].IsMultiplexerSwitch {
continue
}
if multiplexerSwitch != nil {
pass.Reportf(message.Signals[i].Pos, "more than one multiplexer switch")
continue
}
multiplexerSwitch = &message.Signals[i]
if multiplexerSwitch.IsSigned {
pass.Reportf(message.Signals[i].Pos, "signed multiplexer switch")
continue
}
if multiplexerSwitch.IsMultiplexed {
pass.Reportf(message.Signals[i].Pos, "can't be multiplexer and multiplexed")
continue
}
}
for i := range message.Signals {
signal := &message.Signals[i]
if !signal.IsMultiplexed {
continue
}
if multiplexerSwitch == nil {
pass.Reportf(message.Signals[i].Pos, "no multiplexer switch for multiplexed signal")
continue
}
multiplexerSwitchMaxValue := uint64((1 << multiplexerSwitch.Size) - 1)
if signal.MultiplexerSwitch > multiplexerSwitchMaxValue {
pass.Reportf(signal.Pos, "multiplexer switch exceeds max value: %v", multiplexerSwitchMaxValue)
continue
}
}
}
return nil
}

View File

@@ -0,0 +1,114 @@
package multiplexedsignals
import (
"testing"
"text/scanner"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis/analysistest"
)
func TestAnalyzer(t *testing.T) {
analysistest.Run(t, Analyzer(), []*analysistest.Case{
{
Name: "valid",
Data: `
BO_ 200 SENSOR_SONARS: 8 SENSOR
SG_ SENSOR_SONARS_mux M : 0|4@1+ (1,0) [0|0] "" DRIVER,IO
SG_ SENSOR_SONARS_err_count : 4|12@1+ (1,0) [0|0] "" DRIVER,IO
SG_ SENSOR_SONARS_left m0 : 16|12@1+ (0.1,0) [0|0] "" DRIVER,IO
SG_ SENSOR_SONARS_middle m0 : 28|12@1+ (0.1,0) [0|0] "" DRIVER,IO
SG_ SENSOR_SONARS_right m0 : 40|12@1+ (0.1,0) [0|0] "" DRIVER,IO
SG_ SENSOR_SONARS_rear m0 : 52|12@1+ (0.1,0) [0|0] "" DRIVER,IO
SG_ SENSOR_SONARS_no_filt_left m1 : 16|12@1+ (0.1,0) [0|0] "" DBG
SG_ SENSOR_SONARS_no_filt_middle m1 : 28|12@1+ (0.1,0) [0|0] "" DBG
SG_ SENSOR_SONARS_no_filt_right m1 : 40|12@1+ (0.1,0) [0|0] "" DBG
SG_ SENSOR_SONARS_no_filt_rear m1 : 52|12@1+ (0.1,0) [0|0] "" DBG
`,
},
{
Name: "multiple multiplexer switches",
Data: `
BO_ 200 SENSOR_SONARS: 8 SENSOR
SG_ SENSOR_SONARS_mux M : 0|4@1+ (1,0) [0|0] "" DRIVER,IO
SG_ SENSOR_SONARS_err_count M : 4|12@1+ (1,0) [0|0] "" DRIVER,IO
SG_ SENSOR_SONARS_left m0 : 16|12@1+ (0.1,0) [0|0] "" DRIVER,IO
SG_ SENSOR_SONARS_middle m0 : 28|12@1+ (0.1,0) [0|0] "" DRIVER,IO
SG_ SENSOR_SONARS_right m0 : 40|12@1+ (0.1,0) [0|0] "" DRIVER,IO
SG_ SENSOR_SONARS_rear m0 : 52|12@1+ (0.1,0) [0|0] "" DRIVER,IO
SG_ SENSOR_SONARS_no_filt_left m1 : 16|12@1+ (0.1,0) [0|0] "" DBG
SG_ SENSOR_SONARS_no_filt_middle m1 : 28|12@1+ (0.1,0) [0|0] "" DBG
SG_ SENSOR_SONARS_no_filt_right m1 : 40|12@1+ (0.1,0) [0|0] "" DBG
SG_ SENSOR_SONARS_no_filt_rear m1 : 52|12@1+ (0.1,0) [0|0] "" DBG
`,
Diagnostics: []*analysis.Diagnostic{
{
Pos: scanner.Position{Line: 3, Column: 2},
Message: "more than one multiplexer switch",
},
},
},
{
Name: "signed multiplexer switch",
Data: `
BO_ 200 SENSOR_SONARS: 8 SENSOR
SG_ SENSOR_SONARS_mux M : 0|4@1- (1,0) [0|0] "" DRIVER,IO
SG_ SENSOR_SONARS_err_count : 4|12@1+ (1,0) [0|0] "" DRIVER,IO
SG_ SENSOR_SONARS_left m0 : 16|12@1+ (0.1,0) [0|0] "" DRIVER,IO
SG_ SENSOR_SONARS_middle m0 : 28|12@1+ (0.1,0) [0|0] "" DRIVER,IO
SG_ SENSOR_SONARS_right m0 : 40|12@1+ (0.1,0) [0|0] "" DRIVER,IO
SG_ SENSOR_SONARS_rear m0 : 52|12@1+ (0.1,0) [0|0] "" DRIVER,IO
SG_ SENSOR_SONARS_no_filt_left m1 : 16|12@1+ (0.1,0) [0|0] "" DBG
SG_ SENSOR_SONARS_no_filt_middle m1 : 28|12@1+ (0.1,0) [0|0] "" DBG
SG_ SENSOR_SONARS_no_filt_right m1 : 40|12@1+ (0.1,0) [0|0] "" DBG
SG_ SENSOR_SONARS_no_filt_rear m1 : 52|12@1+ (0.1,0) [0|0] "" DBG
`,
Diagnostics: []*analysis.Diagnostic{
{
Pos: scanner.Position{Line: 2, Column: 2},
Message: "signed multiplexer switch",
},
},
},
{
Name: "no multiplexer switch",
Data: `
BO_ 200 SENSOR_SONARS: 8 SENSOR
SG_ SENSOR_SONARS_err_count : 4|12@1+ (1,0) [0|0] "" DRIVER,IO
SG_ SENSOR_SONARS_left m0 : 16|12@1+ (0.1,0) [0|0] "" DRIVER,IO
`,
Diagnostics: []*analysis.Diagnostic{
{
Pos: scanner.Position{Line: 3, Column: 2},
Message: "no multiplexer switch for multiplexed signal",
},
},
},
{
Name: "too big multiplexer switch",
Data: `
BO_ 200 SENSOR_SONARS: 8 SENSOR
SG_ SENSOR_SONARS_mux M : 0|4@1+ (1,0) [0|0] "" DRIVER,IO
SG_ SENSOR_SONARS_err_count : 4|12@1+ (1,0) [0|0] "" DRIVER,IO
SG_ SENSOR_SONARS_left m16 : 16|12@1+ (0.1,0) [0|0] "" DRIVER,IO
SG_ SENSOR_SONARS_middle m0 : 28|12@1+ (0.1,0) [0|0] "" DRIVER,IO
SG_ SENSOR_SONARS_right m0 : 40|12@1+ (0.1,0) [0|0] "" DRIVER,IO
SG_ SENSOR_SONARS_rear m0 : 52|12@1+ (0.1,0) [0|0] "" DRIVER,IO
SG_ SENSOR_SONARS_no_filt_left m1 : 16|12@1+ (0.1,0) [0|0] "" DBG
SG_ SENSOR_SONARS_no_filt_middle m1 : 28|12@1+ (0.1,0) [0|0] "" DBG
SG_ SENSOR_SONARS_no_filt_right m1 : 40|12@1+ (0.1,0) [0|0] "" DBG
SG_ SENSOR_SONARS_no_filt_rear m1 : 52|12@1+ (0.1,0) [0|0] "" DBG
`,
Diagnostics: []*analysis.Diagnostic{
{
Pos: scanner.Position{Line: 4, Column: 2},
Message: "multiplexer switch exceeds max value: 15",
},
},
},
})
}

View File

@@ -0,0 +1,27 @@
package newsymbols
import (
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis"
)
func Analyzer() *analysis.Analyzer {
return &analysis.Analyzer{
Name: "newsymbols",
Doc: "check that the new symbols definition is empty",
Run: run,
}
}
func run(pass *analysis.Pass) error {
for _, def := range pass.File.Defs {
newSymbolsDef, ok := def.(*dbc.NewSymbolsDef)
if !ok {
continue // not a new symbols definition
}
if len(newSymbolsDef.Symbols) > 0 {
pass.Reportf(newSymbolsDef.Pos, "new symbols should be empty")
}
}
return nil
}

View File

@@ -0,0 +1,32 @@
package newsymbols
import (
"testing"
"text/scanner"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis/analysistest"
)
func TestAnalyzer(t *testing.T) {
analysistest.Run(t, Analyzer(), []*analysistest.Case{
{
Name: "ok",
Data: `NS_ :`,
},
{
Name: "not ok",
Data: `
NS_ :
BA_DEF_DEF_REL_
BA_DEF_SGTYPE_`,
Diagnostics: []*analysis.Diagnostic{
{
Pos: scanner.Position{Line: 1, Column: 1},
Message: "new symbols should be empty",
},
},
},
})
}

View File

@@ -0,0 +1,58 @@
package nodereferences
import (
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis"
)
func Analyzer() *analysis.Analyzer {
return &analysis.Analyzer{
Name: "nodereferences",
Doc: "check that all node references refer to declared nodes",
Run: run,
}
}
func run(pass *analysis.Pass) error {
declaredNodes := map[dbc.Identifier]struct{}{
dbc.NodePlaceholder: {}, // placeholder is implicitly declared
}
// collect declared nodes
for _, def := range pass.File.Defs {
if def, ok := def.(*dbc.NodesDef); ok {
for _, nodeName := range def.NodeNames {
declaredNodes[nodeName] = struct{}{}
}
}
}
// verify node references
for _, def := range pass.File.Defs {
switch def := def.(type) {
case *dbc.MessageDef:
if _, ok := declaredNodes[def.Transmitter]; !ok {
pass.Reportf(def.Pos, "undeclared transmitter node: %v", def.Transmitter)
}
for i := range def.Signals {
signal := &def.Signals[i]
for _, receiver := range signal.Receivers {
if _, ok := declaredNodes[receiver]; !ok {
pass.Reportf(signal.Pos, "undeclared receiver node: %v", receiver)
}
}
}
case *dbc.EnvironmentVariableDef:
for _, accessNode := range def.AccessNodes {
if _, ok := declaredNodes[accessNode]; !ok {
pass.Reportf(def.Pos, "undeclared access node: %v", accessNode)
}
}
case *dbc.MessageTransmittersDef:
for _, transmitter := range def.Transmitters {
if _, ok := declaredNodes[transmitter]; !ok {
pass.Reportf(def.Pos, "undeclared transmitter node: %v", transmitter)
}
}
}
}
return nil
}

View File

@@ -0,0 +1,52 @@
package nodereferences
import (
"testing"
"text/scanner"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis/analysistest"
)
func TestAnalyzer(t *testing.T) {
analysistest.Run(t, Analyzer(), []*analysistest.Case{
{
Name: "valid",
Data: `
BU_: ECU1 ECU2
BO_ 42 TestMessage: 8 ECU2
SG_ CellTempLowest : 32|8@0+ (1,-40) [-40|215] "C" ECU1
`,
},
{
Name: "undeclared transmitter",
Data: `
BU_: ECU1 ECU2
BO_ 42 TestMessage: 8 ECU3
SG_ CellTempLowest : 32|8@0+ (1,-40) [-40|215] "C" ECU1
`,
Diagnostics: []*analysis.Diagnostic{
{
Pos: scanner.Position{Line: 2, Column: 1},
Message: "undeclared transmitter node: ECU3",
},
},
},
{
Name: "undeclared receiver",
Data: `
BU_: ECU1 ECU2
BO_ 42 TestMessage: 8 ECU2
SG_ CellTempLowest : 32|8@0+ (1,-40) [-40|215] "C" ECU2,ECU3
`,
Diagnostics: []*analysis.Diagnostic{
{
Pos: scanner.Position{Line: 3, Column: 2},
Message: "undeclared receiver node: ECU3",
},
},
},
})
}

View File

@@ -0,0 +1,31 @@
package noreservedsignals
import (
"strings"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis"
)
func Analyzer() *analysis.Analyzer {
return &analysis.Analyzer{
Name: "noreservedsignals",
Doc: `checks that no signals have the prefix "Reserved"`,
Run: run,
}
}
func run(pass *analysis.Pass) error {
for _, d := range pass.File.Defs {
messageDef, ok := d.(*dbc.MessageDef)
if !ok {
continue
}
for _, signalDef := range messageDef.Signals {
if strings.HasPrefix(string(signalDef.Name), "Reserved") {
pass.Reportf(signalDef.Pos, "remove reserved signals")
}
}
}
return nil
}

View File

@@ -0,0 +1,35 @@
package noreservedsignals
import (
"testing"
"text/scanner"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis/analysistest"
)
func TestAnalyzer(t *testing.T) {
analysistest.Run(t, Analyzer(), []*analysistest.Case{
{
Name: "ok",
Data: `
BO_ 400 MotorStatus: 3 MOTOR
SG_ HasWheelError : 0|1@1+ (1,0) [0|0] "" DRIVER,IO
`,
},
{
Name: "not ok",
Data: `
BO_ 400 MotorStatus: 3 MOTOR
SG_ Reserved1 : 0|1@1+ (1,0) [0|0] "" DRIVER,IO
`,
Diagnostics: []*analysis.Diagnostic{
{
Pos: scanner.Position{Line: 2, Column: 2},
Message: "remove reserved signals",
},
},
},
})
}

View File

@@ -0,0 +1,38 @@
package requireddefinitions
import (
"reflect"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis"
)
func Analyzer() *analysis.Analyzer {
return &analysis.Analyzer{
Name: "requireddefinitions",
Doc: "check that the file contains exactly one of all required definitions",
Run: run,
}
}
func requiredDefinitions() []dbc.Def {
return []dbc.Def{
&dbc.BitTimingDef{},
&dbc.NodesDef{},
}
}
func run(pass *analysis.Pass) error {
counts := make(map[reflect.Type]int)
for _, def := range pass.File.Defs {
counts[reflect.TypeOf(def)]++
}
for _, requiredDef := range requiredDefinitions() {
if counts[reflect.TypeOf(requiredDef)] == 0 {
// we have no definition to return, so return the first
pass.Reportf(pass.File.Defs[0].Position(), "missing required definition(s)")
break
}
}
return nil
}

View File

@@ -0,0 +1,47 @@
package requireddefinitions
import (
"testing"
"text/scanner"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis/analysistest"
)
func TestAnalyzer(t *testing.T) {
analysistest.Run(t, Analyzer(), []*analysistest.Case{
{
Name: "ok",
Data: `
BS_:
BU_: ECU1
`,
},
{
Name: "missing bit timing",
Data: `
BU_: ECU1
`,
Diagnostics: []*analysis.Diagnostic{
{
Pos: scanner.Position{Line: 1, Column: 1},
Message: "missing required definition(s)",
},
},
},
{
Name: "missing nodes",
Data: `
BS_:
`,
Diagnostics: []*analysis.Diagnostic{
{
Pos: scanner.Position{Line: 1, Column: 1},
Message: "missing required definition(s)",
},
},
},
})
}

View File

@@ -0,0 +1,31 @@
package signalbounds
import (
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis"
)
func Analyzer() *analysis.Analyzer {
return &analysis.Analyzer{
Name: "signalbounds",
Doc: "check that signal start and end bits are within bounds of the message size",
Run: run,
}
}
func run(pass *analysis.Pass) error {
for _, def := range pass.File.Defs {
message, ok := def.(*dbc.MessageDef)
if !ok || dbc.IsIndependentSignalsMessage(message) {
continue
}
for i := range message.Signals {
signal := &message.Signals[i]
if signal.StartBit >= 8*message.Size {
pass.Reportf(signal.Pos, "start bit out of bounds")
}
// TODO: Check end bit
}
}
return nil
}

View File

@@ -0,0 +1,41 @@
package signalbounds
import (
"testing"
"text/scanner"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis/analysistest"
)
func TestAnalyzer(t *testing.T) {
analysistest.Run(t, Analyzer(), []*analysistest.Case{
{
Name: "ok",
Data: `
BO_ 500 IO_DEBUG: 4 IO
SG_ IO_DEBUG_test_unsigned : 0|8@1+ (1,0) [0|0] "" DBG
SG_ IO_DEBUG_test_enum : 8|8@1+ (1,0) [0|0] "" DBG
SG_ IO_DEBUG_test_signed : 16|8@1- (1,0) [0|0] "" DBG
SG_ IO_DEBUG_test_float : 24|8@1+ (0.5,0) [0|0] "" DBG
`,
},
{
Name: "start bit out of bounds",
Data: `
BO_ 500 IO_DEBUG: 4 IO
SG_ IO_DEBUG_test_unsigned : 0|8@1+ (1,0) [0|0] "" DBG
SG_ IO_DEBUG_test_enum : 8|8@1+ (1,0) [0|0] "" DBG
SG_ IO_DEBUG_test_signed : 16|8@1- (1,0) [0|0] "" DBG
SG_ IO_DEBUG_test_float : 32|8@1+ (0.5,0) [0|0] "" DBG
`,
Diagnostics: []*analysis.Diagnostic{
{
Pos: scanner.Position{Line: 5, Column: 2},
Message: "start bit out of bounds",
},
},
},
})
}

View File

@@ -0,0 +1,30 @@
package signalnames
import (
"github.com/fiskerinc/cloud-services/pkg/can-go/internal/identifiers"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis"
)
func Analyzer() *analysis.Analyzer {
return &analysis.Analyzer{
Name: "signalnames",
Doc: "check that signal names are valid CamelCase identifiers",
Run: run,
}
}
func run(pass *analysis.Pass) error {
for _, d := range pass.File.Defs {
messageDef, ok := d.(*dbc.MessageDef)
if !ok {
continue
}
for _, signalDef := range messageDef.Signals {
if !identifiers.IsCamelCase(string(signalDef.Name)) {
pass.Reportf(signalDef.Pos, "signal names must be CamelCase")
}
}
}
return nil
}

View File

@@ -0,0 +1,35 @@
package signalnames
import (
"testing"
"text/scanner"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis/analysistest"
)
func TestAnalyzer(t *testing.T) {
analysistest.Run(t, Analyzer(), []*analysistest.Case{
{
Name: "ok",
Data: `
BO_ 400 MotorStatus: 3 MOTOR
SG_ HasWheelError : 0|1@1+ (1,0) [0|0] "" DRIVER,IO
`,
},
{
Name: "not ok",
Data: `
BO_ 400 MOTOR_STATUS: 3 MOTOR
SG_ IS_OVERHEATED : 0|1@1+ (1,0) [0|0] "" DRIVER,IO
`,
Diagnostics: []*analysis.Diagnostic{
{
Pos: scanner.Position{Line: 2, Column: 2},
Message: "signal names must be CamelCase",
},
},
},
})
}

View File

@@ -0,0 +1,42 @@
package singletondefinitions
import (
"reflect"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis"
)
func Analyzer() *analysis.Analyzer {
return &analysis.Analyzer{
Name: "singletondefinitions",
Doc: "check that the file contains at most one of all singleton definitions",
Run: run,
}
}
func singletonDefinitions() []dbc.Def {
return []dbc.Def{
&dbc.VersionDef{},
&dbc.NewSymbolsDef{},
&dbc.BitTimingDef{},
&dbc.NodesDef{},
}
}
func run(pass *analysis.Pass) error {
defsByType := make(map[reflect.Type][]dbc.Def)
for _, def := range pass.File.Defs {
t := reflect.TypeOf(def)
defsByType[t] = append(defsByType[t], def)
}
for _, singletonDef := range singletonDefinitions() {
singletonDefs := defsByType[reflect.TypeOf(singletonDef)]
if len(singletonDefs) > 1 {
for i := 1; i < len(singletonDefs); i++ {
pass.Reportf(singletonDefs[i].Position(), "more than one definition not allowed")
}
}
}
return nil
}

View File

@@ -0,0 +1,91 @@
package singletondefinitions
import (
"testing"
"text/scanner"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis/analysistest"
)
func TestAnalyzer(t *testing.T) {
analysistest.Run(t, Analyzer(), []*analysistest.Case{
{
Name: "ok",
Data: `
VERSION "foo"
NS_:
BS_:
BU_: ECU1
`,
},
{
Name: "multiple versions",
Data: `
VERSION "foo"
VERSION "foo"
NS_:
BS_:
BU_: ECU1
`,
Diagnostics: []*analysis.Diagnostic{
{
Pos: scanner.Position{Line: 2, Column: 1},
Message: "more than one definition not allowed",
},
},
},
{
Name: "multiple new symbols",
Data: `
VERSION "foo"
NS_:
NS_:
BS_:
BU_: ECU1
`,
Diagnostics: []*analysis.Diagnostic{
{
Pos: scanner.Position{Line: 3, Column: 1},
Message: "more than one definition not allowed",
},
},
},
{
Name: "multiple bit timing",
Data: `
VERSION "foo"
NS_:
BS_:
BS_:
BU_: ECU1
`,
Diagnostics: []*analysis.Diagnostic{
{
Pos: scanner.Position{Line: 4, Column: 1},
Message: "more than one definition not allowed",
},
},
},
{
Name: "multiple nodes",
Data: `
VERSION "foo"
NS_:
BS_:
BU_: ECU1
BU_: ECU2
`,
Diagnostics: []*analysis.Diagnostic{
{
Pos: scanner.Position{Line: 5, Column: 1},
Message: "more than one definition not allowed",
},
},
},
})
}

View File

@@ -0,0 +1,51 @@
package siunits
import (
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis"
)
func Analyzer() *analysis.Analyzer {
return &analysis.Analyzer{
Name: "unitsuffixes",
Doc: "check that signals with SI units have the correct symbols",
Run: run,
}
}
const (
metersPerSecond = "m/s"
kilometersPerHour = "km/h"
meters = "m"
degrees = "°"
radians = "rad"
)
// symbolMap returns a map from non-standard unit symbols to SI unit symbols.
func symbolMap() map[string]string {
return map[string]string{
"kph": kilometersPerHour,
"mps": metersPerSecond,
"meters/sec": metersPerSecond,
"meters": meters,
"deg": degrees,
"degrees": degrees,
"radians": radians,
}
}
func run(pass *analysis.Pass) error {
symbols := symbolMap()
for _, def := range pass.File.Defs {
message, ok := def.(*dbc.MessageDef)
if !ok {
continue
}
for _, signal := range message.Signals {
if symbol, ok := symbols[signal.Unit]; ok {
pass.Reportf(signal.Pos, "signal with unit %s should have SI unit %s", signal.Unit, symbol)
}
}
}
return nil
}

View File

@@ -0,0 +1,35 @@
package siunits
import (
"testing"
"text/scanner"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis/analysistest"
)
func TestAnalyzer(t *testing.T) {
analysistest.Run(t, Analyzer(), []*analysistest.Case{
{
Name: "ok",
Data: `
BO_ 400 TestMessage: 3 ECU1
SG_ SpeedMps : 0|1@1+ (1,0) [0|0] "m/s" DRIVER,IO
`,
},
{
Name: "not ok",
Data: `
BO_ 400 TestMessage: 3 ECU1
SG_ SpeedMps : 0|1@1+ (1,0) [0|0] "meters/sec" DRIVER,IO
`,
Diagnostics: []*analysis.Diagnostic{
{
Pos: scanner.Position{Line: 2, Column: 2},
Message: "signal with unit meters/sec should have SI unit m/s",
},
},
},
})
}

View File

@@ -0,0 +1,29 @@
package uniquenodenames
import (
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis"
)
func Analyzer() *analysis.Analyzer {
return &analysis.Analyzer{
Name: "uniquenodenames",
Doc: "check that all declared node names are unique",
Run: run,
}
}
func run(pass *analysis.Pass) error {
nodeNames := make(map[dbc.Identifier]struct{})
for _, def := range pass.File.Defs {
if def, ok := def.(*dbc.NodesDef); ok {
for _, nodeName := range def.NodeNames {
if _, ok := nodeNames[nodeName]; ok {
pass.Reportf(def.Pos, "non-unique node name")
}
nodeNames[nodeName] = struct{}{}
}
}
}
return nil
}

View File

@@ -0,0 +1,29 @@
package uniquenodenames
import (
"testing"
"text/scanner"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis/analysistest"
)
func TestAnalyzer(t *testing.T) {
analysistest.Run(t, Analyzer(), []*analysistest.Case{
{
Name: "ok",
Data: `BU_: ECU1 ECU2 ECU3`,
},
{
Name: "duplicates",
Data: `BU_: ECU1 ECU2 ECU3 ECU1`,
Diagnostics: []*analysis.Diagnostic{
{
Pos: scanner.Position{Line: 1, Column: 1},
Message: "non-unique node name",
},
},
},
})
}

View File

@@ -0,0 +1,33 @@
package uniquesignalnames
import (
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis"
)
func Analyzer() *analysis.Analyzer {
return &analysis.Analyzer{
Name: "uniquesignalnames",
Doc: "check that all signal names are unique",
Run: run,
}
}
func run(pass *analysis.Pass) error {
for _, def := range pass.File.Defs {
message, ok := def.(*dbc.MessageDef)
if !ok || dbc.IsIndependentSignalsMessage(message) {
continue
}
signalNames := make(map[dbc.Identifier]struct{})
for i := range message.Signals {
signal := &message.Signals[i]
if _, ok := signalNames[signal.Name]; ok {
pass.Reportf(signal.Pos, "non-unique signal name")
} else {
signalNames[signal.Name] = struct{}{}
}
}
}
return nil
}

View File

@@ -0,0 +1,37 @@
package uniquesignalnames
import (
"testing"
"text/scanner"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis/analysistest"
)
func TestAnalyzer(t *testing.T) {
analysistest.Run(t, Analyzer(), []*analysistest.Case{
{
Name: "ok",
Data: `
BO_ 101 MOTOR_CMD: 1 DRIVER
SG_ MOTOR_CMD_steer : 0|4@1- (1,-5) [-5|5] "" MOTOR
SG_ MOTOR_CMD_drive : 4|4@1+ (1,0) [0|9] "" MOTOR
`,
},
{
Name: "duplicate",
Data: `
BO_ 101 MOTOR_CMD: 1 DRIVER
SG_ MOTOR_CMD_steer : 0|4@1- (1,-5) [-5|5] "" MOTOR
SG_ MOTOR_CMD_steer : 4|4@1+ (1,0) [0|9] "" MOTOR
`,
Diagnostics: []*analysis.Diagnostic{
{
Pos: scanner.Position{Line: 3, Column: 2},
Message: "non-unique signal name",
},
},
},
})
}

View File

@@ -0,0 +1,44 @@
package unitsuffixes
import (
"strings"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis"
)
func Analyzer() *analysis.Analyzer {
return &analysis.Analyzer{
Name: "unitsuffixes",
Doc: "check that signals with units have correct name suffixes",
Run: run,
}
}
func unitSuffixes() map[string]string {
return map[string]string{
"°": "Degrees",
"rad": "Radians",
"%": "Percent",
"km/h": "Kph",
"m/s": "Mps",
}
}
func run(pass *analysis.Pass) error {
suffixes := unitSuffixes()
for _, def := range pass.File.Defs {
message, ok := def.(*dbc.MessageDef)
if !ok {
continue
}
for _, signal := range message.Signals {
if suffix, ok := suffixes[signal.Unit]; ok {
if !strings.HasSuffix(string(signal.Name), suffix) {
pass.Reportf(signal.Pos, "signal with unit %s must have suffix %s", signal.Unit, suffix)
}
}
}
}
return nil
}

View File

@@ -0,0 +1,35 @@
package unitsuffixes
import (
"testing"
"text/scanner"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis/analysistest"
)
func TestAnalyzer(t *testing.T) {
analysistest.Run(t, Analyzer(), []*analysistest.Case{
{
Name: "ok",
Data: `
BO_ 400 TestMessage: 3 ECU1
SG_ ValuePercent : 0|1@1+ (1,0) [0|0] "%" DRIVER,IO
`,
},
{
Name: "not ok",
Data: `
BO_ 400 TestMessage: 3 ECU1
SG_ ValuePct : 0|1@1+ (1,0) [0|0] "%" DRIVER,IO
`,
Diagnostics: []*analysis.Diagnostic{
{
Pos: scanner.Position{Line: 2, Column: 2},
Message: "signal with unit % must have suffix Percent",
},
},
},
})
}

View File

@@ -0,0 +1,43 @@
package valuedescriptions
import (
"fmt"
"github.com/fiskerinc/cloud-services/pkg/can-go/internal/identifiers"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis"
)
func Analyzer() *analysis.Analyzer {
return &analysis.Analyzer{
Name: "valuedescriptions",
Doc: "check that value descriptions are valid CamelCase",
Run: run,
}
}
func run(pass *analysis.Pass) error {
for _, def := range pass.File.Defs {
var valueDescriptions []dbc.ValueDescriptionDef
switch def := def.(type) {
case *dbc.ValueTableDef:
valueDescriptions = def.ValueDescriptions
case *dbc.ValueDescriptionsDef:
valueDescriptions = def.ValueDescriptions
default:
continue
}
for _, vd := range valueDescriptions {
vd := vd
if !identifiers.IsCamelCase(vd.Description) {
// Descriptor has format "<value> <quote><description>"
//
// So we increase the column position by the size of value + 2 (space and quotes) so the lint
// error marker is on the description and not on the value
vd.Pos.Column += len(fmt.Sprintf("%d", int64(vd.Value))) + 2
pass.Reportf(vd.Pos, "value description must be CamelCase (numbers ignored)")
}
}
}
return nil
}

View File

@@ -0,0 +1,42 @@
package valuedescriptions
import (
"testing"
"text/scanner"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis/analysistest"
)
func TestAnalyzer(t *testing.T) {
analysistest.Run(t, Analyzer(), []*analysistest.Case{
{
Name: "ok",
Data: `VAL_ 100 Command 2 "Reboot" 1 "Sync" 0 "Noop";`,
},
{
Name: "ok",
Data: `VAL_ 100 Command 2 "11Reboot" 1 "123" 0 "Noop";`,
},
{
Name: "underscore",
Data: `VAL_ 100 Command 2 "Reboot_Command" 1 "Sync" 0 "Noop";`,
Diagnostics: []*analysis.Diagnostic{
{
Pos: scanner.Position{Line: 1, Column: 21},
Message: "value description must be CamelCase (numbers ignored)",
},
},
},
{
Name: "several digits value",
Data: `VAL_ 100 Command 234 "Reboot_Command" 1 "Sync" 0 "Noop";`,
Diagnostics: []*analysis.Diagnostic{
{
Pos: scanner.Position{Line: 1, Column: 23},
Message: "value description must be CamelCase (numbers ignored)",
},
},
},
})
}

View File

@@ -0,0 +1,27 @@
package version
import (
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis"
)
func Analyzer() *analysis.Analyzer {
return &analysis.Analyzer{
Name: "version",
Doc: "check that the version definition is empty",
Run: run,
}
}
func run(pass *analysis.Pass) error {
for _, def := range pass.File.Defs {
versionDef, ok := def.(*dbc.VersionDef)
if !ok {
continue // not a version definition
}
if len(versionDef.Version) > 0 {
pass.Reportf(versionDef.Pos, "version should be empty")
}
}
return nil
}

View File

@@ -0,0 +1,29 @@
package version
import (
"testing"
"text/scanner"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/dbc/analysis/analysistest"
)
func TestAnalyzer(t *testing.T) {
analysistest.Run(t, Analyzer(), []*analysistest.Case{
{
Name: "ok",
Data: `VERSION ""`,
},
{
Name: "not ok",
Data: `VERSION "foo"`,
Diagnostics: []*analysis.Diagnostic{
{
Pos: scanner.Position{Line: 1, Column: 1},
Message: "version should be empty",
},
},
},
})
}

View File

@@ -0,0 +1,28 @@
package dbc
import "fmt"
// AttributeValueType represents an attribute value type.
type AttributeValueType string
const (
AttributeValueTypeInt AttributeValueType = "INT"
AttributeValueTypeHex AttributeValueType = "HEX"
AttributeValueTypeFloat AttributeValueType = "FLOAT"
AttributeValueTypeString AttributeValueType = "STRING"
AttributeValueTypeEnum AttributeValueType = "ENUM"
)
// Validate returns an error for invalid attribute value types.
func (a AttributeValueType) Validate() error {
switch a {
case AttributeValueTypeInt:
case AttributeValueTypeHex:
case AttributeValueTypeFloat:
case AttributeValueTypeString:
case AttributeValueTypeEnum:
default:
return fmt.Errorf("invalid attribute value type: %v", a)
}
return nil
}

View File

@@ -0,0 +1,23 @@
package dbc
import (
"testing"
"gotest.tools/v3/assert"
)
func TestAttributeValueType_Validate(t *testing.T) {
for _, tt := range []AttributeValueType{
AttributeValueTypeInt,
AttributeValueTypeHex,
AttributeValueTypeFloat,
AttributeValueTypeString,
AttributeValueTypeEnum,
} {
assert.NilError(t, tt.Validate())
}
}
func TestAttributeValueType_Validate_Error(t *testing.T) {
assert.ErrorContains(t, AttributeValueType("foo").Validate(), "invalid attribute value type")
}

720
pkg/can-go/pkg/dbc/def.go Normal file
View File

@@ -0,0 +1,720 @@
package dbc
import (
"strconv"
"text/scanner"
)
// Def represents a single definition within a DBC file.
type Def interface {
// Position of the definition.
Position() scanner.Position
// parseFrom parses the definition from a parser.
parseFrom(*Parser)
}
// VersionDef defines the version of a DBC file.
type VersionDef struct {
Pos scanner.Position
Version string
}
var _ Def = &VersionDef{}
func (d *VersionDef) parseFrom(p *Parser) {
d.Pos = p.keyword(KeywordVersion).pos
d.Version = p.string()
}
// Position returns the position of the definition.
func (d *VersionDef) Position() scanner.Position {
return d.Pos
}
// NewSymbolsDef defines new symbol entries in a DBC file.
type NewSymbolsDef struct {
Pos scanner.Position
Symbols []Keyword
}
var _ Def = &NewSymbolsDef{}
func (d *NewSymbolsDef) parseFrom(p *Parser) {
p.useWhitespace(significantTab)
defer p.useWhitespace(defaultWhitespace)
d.Pos = p.keyword(KeywordNewSymbols).pos
p.token(':')
for p.peekToken().typ == '\t' {
p.token('\t')
d.Symbols = append(d.Symbols, Keyword(p.identifier()))
}
}
// Position returns the position of the definition.
func (d *NewSymbolsDef) Position() scanner.Position {
return d.Pos
}
// BitTimingDef defines the baud rate and the settings of the BTR registers of a CAN network.
//
// This definition is obsolete and not used anymore.
type BitTimingDef struct {
Pos scanner.Position
BaudRate uint64
BTR1 uint64
BTR2 uint64
}
var _ Def = &BitTimingDef{}
func (d *BitTimingDef) parseFrom(p *Parser) {
d.Pos = p.keyword(KeywordBitTiming).pos
p.token(':')
d.BaudRate = p.optionalUint()
if p.peekToken().typ == ':' {
d.BTR1 = p.optionalUint()
}
if p.peekToken().typ == ',' {
d.BTR2 = p.optionalUint()
}
}
// Position returns the position of the definition.
func (d *BitTimingDef) Position() scanner.Position {
return d.Pos
}
// NodesDef defines the names of all nodes participating in the network.
//
// This definition is required in every DBC file.
//
// All node names must be unique.
type NodesDef struct {
Pos scanner.Position
NodeNames []Identifier
}
var _ Def = &NodesDef{}
func (d *NodesDef) parseFrom(p *Parser) {
p.useWhitespace(significantNewline)
defer p.useWhitespace(defaultWhitespace)
d.Pos = p.keyword(KeywordNodes).pos
p.token(':')
for p.peekToken().typ == scanner.Ident {
d.NodeNames = append(d.NodeNames, p.identifier())
}
if p.peekToken().typ != scanner.EOF {
p.token('\n')
}
}
// Position returns the position of the definition.
func (d *NodesDef) Position() scanner.Position {
return d.Pos
}
// ValueDescriptionDef defines a textual description for a single signal value.
//
// The value may either be a signal raw value transferred on the bus or the value of an environment variable in a
// remaining bus simulation.
type ValueDescriptionDef struct {
Pos scanner.Position
Value float64
Description string
}
var _ Def = &ValueDescriptionDef{}
func (d *ValueDescriptionDef) parseFrom(p *Parser) {
d.Pos = p.peekToken().pos
d.Value = p.float()
d.Description = p.string()
}
// Position returns the position of the definition.
func (d *ValueDescriptionDef) Position() scanner.Position {
return d.Pos
}
// ValueTableDef defines a global value table.
//
// The value descriptions in value tables define value encodings for signal raw values.
//
// In commonly used DBC files, the global value tables aren't used, but the value descriptions are defined for each
// signal independently.
type ValueTableDef struct {
Pos scanner.Position
TableName Identifier
ValueDescriptions []ValueDescriptionDef
}
var _ Def = &ValueTableDef{}
func (d *ValueTableDef) parseFrom(p *Parser) {
d.Pos = p.keyword(KeywordValueTable).pos
d.TableName = p.identifier()
for p.peekToken().typ != ';' {
valueDescriptionDef := ValueDescriptionDef{}
valueDescriptionDef.parseFrom(p)
d.ValueDescriptions = append(d.ValueDescriptions, valueDescriptionDef)
}
p.token(';')
}
// Position returns the position of the definition.
func (d *ValueTableDef) Position() scanner.Position {
return d.Pos
}
// MessageDef defines a frame in the network.
//
// The definition includes the name of a frame as well as its properties and the signals transferred.
type MessageDef struct {
// Pos is the position of the message definition.
Pos scanner.Position
// MessageID contains the message CAN ID.
//
// The CAN ID has to be unique within the DBC file.
//
// If the most significant bit of the message ID is set, the ID is an extended CAN ID. The extended CAN ID can be
// determined by masking out the most significant bit with the mask 0xCFFFFFFF.
MessageID MessageID
// Name is the name of the message.
//
// The message name has to be unique within the DBC file.
Name Identifier
// Size specifies the size of the message in bytes.
Size uint64
// Transmitter specifies the name of the node transmitting the message.
//
// The transmitter has to be defined in the set of node names in the nodes definition.
//
// If the message has no transmitter, the string 'Vector__XXX' has to be given here.
Transmitter Identifier
// Signals specifies the signals of the message.
Signals []SignalDef
}
var _ Def = &MessageDef{}
func (d *MessageDef) parseFrom(p *Parser) {
d.Pos = p.keyword(KeywordMessage).pos
d.MessageID = p.messageID()
d.Name = p.identifier()
p.token(':')
d.Size = p.uint()
d.Transmitter = p.identifier()
for p.peekToken().typ != scanner.EOF && p.peekKeyword() == KeywordSignal {
signalDef := SignalDef{}
signalDef.parseFrom(p)
d.Signals = append(d.Signals, signalDef)
}
}
// Position returns the position of the definition.
func (d *MessageDef) Position() scanner.Position {
return d.Pos
}
// SignalDef defines a signal within a message.
type SignalDef struct {
// Pos is the position of the definition.
Pos scanner.Position
// Name of the signal.
//
// Has to be unique for all signals within the same message.
Name Identifier
// StartBit specifies the position of the signal within the data field of the frame.
//
// For signals with byte order Intel (little-endian) the position of the least-significant bit is given.
//
// For signals with byte order Motorola (big-endian) the position of the most significant bit is given.
//
// The bits are counted in a saw-tooth manner.
//
// The start bit has to be in the range of [0 ,8*message_size-1].
StartBit uint64
// Size specifies the size of the signal in bits.
Size uint64
// IsBigEndian is true if the signal's byte order is Motorola (big-endian).
IsBigEndian bool
// IsSigned is true if the signal is signed.
IsSigned bool
// IsMultiplexerSwitch is true if the signal is a multiplexer switch.
//
// A multiplexer indicator of 'M' defines the signal as the multiplexer switch.
// Only one signal within a single message can be the multiplexer switch.
IsMultiplexerSwitch bool
// IsMultiplexed is true if the signal is multiplexed by the message's multiplexer switch.
IsMultiplexed bool
// MultiplexerSwitch is the multiplexer switch value of the signal.
//
// The multiplexed signal is transferred in the message if the switch value of the multiplexer signal is equal to
// its multiplexer switch value.
MultiplexerSwitch uint64
// Offset is the signals physical value offset.
//
// Together with the factor, the offset defines the linear conversion rule to convert the signal's raw value into
// the signal's physical value and vice versa.
//
// physical_value = raw_value * factor + offset
// raw_value = (physical_value - offset) / factor
Offset float64
// Factor is the signal's physical value factor.
//
// See: Offset.
Factor float64
// Minimum defines the signal's minimum physical value.
Minimum float64
// Maximum defines the signal's maximum physical value.
Maximum float64
// Unit defines the unit of the signal's physical value.
Unit string
// Receivers specifies the nodes receiving the signal.
//
// If the signal has no receiver, the string 'Vector__XXX' has to be given here.
Receivers []Identifier
}
var _ Def = &SignalDef{}
func (d *SignalDef) parseFrom(p *Parser) {
d.Pos = p.keyword(KeywordSignal).pos
d.Name = p.identifier()
// Parse: Multiplexing
if p.peekToken().typ != ':' {
tok := p.nextToken()
if tok.typ != scanner.Ident {
p.failf(tok.pos, "expected ident")
}
switch {
case tok.txt == "M":
d.IsMultiplexerSwitch = true
case tok.txt[0] == 'm' && len(tok.txt) > 1:
d.IsMultiplexed = true
i, err := strconv.Atoi(tok.txt[1:])
if err != nil || i < 0 {
p.failf(tok.pos, "invalid multiplexer value")
}
d.MultiplexerSwitch = uint64(i)
default:
p.failf(tok.pos, "expected multiplexer")
}
}
p.token(':')
d.StartBit = p.uint()
p.token('|')
d.Size = p.uint()
p.token('@')
d.IsBigEndian = p.intInRange(0, 1) == 0
d.IsSigned = p.anyOf('-', '+') == '-'
p.token('(')
d.Factor = p.float()
p.token(',')
d.Offset = p.float()
p.token(')')
p.token('[')
d.Minimum = p.float()
p.token('|')
d.Maximum = p.float()
p.token(']')
d.Unit = p.string()
// Parse: Receivers
d.Receivers = append(d.Receivers, p.identifier())
for p.peekToken().typ == ',' {
p.token(',')
d.Receivers = append(d.Receivers, p.identifier())
}
}
// Position returns the position of the definition.
func (d *SignalDef) Position() scanner.Position {
return d.Pos
}
// SignalValueTypeDef defines an extended type definition for a signal.
type SignalValueTypeDef struct {
Pos scanner.Position
MessageID MessageID
SignalName Identifier
SignalValueType SignalValueType
}
var _ Def = &SignalValueTypeDef{}
func (d *SignalValueTypeDef) parseFrom(p *Parser) {
d.Pos = p.keyword(KeywordSignalValueType).pos
d.MessageID = p.messageID()
d.SignalName = p.identifier()
p.optionalToken(':') // SPECIAL-CASE: colon not part of spec, but encountered in the wild
d.SignalValueType = p.signalValueType()
p.token(';')
}
// Position returns the position of the definition.
func (d *SignalValueTypeDef) Position() scanner.Position {
return d.Pos
}
// MessageTransmittersDef defines multiple transmitter nodes of a single message.
//
// This definition is used to describe communication data for higher layer protocols.
//
// This is not used to define CAN layer-2 communication.
type MessageTransmittersDef struct {
Pos scanner.Position
MessageID MessageID
Transmitters []Identifier
}
var _ Def = &MessageTransmittersDef{}
func (d *MessageTransmittersDef) parseFrom(p *Parser) {
d.Pos = p.keyword(KeywordMessageTransmitters).pos
d.MessageID = p.messageID()
p.token(':')
for p.peekToken().typ != ';' {
d.Transmitters = append(d.Transmitters, p.identifier())
// SPECIAL-CASE: Comma not included in spec, but encountered in the wild
p.optionalToken(',')
}
p.token(';')
}
// Position returns the position of the definition.
func (d *MessageTransmittersDef) Position() scanner.Position {
return d.Pos
}
// ValueDescriptionsDef defines inline descriptions for specific raw signal values.
type ValueDescriptionsDef struct {
Pos scanner.Position
ObjectType ObjectType
MessageID MessageID
SignalName Identifier
EnvironmentVariableName Identifier
ValueDescriptions []ValueDescriptionDef
}
var _ Def = &ValueDescriptionsDef{}
func (d *ValueDescriptionsDef) parseFrom(p *Parser) {
d.Pos = p.keyword(KeywordValueDescriptions).pos
if p.peekToken().typ == scanner.Ident {
d.ObjectType = ObjectTypeEnvironmentVariable
d.EnvironmentVariableName = p.identifier()
} else {
d.ObjectType = ObjectTypeSignal
d.MessageID = p.messageID()
d.SignalName = p.identifier()
}
for p.peekToken().typ != ';' {
valueDescriptionDef := ValueDescriptionDef{}
valueDescriptionDef.parseFrom(p)
d.ValueDescriptions = append(d.ValueDescriptions, valueDescriptionDef)
}
p.token(';')
}
// Position returns the position of the definition.
func (d *ValueDescriptionsDef) Position() scanner.Position {
return d.Pos
}
// EnvironmentVariableDef defines an environment variable.
//
// DBC files that describe the CAN communication and don't define any additional data for system or remaining bus
// simulations don't include environment variables.
type EnvironmentVariableDef struct {
Pos scanner.Position
Name Identifier
Type EnvironmentVariableType
Minimum float64
Maximum float64
Unit string
InitialValue float64
ID uint64
AccessType AccessType
AccessNodes []Identifier
}
var _ Def = &EnvironmentVariableDef{}
func (d *EnvironmentVariableDef) parseFrom(p *Parser) {
d.Pos = p.keyword(KeywordEnvironmentVariable).pos
d.Name = p.identifier()
p.token(':')
d.Type = p.environmentVariableType()
p.token('[')
d.Minimum = p.float()
p.token('|')
d.Maximum = p.float()
p.token(']')
d.Unit = p.string()
d.InitialValue = p.float()
d.ID = p.uint()
d.AccessType = p.accessType()
d.AccessNodes = append(d.AccessNodes, p.identifier())
for p.peekToken().typ == ',' {
p.token(',')
d.AccessNodes = append(d.AccessNodes, p.identifier())
}
p.token(';')
}
// Position returns the position of the definition.
func (d *EnvironmentVariableDef) Position() scanner.Position {
return d.Pos
}
// EnvironmentVariableDataDef defines an environment variable as being of type "data".
//
// Environment variables of this type can store an arbitrary binary data of the given length.
// The length is given in bytes.
type EnvironmentVariableDataDef struct {
Pos scanner.Position
// EnvironmentVariableName is the name of the environment variable.
EnvironmentVariableName Identifier
// DataSize is the size of the environment variable data in bytes.
DataSize uint64
}
var _ Def = &EnvironmentVariableDataDef{}
func (d *EnvironmentVariableDataDef) parseFrom(p *Parser) {
d.Pos = p.keyword(KeywordEnvironmentVariableData).pos
d.EnvironmentVariableName = p.identifier()
p.token(':')
d.DataSize = p.uint()
p.token(';')
}
// Position returns the position of the definition.
func (d *EnvironmentVariableDataDef) Position() scanner.Position {
return d.Pos
}
// CommentDef defines a comment.
type CommentDef struct {
Pos scanner.Position
ObjectType ObjectType
NodeName Identifier
MessageID MessageID
SignalName Identifier
EnvironmentVariableName Identifier
Comment string
}
var _ Def = &CommentDef{}
func (d *CommentDef) parseFrom(p *Parser) {
d.Pos = p.keyword(KeywordComment).pos
d.ObjectType = p.optionalObjectType()
switch d.ObjectType {
case ObjectTypeNetworkNode:
d.NodeName = p.identifier()
case ObjectTypeMessage:
d.MessageID = p.messageID()
case ObjectTypeSignal:
d.MessageID = p.messageID()
d.SignalName = p.identifier()
case ObjectTypeEnvironmentVariable:
d.EnvironmentVariableName = p.identifier()
}
d.Comment = p.string()
p.token(';')
}
// Position returns the position of the definition.
func (d *CommentDef) Position() scanner.Position {
return d.Pos
}
// AttributeDef defines a user-defined attribute.
//
// User-defined attributes are a means to extend the object properties of the DBC file.
//
// These additional attributes have to be defined using an attribute definition with an attribute default value.
//
// For each object having a value defined for the attribute, an attribute value entry has to be defined.
//
// If no attribute value entry is defined for an object, the value of the object's attribute is the attribute's default.
type AttributeDef struct {
Pos scanner.Position
ObjectType ObjectType
Name Identifier
Type AttributeValueType
MinimumInt int64
MaximumInt int64
MinimumFloat float64
MaximumFloat float64
EnumValues []string
}
var _ Def = &AttributeDef{}
func (d *AttributeDef) parseFrom(p *Parser) {
d.Pos = p.keyword(KeywordAttribute).pos
d.ObjectType = p.optionalObjectType()
d.Name = p.stringIdentifier()
d.Type = p.attributeValueType()
switch d.Type {
case AttributeValueTypeInt, AttributeValueTypeHex:
if p.peekToken().typ != ';' {
d.MinimumInt = p.int()
d.MaximumInt = p.int()
}
case AttributeValueTypeFloat:
if p.peekToken().typ != ';' {
// SPECIAL CASE: Support attributes without min/max
d.MinimumFloat = p.float()
d.MaximumFloat = p.float()
}
case AttributeValueTypeEnum:
d.EnumValues = append(d.EnumValues, p.string())
for p.peekToken().typ == ',' {
p.token(',')
d.EnumValues = append(d.EnumValues, p.string())
}
}
p.token(';')
}
// Position returns the position of the definition.
func (d *AttributeDef) Position() scanner.Position {
return d.Pos
}
// AttributeDefaultValueDef defines the default value for an attribute.
type AttributeDefaultValueDef struct {
Pos scanner.Position
AttributeName Identifier
DefaultIntValue int64
DefaultFloatValue float64
DefaultStringValue string
}
var _ Def = &AttributeDefaultValueDef{}
func (d *AttributeDefaultValueDef) parseFrom(p *Parser) {
d.Pos = p.keyword(KeywordAttributeDefault).pos
d.AttributeName = Identifier(p.string())
// look up attribute type
for _, prevDef := range p.defs {
if attributeDef, ok := prevDef.(*AttributeDef); ok && attributeDef.Name == d.AttributeName {
switch attributeDef.Type {
case AttributeValueTypeInt, AttributeValueTypeHex:
d.DefaultIntValue = p.int()
case AttributeValueTypeFloat:
d.DefaultFloatValue = p.float()
case AttributeValueTypeString:
d.DefaultStringValue = p.string()
case AttributeValueTypeEnum:
d.DefaultStringValue = p.enumValue(attributeDef.EnumValues)
}
break
}
}
p.token(';')
}
// Position returns the position of the definition.
func (d *AttributeDefaultValueDef) Position() scanner.Position {
return d.Pos
}
// AttributeValueForObjectDef defines a value for an attribute and an object.
type AttributeValueForObjectDef struct {
Pos scanner.Position
AttributeName Identifier
ObjectType ObjectType
MessageID MessageID
SignalName Identifier
NodeName Identifier
EnvironmentVariableName Identifier
IntValue int64
FloatValue float64
StringValue string
}
var _ Def = &AttributeValueForObjectDef{}
func (d *AttributeValueForObjectDef) parseFrom(p *Parser) {
d.Pos = p.keyword(KeywordAttributeValue).pos
d.AttributeName = Identifier(p.string())
d.ObjectType = p.optionalObjectType()
switch d.ObjectType {
case ObjectTypeMessage:
d.MessageID = p.messageID()
case ObjectTypeSignal:
d.MessageID = p.messageID()
d.SignalName = p.identifier()
case ObjectTypeNetworkNode:
d.NodeName = p.identifier()
case ObjectTypeEnvironmentVariable:
d.EnvironmentVariableName = p.identifier()
}
// look up attribute type
for _, prevDef := range p.defs {
if attributeDef, ok := prevDef.(*AttributeDef); ok && attributeDef.Name == d.AttributeName {
switch attributeDef.Type {
case AttributeValueTypeInt, AttributeValueTypeHex:
d.IntValue = p.int()
case AttributeValueTypeFloat:
d.FloatValue = p.float()
case AttributeValueTypeString:
d.StringValue = p.string()
case AttributeValueTypeEnum:
d.StringValue = p.enumValue(attributeDef.EnumValues)
}
break
}
}
p.token(';')
}
// Position returns the position of the definition.
func (d *AttributeValueForObjectDef) Position() scanner.Position {
return d.Pos
}
// UnknownDef represents an unknown or unsupported DBC definition.
type UnknownDef struct {
Pos scanner.Position
Keyword Keyword
}
var _ Def = &UnknownDef{}
func (d *UnknownDef) parseFrom(p *Parser) {
tok := p.peekToken()
d.Pos = tok.pos
d.Keyword = Keyword(tok.txt)
p.discardLine()
}
// Position returns the position of the definition.
func (d *UnknownDef) Position() scanner.Position {
return d.Pos
}

View File

@@ -0,0 +1,4 @@
// Package dbc provides primitives for parsing, formatting and linting DBC files.
//
// The implementation adheres to the "DBC File Format Documentation Version 01/2007" unless specified otherwise.
package dbc

View File

@@ -0,0 +1,24 @@
package dbc
import "fmt"
// EnvironmentVariableType represents the type of an environment variable.
type EnvironmentVariableType uint64
const (
EnvironmentVariableTypeInteger EnvironmentVariableType = 0
EnvironmentVariableTypeFloat EnvironmentVariableType = 1
EnvironmentVariableTypeString EnvironmentVariableType = 2
)
// Validate returns an error for invalid environment variable types.
func (e EnvironmentVariableType) Validate() error {
switch e {
case EnvironmentVariableTypeInteger:
case EnvironmentVariableTypeFloat:
case EnvironmentVariableTypeString:
default:
return fmt.Errorf("invalid environment variable type: %v", e)
}
return nil
}

View File

@@ -0,0 +1,21 @@
package dbc
import (
"testing"
"gotest.tools/v3/assert"
)
func TestEnvironmentVariableType_Validate(t *testing.T) {
for _, tt := range []EnvironmentVariableType{
EnvironmentVariableTypeInteger,
EnvironmentVariableTypeFloat,
EnvironmentVariableTypeString,
} {
assert.NilError(t, tt.Validate())
}
}
func TestEnvironmentVariableType_Validate_Error(t *testing.T) {
assert.Error(t, EnvironmentVariableType(42).Validate(), "invalid environment variable type: 42")
}

View File

@@ -0,0 +1,74 @@
package dbc
import (
"errors"
"fmt"
"text/scanner"
)
// Error represents an error in a DBC file.
type Error interface {
error
// Position of the error in the DBC file.
Position() scanner.Position
// Reason for the error.
Reason() string
}
// validationError is an error resulting from an invalid DBC definition.
type validationError struct {
pos scanner.Position
reason string
cause error
}
func (e *validationError) Unwrap() error {
return e.cause
}
var _ Error = &validationError{}
func (e *validationError) Error() string {
return fmt.Sprintf("%v: %s (validate)", e.Position(), e.reason)
}
// Reason returns the reason for the error.
func (e *validationError) Reason() string {
return e.reason
}
// Position returns the position of the validation error in the DBC file.
//
// When the validation error results from an invalid nested definition, the position of the nested definition is
// returned.
func (e *validationError) Position() scanner.Position {
var errValidation *validationError
if errors.As(e.cause, &errValidation) {
return errValidation.Position()
}
return e.pos
}
// parseError is an error resulting from a failure to parse a DBC file.
type parseError struct {
pos scanner.Position
reason string
}
var _ Error = &parseError{}
func (e *parseError) Error() string {
return fmt.Sprintf("%v: %s (parse)", e.pos, e.reason)
}
// Reason returns the reason for the error.
func (e *parseError) Reason() string {
return e.reason
}
// Position returns the position of the parse error in the DBC file.
func (e *parseError) Position() scanner.Position {
return e.pos
}

View File

@@ -0,0 +1,11 @@
package dbc
// File is a parsed DBC source file.
type File struct {
// Name of the file.
Name string
// Data contains the raw file data.
Data []byte
// Defs in the file.
Defs []Def
}

View File

@@ -0,0 +1,36 @@
package dbc
import (
"fmt"
"github.com/fiskerinc/cloud-services/pkg/can-go/internal/identifiers"
)
// Identifier represents a DBC identifier.
type Identifier string
// maxIdentifierLength is the length of the longest valid DBC identifier.
const maxIdentifierLength = 128
// Validate returns an error for invalid DBC identifiers.
func (id Identifier) Validate() (err error) {
defer func() {
if err != nil {
err = fmt.Errorf("invalid identifier '%s': %w", id, err)
}
}()
if len(id) == 0 {
return fmt.Errorf("zero-length")
}
if len(id) > maxIdentifierLength {
return fmt.Errorf("length %v exceeds max length %v", len(id), maxIdentifierLength)
}
for i, r := range id {
if i == 0 && r != '_' && !identifiers.IsAlphaChar(r) { // first char
return fmt.Errorf("invalid first char: '%v'", r)
} else if i > 0 && r != '_' && !identifiers.IsAlphaChar(r) && !identifiers.IsNumChar(r) {
return fmt.Errorf("invalid char: '%v'", r)
}
}
return nil
}

View File

@@ -0,0 +1,42 @@
package dbc
import (
"fmt"
"strings"
"testing"
"gotest.tools/v3/assert"
)
func TestIdentifier_Validate(t *testing.T) {
for _, tt := range []Identifier{
"_",
"_foo",
"foo",
"foo32",
"_43",
Identifier(strings.Repeat("a", maxIdentifierLength)),
} {
tt := tt
t.Run(fmt.Sprintf("%v", tt), func(t *testing.T) {
assert.NilError(t, tt.Validate())
})
}
}
func TestIdentifier_Validate_Error(t *testing.T) {
for _, tt := range []Identifier{
"42",
"",
"42foo",
"☃",
"foo☃",
"foo bar",
Identifier(strings.Repeat("a", maxIdentifierLength+1)),
} {
tt := tt
t.Run(fmt.Sprintf("%v", tt), func(t *testing.T) {
assert.ErrorContains(t, tt.Validate(), "invalid identifier")
})
}
}

View File

@@ -0,0 +1,22 @@
package dbc
// Independent signals constants.
//
// DBC files may contain a special message with the following message name and message ID.
//
// This message will have size 0 and may contain duplicate signal names.
const (
// IndependentSignalsMessageName is the message name used by the special independent signals message.
IndependentSignalsMessageName Identifier = "VECTOR__INDEPENDENT_SIG_MSG"
// IndependentSignalsMessageName is the message ID used by the special independent signals message.
IndependentSignalsMessageID MessageID = 0xc0000000
// IndependentSignalsMessageSize is the size used by the special independent signals message.
IndependentSignalsMessageSize = 0
)
// IsIndependentSignalsMessage returns true if m is the special independent signals message.
func IsIndependentSignalsMessage(m *MessageDef) bool {
return m.Name == IndependentSignalsMessageName &&
m.MessageID == IndependentSignalsMessageID &&
m.Size == IndependentSignalsMessageSize
}

View File

@@ -0,0 +1,25 @@
package dbc
// Keyword represents a DBC keyword.
type Keyword string
const (
KeywordAttribute Keyword = "BA_DEF_"
KeywordAttributeDefault Keyword = "BA_DEF_DEF_"
KeywordAttributeValue Keyword = "BA_"
KeywordBitTiming Keyword = "BS_"
KeywordComment Keyword = "CM_"
KeywordEnvironmentVariable Keyword = "EV_"
KeywordEnvironmentVariableData Keyword = "ENVVAR_DATA_"
KeywordMessage Keyword = "BO_"
KeywordMessageTransmitters Keyword = "BO_TX_BU_"
KeywordNewSymbols Keyword = "NS_"
KeywordNodes Keyword = "BU_"
KeywordSignal Keyword = "SG_"
KeywordSignalGroup Keyword = "SIG_GROUP_"
KeywordSignalType Keyword = "SGTYPE_"
KeywordSignalValueType Keyword = "SIG_VALTYPE_"
KeywordValueDescriptions Keyword = "VAL_"
KeywordValueTable Keyword = "VAL_TABLE_"
KeywordVersion Keyword = "VERSION"
)

View File

@@ -0,0 +1,44 @@
package dbc
import "fmt"
// MessageID represents a message ID.
type MessageID uint32
// ID constants.
const (
// maxID is the largest valid standard CAN ID.
maxID = 0x7ff
// maxExtendedID is the largest valid extended CAN ID.
maxExtendedID = 0x1fffffff
)
// messageIDExtendedFlag is a bit flag that is set for extended message IDs.
const messageIDExtendedFlag MessageID = 0x80000000
// messageIDIndependentSignals is a special message ID used for the "independent signals" message.
const messageIDIndependentSignals MessageID = 0xc0000000
// IsExtended returns true if the message ID is an extended CAN ID.
func (m MessageID) IsExtended() bool {
return m != messageIDIndependentSignals && m&messageIDExtendedFlag > 0
}
// ToCAN returns the CAN id value of the message ID (i.e. with bit flags removed).
func (m MessageID) ToCAN() uint32 {
return uint32(m &^ messageIDExtendedFlag)
}
// Validate returns an error for invalid message IDs.
func (m MessageID) Validate() error {
if m == messageIDIndependentSignals {
return nil
}
if m.IsExtended() && m.ToCAN() > maxExtendedID {
return fmt.Errorf("invalid extended ID: %v", m)
}
if !m.IsExtended() && m.ToCAN() > maxID {
return fmt.Errorf("invalid standard ID: %v", m)
}
return nil
}

View File

@@ -0,0 +1,71 @@
package dbc
import (
"fmt"
"testing"
"gotest.tools/v3/assert"
)
func TestMessageID_Validate(t *testing.T) {
for _, tt := range []MessageID{
0,
1,
maxID,
0 | messageIDExtendedFlag,
1 | messageIDExtendedFlag,
maxID | messageIDExtendedFlag,
maxExtendedID | messageIDExtendedFlag,
messageIDIndependentSignals,
} {
tt := tt
t.Run(fmt.Sprintf("%v", tt), func(t *testing.T) {
assert.NilError(t, tt.Validate())
})
}
}
func TestMessageID_Validate_Error(t *testing.T) {
for _, tt := range []MessageID{
maxID + 1,
(maxExtendedID + 1) | messageIDExtendedFlag,
0xffffffff,
} {
tt := tt
t.Run(fmt.Sprintf("%v", tt), func(t *testing.T) {
assert.ErrorContains(t, tt.Validate(), "invalid")
})
}
}
func TestMessageID_ToCAN(t *testing.T) {
for _, tt := range []struct {
messageID MessageID
expected uint32
}{
{messageID: 1, expected: 1},
{messageID: messageIDIndependentSignals, expected: 0x40000000},
{messageID: 2566857156, expected: 419373508},
} {
tt := tt
t.Run(fmt.Sprintf("%v", tt.messageID), func(t *testing.T) {
assert.Equal(t, tt.expected, tt.messageID.ToCAN())
})
}
}
func TestMessageID_IsExtended(t *testing.T) {
for _, tt := range []struct {
messageID MessageID
expected bool
}{
{messageID: 1, expected: false},
{messageID: messageIDIndependentSignals, expected: false},
{messageID: 2566857156, expected: true},
} {
tt := tt
t.Run(fmt.Sprintf("%v", tt.messageID), func(t *testing.T) {
assert.Equal(t, tt.expected, tt.messageID.IsExtended())
})
}
}

View File

@@ -0,0 +1,28 @@
package dbc
import "fmt"
// ObjectType identifies the type of a DBC object.
type ObjectType string
const (
ObjectTypeUnspecified ObjectType = ""
ObjectTypeNetworkNode ObjectType = "BU_"
ObjectTypeMessage ObjectType = "BO_"
ObjectTypeSignal ObjectType = "SG_"
ObjectTypeEnvironmentVariable ObjectType = "EV_"
)
// Validate returns an error for invalid object types.
func (o ObjectType) Validate() error {
switch o {
case ObjectTypeUnspecified:
case ObjectTypeNetworkNode:
case ObjectTypeMessage:
case ObjectTypeSignal:
case ObjectTypeEnvironmentVariable:
default:
return fmt.Errorf("invalid object type: %v", o)
}
return nil
}

Some files were not shown because too many files have changed in this diff Show More