virtual-vehicle: add simulator for mini cluster
- Uses public dev manufacturer endpoint for cert registration - Connects to local gateway via websocket - Generates fake CAN frames and sends telemetry - Image: localhost:32000/virtual-vehicle:latest
This commit is contained in:
@@ -12,6 +12,7 @@ resources:
|
|||||||
- services/jetfire/
|
- services/jetfire/
|
||||||
- services/optimus/
|
- services/optimus/
|
||||||
- services/ota/
|
- services/ota/
|
||||||
|
- services/virtual-vehicle/
|
||||||
# - services/cost/
|
# - services/cost/
|
||||||
|
|
||||||
labels:
|
labels:
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: virtual-vehicle
|
||||||
|
namespace: cloud-services
|
||||||
|
labels:
|
||||||
|
app: virtual-vehicle
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: virtual-vehicle
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: virtual-vehicle
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: virtual-vehicle
|
||||||
|
image: localhost:32000/virtual-vehicle:latest
|
||||||
|
imagePullPolicy: IfNotPresent
|
||||||
|
env:
|
||||||
|
- name: MANUFACTURER_URL
|
||||||
|
value: "https://dev-gw.cloud.fiskerinc.com/manufacture/manufacturer"
|
||||||
|
- name: GATEWAY_WS_URL
|
||||||
|
value: "ws://gateway.cloud-services.svc.cluster.local:8077/session"
|
||||||
|
- name: API_KEY
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: trex
|
||||||
|
key: API_KEY
|
||||||
|
- name: VIN_PREFIX
|
||||||
|
value: "MINIVIRT"
|
||||||
|
- name: SEND_INTERVAL_MS
|
||||||
|
value: "1000"
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 50m
|
||||||
|
memory: 32Mi
|
||||||
|
limits:
|
||||||
|
memory: 64Mi
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
|
||||||
|
resources:
|
||||||
|
- deployment.yaml
|
||||||
|
- secret.yaml
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: trex
|
||||||
|
namespace: cloud-services
|
||||||
|
type: Opaque
|
||||||
|
stringData:
|
||||||
|
API_KEY: "349bf86d-7249-4c76-82aa-db56782703f5"
|
||||||
1
go.work
1
go.work
@@ -10,4 +10,5 @@ use (
|
|||||||
./services/jetfire
|
./services/jetfire
|
||||||
./services/optimus
|
./services/optimus
|
||||||
./services/ota_update_go
|
./services/ota_update_go
|
||||||
|
./services/virtual-vehicle
|
||||||
)
|
)
|
||||||
|
|||||||
35
services/virtual-vehicle/README.md
Normal file
35
services/virtual-vehicle/README.md
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# Virtual Vehicle Simulator
|
||||||
|
|
||||||
|
A lightweight vehicle simulator that generates CAN telemetry and sends it to the cloud gateway.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This service simulates a connected vehicle (T.Rex) by:
|
||||||
|
1. Registering with the manufacturer API to get certificates
|
||||||
|
2. Connecting to the gateway via WebSocket with mTLS
|
||||||
|
3. Generating and sending CAN frames
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
| Env Var | Description | Default |
|
||||||
|
|---------|-------------|---------|
|
||||||
|
| MANUFACTURER_URL | URL for vehicle registration | http://gateway:8077/manufacture/manufacturer |
|
||||||
|
| GATEWAY_WS_URL | WebSocket URL for gateway | ws://gateway:8077/session |
|
||||||
|
| API_KEY | API key for manufacturer endpoint | - |
|
||||||
|
| VIN_PREFIX | Prefix for generated VIN | VIRTUAL |
|
||||||
|
| SEND_INTERVAL_MS | Telemetry interval in ms | 1000 |
|
||||||
|
|
||||||
|
## CAN Frame IDs
|
||||||
|
|
||||||
|
| CAN ID | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| 0x100 | Speed |
|
||||||
|
| 0x101 | RPM |
|
||||||
|
| 0x102 | Battery SOC |
|
||||||
|
| 0x103 | Battery voltage |
|
||||||
|
| 0x104 | Temperature |
|
||||||
|
| 0x105 | GPS latitude |
|
||||||
|
| 0x106 | GPS longitude |
|
||||||
|
| 0x200 | Door status |
|
||||||
|
| 0x201 | Light status |
|
||||||
|
| 0x300 | HVAC |
|
||||||
22
services/virtual-vehicle/go.mod
Normal file
22
services/virtual-vehicle/go.mod
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
module github.com/fiskerinc/cloud-services/services/virtual-vehicle
|
||||||
|
|
||||||
|
go 1.25
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/fiskerinc/cloud-services/pkg v0.0.0
|
||||||
|
github.com/gobwas/ws v1.4.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/ReneKroon/ttlcache/v2 v2.11.0 // indirect
|
||||||
|
github.com/gobwas/httphead v0.1.0 // indirect
|
||||||
|
github.com/gobwas/pool v0.2.1 // indirect
|
||||||
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
|
github.com/rs/zerolog v1.29.1 // indirect
|
||||||
|
golang.org/x/sync v0.18.0 // indirect
|
||||||
|
golang.org/x/sys v0.38.0 // indirect
|
||||||
|
)
|
||||||
|
|
||||||
|
replace github.com/fiskerinc/cloud-services/pkg => ../../pkg
|
||||||
91
services/virtual-vehicle/go.sum
Normal file
91
services/virtual-vehicle/go.sum
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
github.com/ReneKroon/ttlcache/v2 v2.11.0 h1:OvlcYFYi941SBN3v9dsDcC2N8vRxyHcCmJb3Vl4QMoM=
|
||||||
|
github.com/ReneKroon/ttlcache/v2 v2.11.0/go.mod h1:mBxvsNY+BT8qLLd6CuAJubbKo6r0jh3nb5et22bbfGY=
|
||||||
|
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
|
||||||
|
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
|
||||||
|
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
||||||
|
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||||
|
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
|
||||||
|
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
|
||||||
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
|
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
|
||||||
|
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||||
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||||
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
|
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||||
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||||
|
github.com/rs/zerolog v1.29.1 h1:cO+d60CHkknCbvzEWxP0S9K6KqyTjrCNUy1LdQLCGPc=
|
||||||
|
github.com/rs/zerolog v1.29.1/go.mod h1:Le6ESbR7hc+DP6Lt1THiV8CQSdkkNrd3R0XbEgp3ZBU=
|
||||||
|
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.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
|
||||||
|
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||||
|
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
|
||||||
|
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
|
||||||
|
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
|
||||||
|
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
|
||||||
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
|
||||||
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
|
golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||||
|
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||||
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.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/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||||
|
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
|
golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
|
golang.org/x/tools v0.0.0-20210112230658-8b4aab62c064/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
|
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/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
283
services/virtual-vehicle/main.go
Normal file
283
services/virtual-vehicle/main.go
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"compress/flate"
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"math/rand"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/fiskerinc/cloud-services/pkg/logger"
|
||||||
|
"github.com/fiskerinc/cloud-services/pkg/utils/app"
|
||||||
|
"github.com/fiskerinc/cloud-services/pkg/utils/envtool"
|
||||||
|
"github.com/gobwas/ws"
|
||||||
|
"github.com/gobwas/ws/wsflate"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
manufacturerURL = envtool.GetEnv("MANUFACTURER_URL", "http://gateway.cloud-services.svc.cluster.local:8077/manufacture/manufacturer")
|
||||||
|
gatewayWSURL = envtool.GetEnv("GATEWAY_WS_URL", "ws://gateway.cloud-services.svc.cluster.local:8077/session")
|
||||||
|
apiKey = envtool.GetEnv("API_KEY", "")
|
||||||
|
vinPrefix = envtool.GetEnv("VIN_PREFIX", "VIRTUAL")
|
||||||
|
sendInterval = envtool.GetEnvInt("SEND_INTERVAL_MS", 1000)
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
app.Setup("virtual-vehicle", func() {})
|
||||||
|
|
||||||
|
// Generate random VIN
|
||||||
|
vin := generateVIN(vinPrefix)
|
||||||
|
logger.Info().Str("vin", vin).Msg("Generated VIN")
|
||||||
|
|
||||||
|
// Register vehicle and get certificates
|
||||||
|
cert, key, err := registerVehicle(vin)
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatal().Err(err).Msg("Failed to register vehicle")
|
||||||
|
}
|
||||||
|
logger.Info().Str("vin", vin).Msg("Vehicle registered successfully")
|
||||||
|
|
||||||
|
// Load TLS certificate
|
||||||
|
tlsCert, err := tls.X509KeyPair([]byte(cert), []byte(key))
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatal().Err(err).Msg("Failed to load TLS certificate")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect to gateway
|
||||||
|
conn, err := connectToGateway(vin, tlsCert)
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatal().Err(err).Msg("Failed to connect to gateway")
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
logger.Info().Str("vin", vin).Msg("Connected to gateway")
|
||||||
|
|
||||||
|
// Handle shutdown
|
||||||
|
sigChan := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
|
||||||
|
// Start sending telemetry
|
||||||
|
ticker := time.NewTicker(time.Duration(sendInterval) * time.Millisecond)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
if err := sendTelemetry(conn, vin); err != nil {
|
||||||
|
logger.Error().Err(err).Msg("Failed to send telemetry")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case <-sigChan:
|
||||||
|
logger.Info().Msg("Shutting down")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ManufacturerResponse struct {
|
||||||
|
Certificates []Certificate `json:"certificates"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Certificate struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
PublicKey string `json:"public_key"`
|
||||||
|
PrivateKey string `json:"private_key"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func registerVehicle(vin string) (cert, key string, err error) {
|
||||||
|
payload := map[string]string{"vin": vin}
|
||||||
|
body, _ := json.Marshal(payload)
|
||||||
|
|
||||||
|
req, err := http.NewRequest("POST", manufacturerURL, bytes.NewReader(body))
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
if apiKey != "" {
|
||||||
|
req.Header.Set("Api-Key", apiKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &http.Client{Timeout: 30 * time.Second}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode >= 300 {
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
return "", "", fmt.Errorf("manufacturer API returned %d: %s", resp.StatusCode, string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
var mfgResp ManufacturerResponse
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&mfgResp); err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range mfgResp.Certificates {
|
||||||
|
if c.Type == "TBOX" {
|
||||||
|
return c.PublicKey, c.PrivateKey, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", "", fmt.Errorf("no TBOX certificate found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func connectToGateway(vin string, tlsCert tls.Certificate) (*wsConn, error) {
|
||||||
|
dialer := ws.Dialer{
|
||||||
|
TLSConfig: &tls.Config{
|
||||||
|
Certificates: []tls.Certificate{tlsCert},
|
||||||
|
InsecureSkipVerify: true,
|
||||||
|
},
|
||||||
|
Header: ws.HandshakeHeaderHTTP(http.Header{
|
||||||
|
"User-Agent": []string{fmt.Sprintf("Fisker Ocean T.Rex 1.0.0 %s", vin)},
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, _, _, err := dialer.Dial(nil, gatewayWSURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &wsConn{conn: conn}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type wsConn struct {
|
||||||
|
conn net.Conn
|
||||||
|
writer *wsflate.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *wsConn) Close() error {
|
||||||
|
return w.conn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *wsConn) Write(data []byte) error {
|
||||||
|
// Compress the data
|
||||||
|
var buf bytes.Buffer
|
||||||
|
fw, _ := flate.NewWriter(&buf, flate.BestSpeed)
|
||||||
|
fw.Write(data)
|
||||||
|
fw.Close()
|
||||||
|
|
||||||
|
frame := ws.NewFrame(ws.OpText, true, buf.Bytes())
|
||||||
|
frame.Header.Rsv = ws.Rsv(true, false, false) // Set compression bit
|
||||||
|
return ws.WriteFrame(w.conn, frame)
|
||||||
|
}
|
||||||
|
|
||||||
|
type TelemetryMessage struct {
|
||||||
|
Handler string `json:"handler"`
|
||||||
|
Data json.RawMessage `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CANFrame struct {
|
||||||
|
ID uint32 `json:"id"`
|
||||||
|
Value []byte `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CANData struct {
|
||||||
|
Frames []CANFrame `json:"frames"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendTelemetry(conn *wsConn, vin string) error {
|
||||||
|
// Generate fake CAN frames
|
||||||
|
frames := generateFakeCANFrames()
|
||||||
|
|
||||||
|
canData := CANData{Frames: frames}
|
||||||
|
canDataBytes, _ := json.Marshal(canData)
|
||||||
|
|
||||||
|
msg := TelemetryMessage{
|
||||||
|
Handler: "can",
|
||||||
|
Data: canDataBytes,
|
||||||
|
}
|
||||||
|
|
||||||
|
msgBytes, err := json.Marshal(msg)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Debug().Str("vin", vin).Int("frames", len(frames)).Msg("Sending telemetry")
|
||||||
|
return conn.Write(msgBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateFakeCANFrames() []CANFrame {
|
||||||
|
// Generate 10-50 random CAN frames
|
||||||
|
numFrames := rand.Intn(40) + 10
|
||||||
|
frames := make([]CANFrame, numFrames)
|
||||||
|
|
||||||
|
// Common CAN IDs for vehicle telemetry
|
||||||
|
canIDs := []uint32{
|
||||||
|
0x100, // Speed
|
||||||
|
0x101, // RPM
|
||||||
|
0x102, // Battery SOC
|
||||||
|
0x103, // Battery voltage
|
||||||
|
0x104, // Temperature
|
||||||
|
0x105, // GPS lat
|
||||||
|
0x106, // GPS lon
|
||||||
|
0x200, // Door status
|
||||||
|
0x201, // Light status
|
||||||
|
0x300, // HVAC
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < numFrames; i++ {
|
||||||
|
frames[i] = CANFrame{
|
||||||
|
ID: canIDs[rand.Intn(len(canIDs))],
|
||||||
|
Value: make([]byte, 8),
|
||||||
|
}
|
||||||
|
rand.Read(frames[i].Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return frames
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateVIN(prefix string) string {
|
||||||
|
// Pad prefix to 8 chars
|
||||||
|
for len(prefix) < 8 {
|
||||||
|
prefix += "X"
|
||||||
|
}
|
||||||
|
if len(prefix) > 8 {
|
||||||
|
prefix = prefix[:8]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate remaining 9 characters (position 9 is check digit, calculated later)
|
||||||
|
chars := "ABCDEFGHJKLMNPRSTUVWXYZ0123456789"
|
||||||
|
suffix := make([]byte, 8)
|
||||||
|
for i := range suffix {
|
||||||
|
suffix[i] = chars[rand.Intn(len(chars))]
|
||||||
|
}
|
||||||
|
|
||||||
|
vin := prefix + "X" + string(suffix) // X is placeholder for check digit
|
||||||
|
|
||||||
|
// Calculate check digit
|
||||||
|
checkDigit := calculateCheckDigit(vin)
|
||||||
|
vin = vin[:8] + string(checkDigit) + vin[9:]
|
||||||
|
|
||||||
|
return vin
|
||||||
|
}
|
||||||
|
|
||||||
|
func calculateCheckDigit(vin string) byte {
|
||||||
|
weights := []int{8, 7, 6, 5, 4, 3, 2, 10, 0, 9, 8, 7, 6, 5, 4, 3, 2}
|
||||||
|
values := map[byte]int{
|
||||||
|
'A': 1, 'B': 2, 'C': 3, 'D': 4, 'E': 5, 'F': 6, 'G': 7, 'H': 8,
|
||||||
|
'J': 1, 'K': 2, 'L': 3, 'M': 4, 'N': 5, 'P': 7, 'R': 9,
|
||||||
|
'S': 2, 'T': 3, 'U': 4, 'V': 5, 'W': 6, 'X': 7, 'Y': 8, 'Z': 9,
|
||||||
|
'0': 0, '1': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9,
|
||||||
|
}
|
||||||
|
|
||||||
|
sum := 0
|
||||||
|
for i, c := range vin {
|
||||||
|
if v, ok := values[byte(c)]; ok {
|
||||||
|
sum += v * weights[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
remainder := sum % 11
|
||||||
|
if remainder == 10 {
|
||||||
|
return 'X'
|
||||||
|
}
|
||||||
|
return byte('0' + remainder)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user