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

114
README.md
View File

@@ -1,49 +1,85 @@
# cloud-services
# Cloud Services
Go microservices for the vehicle cloud platform.
## Quick Start
```bash
# Install devbox (if needed)
curl -fsSL https://get.jetify.com/devbox | bash
# Enter dev environment
devbox shell
# Run a service locally
cd services/gateway
go run .
```
Refactored cloud microservices from project-ai.
## Structure
```
services/ # Individual Go microservices
shared/ # Shared Go modules
deploy/ # Kubernetes manifests (kustomize)
base/ # Base configs
overlays/ # Environment-specific (development, etc.)
cloud-services/
├── pkg/ # Shared Go packages
│ ├── kafka/ # Pure Go Kafka client (franz-go)
├── dbc/ # CAN database signal definitions
├── can-go/ # CAN protocol library
│ └── ... # Other shared modules
├── services/
│ └── gateway/ # API gateway service
├── deploy/
│ ├── base/ # Base k8s manifests
│ └── overlays/ # Environment-specific configs
└── scripts/ # Build and utility scripts
```
## Quick Start
```bash
# Build all
go build ./...
# Build gateway
go build ./services/gateway
# Run tests
go test ./...
# Build Docker image
docker build -t gateway -f services/gateway/Dockerfile .
```
## Services
| Service | Description |
|---------|-------------|
| gateway | API gateway, routes requests |
| auth | Authentication (Keycloak integration) |
| ota | OTA update management |
| depot | Vehicle registration & management |
| attendant | Event processing |
| cargo | Data ingestion to storage |
| ditto | Digital twin state |
| manufacture | Manufacturing integration |
| aftersales | Aftersales/diagnostic services |
## Local Development
Services connect to:
- PostgreSQL: `cloud-dev-rw.cnpg-system.svc:5432`
- MongoDB: `cloud-dev-svc.mongodb.svc:27017`
- Redis: `cloud-dev.redis.svc:6379`
- Kafka: `cloud-dev-kafka-bootstrap.kafka.svc:9092`
- Keycloak: `https://keycloak.mini.cloud.fiskerinc.com`
### Gateway
WebSocket gateway for TRex, HMI, and Mobile connections. Handles auth, message routing to Kafka.
- Port 8077: HTTP/WebSocket
- Port 11011: Health check
## Development
### Prerequisites
- Go 1.25+
- Docker (for container builds)
### Module Structure
Uses Go workspaces (`go.work`) for local development:
- `./pkg` - shared packages
- `./pkg/can-go` - CAN protocol library
- `./services/gateway` - gateway service
### Generating DBC Code
CAN signal definitions are generated from DBC files. See `pkg/dbc/README.md`.
```bash
./scripts/generate-dbc.sh /path/to/dbc/files
```
## Deployment
ArgoCD syncs from this repo. Push to main → auto-deploy to mini cluster.
Kubernetes manifests in `deploy/` use Kustomize overlays:
```bash
# Development
kubectl apply -k deploy/overlays/development
# Or via ArgoCD
# See k8s-gitops-setup repo
```
## Environment Variables
| Variable | Default | Description |
|----------|---------|-------------|
| `KAFKA_HOSTS` | `localhost:9092` | Kafka brokers |
| `REDIS_HOST` | `localhost` | Redis host |
| `REDIS_PORT` | `6379` | Redis port |
| `JWK_URL` | - | JWKS endpoint for JWT validation |
| `LOG_LEVEL` | `info` | Log level |

View File

@@ -23,6 +23,7 @@ data:
# Redis
REDIS_HOST: cloud-dev.redis.svc.cluster.local
REDIS_PORT: "6379"
REDIS_PASSWORD: ""
REDIS_IDLETIMEOUT_MS: "3600000"
REDIS_MAXIDLECONN: "10"
REDIS_MAXACTIVECONN: "10"
@@ -38,6 +39,7 @@ data:
# Auth (Keycloak)
OIDC_ISSUER: https://keycloak.mini.cloud.fiskerinc.com/realms/compute-auth
OIDC_JWK_URL: https://keycloak.mini.cloud.fiskerinc.com/realms/compute-auth/protocol/openid-connect/certs
JWK_URL: https://keycloak.mini.cloud.fiskerinc.com/realms/compute-auth/protocol/openid-connect/certs
# Vault
VAULT_URL: http://vault.vault.svc.cluster.local:8200/v1

View File

@@ -6,9 +6,7 @@ namespace: cloud-services
resources:
- ../../base
- secrets.yaml
# Services (uncomment as migrated)
# - services/gateway/
# - services/auth/
- services/gateway/
commonLabels:
environment: development

View File

