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:
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