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 logger.Info().Str("url", manufacturerURL).Msg("Registering with manufacturer") cert, key, err := registerVehicle(vin) if err != nil { logger.Error().Err(err).Msg("Failed to register vehicle") time.Sleep(time.Second) // Allow log to flush os.Exit(1) } logger.Info().Str("vin", vin).Msg("Vehicle registered successfully") // Load TLS certificate tlsCert, err := tls.X509KeyPair([]byte(cert), []byte(key)) if err != nil { logger.Error().Err(err).Msg("Failed to load TLS certificate") os.Exit(1) } // Connect to gateway logger.Info().Str("url", gatewayWSURL).Msg("Connecting 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) logger.Debug().Str("payload", string(body)).Msg("Creating request") req, err := http.NewRequest("POST", manufacturerURL, bytes.NewReader(body)) if err != nil { return "", "", fmt.Errorf("create request: %w", err) } req.Header.Set("Content-Type", "application/json") if apiKey != "" { req.Header.Set("Api-Key", apiKey) logger.Debug().Msg("API key set") } else { logger.Warn().Msg("No API key configured") } logger.Debug().Msg("Sending HTTP request") client := &http.Client{Timeout: 30 * time.Second} resp, err := client.Do(req) if err != nil { return "", "", fmt.Errorf("http request: %w", err) } defer resp.Body.Close() logger.Debug().Int("status", resp.StatusCode).Msg("Got response") 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) }