@@ -0,0 +1,62 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: gateway
namespace: cloud-services
labels:
app: gateway
spec:
replicas: 1
selector:
matchLabels:
app: gateway
template:
metadata:
labels:
app: gateway
spec:
containers:
- name: gateway
image: localhost:32000/gateway:latest
imagePullPolicy: Always
ports:
- containerPort: 8077
name: http
- containerPort: 11011
name: health
envFrom:
- configMapRef:
name: cloud-common-config
- secretRef:
name: cloud-db-credentials
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
memory: 256Mi
livenessProbe:
httpGet:
path: /liveness
port: 11011
initialDelaySeconds: 10
periodSeconds: 30
readinessProbe:
httpGet:
path: /readiness
port: 11011
initialDelaySeconds: 5
periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
name: gateway
namespace: cloud-services
spec:
selector:
app: gateway
ports:
- port: 8077
targetPort: 8077
name: http

View File

@@ -0,0 +1,5 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml

View File

@@ -1,6 +1,7 @@
go 1.24
go 1.25
use (
./pkg
./pkg/can-go
./services/gateway
)

View File

@@ -1,6 +1,6 @@
package adminroles
import "fiskerinc.com/modules/utils/envtool"
import "github.com/fiskerinc/cloud-services/pkg/utils/envtool"
// RoleID for groups
type RoleID string

View File

@@ -3,7 +3,7 @@ package adminroles
import (
"strings"
"fiskerinc.com/modules/validator"
"github.com/fiskerinc/cloud-services/pkg/validator"
"github.com/pkg/errors"
)

View File

@@ -3,8 +3,8 @@ package adminroles_test
import (
"testing"
"fiskerinc.com/modules/adminroles"
"fiskerinc.com/modules/testhelper"
"github.com/fiskerinc/cloud-services/pkg/adminroles"
"github.com/fiskerinc/cloud-services/pkg/testhelper"
)
const testRole = "7bcdcdb2-3279-44bf-a998-771bab4b33e1"

View File

@@ -3,8 +3,8 @@ package americanlease
import (
"testing"
"fiskerinc.com/modules/validator"
"fiskerinc.com/modules/vindecoder"
"github.com/fiskerinc/cloud-services/pkg/validator"
"github.com/fiskerinc/cloud-services/pkg/vindecoder"
)

View File

