- 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
284 lines
6.6 KiB
Go
284 lines
6.6 KiB
Go
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)
|
|
}
|