Files
cloud-services/services/virtual-vehicle/main.go
2026-02-02 21:28:52 -05:00

297 lines
7.1 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
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)
}