@@ -4,9 +4,9 @@ import (
"strings"
"sync"
"fiskerinc.com/modules/common"
"fiskerinc.com/modules/logger"
"fiskerinc.com/modules/utils/envtool"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/fiskerinc/cloud-services/pkg/utils/envtool"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/cognitoidentityprovider"

View File

@@ -7,14 +7,14 @@ import (
"net/http"
"strings"
"fiskerinc.com/modules/common"
"fiskerinc.com/modules/db/queries"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/db/queries"
"fiskerinc.com/modules/httpclient"
"fiskerinc.com/modules/jwt"
"fiskerinc.com/modules/logger"
"fiskerinc.com/modules/utils/envtool"
"fiskerinc.com/modules/utils"
"github.com/fiskerinc/cloud-services/pkg/httpclient"
"github.com/fiskerinc/cloud-services/pkg/jwt"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/fiskerinc/cloud-services/pkg/utils/envtool"
"github.com/fiskerinc/cloud-services/pkg/utils"
)
var getUserURL string = envtool.GetEnv("AUTH_GET_USER", "https://dev-auth.fiskerdps.com/auth/me")

View File

@@ -10,10 +10,10 @@ import (
"sync"
"time"
"fiskerinc.com/modules/httpclient"
"fiskerinc.com/modules/jwt"
"fiskerinc.com/modules/logger"
"fiskerinc.com/modules/utils/envtool"
"github.com/fiskerinc/cloud-services/pkg/httpclient"
"github.com/fiskerinc/cloud-services/pkg/jwt"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/fiskerinc/cloud-services/pkg/utils/envtool"
"github.com/pkg/errors"
)

View File

@@ -6,10 +6,10 @@ import (
"net/http"
"testing"
auth "fiskerinc.com/modules/auth"
"fiskerinc.com/modules/httpclient"
"fiskerinc.com/modules/httpclient/mock"
"fiskerinc.com/modules/testhelper"
auth "github.com/fiskerinc/cloud-services/pkg/auth"
"github.com/fiskerinc/cloud-services/pkg/httpclient"
"github.com/fiskerinc/cloud-services/pkg/httpclient/mock"
"github.com/fiskerinc/cloud-services/pkg/testhelper"
)
const responseUserConsentJSON = `{"access_token":"eyJraWQiOiJqSXowUVRjc0tDVCtoeEd6MlMwK0NoUHlON3c4cmlQXC9sNm1xekFYUmw2bz0iLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIyZGQ2ZmVkOS1lNTgyLTQ1MWItYTkzYi01Yjk0MTBkZmJjNDMiLCJjb2duaXRvOmdyb3VwcyI6WyJ1cy13ZXN0LTJfQVd3akxYeW0yX0F6dXJlQUQiXSwidG9rZW5fdXNlIjoiYWNjZXNzIiwic2NvcGUiOiJodHRwczpcL1wvZmlza2VyaW5jLmNvbVwvb3RhdXBkYXRlLnJlYWQgaHR0cHM6XC9cL2Zpc2tlcmluYy5jb21cL290YXVwZGF0ZS5jcmVhdGUgb3BlbmlkIGVtYWlsIiwiYXV0aF90aW1lIjoxNjEzNjA3NTc2LCJpc3MiOiJodHRwczpcL1wvY29nbml0by1pZHAudXMtd2VzdC0yLmFtYXpvbmF3cy5jb21cL3VzLXdlc3QtMl9BV3dqTFh5bTIiLCJleHAiOjE2MTM2MTExNzYsImlhdCI6MTYxMzYwNzU3NiwidmVyc2lvbiI6MiwianRpIjoiYzUyNjI0YjItYmJkYi00N2RiLTllNTgtOGU5ZmU3Yjg1ODMxIiwiY2xpZW50X2lkIjoiN2NrMnRmb3FhdmM3MmM0NWhoN3RnZTQya2QiLCJ1c2VybmFtZSI6ImF6dXJlYWRfand1QGZpc2tlcmluYy5jb20ifQ.FvlES5AgjhymQKnHP41D2Ude0Ten6L8REBRXTyu5dyWGrG4vTfBGoxlkGE2-MEFc0s6uhbdST_E2Mc5QNlXG47ibK14tFl6kOqDd74TCfg5sWghb_nSjC-M769eUHQSQcs4L8jcnEt0bjqMmPtt8lZwu3VS7mkSRXD6_hX43rPLGUpMaz5RqKlfHX8YUyD6UnENW9Gg3zonPRsPWVtupc494B_pSZGuFs-jVzBDgb_SdrGt5wb3GazsNcB8KeAf0m0QoEiApsCYxKGUG9eQZw_CAUrhCj9mFT-xJuyvEp0t6B8HDHrdW4mIHblKqhZok1mPwCntJmOfyOs3niNaILg","id_token":"eyJraWQiOiJlUTNuZFJLaUVcL084VUZ5RHFsYjN0S1RzWG00SzVPMlc4NXd3VWkzT2tNZz0iLCJhbGciOiJSUzI1NiJ9.eyJhdF9oYXNoIjoiMHFmcmdyVlZfOW1XRWp5MVdOZDl5QSIsInN1YiI6IjJkZDZmZWQ5LWU1ODItNDUxYi1hOTNiLTViOTQxMGRmYmM0MyIsImNvZ25pdG86Z3JvdXBzIjpbInVzLXdlc3QtMl9BV3dqTFh5bTJfQXp1cmVBRCJdLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsImlzcyI6Imh0dHBzOlwvXC9jb2duaXRvLWlkcC51cy13ZXN0LTIuYW1hem9uYXdzLmNvbVwvdXMtd2VzdC0yX0FXd2pMWHltMiIsImNvZ25pdG86dXNlcm5hbWUiOiJhenVyZWFkX2p3dUBmaXNrZXJpbmMuY29tIiwiYXVkIjoiN2NrMnRmb3FhdmM3MmM0NWhoN3RnZTQya2QiLCJpZGVudGl0aWVzIjpbeyJ1c2VySWQiOiJqd3VAZmlza2VyaW5jLmNvbSIsInByb3ZpZGVyTmFtZSI6IkF6dXJlQUQiLCJwcm92aWRlclR5cGUiOiJTQU1MIiwiaXNzdWVyIjoiaHR0cHM6XC9cL3N0cy53aW5kb3dzLm5ldFwvNWFhNGI2NDAtYzlmYy00YTliLWIzYTMtZDRhN2QwMDhmYjVlXC8iLCJwcmltYXJ5IjoidHJ1ZSIsImRhdGVDcmVhdGVkIjoiMTYxMjkwMjQxMzM4MyJ9XSwidG9rZW5fdXNlIjoiaWQiLCJhdXRoX3RpbWUiOjE2MTM2MDc1NzYsImV4cCI6MTYxMzYxMTE3NiwiaWF0IjoxNjEzNjA3NTc2LCJlbWFpbCI6Imp3dUBmaXNrZXJpbmMuY29tIn0.NbEWEgX48Z-zz3gREEH44OpnvhoYDcm9RlVdqKVoSJ777g0A0LDpGwz7UGcqvZLeQLPsHaMyV8-sblLvKQvpsenJfq81XddVWCAqI55VCdbnouCphIDYOEPNbWs9ORdrXxciALTt1AAehsF0dTDG0V5fce5Vku2qZZbpELdq9r4CBJQXWtFiV8lUaZPEMNJbZVdh1KjwJSpeF8CtJGKUXIIm6tAYVVKc27YWgxe2fh3zhke5MUnGYrb98-RLmDUwpUQ4eBnXu9gtA-9qIpOumXkftogWpeNZ7Rc0tAI8ZvOmG8plFyYoRMrKuC4kECeUdrsRJlCv4ijpK_L7GwEL9Q","refresh_token":"eyJjdHkiOiJKV1QiLCJlbmMiOiJBMjU2R0NNIiwiYWxnIjoiUlNBLU9BRVAifQ.LHo8ysGz7T3sJtwf8aHpWFzH8B84yDvfL4Q0YuRd0kfKSA51z4hKFPSLpo4PiFodJ-VPugJQWfSYXXpe4Tjd3bdTH-oYDJcJvRHIV3ZIID0EApt53lkxsFWV_9b33bltLYyJ7DnclQq1GnfgohDhD9F2CpnN3Xa-ntVmF9ntLe6wxZvk_zdBlbhIPwCc4FuPDIB_skNaciWCzU9LUfzvcfZJAR8KztM8ofDm3YJGZrRJltz6In78ZlN1sIlFuPSIRy56sg3yG3wMfe9Lrst0VacG_2fy6Ccg9VuLqD_xnzMmzjwMF9PGdnO5DlCblWwrHsDE6FkTuDy7ojnPJpPJlw.gzFbDwAmMKp-4eiE.AfY0IecXygmkDUkUGIIG7JmhBSzk4VD_sEAwuTeOufKD_duvNXFTQYNU_QvDc7M-9Vssbbb35dMMw3KLxW2IbC7fll8lNvHHMm1gkxlVxK1h5uRhmgt0q7tyMwLw1iKUDqOa177faHZJISN_gvfh-rlbNswswDGU061dyFh-w6Ck8SXoPnWfp9GxZJBgxzZ5uBV1D7_1bAghqWYNMsMUTSvOYyeWvVJHap-gjtGc491Vf97z6mh9PDBvIi734D90NbV5idZ11CCW7liI5L7kgRwuHZVxiu_NpkPED7dWcaBhOATur4r3P28U39JC5P5FD4JXlqyPl9FXVBkW049E1vdJrrkV3IbiqUMVXlkUeq6G87YUTdmt8qRPgiOc-G6g84RxSPQE55uojbuSSlON2CKZYmSmFVM0X7bBU42wP1wNP7Jq3LTjHcj4rOaN1ozffJxyGs54r7NP4D9u3nt2ozNkjk_DNK3UmxDPaQtZAtFO1d-T7UXv2BvzoCN2LGilzxVi04p9LcvoTDzI5GUY9OsjGUsdSZJvISylHAMMDi8nSxsBBSPD18fzV0tLhdjGM0-XljiM4bjZWNR4Nvraus33p8U4k5lmn2bx13JfHvDa3Zqf_aK57lam6Zf_6mvAK4I7A40WmiolJCxeEeDD54ljF0kAluT4sw6sxVY8It80A95TGFd0lm5e-tGrFKIoqRyPV5uwzzz3XT1HVPJda1ufdGhSUj8slsyqTUrnphh6JWbRfA9mrLdKQKuqM3xEslAwYZOhX6qOzADbo5WQMneTwn34QixMT4A6imaDBc6P4cOaLo7hNyS3e1h6SfwigEX9H42wkC3TWOiITakFq3tKkVwahMkdeds_uxloNoeicdGePjob6BfU8xq0IKxJh9UoeCsSX4KVtIrErHyYuoU-_ENZXYArSwfqorKgdjmQAa13NQjOiHzpgA5HngSCK1xy6zq9NvNA7hUe9O0gTqrFZDsYhRWSYEuOt5QpxJYalPGKIPXlsUJOURfR_J0iT52UxiOZIuXmpk-X8fgDhM_0fZm8GQ2GaIsf3nR49h7QnZRG9azTZV5q9Bs0bqPvP7wRL5xenByeIBsdwP6Bwaqd3n5BkFn-LE7eo6UPn_9o7Gx3g9VlN8pG7SIo_3a07L0yauJIO4ahL5aC07uBCLu3pJQW0ftlVpLdAA8gh3XPhfvOuH4XV7yU1fQhqGKich1hhHx2dFHyVr5mJPxc1VQvAyyAhxjvyQ2TTLfvSiYLOP1vFCVjUwb_RjF5tuh3ArH9IlXH9kIBVQbiSOWfgQ1PqD-go4jqDR_ie3aGc5Fm8Vd6lSh_2HW2GR0Ht0ASoCbl3C5roXCRkyTTaGl3nX6uwfg.thw4B0ug4OIZsZrDzCtJcQ","expires_in": 3600,"token_type": "Bearer"}`

View File

@@ -8,7 +8,7 @@ import (
"strings"
"time"
"fiskerinc.com/modules/logger"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/Azure/azure-storage-blob-go/azblob"
"github.com/pkg/errors"
)

View File

@@ -4,10 +4,10 @@ import (
"sync"
"time"
"fiskerinc.com/modules/common"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/pkg/errors"
"fiskerinc.com/modules/db/queries"
"github.com/fiskerinc/cloud-services/pkg/db/queries"
"github.com/ReneKroon/ttlcache/v2"
)

View File

@@ -3,13 +3,13 @@ package cache_test
import (
"testing"
"fiskerinc.com/modules/adminroles"
"fiskerinc.com/modules/cache"
"fiskerinc.com/modules/common"
"fiskerinc.com/modules/db"
"fiskerinc.com/modules/db/queries"
"fiskerinc.com/modules/redis"
"fiskerinc.com/modules/testhelper"
"github.com/fiskerinc/cloud-services/pkg/adminroles"
"github.com/fiskerinc/cloud-services/pkg/cache"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/db"
"github.com/fiskerinc/cloud-services/pkg/db/queries"
"github.com/fiskerinc/cloud-services/pkg/redis"
"github.com/fiskerinc/cloud-services/pkg/testhelper"
)
func TestIntegration(t *testing.T) {

View File

@@ -1,7 +1,7 @@
package cache
import (
"fiskerinc.com/modules/common"
"github.com/fiskerinc/cloud-services/pkg/common"
)
type CarDTCsCacheInterface interface {

View File

@@ -3,8 +3,8 @@ package cache_test
import (
"testing"
m "fiskerinc.com/modules/common"
"fiskerinc.com/modules/cache"
m "github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/cache"
"github.com/stretchr/testify/assert"
)

View File

@@ -7,10 +7,10 @@ import (
"strings"
"time"
"fiskerinc.com/modules/dbc/state"
"fiskerinc.com/modules/logger"
"fiskerinc.com/modules/redis"
"fiskerinc.com/modules/utils/querystring"
"github.com/fiskerinc/cloud-services/pkg/dbc/state"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/fiskerinc/cloud-services/pkg/redis"
"github.com/fiskerinc/cloud-services/pkg/utils/querystring"
redigo "github.com/gomodule/redigo/redis"
"github.com/pkg/errors"
)

View File

@@ -1,10 +1,10 @@
package cache
import (
"fiskerinc.com/modules/common"
"fiskerinc.com/modules/db/queries"
"fiskerinc.com/modules/logger"
"fiskerinc.com/modules/redis"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/db/queries"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/fiskerinc/cloud-services/pkg/redis"
"github.com/pkg/errors"
)

View File

@@ -4,12 +4,12 @@ import (
"encoding/json"
"testing"
"fiskerinc.com/modules/cache"
"fiskerinc.com/modules/db/queries"
"fiskerinc.com/modules/db/queries/mocks"
"fiskerinc.com/modules/redis"
"fiskerinc.com/modules/redis/tester"
"fiskerinc.com/modules/testhelper"
"github.com/fiskerinc/cloud-services/pkg/cache"
"github.com/fiskerinc/cloud-services/pkg/db/queries"
"github.com/fiskerinc/cloud-services/pkg/db/queries/mocks"
"github.com/fiskerinc/cloud-services/pkg/redis"
"github.com/fiskerinc/cloud-services/pkg/redis/tester"
"github.com/fiskerinc/cloud-services/pkg/testhelper"
)
var mockRedis redis.Client

View File

@@ -3,10 +3,10 @@ package cache
import (
"encoding/json"
"fiskerinc.com/modules/common"
"fiskerinc.com/modules/db/queries"
"fiskerinc.com/modules/logger"
"fiskerinc.com/modules/redis"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/db/queries"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/fiskerinc/cloud-services/pkg/redis"
r "github.com/gomodule/redigo/redis"
"github.com/pkg/errors"

View File

@@ -3,12 +3,12 @@ package cache_test
import (
"testing"
"fiskerinc.com/modules/cache"
"fiskerinc.com/modules/common"
"fiskerinc.com/modules/db"
"fiskerinc.com/modules/db/queries"
"fiskerinc.com/modules/db/queries/mocks"
"fiskerinc.com/modules/redis"
"github.com/fiskerinc/cloud-services/pkg/cache"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/db"
"github.com/fiskerinc/cloud-services/pkg/db/queries"
"github.com/fiskerinc/cloud-services/pkg/db/queries/mocks"
"github.com/fiskerinc/cloud-services/pkg/redis"
)
func TestRetrieveFileEncryptionParams(t *testing.T) {

View File

@@ -1,8 +1,8 @@
package cache
import (
"fiskerinc.com/modules/common"
"fiskerinc.com/modules/redis"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/redis"
)
func FillCarFilterOnline(redisCLI redis.Client, filter *common.CarSearch) error {

View File

@@ -3,9 +3,9 @@ package cache_test
import (
"testing"
"fiskerinc.com/modules/cache"
m "fiskerinc.com/modules/common"
"fiskerinc.com/modules/redis/tester"
"github.com/fiskerinc/cloud-services/pkg/cache"
m "github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/redis/tester"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
)

View File

@@ -5,7 +5,7 @@ import (
"strconv"
"testing"
"fiskerinc.com/modules/cache"
"github.com/fiskerinc/cloud-services/pkg/cache"
"github.com/stretchr/testify/assert"
)

View File

@@ -3,11 +3,11 @@ package cache
import (
"encoding/json"
"fiskerinc.com/modules/common"
"fiskerinc.com/modules/db/queries"
"fiskerinc.com/modules/duration"
"fiskerinc.com/modules/logger"
"fiskerinc.com/modules/redis"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/db/queries"
"github.com/fiskerinc/cloud-services/pkg/duration"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/fiskerinc/cloud-services/pkg/redis"
"github.com/google/uuid"
"github.com/pkg/errors"
)

View File

@@ -4,11 +4,11 @@ import (
"fmt"
"testing"
"fiskerinc.com/modules/cache"
"fiskerinc.com/modules/common"
"fiskerinc.com/modules/db/queries"
"fiskerinc.com/modules/redis"
"fiskerinc.com/modules/testhelper"
"github.com/fiskerinc/cloud-services/pkg/cache"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/db/queries"
"github.com/fiskerinc/cloud-services/pkg/redis"
"github.com/fiskerinc/cloud-services/pkg/testhelper"
"github.com/google/uuid"
)

View File

@@ -3,13 +3,13 @@ package cache
import (
"encoding/json"
"fiskerinc.com/modules/common"
"fiskerinc.com/modules/logger"
"fiskerinc.com/modules/mongo"
"fiskerinc.com/modules/redis"
"fiskerinc.com/modules/utils/envtool"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/fiskerinc/cloud-services/pkg/mongo"
"github.com/fiskerinc/cloud-services/pkg/redis"
"github.com/fiskerinc/cloud-services/pkg/utils/envtool"
"fiskerinc.com/modules/utils/elptr"
"github.com/fiskerinc/cloud-services/pkg/utils/elptr"
redigo "github.com/gomodule/redigo/redis"
"github.com/pkg/errors"
)

View File

@@ -5,12 +5,12 @@ import (
"sort"
"testing"
"fiskerinc.com/modules/cache"
"fiskerinc.com/modules/common"
"fiskerinc.com/modules/mongo"
"fiskerinc.com/modules/redis"
"fiskerinc.com/modules/testhelper"
"fiskerinc.com/modules/utils/elptr"
"github.com/fiskerinc/cloud-services/pkg/cache"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/mongo"
"github.com/fiskerinc/cloud-services/pkg/redis"
"github.com/fiskerinc/cloud-services/pkg/testhelper"
"github.com/fiskerinc/cloud-services/pkg/utils/elptr"
redigo "github.com/gomodule/redigo/redis"
"github.com/stretchr/testify/assert"
)

View File

@@ -6,11 +6,11 @@ import (
"strings"
"time"
"fiskerinc.com/modules/common"
dt "fiskerinc.com/modules/dbc/state"
"fiskerinc.com/modules/logger"
"fiskerinc.com/modules/redis"
"fiskerinc.com/modules/utils/querystring"
"github.com/fiskerinc/cloud-services/pkg/common"
dt "github.com/fiskerinc/cloud-services/pkg/dbc/state"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/fiskerinc/cloud-services/pkg/redis"
"github.com/fiskerinc/cloud-services/pkg/utils/querystring"
redigo "github.com/gomodule/redigo/redis"
"github.com/pkg/errors"

View File

@@ -1,9 +1,9 @@
package cache
import (
"fiskerinc.com/modules/common"
"fiskerinc.com/modules/logger"
"fiskerinc.com/modules/redis"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/fiskerinc/cloud-services/pkg/redis"
redigo "github.com/gomodule/redigo/redis"
"github.com/pkg/errors"

View File

@@ -5,10 +5,10 @@ import (
"testing"
"time"
"fiskerinc.com/modules/cache"
"fiskerinc.com/modules/common"
"fiskerinc.com/modules/redis"
"fiskerinc.com/modules/redis/tester"
"github.com/fiskerinc/cloud-services/pkg/cache"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/redis"
"github.com/fiskerinc/cloud-services/pkg/redis/tester"
"github.com/stretchr/testify/assert"
)

View File

@@ -2,8 +2,8 @@ package cache
import (
"encoding/json"
"fiskerinc.com/modules/common"
orm "fiskerinc.com/modules/db/queries"
"github.com/fiskerinc/cloud-services/pkg/common"
orm "github.com/fiskerinc/cloud-services/pkg/db/queries"
"fmt"
"github.com/ReneKroon/ttlcache/v2"
"github.com/pkg/errors"

8
pkg/cache/verify.go vendored
View File

@@ -1,10 +1,10 @@
package cache
import (
"fiskerinc.com/modules/common"
"fiskerinc.com/modules/db/queries"
"fiskerinc.com/modules/logger"
"fiskerinc.com/modules/redis"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/db/queries"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/fiskerinc/cloud-services/pkg/redis"
redigo "github.com/gomodule/redigo/redis"
"github.com/pkg/errors"

View File

@@ -3,12 +3,12 @@ package cache_test
import (
"testing"
"fiskerinc.com/modules/cache"
"fiskerinc.com/modules/common"
"fiskerinc.com/modules/db/queries/mocks"
"fiskerinc.com/modules/redis"
"fiskerinc.com/modules/redis/tester"
"fiskerinc.com/modules/testhelper"
"github.com/fiskerinc/cloud-services/pkg/cache"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/db/queries/mocks"
"github.com/fiskerinc/cloud-services/pkg/redis"
"github.com/fiskerinc/cloud-services/pkg/redis/tester"
"github.com/fiskerinc/cloud-services/pkg/testhelper"
redigo "github.com/gomodule/redigo/redis"
)

8
pkg/cache/vins.go vendored
View File

@@ -3,10 +3,10 @@ package cache
import (
"errors"
"fiskerinc.com/modules/common"
"fiskerinc.com/modules/db/queries"
"fiskerinc.com/modules/logger"
"fiskerinc.com/modules/redis"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/db/queries"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/fiskerinc/cloud-services/pkg/redis"
)
// RetrieveVINs retrieves VINs from redis or from DB based on driver ID and proceeds to cache VINs

View File

@@ -4,9 +4,9 @@ import (
"encoding/json"
"testing"
"fiskerinc.com/modules/cache"
"fiskerinc.com/modules/redis"
"fiskerinc.com/modules/testhelper"
"github.com/fiskerinc/cloud-services/pkg/cache"
"github.com/fiskerinc/cloud-services/pkg/redis"
"github.com/fiskerinc/cloud-services/pkg/testhelper"
)
type mockRedisCacheVINs struct {

View File

@@ -7,10 +7,10 @@ import (
"strings"
"time"
"fiskerinc.com/modules/dbc/state"
"fiskerinc.com/modules/logger"
"fiskerinc.com/modules/redis"
"fiskerinc.com/modules/utils/querystring"
"github.com/fiskerinc/cloud-services/pkg/dbc/state"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/fiskerinc/cloud-services/pkg/redis"
"github.com/fiskerinc/cloud-services/pkg/utils/querystring"
redigo "github.com/gomodule/redigo/redis"
"github.com/pkg/errors"
)

View File

@@ -1,10 +1,10 @@
package cachev2
import (
"fiskerinc.com/modules/common"
"fiskerinc.com/modules/db/queries"
"fiskerinc.com/modules/logger"
redis "fiskerinc.com/modules/redisv2"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/db/queries"
"github.com/fiskerinc/cloud-services/pkg/logger"
redis "github.com/fiskerinc/cloud-services/pkg/redisv2"
"github.com/pkg/errors"
)

View File

@@ -4,12 +4,12 @@ import (
"encoding/json"
"testing"
cache "fiskerinc.com/modules/cachev2"
"fiskerinc.com/modules/db/queries"
"fiskerinc.com/modules/db/queries/mocks"
"fiskerinc.com/modules/redis/tester"
redis "fiskerinc.com/modules/redisv2"
"fiskerinc.com/modules/testhelper"
cache "github.com/fiskerinc/cloud-services/pkg/cachev2"
"github.com/fiskerinc/cloud-services/pkg/db/queries"
"github.com/fiskerinc/cloud-services/pkg/db/queries/mocks"
"github.com/fiskerinc/cloud-services/pkg/redis/tester"
redis "github.com/fiskerinc/cloud-services/pkg/redisv2"
"github.com/fiskerinc/cloud-services/pkg/testhelper"
)
var mockRedis redis.Client

View File

@@ -3,13 +3,13 @@ package cachev2
import (
"encoding/json"
"fiskerinc.com/modules/common"
"fiskerinc.com/modules/logger"
"fiskerinc.com/modules/mongo"
"fiskerinc.com/modules/redis"
"fiskerinc.com/modules/utils/envtool"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/fiskerinc/cloud-services/pkg/mongo"
"github.com/fiskerinc/cloud-services/pkg/redis"
"github.com/fiskerinc/cloud-services/pkg/utils/envtool"
"fiskerinc.com/modules/utils/elptr"
"github.com/fiskerinc/cloud-services/pkg/utils/elptr"
redigo "github.com/gomodule/redigo/redis"
"github.com/pkg/errors"
)

View File

@@ -5,12 +5,12 @@ import (
"sort"
"testing"
cache "fiskerinc.com/modules/cachev2"
"fiskerinc.com/modules/common"
"fiskerinc.com/modules/mongo"
"fiskerinc.com/modules/redis"
"fiskerinc.com/modules/testhelper"
"fiskerinc.com/modules/utils/elptr"
cache "github.com/fiskerinc/cloud-services/pkg/cachev2"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/mongo"
"github.com/fiskerinc/cloud-services/pkg/redis"
"github.com/fiskerinc/cloud-services/pkg/testhelper"
"github.com/fiskerinc/cloud-services/pkg/utils/elptr"
redigo "github.com/gomodule/redigo/redis"
"github.com/stretchr/testify/assert"
)

View File

@@ -6,11 +6,11 @@ import (
"strings"
"time"
"fiskerinc.com/modules/common"
dt "fiskerinc.com/modules/dbc/state"
"fiskerinc.com/modules/logger"
redis "fiskerinc.com/modules/redisv2"
"fiskerinc.com/modules/utils/querystring"
"github.com/fiskerinc/cloud-services/pkg/common"
dt "github.com/fiskerinc/cloud-services/pkg/dbc/state"
"github.com/fiskerinc/cloud-services/pkg/logger"
redis "github.com/fiskerinc/cloud-services/pkg/redisv2"
"github.com/fiskerinc/cloud-services/pkg/utils/querystring"
redispkg "github.com/redis/go-redis/v9"
"github.com/pkg/errors"

View File

@@ -3,12 +3,12 @@ package cachev2
import (
"context"
"fiskerinc.com/modules/common"
"fiskerinc.com/modules/logger"
redis "fiskerinc.com/modules/redisv2"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/logger"
redis "github.com/fiskerinc/cloud-services/pkg/redisv2"
"github.com/pkg/errors"
"fiskerinc.com/modules/db/queries"
"github.com/fiskerinc/cloud-services/pkg/db/queries"
)
func GetVINListDigitalTwin(vins []string, redisClient *redis.Connection) (digitalTwins map[string]common.CarState, errorList []error) {

View File

@@ -5,9 +5,9 @@ import (
"testing"
"time"
cache "fiskerinc.com/modules/cachev2"
"fiskerinc.com/modules/common"
redis "fiskerinc.com/modules/redisv2"
cache "github.com/fiskerinc/cloud-services/pkg/cachev2"
"github.com/fiskerinc/cloud-services/pkg/common"
redis "github.com/fiskerinc/cloud-services/pkg/redisv2"
"github.com/go-redis/redismock/v9"
"github.com/stretchr/testify/assert"
)

View File

@@ -4,9 +4,9 @@ import (
"context"
"errors"
"fiskerinc.com/modules/common"
redis "fiskerinc.com/modules/redisv2"
"fiskerinc.com/modules/utils/elptr"
"github.com/fiskerinc/cloud-services/pkg/common"
redis "github.com/fiskerinc/cloud-services/pkg/redisv2"
"github.com/fiskerinc/cloud-services/pkg/utils/elptr"
)
func GetTowManDigitalTwin(vin string, redisClient *redis.Connection)(tdt common.TowmanDigitalTwin, err error){

View File

@@ -3,9 +3,9 @@ package cachev2
import (
"fmt"
"fiskerinc.com/modules/common"
"fiskerinc.com/modules/db/queries"
redis "fiskerinc.com/modules/redisv2"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/db/queries"
redis "github.com/fiskerinc/cloud-services/pkg/redisv2"
"github.com/pkg/errors"
)

View File

@@ -3,12 +3,12 @@ package cachev2_test
import (
"testing"
"fiskerinc.com/modules/cache"
"fiskerinc.com/modules/common"
"fiskerinc.com/modules/db/queries/mocks"
"fiskerinc.com/modules/redis"
"fiskerinc.com/modules/redis/tester"
"fiskerinc.com/modules/testhelper"
"github.com/fiskerinc/cloud-services/pkg/cache"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/db/queries/mocks"
"github.com/fiskerinc/cloud-services/pkg/redis"
"github.com/fiskerinc/cloud-services/pkg/redis/tester"
"github.com/fiskerinc/cloud-services/pkg/testhelper"
redigo "github.com/gomodule/redigo/redis"
)

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
}

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