Initial cloud-services repo - gateway service + pkg modules

This commit is contained in:
Chris Rai
2026-01-30 23:14:52 -05:00
commit fbb820d7b3
1037 changed files with 171318 additions and 0 deletions

View File

@@ -0,0 +1,37 @@
package actionlogger
import (
"time"
"fiskerinc.com/modules/common/dbbasemodel"
"github.com/google/uuid"
)
// Use Action Log to track actions taken against a car including remote commands, car updates etc. Mostly worried about car commands for now
type ActionLog struct {
VIN string
UserIdentifier string
//UserType string // Not sure how different users can be identified between fisker customers and api admins
Action Action // Short Hand of description
Description string // JSON string explaining full action
// TrackingID *uuid.UUID // If we want to log the same action as it goes through, this tracking ID will follow the same request through
CallLocation string // Informative field where we created this. Can use runtime reflection if we want, but is more expensive
dbbasemodel.DBModelBase
}
type Action string
const (
RemoteCommand Action = "RemoteCommand"
CarConfigurationUpdate Action = "CarConfigurationUpdate"
CarUpdate Action = "CarUpdate"
)
type ActionLogFilter struct {
VINs []string `pg:",array"`
Actions []string `pg:",array"`
Before time.Time
After time.Time
Limit int
TrackingID *uuid.UUID
}

View File

@@ -0,0 +1,28 @@
package common
type AddCarRequest struct {
VIN string `json:"vin" validate:"required,vin"`
LogLevel LogLevel `json:"log_level,omitempty" swaggertype:"string"`
CANBus *CANBus `json:"canbus,omitempty"`
IDPSEnabled bool `json:"idps_enabled,omitempty"`
}
type UpdateCarRequest struct {
VIN string `json:"vin" validate:"required,vin"`
ICCID string `json:"iccid,omitempty" validate:"omitempty,max=50"`
Year int `json:"year,omitempty" validate:"required,gte=1000,lte=9999"`
Model string `json:"model,omitempty" validate:"required,max=256"`
Trim string `json:"trim,omitempty" validate:"required,max=256"`
Country string `json:"country,omitempty" validate:"max=256"`
Powertrain string `json:"powertrain,omitempty" validate:"max=256"`
Restraint string `json:"restraint,omitempty" validate:"max=256"`
BodyType string `json:"body_type,omitempty" validate:"max=256"`
LogLevel LogLevel `json:"log_level,omitempty" swaggertype:"string"`
DLTEnabled bool `json:"dlt_enabled,omitempty" swaggertype:"boolean"`
DLTLevel int `json:"dlt_level,omitempty" validate:"oneof=0 1 2 3 4 5 6 255"`
CANBus *CANBus `json:"canbus,omitempty"`
IDPSEnabled bool `json:"idps_enabled,omitempty"`
DebugMask string `json:"debug_mask,omitempty"`
Tags []string `json:"tags,omitempty"`
SUMSVersion string `json:"sums_version,omitempty"`
}

26
pkg/common/apicalls.go Normal file
View File

@@ -0,0 +1,26 @@
package common
import "time"
type AccessType string
const AccessTypeJWT = "jwt"
const AccessTypeAPIToken = "api_token"
type APICall struct {
// ClientID can be email or api_token.
ClientID string `json:"client_id" pg:"client_id"`
// Check allowed access types above.
AccessType string `json:"access_type" pg:"access_type"`
Method string `json:"method" pg:"method"`
Endpoint string `json:"endpoint" pg:"endpoint"`
CreatedAt *time.Time `json:"created_at" pg:"default:now()"`
}
// APICallsSearch is supposed to be used for calls only.
type APICallsSearch struct {
Search string
From *time.Time
To *time.Time
}

19
pkg/common/apitoken.go Normal file
View File

@@ -0,0 +1,19 @@
package common
import (
"fmt"
"time"
"fiskerinc.com/modules/common/dbbasemodel"
)
type APIToken struct {
Token string `json:"token" validate:"required,max=1000" pg:",pk"`
Roles string `json:"roles" validate:"required,max=10000"`
Description string `json:"description" validate:"required,max=1000"`
ExpiresAt *time.Time `json:"expires_at" pg:"expires_at"`
dbbasemodel.DBModelBase
}
func (a *APIToken) String() string {
return fmt.Sprintf("APIToken<%s>", a.Token)
}

View File

@@ -0,0 +1,32 @@
package common
func NewApprovalUpdates(cu *CarUpdate) ApprovalUpdate {
a := ApprovalUpdate{}
a.Update(cu)
return a
}
type ApprovalUpdate struct {
ID int64 `json:"id,omitempty"`
VIN string `json:"vin"`
Name string `json:"name,omitempty"`
Version string `json:"version,omitempty"`
Description string `json:"description,omitempty"`
ReleaseNotes string `json:"release_notes,omitempty"`
}
func (a *ApprovalUpdate) Update(cu *CarUpdate) {
a.ID = cu.ID
a.VIN = cu.VIN
if cu.UpdateManifest == nil {
return
}
m := cu.UpdateManifest
a.Name = m.Name
a.Version = m.Version
a.Description = m.Description
a.ReleaseNotes = m.ReleaseNotes
}

View File

@@ -0,0 +1,12 @@
package authproviders
import "fiskerinc.com/modules/utils/envtool"
const (
Default = "Default" // This is for any provider
FiskerAD = "Fisker"
FiskerQA = "Fisker-QA"
FiskerAPIKey = "FiskerAPIKey"
)
var Magna = envtool.GetEnv("MAGNA_PROVIDER", "REPLACE_ME")

View File

@@ -0,0 +1,54 @@
package common_test
import (
"encoding/json"
"testing"
"fiskerinc.com/modules/common"
"fiskerinc.com/modules/testhelper"
)
type testHexBinary struct {
Data *common.BinaryHex `json:"data"`
}
func TestBinaryHexMarshalJSON(t *testing.T) {
expected := `"0000ffff"`
value := common.BinaryHex{00, 00, 0xff, 0xff}
json, err := value.MarshalJSON()
testhelper.NoError(t, "MarshalJSON error", err)
testhelper.Equal(t, "MarshalJSON json", expected, string(json))
}
func TestBinaryHexUnmarshalJSON(t *testing.T) {
expected := []byte{00, 00, 0xff, 0xff}
value := common.BinaryHex{}
err := value.UnmarshalJSON([]byte(`"0000ffff"`))
testhelper.NoError(t, "UnmarshalJSON error", err)
testhelper.EqualByteArray(t, "UnmarshalJSON len", expected, value)
}
func TestBinaryHexStructMarshalJSON(t *testing.T) {
expected := `{"data":"0000ffff"}`
value := testHexBinary{Data: &common.BinaryHex{00, 00, 0xff, 0xff}}
json, err := json.Marshal(value)
testhelper.NoError(t, "MarshalJSON error", err)
testhelper.Equal(t, "MarshalJSON json", expected, string(json))
}
func TestBinaryHexStructUnmarshalJSON(t *testing.T) {
obj := testHexBinary{}
data := `{"data":"0e00ffff"}`
expected := []byte{0x0e, 00, 0xff, 0xff}
err := json.Unmarshal([]byte(data), &obj)
testhelper.NoError(t, "UnmarshalJSON error", err)
testhelper.EqualByteArray(t, "UnmarshalJSON json", *obj.Data, expected)
}

56
pkg/common/binary_hex.go Normal file
View File

@@ -0,0 +1,56 @@
package common
import (
"encoding/hex"
"fmt"
"strings"
)
func NewBinaryHex(data []byte) BinaryHex {
var result BinaryHex = data
return result
}
type BinaryHex []byte
func (bh BinaryHex) Bytes() []byte {
return []byte(bh)
}
func (bh *BinaryHex) SetBytes(value []byte) {
v := BinaryHex(value)
*bh = v
}
func (bh BinaryHex) String() string {
return hex.EncodeToString(bh)
}
// TODO Hack to render []byte as int array for the JSON RPC. Otherwise Go renders in base64
func (bh BinaryHex) UintArray() []uint {
bytes := bh.Bytes()
result := make([]uint, len(bytes))
for i, d := range bytes {
result[i] = uint(d)
}
return result
}
func (bh *BinaryHex) SetHex(data string) error {
x, err := hex.DecodeString(strings.Trim(string(data), `"`))
if err != nil {
return err
}
*bh = x
return nil
}
func (bh BinaryHex) MarshalJSON() ([]byte, error) {
return []byte(fmt.Sprintf(`"%s"`, bh.String())), nil
}
func (bh *BinaryHex) UnmarshalJSON(data []byte) error {
return bh.SetHex(string(data))
}

View File

@@ -0,0 +1,7 @@
package common
// BulkCarCommands for sending commands to multiple cars
type BulkCarCommands struct {
VINs []string `json:"vins,omitempty" validate:"required,max=1000,dive,vin"`
RemoteCommandSource
}

112
pkg/common/can.go Normal file
View File

@@ -0,0 +1,112 @@
package common
import (
"time"
"fiskerinc.com/modules/grpc/kafka_grpc"
)
// CANFrame provides struct for can bus messages
type CANFrame struct {
TimestampUSec int `json:"epoch_usec" parquet:"name=epoch_usec, type=INT64"`
ID int `json:"id" parquet:"name=id, type=INT32"`
Data string `json:"data" parquet:"name=data, type=BYTE_ARRAY"`
}
type CANBusMessage struct {
EpochUsec int `json:"epoch_usec"`
Dropped int `json:"dropped"`
Filtered int `json:"filtered"`
Frames []CANFrame `json:"frames"`
}
// CANSignal provides struct for parsed can bus messages
type CANSignal struct {
VIN string `json:"vin"`
Timestamp float64 `json:"timestamp" parquet:"name=timestamp, type=FLOAT"`
ID int `json:"id" parquet:"name=id, type=INT32"`
Name string `json:"name" parquet:"name=name, type=BYTE_ARRAY"`
Value float64 `json:"value" parquet:"name=value, type=FLOAT"`
Description string `json:"description,omitempty"`
}
type CANSignalExport struct {
VIN string `json:"vin"`
Timestamp time.Time `json:"timestamp"`
Name string `json:"name"`
Value float64 `json:"value"`
TimestampInMilli int64 `json:"tm"`
}
// "missing destination name \"BCM_FrntDrDoorLockSts\" in *common.CANSignal"
type CANSignalQuery struct {
VIN string `json:"vin" validate:"required"`
TimestampStart float64 `json:"timestamp_start" validate:"required"`
TimestampEnd float64 `json:"timestamp_end" validate:"required"`
Offset int `json:"offset"`
Limit int `json:"limit"`
SelectAll bool `json:"select_all"`
CanSignals []string `json:"can_signals"`
}
type ExportCANSignalQuery struct {
VIN string `json:"vin" validate:"required"`
TimestampStart int64 `json:"timestamp_start" validate:"required"`
TimestampEnd int64 `json:"timestamp_end" validate:"required"`
Offset int `json:"offset"`
Limit int `json:"limit"`
SelectAll bool `json:"select_all"`
CanSignals []string `json:"can_signals"`
}
type CANSignalNameList struct {
Signal_Name string `json:"signal_name"`
}
type CANSignalData struct {
cansignals []CANSignal `json:"cansignals"`
}
type CANSignalBatchPayload struct {
Data CANSignalData `json:"data"`
}
// CANFilter provides struct for filtering can messages based on ID
type CANFilter struct {
CANID string `json:"can_id" bson:"can_id" validate:"required,can_id"`
Interval *int `json:"interval,omitempty" bson:"interval" validate:"omitempty,gte=0"`
EdgeMask *BinaryHex `json:"edge_mask,omitempty" bson:"edge_mask,omitempty" validate:"omitempty,lte=10000"`
}
// CANFilterWithFleet is used only for rendering vehicle's fleets' filters.
type CANFilterWithFleet struct {
CANID string `json:"can_id" bson:"can_id" validate:"required,can_id"`
Interval *int `json:"interval,omitempty" bson:"interval" validate:"gte=0"`
Fleet string `json:"fleet,omitempty" bson:"fleet,omitempty"`
EdgeMask *BinaryHex `json:"edge_mask,omitempty" bson:"edge_mask,omitempty" validate:"omitempty,lte=10000"`
}
func (c *CANSignalBatchPayload) ToGrpc(data []CANSignal) *kafka_grpc.GRPC_CANSignalBatchPayload {
grpccansignals := make([]*kafka_grpc.GRPC_CANSignal, len(data))
msg := kafka_grpc.GRPC_CANSignalData{
Cansignals: grpccansignals,
}
for i, cs := range data {
msg.Cansignals[i] = &kafka_grpc.GRPC_CANSignal{
Vin: cs.VIN,
Timestamp: cs.Timestamp,
Id: int32(cs.ID),
Name: cs.Name,
Value: cs.Value,
Description: cs.Description,
}
}
batchPayload := kafka_grpc.GRPC_CANSignalBatchPayload{
Data: &msg,
}
return &batchPayload
}

61
pkg/common/car.go Normal file
View File

@@ -0,0 +1,61 @@
package common
import (
"fmt"
"fiskerinc.com/modules/common/dbbasemodel"
)
const (
InfoSourceAutoCreated = "autocreated"
CarSoldStatusRetailed = "Retailed"
)
type RegionCode string
const (
US RegionCode = "US"
EU RegionCode = "EU"
)
// Car schema
type Car struct {
VIN string `pg:",pk" json:"vin" validate:"required,vin"`
Region RegionCode `json:"region,omitempty"`
Country string `json:"country,omitempty" validate:"max=256"`
Year int `json:"year,omitempty" validate:"required,gte=1000,lte=9999"`
Model string `json:"model,omitempty" validate:"required,max=256"`
Trim string `json:"trim,omitempty" validate:"required,max=256"`
Powertrain string `json:"powertrain,omitempty" validate:"max=256"`
Restraint string `json:"restraint,omitempty" validate:"max=256"`
BodyType string `json:"body_type,omitempty" validate:"max=256"`
ECUList string `json:"ecu_list,omitempty"`
ICCID string `json:"iccid,omitempty" validate:"omitempty,max=50"`
InfoSource string `pg:"info_source" json:"-"`
Blocked bool `pg:"blocked" json:"-"`
Tags []string `json:"tags,omitempty" pg:"tags,array" validate:"omitempty,max=50"`
Drivers []CarToDriver `pg:"-" json:"drivers,omitempty"`
Manifests []StatusManifest `pg:"-" json:"manifests,omitempty"`
Document string `pg:"-" json:"document,omitempty"`
SoldStatus string `pg:"sold_status" json:"sold_status,omitempty"`
SUMSVersion string `pg:"sums_version" json:"sums_version,omitempty"`
OSVersion string `pg:"-" json:"os_version,omitempty"`
Flashpack string `json:"flashpack,omitempty"`
dbbasemodel.DBModelBase
}
// CarToDriver table storing cars-to-drivers
type CarToDriver struct {
ID int64 `json:"id"`
VIN string `pg:",unique:carid_driverid" json:"vin,omitempty" validate:"required,vin"`
DriverID string `pg:",unique:carid_driverid" json:"driverid,omitempty" validate:"required,max=256"`
DriverRole string `json:"role" validate:"required,max=100"`
BLEKey string `pg:"ble_key" json:"ble_key,omitempty" validation:"hexdecimal,max=32"`
Settings []CarSetting `pg:"-" json:"settings,omitempty"`
Subscriptions []Subscription `pg:"rel:has-many" json:"subscriptions,omitempty"`
dbbasemodel.DBModelBase
}
func (c Car) String() string {
return fmt.Sprintf("Car<%s %s %d>", c.VIN, c.Model, c.Year)
}

View File

@@ -0,0 +1,14 @@
package common
const (
LockDoor string = "lock"
UnlockDoor string = "unlock"
)
type CarCommandLocks struct {
LeftFront string `json:"left_front,omitempty"`
RightFront string `json:"right_front,omitempty"`
LeftRear string `json:"left_rear,omitempty"`
RightRear string `json:"right_rear,omitempty"`
Trunk string `json:"trunk,omitempty"`
}

View File

@@ -0,0 +1,16 @@
package common
const (
OpenWindow string = "open"
CloseWindow string = "close"
)
type CarCommandWindows struct {
LeftFront string `json:"left_front,omitempty"`
RightFront string `json:"right_front,omitempty"`
LeftRear string `json:"left_rear,omitempty"`
RightRear string `json:"right_rear,omitempty"`
LeftRearQuarter string `json:"left_rear_quarter,omitempty"`
RightRearQuarter string `json:"right_rear_quarter,omitempty"`
RearWindshield string `json:"rear_windshield,omitempty"`
}

76
pkg/common/car_data.go Normal file
View File

@@ -0,0 +1,76 @@
package common
import (
"encoding/json"
"fiskerinc.com/modules/grpc/kafka_grpc"
"github.com/pkg/errors"
)
type CANBusFrame struct {
EpochUsec int64 `json:"epoch_usec"`
ID int `json:"id"`
Data string `json:"data"`
}
type CANBusMessageRaw struct {
EpochUsec int64 `json:"epoch_usec"`
Dropped int `json:"dropped"`
Filtered int `json:"filtered"`
Frames []CANBusFrame `json:"frames"`
}
type CarDataBatchPayloadRaw struct {
Handler string `json:"handler"`
Data CANBusMessageRaw `json:"data"`
Version string `json:"version"`
}
// CarDataBatchPayload is a payload received from T.Rex
//
// it contains batches of CANMessages (can.go)
type CarDataBatchPayload struct {
Handler string `json:"handler"`
Data CANBusMessage `json:"data"`
Version string `json:"version"`
}
func (c *CarDataBatchPayload) Marshal() ([]byte, error) {
data, err := json.Marshal(*c)
return data, errors.WithStack(err)
}
func (c *CarDataBatchPayload) Unmarshal(data []byte) error {
err := json.Unmarshal(data, c)
return errors.WithStack(err)
}
func (c *CarDataBatchPayload) ToGrpc(data MessageRawJSON, vin string) (*kafka_grpc.GRPC_BatchPayload, error) {
var payload CANBusMessageRaw
err := json.Unmarshal(data.Data, &payload)
if err != nil {
return nil, errors.WithStack(err)
}
frames := make([]*kafka_grpc.GRPC_CANFrame, len(payload.Frames))
msg := kafka_grpc.GRPC_CANData{
EpochUsec: payload.EpochUsec,
Dropped: int32(payload.Dropped),
Filtered: int32(payload.Filtered),
Frames: frames,
Vin: vin,
}
for i, frame := range payload.Frames {
msg.Frames[i] = &kafka_grpc.GRPC_CANFrame{
Epoch: frame.EpochUsec,
ID: int32(frame.ID),
Value: []byte(frame.Data),
}
}
batch_payload := kafka_grpc.GRPC_BatchPayload{
Handler: data.Handler,
Data: &msg,
Version: data.Version,
}
return &batch_payload, nil
}

9
pkg/common/car_driver.go Normal file
View File

@@ -0,0 +1,9 @@
package common
type CarToDriverModel struct {
User UserProfile `json:"user,omitempty"`
DriverID string `json:"driver_id"`
Role string `json:"role"`
Settings []CarSetting `json:"settings"`
Subscriptions []Subscription `json:"subscriptions"`
}

102
pkg/common/car_ecu.go Normal file
View File

@@ -0,0 +1,102 @@
package common
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"fiskerinc.com/modules/common/dbbasemodel"
)
type CarECU struct {
VIN string `json:"vin,omitempty" pg:",unique:vin_ecu" validate:"required,vin"`
ECU string `json:"ecu" pg:",unique:vin_ecu" validate:"required,max=100"`
Version string `json:"sw_version" validate:"max=255"`
SerialNumber string `json:"serial_number,omitempty" validate:"max=1024"`
HWVersion string `json:"hw_version,omitempty" validate:"max=1024"`
BootLoaderVersion string `json:"boot_loader_version,omitempty" validate:"max=1024"`
Fingerprint string `json:"fingerprint,omitempty" validate:"max=1024"`
// cloud/attendant/handlers/car_update_state.go JSON message config was renamed to code_data_string
Config string `json:"code_data_string,omitempty" pg:"code_data_string" validate:"max=2048"` // config was renamed to code_data_string
Vendor string `json:"vendor,omitempty" validate:"max=1024"`
SupplierSWVersion string `json:"supplier_sw_version,omitempty" validate:"max=1024"`
Epoch_usec int64 `json:"epoch_usec" pg:"epoch_usec"`
ASSYNumber string `json:"assy_number,omitempty" pg:"assy_number"`
dbbasemodel.DBModelBase
}
func (c *CarECU) CacheKey() string {
return fmt.Sprintf("%s:%s", c.VIN, c.ECU)
}
func (c *CarECU) HashValues() string {
data := []byte(fmt.Sprintf("%s:%s:%s:%s:%s:%s:%s:%s:%s", c.Version, c.SerialNumber, c.HWVersion, c.BootLoaderVersion, c.Fingerprint, c.Config, c.Vendor, c.SupplierSWVersion, c.ASSYNumber))
hash := sha256.Sum256(data)
signature := hex.EncodeToString(hash[:])
return signature
}
// Ensure we always have the correct car_ecu name for OTA
func (c *CarECU) TransformECUName() {
replacement, ok := OTAUpdateECUReplacement[c.ECU]
if ok {
c.ECU = replacement
}
}
type CarECUFilter struct {
VIN string
ECUs []string
Search string
Unique bool
FlashPackNumberExist bool
HWVersionRequired bool // Ensure that the hw_version has a value. This may not retrieve the latest entry for the ecu, but until we find out why hw_versions are being inserted as empty
}
type CarFlashpackVersion struct {
Flashpack string `json:"flashpack" validate:"required,numeric"`
CarModel string `json:"car_model" validate:"required"`
CarTrim string `json:"car_trim" validate:"required"`
CarYear int `json:"car_year" validate:"required"`
CarECUName string `json:"car_ecu_name" validate:"required"`
CarECUVersion string `json:"car_ecu_version" validate:"required"`
dbbasemodel.DBModelBase
}
type CarECUVersion struct {
CarECUName string `json:"car_ecu_name"`
CarECUVersion string `json:"car_ecu_version"`
}
type CarFlashpackVersionRequest struct {
Flashpack string `json:"flashpack" validate:"required,numeric"`
CarModel string `json:"car_model" validate:"required"`
CarTrim string `json:"car_trim" validate:"required"`
CarYear int `json:"car_year" validate:"required"`
}
type ECUVersionRequest struct {
CarECUName string `json:"car_ecu_name" validate:"required"`
CarECUVersion string `json:"car_ecu_version" validate:"required"`
}
type CarFlashpackVersionAddRequest struct {
Flashpack string `json:"flashpack" validate:"required,numeric"`
CarModel string `json:"car_model" validate:"required"`
CarTrim string `json:"car_trim" validate:"required"`
CarYear int `json:"car_year" validate:"required"`
ECUVersions []ECUVersionRequest `json:"ecu_versions" validate:"required"`
}
type CarFlashpackVersionResponse struct {
Flashpack string `json:"flashpack"`
CarModel string `json:"car_model"`
CarTrim string `json:"car_trim"`
CarYear int `json:"car_year"`
}
type CarFlashpackVersionInfoResponse struct {
Flashpack string `json:"flashpack"`
NextFlashpack string `json:"next_flashpack"`
ECUVersions []CarECUVersion `json:"ecu_versions"`
}

View File

@@ -0,0 +1,41 @@
package common
import (
"encoding/json"
"github.com/pkg/errors"
)
// Location represents the location state of the car
type Location struct {
Altitude float64 `json:"altitude" redis:"altitude"`
Longitude float64 `json:"longitude" redis:"longitude"`
Latitude float64 `json:"latitude" redis:"latitude"`
Heading float64 `json:"-" redis:"heading"`
}
func (l *Location) Marshal() ([]byte, error) {
data, err := json.Marshal(*l)
return data, errors.WithStack(err)
}
func (l *Location) Unmarshal(data []byte) error {
err := json.Unmarshal(data, l)
return errors.WithStack(err)
}
func (it *Location) MarshalBinary() ([]byte, error) {
return json.Marshal(it)
}
func (it *Location) UnmarshalBinary(data []byte) error {
return json.Unmarshal(data, it)
}
type JSONCarLocation struct {
VIN string `json:"vin"`
Altitude float64 `json:"altitude"`
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
Heading float64 `json:"heading"`
}

15
pkg/common/car_search.go Normal file
View File

@@ -0,0 +1,15 @@
package common
type CarSearch struct {
Search string `json:"search" validate:"max=1024"`
VINs string `json:"vins" validate:"omitempty"`
Online *CarOnlineFilter
NoEU bool `json:"no_eu", validate:"omitempty"`
Car
}
type CarOnlineFilter struct {
Online *bool
HMI *bool
VINsOnline []string
}

80
pkg/common/car_setting.go Normal file
View File

@@ -0,0 +1,80 @@
package common
import (
"fiskerinc.com/modules/common/dbbasemodel"
)
type CarSetting struct {
VIN string `pg:"vin" json:"-"`
DriverID string `pg:"driver_id" json:"-"`
Name string `pg:",pk" json:"name"`
Value string `json:"value"`
Type string `json:"type"`
dbbasemodel.DBModelBase
}
type MobileSettingsUpdate struct {
VIN string `json:"vin"`
Settings []CarSetting `json:"settings"`
}
type HMISettingsUpdate struct {
DriverID string `json:"driver_id"`
Settings []CarSetting `json:"settings"`
}
/*
It would be nice if we could do this easily, but requires a cast to string which isn't so nice
type CarSettingEnum string
*/
const (
SEQUENCE_NUMBER string = "SEQUENCE_NUMBER"
BODY_COLOR string = "BODY_COLOR"
DELIVERY_DESTINATION string = "DELIVERY_DESTINATION"
)
// Take in the feature codes for the car, and convert it to body color string, will probably change
func FeatureCodeToBodyColor(VehicleFeatures []FeatureCodes) (bodyColor string) {
var colorCode string
for x := range VehicleFeatures {
if VehicleFeatures[x].FamilyCode == "0103" {
colorCode = VehicleFeatures[x].FeatureCode
break
}
}
switch colorCode {
case "010300":
return "PRIMERED"
case "010301":
return "SOLID_WHITE"
case "010302":
return "SOLID_BLACK"
case "010303":
return "BLUE_GREY_MET"
case "010304":
return "MID_BLUE_GLOSS"
case "010305":
return "MID_BLUE_MATTE"
case "010306":
return "VIVID_BLUE"
case "010307":
return "SPE_COOL_SILVER"
case "010308":
return "STEALTH_GREEN"
case "010309":
return "VIVID_ORANGE"
case "010310":
return "EARTH_COPPER"
case "010311":
return "METALLIC_BLUE_BLACK"
case "010312":
return "WHITE_PEARL"
case "010313":
return "STEALTH_GREEN_GLOSS"
case "010314":
return "SOLID_RED"
default:
return "MISSING_COLOR"
}
}

552
pkg/common/car_state.go Normal file
View File

@@ -0,0 +1,552 @@
package common
import (
"encoding/json"
"time"
"github.com/pkg/errors"
)
type CarState struct {
Online bool `json:"online"`
OnlineHMI bool `json:"online_hmi"`
Battery *Battery `json:"battery,omitempty"`
MaxRange *MaxRange `json:"max_range,omitempty"`
Doors *Doors `json:"doors,omitempty"`
Location *Location `json:"location,omitempty"`
Locks *Locks `json:"door_locks,omitempty"`
Windows *Windows `json:"windows,omitempty"`
MiscWindows *MiscWindows `json:"misc_windows,omitempty"`
Sunroof *Sunroof `json:"sunroof,omitempty"`
CabinClimate *CabinClimate `json:"cabin_climate,omitempty"`
RearDefrost *RearDefrost `json:"rear_defrost,omitempty"`
DriverSeatHeat *DriverSeatHeat `json:"driver_seat_heat,omitempty"`
PassengerSeatHeat *PassengerSeatHeat `json:"passenger_seat_heat,omitempty"`
SteeringWheelHeat *SteeringWheelHeat `json:"steering_wheel_heat,omitempty"`
AmbientTemperature *AmbientTemperature `json:"ambient_temperature,omitempty"`
CellTemperature *CellTemperature `json:"cell_temp,omitempty"`
VehicleSpeed *VehicleSpeed `json:"vehicle_speed,omitempty"`
VCU0x260 *VCU0x260Descriptor `json:"vcu0x260,omitempty"`
ChargingMetrics *VCUChargingMetrics `json:"charging_metrics,omitempty"`
Gear *Gear `json:"gear,omitempty"`
StateOfCharge *StateOfCharge `json:"state_of_charge,omitempty"`
TRexVersion string `json:"trex_version,omitempty"`
DBCVersion string `json:"dbc_version,omitempty"`
IP string `json:"ip,omitempty"`
UpdatedAt *time.Time `json:"updated,omitempty"`
SafeState *SafeState `json:"safe_state,omitempty"`
DriverOccupySeatState *int `json:"driver_occupy_seat_state,omitempty"`
PowerMode *int `json:"power_mode,omitempty"`
ChargingStatus *int `json:"charging_status,omitempty"`
VehicleReadyState *VehicleReadyState `json:"vehicle_ready_state,omitempty"`
Battery12V *Battery12V `json:"battery_12v,omitempty"` // maybe make it Battery12v
ExpandedSignals *ExpandedSignals `json:"expanded_signals,omitempty"`
}
func (c *CarState) GetBattery() *Battery {
if c.Battery == nil {
c.Battery = &Battery{}
}
return c.Battery
}
func (c *CarState) GetMaxRange() *MaxRange {
if c.MaxRange == nil {
c.MaxRange = &MaxRange{}
}
return c.MaxRange
}
func (c *CarState) GetDoors() *Doors {
if c.Doors == nil {
c.Doors = &Doors{}
}
return c.Doors
}
func (c *CarState) UpdateLocation(value []byte) error {
loc := Location{}
err := loc.Unmarshal(value)
if err != nil {
return err
}
location := c.GetLocation()
location.Latitude = loc.Latitude
location.Longitude = loc.Longitude
// Altitude could have already been set from GPS_ALTITUDE so do not overwrite unless it is a non-zero
if loc.Altitude != 0 {
location.Altitude = loc.Altitude
}
return nil
}
func (c *CarState) GetLocation() *Location {
if c.Location == nil {
c.Location = &Location{}
}
return c.Location
}
func (c *CarState) GetLocks() *Locks {
if c.Locks == nil {
c.Locks = &Locks{}
}
return c.Locks
}
func (c *CarState) GetWindows() *Windows {
if c.Windows == nil {
c.Windows = &Windows{}
}
return c.Windows
}
func (c *CarState) GetMiscWindows() *MiscWindows {
if c.MiscWindows == nil {
c.MiscWindows = &MiscWindows{}
}
return c.MiscWindows
}
func (c *CarState) GetSunroof() *Sunroof {
if c.Sunroof == nil {
c.Sunroof = &Sunroof{}
}
return c.Sunroof
}
func (c *CarState) GetCabinClimate() *CabinClimate {
if c.CabinClimate == nil {
c.CabinClimate = &CabinClimate{}
}
return c.CabinClimate
}
func (c *CarState) GetRearDefrost() *RearDefrost {
if c.RearDefrost == nil {
c.RearDefrost = &RearDefrost{}
}
return c.RearDefrost
}
func (c *CarState) GetDriverSeatHeat() *DriverSeatHeat {
if c.DriverSeatHeat == nil {
c.DriverSeatHeat = &DriverSeatHeat{}
}
return c.DriverSeatHeat
}
func (c *CarState) GetPassengerSeatHeat() *PassengerSeatHeat {
if c.PassengerSeatHeat == nil {
c.PassengerSeatHeat = &PassengerSeatHeat{}
}
return c.PassengerSeatHeat
}
func (c *CarState) GetSteeringWheelHeat() *SteeringWheelHeat {
if c.SteeringWheelHeat == nil {
c.SteeringWheelHeat = &SteeringWheelHeat{}
}
return c.SteeringWheelHeat
}
func (c *CarState) GetAmbientTemperature() *AmbientTemperature {
if c.AmbientTemperature == nil {
c.AmbientTemperature = &AmbientTemperature{}
}
return c.AmbientTemperature
}
func (c *CarState) GetCellTemperature() *CellTemperature {
if c.CellTemperature == nil {
c.CellTemperature = &CellTemperature{}
}
return c.CellTemperature
}
func (c *CarState) GetVCU0x260() *VCU0x260Descriptor {
if c.VCU0x260 == nil {
c.VCU0x260 = &VCU0x260Descriptor{}
}
return c.VCU0x260
}
func (c *CarState) GetChargingMetrics() *VCUChargingMetrics {
if c.ChargingMetrics == nil {
c.ChargingMetrics = &VCUChargingMetrics{}
}
return c.ChargingMetrics
}
func (c *CarState) GetGear() *Gear {
if c.Gear == nil {
c.Gear = &Gear{}
}
return c.Gear
}
func (c *CarState) GetStateOfCharge() *StateOfCharge {
if c.StateOfCharge == nil {
c.StateOfCharge = &StateOfCharge{}
}
return c.StateOfCharge
}
func (c *CarState) GetVehicleSpeed() *VehicleSpeed {
if c.VehicleSpeed == nil {
c.VehicleSpeed = &VehicleSpeed{}
}
return c.VehicleSpeed
}
// Battery represents the battery state of the car
type Battery struct {
Percent int `json:"percent" redis:"percent"`
TotalMileageOdometer int `json:"total_mileage_odometer"`
BatteryVoltage float64 `json:"battery_voltage"` // The fact that this is called battery voltage is really dumb. Its the 12 volt battery, not the high voltage one
}
func (b *Battery) Marshal() ([]byte, error) {
data, err := json.Marshal(*b)
return data, errors.WithStack(err)
}
func (b *Battery) Unmarshal(data []byte) error {
err := json.Unmarshal(data, b)
return errors.WithStack(err)
}
// StateOfCharge represents the battery state of charge
type StateOfCharge struct {
Usable int `json:"usable"`
Health int `json:"health"`
}
type SafeState struct {
VehicleSafeState bool `json:"vehicle_safe_state" redis:"vehicle_safe_state"`
VCUSafeState bool `json:"vcu_safe_state" redis:"vcu_safe_state"`
MCUFrontSafeState bool `json:"mcu_front_safe_state" redis:"mcu_front_safe_state"`
MCURearSafeState bool `json:"mcu_rear_safe_state" redis:"mcu_rear_safe_state"`
MCURearDecoupState bool `json:"mcu_rear_decoup_state" redis:"mcu_rear_decoup_state"`
MCUFrontInverterError bool `json:"mcu_front_inverter_error" redis:"mcu_front_inverter_error"`
MCURearInverterError bool `json:"mcu_rear_inverter_error" redis:"mcu_rear_inverter_error"`
}
func (s *SafeState) Marshal() ([]byte, error) {
data, err := json.Marshal(*s)
return data, errors.WithStack(err)
}
func (s *SafeState) Unmarshal(data []byte) error {
err := json.Unmarshal(data, s)
return errors.WithStack(err)
}
func (c *CarState) GetSafeState() *SafeState {
if c.SafeState == nil {
c.SafeState = &SafeState{}
}
return c.SafeState
}
// MaxRange represents the predicted max range of the car
type MaxRange struct {
MaxMiles int `json:"max_miles" redis:"max_miles"`
}
type VehicleReadyState struct {
IsVehicleReady bool `json:"is_vehicle_ready" redis:"is_vehicle_ready"`
}
func (c *CarState) GetVehicleReadyState() *VehicleReadyState {
if c.VehicleReadyState == nil {
c.VehicleReadyState = &VehicleReadyState{}
}
return c.VehicleReadyState
}
func (b *MaxRange) Marshal() ([]byte, error) {
data, err := json.Marshal(*b)
return data, errors.WithStack(err)
}
func (b *MaxRange) Unmarshal(data []byte) error {
err := json.Unmarshal(data, b)
return errors.WithStack(err)
}
// Doors represents the doors state of the car
// false means closed, true means open
type Doors struct {
Hood bool `json:"hood" redis:"hood"`
LeftFront bool `json:"left_front" redis:"left_front"`
LeftRear bool `json:"left_rear" redis:"left_rear"`
RightFront bool `json:"right_front" redis:"right_front"`
RightRear bool `json:"right_rear" redis:"right_rear"`
Trunk bool `json:"trunk" redis:"trunk"`
}
func (d *Doors) Marshal() ([]byte, error) {
data, err := json.Marshal(*d)
return data, errors.WithStack(err)
}
func (d *Doors) Unmarshal(data []byte) error {
err := json.Unmarshal(data, d)
return errors.WithStack(err)
}
// Locks represents the lock state of the car
type Locks struct {
Driver bool `json:"driver" redis:"driver"`
All bool `json:"all" redis:"all"`
}
func (l *Locks) Marshal() ([]byte, error) {
data, err := json.Marshal(*l)
return data, errors.WithStack(err)
}
func (l *Locks) Unmarshal(data []byte) error {
err := json.Unmarshal(data, l)
return errors.WithStack(err)
}
// Windows represents the windows state of the car
//
// value is a percentage 0-100 in increments of 0.5
type Windows struct {
LeftFront int `json:"left_front" redis:"left_front"`
LeftRear int `json:"left_rear" redis:"left_rear"`
RightFront int `json:"right_front" redis:"right_front"`
RightRear int `json:"right_rear" redis:"right_rear"`
}
func (w *Windows) Marshal() ([]byte, error) {
data, err := json.Marshal(*w)
return data, errors.WithStack(err)
}
func (w *Windows) Unmarshal(data []byte) error {
err := json.Unmarshal(data, w)
return errors.WithStack(err)
}
// MiscWindows represents the windows state of the car for misc windows (left / right rear quarter and rear windshield)
// value is a percentage 0-100 in increments of 0.5
type MiscWindows struct {
LeftRearQuarter int `json:"left_rear_quarter" redis:"left_rear_quarter"`
RightRearQuarter int `json:"right_rear_quarter" redis:"right_rear_quarter"`
RearWindshield int `json:"rear_windshield" redis:"rear_windshield"`
}
func (w *MiscWindows) Marshal() ([]byte, error) {
data, err := json.Marshal(*w)
return data, errors.WithStack(err)
}
func (w *MiscWindows) Unmarshal(data []byte) error {
err := json.Unmarshal(data, w)
return errors.WithStack(err)
}
type Sunroof struct {
Sunroof int `json:"sunroof" redis:"sunroof"`
}
func (s *Sunroof) Marshal() ([]byte, error) {
data, err := json.Marshal(*s)
return data, errors.WithStack(err)
}
func (s *Sunroof) Unmarshal(data []byte) error {
err := json.Unmarshal(data, s)
return errors.WithStack(err)
}
type CabinClimate struct {
CabinTemperature int `json:"cabin_temperature" redis:"cabin_temperature"`
InternalTemperature int `json:"internal_temperature" redis:"internal_temperature"`
}
func (w *CabinClimate) Marshal() ([]byte, error) {
data, err := json.Marshal(*w)
return data, errors.WithStack(err)
}
func (w *CabinClimate) Unmarshal(data []byte) error {
err := json.Unmarshal(data, w)
return errors.WithStack(err)
}
type RearDefrost struct {
On bool `json:"on" redis:"on"`
}
func (w *RearDefrost) Marshal() ([]byte, error) {
data, err := json.Marshal(*w)
return data, errors.WithStack(err)
}
func (w *RearDefrost) Unmarshal(data []byte) error {
err := json.Unmarshal(data, w)
return errors.WithStack(err)
}
type DriverSeatHeat struct {
Level int `json:"level" redis:"level"`
}
func (w *DriverSeatHeat) Marshal() ([]byte, error) {
data, err := json.Marshal(*w)
return data, errors.WithStack(err)
}
func (w *DriverSeatHeat) Unmarshal(data []byte) error {
err := json.Unmarshal(data, w)
return errors.WithStack(err)
}
type PassengerSeatHeat struct {
Level int `json:"level" redis:"level"`
}
func (w *PassengerSeatHeat) Marshal() ([]byte, error) {
data, err := json.Marshal(*w)
return data, errors.WithStack(err)
}
func (w *PassengerSeatHeat) Unmarshal(data []byte) error {
err := json.Unmarshal(data, w)
return errors.WithStack(err)
}
type SteeringWheelHeat struct {
On bool `json:"on" redis:"on"`
}
func (w *SteeringWheelHeat) Marshal() ([]byte, error) {
data, err := json.Marshal(*w)
return data, errors.WithStack(err)
}
func (w *SteeringWheelHeat) Unmarshal(data []byte) error {
err := json.Unmarshal(data, w)
return errors.WithStack(err)
}
type AmbientTemperature struct {
Temperature int `json:"temperature" redis:"temperature"`
}
func (w *AmbientTemperature) Marshal() ([]byte, error) {
data, err := json.Marshal(*w)
return data, errors.WithStack(err)
}
func (w *AmbientTemperature) Unmarshal(data []byte) error {
err := json.Unmarshal(data, w)
return errors.WithStack(err)
}
type VehicleSpeed struct {
Speed float64 `json:"speed" redis:"speed"`
}
func (w *VehicleSpeed) Marshal() ([]byte, error) {
data, err := json.Marshal(*w)
return data, errors.WithStack(err)
}
func (w *VehicleSpeed) Unmarshal(data []byte) error {
err := json.Unmarshal(data, w)
return errors.WithStack(err)
}
type CellTemperature struct {
AvgBatteryTemp int `json:"avg_battery_temp"`
}
type VCU0x260Descriptor struct {
ChargeType string `json:"charge_type"`
}
type VCUChargingMetrics struct {
RemainingChargingTime int `json:"remaining_charging_time"`
RemainingChargingTimeFull int `json:"remaining_charging_time_full"`
}
type Gear struct {
InPark bool `json:"in_park"`
Immobilizer string `json:"immobilizer,omitempty"`
}
type Battery12V struct {
IBS_BatteryVoltage *float64 `json:"voltage,omitempty"` // 12 Volt battery voltage
IBS_StateOfCharge *float64 `json:"percent_charge,omitempty"` // Percentages of the voltage out of about 15.5 Volts
IBS_StateOfHealth *int `json:"health,omitempty"` // estimated health of the 12v battery
}
func (c *CarState) GetBattery12V() *Battery12V {
if c.Battery12V == nil {
c.Battery12V = &Battery12V{}
}
return c.Battery12V
}
type ExpandedSignals struct {
// IBS_SOCUpperTolerance *float64 //unconfirmed
// IBS_SOCLowerTolerance *float64 //unconfirmed
IBS_NominalCapacity *int `json:",omitempty"`
IBS_AvailableCapacity *int `json:",omitempty"`
BCM_TotMilg_ODO *float64 `json:",omitempty"`
BMS_SwVersS *int `json:",omitempty"`
BMS_SwVersM *int `json:",omitempty"`
BMS_SwVers *int `json:",omitempty"`
BMS_AccueDchaTotAh *int `json:",omitempty"`
BMS_AccueChrgTotAh *int `json:",omitempty"`
}
func (c *CarState) GetExpandedSignals() *ExpandedSignals {
if c.ExpandedSignals == nil {
c.ExpandedSignals = &ExpandedSignals{}
}
return c.ExpandedSignals
}
// I am quite certain there is no reason to have these custom marshalers, but want to keep with the current form incase of unexpected side effects
func (w *ExpandedSignals) Marshal() ([]byte, error) {
data, err := json.Marshal(*w)
return data, errors.WithStack(err)
}
func (w *ExpandedSignals) Unmarshal(data []byte) error {
err := json.Unmarshal(data, w)
return errors.WithStack(err)
}

View File

@@ -0,0 +1,16 @@
package common
// CarStateAL builds on top of the normal car state, and includes the parsing of a few additional fields
type CarStateAL struct {
*CarState
PKCVersion string `json:"pkc_version"`
SumsVersion string `json:"sums_version"`
OSVersion string `json:"os_version"`
}
type CarPKCOSVersion struct {
Vin string
PKCVersion string
SumsVersion string
OSVersion string
}

View File

@@ -0,0 +1,33 @@
package common
type TowmanDigitalTwin struct {
Online bool `json:"online"`
Location *Location `json:"location"`
Gear *Gear `json:"gear"`
Charging *bool `json:"charging"`
}
func (c *TowmanDigitalTwin) GetLocation() *Location {
if c.Location == nil {
c.Location = &Location{}
}
return c.Location
}
func (c *TowmanDigitalTwin) GetGear() *Gear {
if c.Gear == nil {
c.Gear = &Gear{}
}
return c.Gear
}
func (c *TowmanDigitalTwin) GetCharging() *bool {
if c.Charging == nil {
temp := false
c.Charging = &temp
}
return c.Charging
}

View File

@@ -0,0 +1,5 @@
package common
type CarStateUpdate struct {
ECUs map[string]CarECU `json:"ecus" validate:"required,min=1"`
}

37
pkg/common/car_update.go Normal file
View File

@@ -0,0 +1,37 @@
package common
import (
"fmt"
"fiskerinc.com/modules/common/dbbasemodel"
)
// CarUpdate schema
type CarUpdate struct {
ID int64 `json:"id,omitempty"`
VIN string `pg:",unique:vin_update_manifest" json:"vin" validate:"required,max=17"`
UpdateManifestID int64 `pg:",unique:vin_update_manifest" json:"manifest_id,omitempty" validate:"required"`
Status string `pg:"default:'pending'" json:"status,omitempty" validate:"max=100"`
ErrorCode int `json:"err,omitempty"`
Info string `json:"info,omitempty" pg:"info" validate:"max=1000"`
Username string `json:"username,omitempty" validate:"required"`
UpdateManifest *UpdateManifest `pg:"rel:has-one" json:"updatemanifest,omitempty"`
UpdateSource string
dbbasemodel.DBModelBase
}
func (cu CarUpdate) String() string {
return fmt.Sprintf("CarUpdate<%d %d %s %s>", cu.ID, cu.UpdateManifestID, cu.VIN, cu.Status)
}
func (cu *CarUpdate) Scrub() {
cu.UpdateManifestID = 0
cu.UpdateManifest = nil
cu.CreatedAt = nil
cu.UpdatedAt = nil
}
const (
UPDATE_SOURCE_OTA = "OTA" // The cloud has deployed this update
UPDATE_SOURCE_AFTERSALES = "AFTERSALES" // An update generated to be sent by aftersales.
UPDATE_SOURCE_FLASHPACK = "FLASHPACK"
)

View File

@@ -0,0 +1,29 @@
package common
// CarUpdateProgress represents multi-file update download progress
// If you change this structure and it relevant database entry, please update LogStatusIfNotARepeat
// cloud/modules_go/db/queries/carupdates.go:194
type CarUpdateProgress struct {
FileCurrent uint64 `json:"file_current" redis:"file_size"`
FileTotal uint64 `json:"file_total" redis:"file_total"`
PackageCurrent uint64 `json:"package_current" redis:"current_size"`
PackageTotal uint64 `json:"package_total" redis:"total_size"`
InstalledFiles int `json:"installed" redis:"installed"`
TotalFiles int `json:"total_files" redis:"total_files"`
CarUpdateID int64 `json:"car_update_id" redis:"id"`
ECU string `json:"ecu" redis:"ecu" validate:"max=100"`
Status string `json:"msg" redis:"status,omitempty" validate:"max=1000"`
Info string `json:"extra_info,omitempty" redis:"info,omitempty" validate:"max=1000"`
ErrorCode int `json:"err" redis:"errorcode"`
}
func (cu *CarUpdateProgress) Combine(status *CarUpdateProgress) {
if status == nil {
return
}
cu.PackageCurrent += status.PackageCurrent
cu.PackageTotal += status.PackageTotal
cu.InstalledFiles += status.InstalledFiles
cu.TotalFiles += status.TotalFiles
}

View File

@@ -0,0 +1,37 @@
package common_test
import (
"testing"
"fiskerinc.com/modules/common"
"fiskerinc.com/modules/testhelper"
)
func TestCarUpdateProgressCombine(t *testing.T) {
status1 := common.CarUpdateProgress{
InstalledFiles: 0,
TotalFiles: 1,
PackageCurrent: 100,
PackageTotal: 200,
}
status2 := common.CarUpdateProgress{
InstalledFiles: 1,
TotalFiles: 2,
PackageCurrent: 200,
PackageTotal: 300,
}
status1.Combine(&status2)
if status1.InstalledFiles != 1 {
t.Errorf(testhelper.TestErrorTemplate, "InstalledFiles", 1, status1.InstalledFiles)
}
if status1.TotalFiles != 3 {
t.Errorf(testhelper.TestErrorTemplate, "TotalFiles", 3, status1.TotalFiles)
}
if status1.PackageCurrent != 300 {
t.Errorf(testhelper.TestErrorTemplate, "PackageCurrent", 300, status1.PackageCurrent)
}
if status1.PackageTotal != 500 {
t.Errorf(testhelper.TestErrorTemplate, "PackageTotal", 500, status1.PackageTotal)
}
}

View File

@@ -0,0 +1,17 @@
package common
import (
"fiskerinc.com/modules/common/dbbasemodel"
)
// CarUpdateStatus database model for logging history of car updates
// If this model is changed for the database, please update LogStatusIfNotARepeat
// cloud/modules_go/db/queries/carupdates.go:194
type CarUpdateStatus struct {
ID int64 `json:"id" pg:",pk"`
CarUpdateID int64 `json:"carupdate_id"`
Status string `json:"status" validate:"max=100"`
ErrorCode int `json:"error_code"`
Info string `json:"info,omitempty" pg:"info" validate:"max=1000"`
dbbasemodel.DBModelBase
}

View File

@@ -0,0 +1,20 @@
package common
import "time"
type VersionSource string
const (
DBCVersionSource VersionSource = "DBC"
TREXVersionSource VersionSource = "TREX"
FlashpackVersionSource VersionSource = "Flashpack"
)
// CarVersionLogs is used for logging dbc version changes to DB.
type CarVersionLogs struct {
ID int64 `json:"id" pg:"id"`
VIN string `json:"vin" pg:"vin"`
VersionSource VersionSource `json:"version_source" pg:"version_source"`
Version string `json:"version" pg:"version"`
CreatedAt *time.Time `json:"created_at" pg:"created_at"`
}

View File

@@ -0,0 +1,60 @@
package carupdatestatus
const (
ManifestReceived = "manifest_received"
ManifestAccepted = "manifest_accepted"
ManifestRejected = "manifest_rejected"
ManifestCancelPending = "manifest_cancel_pending"
ManifestCancelReceived = "manifest_cancel_received"
ManifestCancelAccepted = "manifest_cancel_accepted"
ManifestCancelRejected = "manifest_cancel_rejected"
ManifestValidationSucceeded = "manifest_validation_succeeded"
ManifestValidationFailed = "manifest_validation_failed"
DownloadStarted = "download_started"
Downloading = "downloading"
DownloadCompleted = "download_completed"
DownloadFailed = "download_failed"
InstallApprovalAwait = "install_approval_await"
InstallApprovalReceived = "install_approval_received"
InstallStarted = "install_started"
Installing = "installing"
InstallSucceeded = "install_succeeded"
InstallFailed = "install_failed"
RollbackStarted = "rollback_started"
RollbackSucceeded = "rollback_succeeded"
RollbackFailed = "rollback_failed"
CleanupSucceeded = "cleanup_succeeded"
CleanupFailed = "cleanup_failed"
ManifestError = "manifest_error"
ManifestRollback = "manifest_rollback"
ManifestSucceeded = "manifest_succeeded"
ManifestCanceled = "manifest_canceled"
ManifestPending = "manifest_pending"
Pending = "pending"
Sent = "sent"
RequirementsFailed = "requirements_failed"
RequirementsAwait = "requirements_await"
InstallScheduled = "install_scheduled"
InitialFlashPack = "initial_flashpack_install"
)
// These final update statuses are ones that we will want to filter out
var FINAL_UPDATE_STATUS = []string{
ManifestSucceeded,
ManifestCanceled,
ManifestError,
DownloadFailed,
ManifestCancelPending,
RollbackSucceeded,
ManifestRejected,
RollbackFailed,
CleanupSucceeded,
}
var NoRepeatUpdateStatus = map[string]struct{}{}
func init() {
for _, status := range FINAL_UPDATE_STATUS {
NoRepeatUpdateStatus[status] = struct{}{}
}
}

57
pkg/common/certificate.go Normal file
View File

@@ -0,0 +1,57 @@
package common
import (
"crypto/x509"
"encoding/pem"
"fmt"
"time"
"fiskerinc.com/modules/common/dbbasemodel"
)
// CertificateRequest schema
const (
CertCharging string = "CHARGING"
CertICC string = "ICC"
CertTBOX string = "TBOX"
CertAftersales string = "AFTERSALES"
)
type Certificate struct {
PublicKey string `json:"public_key"`
CommonName string `json:"-" validate:"required"`
PrivateKey string `json:"private_key,omitempty" pg:"-"`
EncryptedKey []byte `json:"-" pg:"encrypted_key"`
SerialNumber string `json:"serial_number" pg:",pk"`
Type string `json:"type"`
Valid bool `json:"-" pg:",use_zero"`
CreatedBy string `json:"-" pg:"created_by"`
dbbasemodel.DBModelBase
}
func (cert Certificate) String() string {
return fmt.Sprintf("Certificate for Common Name:<%s>", cert.CommonName)
}
func (cert Certificate) IsExpiredOrInvalidAtTime(t time.Time, certDaysBeforeExp int) (bool, error) {
if !cert.Valid {
return true, nil
}
if cert.PublicKey != "" {
p, _ := pem.Decode([]byte(cert.PublicKey))
if p != nil {
c, err := x509.ParseCertificate(p.Bytes)
if err != nil {
return true, err
}
day := c.NotAfter.AddDate(0, 0, 0-certDaysBeforeExp)
if t.After(day) {
return true, nil
}
}
}
return false, nil
}

View File

@@ -0,0 +1,32 @@
package common
import "fmt"
// CertificateRequest schema
type CertificateRequest struct {
CommonName string `json:"common_name" validate:"required,max=100"`
CertificateType string `json:"type" validate:"max=100"`
}
type CertificateRevokeRequest struct {
Serial string `json:"serial_number" validate:"required,serial,max=1000"`
CertificateType string `json:"type" validate:"max=100"`
}
type CertificateRenewRequest struct {
Type string `json:"type" validate:"required,max=10000"`
CommonName string `json:"common_name" validate:"max=100"`
}
type UpdateCert struct {
SSLCertBase64 string `json:"ssl_cert_base64"`
}
type CertificateInstallRequest struct {
VIN string `pg:",pk" json:"vin" validate:"required,vin"`
ICCID string `json:"iccid,omitempty" validate:"omitempty,max=50"`
}
func (c CertificateRequest) String() string {
return fmt.Sprintf("CertificateRequest for Common Name<%s>", c.CommonName)
}

View File

@@ -0,0 +1,63 @@
package common_test
import (
"testing"
"time"
"fiskerinc.com/modules/common"
"github.com/stretchr/testify/assert"
)
func TestCertIsExpiredOrInvalidAtTime(t *testing.T) {
// this is a fake example cert generated at https://www.samltool.com/self_signed_certs.php
var testcert = `-----BEGIN CERTIFICATE-----
MIIDUzCCAjqgAwIBAgIBADANBgkqhkiG9w0BAQsFADBDMQswCQYDVQQGEwJ1czEL
MAkGA1UECAwCQ0ExFDASBgNVBAoMC0Zpc2tlciBJbmMuMREwDwYDVQQDDAh0ZXN0
Y2VydDAeFw0yMzAxMzExOTM5MjZaFw0yNDAxMzExOTM5MjZaMEMxCzAJBgNVBAYT
AnVzMQswCQYDVQQIDAJDQTEUMBIGA1UECgwLRmlza2VyIEluYy4xETAPBgNVBAMM
CHRlc3RjZXJ0MIIBIzANBgkqhkiG9w0BAQEFAAOCARAAMIIBCwKCAQIA13BpkJvp
tqqGTwnMq+t+A50tzENZ3tmtKLIMeuprTux3oqT9PiUHRTLl0zp2r6X+T0A98P+/
Ad2ybhKtd3qCBEIOkV+M84+q5ecOy2majNQJOgpHNSOtHiAqaZyUslCEtQrLX/Cj
TLT8RvepzxWf7wB9iIj1hYiUFSXWYqWx07TrtcYEdoGiOd8syjRSHr2nMYjOr/K8
4Ihyrze9g5j5Dosp943j2WjPETmGebu6bdi5SsoGbkm6dgtKbTKihuo5RBYKMS7t
xis22jjq4nJigDz506aqY7zRn2Ph1B1CwqxP1O21c7nS78sUmewyKKJY2SX2yB9S
XcfS4uYjFWC+9GcCAwEAAaNQME4wHQYDVR0OBBYEFMnlDS32ShOeQVUahFE3GUoX
p/kEMB8GA1UdIwQYMBaAFMnlDS32ShOeQVUahFE3GUoXp/kEMAwGA1UdEwQFMAMB
Af8wDQYJKoZIhvcNAQELBQADggECAJXUtgm9zuXsDGI1x2zzNY8gjIjsrhToWNAN
tZKIR2eQETEWwzGLVuz/fmpbSdFN/jnlxLQUjaX2YqlU4gSqHcp4ypYLygs+UEbp
tfdFDDfxw/1Oc8BRxAxygt6hnFsGM/uMingc6ON4qKg6UeFx9NTfq4jco+/5YDHL
DNiAv8KUPxreR19bODue6+OKCU6JIkZbMa1/sKzTLkzHbUlHAsxe1JmoqquRvI5z
a/6nNNka6vwyoSSH6PABU976DkPgDS4tSUvz0yUTwss7an6v5YM+i4T+VpA1nMTA
LrSlbsmC+whMPAkl4DE9JtmrM3TQTO10bdWmcpMQuOuQpTmdyCfu
-----END CERTIFICATE-----`
cert1 := common.Certificate{
PublicKey: testcert,
Valid: true,
}
// unexpired cert
result1, err := cert1.IsExpiredOrInvalidAtTime(time.Date(2023, 2, 14, 0, 0, 0, 0, time.Local), 30)
assert.Nil(t, err)
assert.False(t, result1)
// expired cert
result2, err := cert1.IsExpiredOrInvalidAtTime(time.Date(2024, 2, 14, 0, 0, 0, 0, time.Local), 30)
assert.Nil(t, err)
assert.True(t, result2)
// less than 30 days before expiration
result3, err := cert1.IsExpiredOrInvalidAtTime(time.Date(2024, 1, 14, 0, 0, 0, 0, time.Local), 30)
assert.Nil(t, err)
assert.True(t, result3)
cert2 := common.Certificate{
PublicKey: testcert,
Valid: false,
}
// invalid cert
result4, err := cert2.IsExpiredOrInvalidAtTime(time.Date(2023, 2, 14, 0, 0, 0, 0, time.Local), 30)
assert.Nil(t, err)
assert.True(t, result4)
}

View File

@@ -0,0 +1,62 @@
package common
import "time"
type OffPeakCharging struct {
Start time.Time `json:"start" validate:"required"`
End time.Time `json:"end" validate:"required"`
}
type MobileChargeSetting struct {
VIN string `json:"vin" validate:"required,vin"`
ChargeSettings ChargeSettings `json:"charge_settings" validate:"required"`
}
func (m *MobileChargeSetting) GetVIN() string {
return m.VIN
}
func (m *MobileChargeSetting) GetPayload() interface{} {
return m.ChargeSettings
}
func (m *MobileChargeSetting) SetVIN(vin string) {
m.VIN = vin
}
func (m *MobileChargeSetting) SetPayload(payload interface{}) error {
var ok bool
m.ChargeSettings, ok = payload.(ChargeSettings)
if !ok {
return ErrInvalidType
}
return nil
}
type TRexChargeSettings = ChargeSettings
type ChargeSettings struct {
ChargeLimit int `json:"charge_limit" validate:"required"`
MaxCurrent int `json:"max_current" validate:"required"`
MinCharge *int `json:"min_charge,omitempty"`
OffPeakCharging *OffPeakCharging `json:"off_peak_charging,omitempty"`
}
func (m *TRexChargeSettings) SetPayload(payload interface{}) error {
var ok bool
*m, ok = payload.(ChargeSettings)
if !ok {
return ErrInvalidType
}
return nil
}
func (m *TRexChargeSettings) GetMessage() interface{} {
return m
}
func (m *TRexChargeSettings) GetPayload() interface{} {
return *m
}

View File

@@ -0,0 +1,11 @@
package common
import (
"time"
)
type ClickHouseSignal struct {
Timestamp time.Time `json:"timestamp"`
Name string `json:"name"`
Value *float64 `json:"value"`
}

View File

@@ -0,0 +1,16 @@
package common
import (
"fiskerinc.com/modules/common/dbbasemodel"
)
type Configuration struct {
ID string `pg:",pk,unique" json:"id" validate:"required,max=256"`
UniqueCode string `json:"unique_code" validate:"min=8,max=8"`
UserId string `json:"user_id" validate:"required"`
Trim string `json:"trim" validate:"min=2,max=2"`
Color string `json:"color" validate:"min=2,max=2"`
Interior string `json:"interior" validate:"min=2,max=2"`
Wheels string `json:"wheels" validate:"min=2,max=2"`
dbbasemodel.DBModelBase
}

73
pkg/common/consent.go Normal file
View File

@@ -0,0 +1,73 @@
package common
import "time"
type UserConsentFromHMI struct {
Name string `json:"name" validate:"required,user_consent_name"`
Accept bool `json:"accept"`
DriverID string `json:"driver_id" validate:"required"`
}
type UserConsentDataRequest struct {
UserID string `json:"userId"`
ConsentAdminID string `json:"consentAdminId"`
Email string `json:"email,omitempty"`
Phone string `json:"phone,omitempty"`
VehicleIdNum string `json:"vehicleIdNum"`
ConsentName string `json:"consentName"`
ConsentAction string `json:"consentAction"`
PreviousAction string `json:"previousAction,omitempty"`
DataSubjectCategory string `json:"dataSubjectCategory"`
Channel string `json:"channel"`
GuestUser *UserConsentGuestUser `json:"guestUser,omitempty"`
}
type UserConsentGuestUser struct {
ConsentAdminID string `json:"consentAdminId"`
ConsentName string `json:"consentName"`
ConsentAction string `json:"consentAction"`
PreviousAction string `json:"previousAction"`
DataSubjectCategory string `json:"dataSubjectCategory"`
}
type UserConsentDataResponse struct {
UserID string `json:"userId"`
ConsentAdminID string `json:"consentAdminId"`
Email string `json:"email"`
Phone string `json:"phone"`
VehicleIdNum string `json:"vehicleIdNum"`
ConsentName string `json:"consentName"`
ConsentAction string `json:"consentAction"`
PreviousAction string `json:"previousAction"`
DataSubjectCategory string `json:"dataSubjectCategory"`
ConsentRegisteredAt time.Time `json:"consentRegisteredAt"`
Channel string `json:"channel"`
ValidFrom string `json:"validFrom"`
ValidTo string `json:"validTo"`
}
type UserConsentByVehicleNumberRequest struct {
VehicleNumber string `json:"vehicleNumber"`
}
type UserConsentDataTrexMsg struct {
UserID string `json:"driver_id"`
ConsentAdminID string `json:"consent_admin_id"`
VehicleIdNum string `json:"vin"`
ConsentName string `json:"name"`
ConsentAction string `json:"action"`
DataSubjectCategory string `json:"data_subject_category"`
Channel string `json:"channel"`
}
func BuildUserConsentTrexMessage(ucd UserConsentDataResponse) UserConsentDataTrexMsg {
return UserConsentDataTrexMsg{
VehicleIdNum: ucd.VehicleIdNum,
ConsentName: ucd.ConsentName,
ConsentAction: ucd.ConsentAction,
Channel: ucd.Channel,
DataSubjectCategory: ucd.DataSubjectCategory,
ConsentAdminID: ucd.ConsentAdminID,
UserID: ucd.UserID,
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,968 @@
package common
import (
"encoding/json"
"testing"
"time"
"fiskerinc.com/modules/grpc/kafka_grpc"
"fiskerinc.com/modules/utils/elptr"
)
func TestDepotRouteHMIPayload(t *testing.T) {
tests := []struct {
name string
payload *kafka_grpc.GRPC_DepotPayload
expectedResult *ConsumerPayload
expectedError error
}{
{
name: "Valid payload with 'init' handler",
payload: &kafka_grpc.GRPC_DepotPayload{
Handler: "init",
Data: &kafka_grpc.GRPC_DepotPayload_HmiSession{
HmiSession: &kafka_grpc.HMISessionData{
SessionId: "123456",
Vin: "ABC123",
Salt: "salt123",
},
},
},
expectedResult: &ConsumerPayload{
Handler: "init",
Data: func() []byte {
data, _ := json.Marshal(&HMISessionData{
SessionID: "123456",
VIN: "ABC123",
Salt: "salt123",
})
return data
}(),
},
expectedError: nil,
},
// Add more test cases as needed
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result, err := DepotRouteHMIPayload(test.payload)
if err != test.expectedError {
t.Errorf("Expected error: %v, got: %v", test.expectedError, err)
}
if result.Handler != test.expectedResult.Handler {
t.Errorf("Handler mismatch. Expected: %s, got: %s", test.expectedResult.Handler, result.Handler)
}
if string(result.Data) != string(test.expectedResult.Data) {
t.Errorf("Data mismatch. Expected: %s, got: %s", test.expectedResult.Data, result.Data)
}
})
}
}
func TestDepotRouteTRexPayloadMessage(t *testing.T) {
tests := []struct {
name string
payload *kafka_grpc.GRPC_DepotPayload
expectedResult *ConsumerPayload
expectedError error
}{
{
name: "Valid payload with 'init' handler",
payload: &kafka_grpc.GRPC_DepotPayload{
Handler: "init",
Data: &kafka_grpc.GRPC_DepotPayload_InitPayload{
InitPayload: &kafka_grpc.InitPayload{
Data: map[string]string{
"key1": "value1",
"key2": "value2",
},
},
},
},
expectedResult: &ConsumerPayload{
Handler: "init",
Data: func() []byte {
data, _ := json.Marshal(map[string]string{
"key1": "value1",
"key2": "value2",
})
return data
}(),
},
expectedError: nil,
},
{
name: "Payload with nil data",
payload: &kafka_grpc.GRPC_DepotPayload{
Handler: "init",
Data: nil,
},
expectedResult: &ConsumerPayload{
Handler: "init",
Data: nil,
},
expectedError: nil,
},
{
name: "Invalid handlerl handler",
payload: &kafka_grpc.GRPC_DepotPayload{
Handler: "fake_handler",
Data: nil,
},
expectedResult: &ConsumerPayload{
Handler: "fake_handler",
Data: nil,
},
expectedError: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result, err := DepotRouteTRexPayload(test.payload)
if err != test.expectedError {
t.Errorf("Expected error: %v, got: %v", test.expectedError, err)
}
if result.Handler != test.expectedResult.Handler {
t.Errorf("Handler mismatch. Expected: %s, got: %s", test.expectedResult.Handler, result.Handler)
}
if string(result.Data) != string(test.expectedResult.Data) {
t.Errorf("Data mismatch. Expected: %s, got: %s", test.expectedResult.Data, result.Data)
}
})
}
}
func TestAttendantRouteMobilePayload(t *testing.T) {
tests := []struct {
name string
payload *kafka_grpc.GRPC_AttendantPayload
expectedResult *ConsumerPayload
expectedError error
}{
{
name: "Valid payload with 'update_approve' handler",
payload: &kafka_grpc.GRPC_AttendantPayload{
Handler: "update_approve",
Data: &kafka_grpc.GRPC_AttendantPayload_UpdateApprove{
UpdateApprove: &kafka_grpc.UpdateData{
Id: 123,
},
},
},
expectedResult: &ConsumerPayload{
Handler: "update_approve",
Data: func() []byte {
data, _ := json.Marshal(map[string]int{"id": 123})
return data
}(),
},
expectedError: nil,
},
{
name: "Valid payload with 'updates_get' handler",
payload: &kafka_grpc.GRPC_AttendantPayload{
Handler: "updates_get",
Data: &kafka_grpc.GRPC_AttendantPayload_UpdateGet{
UpdateGet: &kafka_grpc.VehicleData{
Vin: "ABC123",
},
},
},
expectedResult: &ConsumerPayload{
Handler: "updates_get",
Data: func() []byte {
data, _ := json.Marshal(map[string]string{"vin": "ABC123"})
return data
}(),
},
expectedError: nil,
},
{
name: "Payload with nil data",
payload: &kafka_grpc.GRPC_AttendantPayload{
Handler: "update_approve",
Data: nil,
},
expectedResult: &ConsumerPayload{
Handler: "update_approve",
Data: nil,
},
expectedError: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result, err := AttendantRouteMobilePayload(test.payload)
if err != test.expectedError {
t.Errorf("Expected error: %v, got: %v", test.expectedError, err)
}
if result.Handler != test.expectedResult.Handler {
t.Errorf("Handler mismatch. Expected: %s, got: %s", test.expectedResult.Handler, result.Handler)
}
if string(result.Data) != string(test.expectedResult.Data) {
t.Errorf("Data mismatch. Expected: %s, got: %s", test.expectedResult.Data, result.Data)
}
})
}
}
func TestUpdateManifestToGRPC(t *testing.T) {
pub1 := "9a1a6949d7f8a511df6e2e2771e444dbd6de97e7d98bdecbb5adc4b8965ce3bef353f523dbea123d7882dc043d415cda02810bad1b6f1b8c6202234a424b7d5b"
pub2 := "407f59557fb64ae98bc30b5370fab138f4827e14784d79bcf707dbe35ba2b85d"
priv1 := "9a1a6949d7f8a511df6e2e2771e444dbd6de97e7d98bdecbb5adc4b8965ce3bef353f523dbea123d7882dc043d415cda02810bad1b6f1b8c6202234a424b7d5c"
var pub1B BinaryHex = []byte(pub1)
var pub2B BinaryHex = []byte(pub2)
var priv1B BinaryHex = []byte(priv1)
date := time.Date(2022, 5, 27, 12, 49, 0, 0, time.UTC)
unixMillis := date.UnixMilli()
toDate := time.Unix(0, unixMillis*int64(time.Millisecond))
flag := true
expectedUpdate := &UpdateManifest{
ID: 1,
ECUs: []*UpdateManifestECU{
{
ID: 3,
Mode: "v",
ECCKeys: &ECCKeys{
ECU: "ecu",
Env: "env",
PubKey1: &pub1B,
PrivKey1: &priv1B,
PubKey2: &pub2B,
},
Files: []*UpdateManifestFile{
{
FileID: "file_id_value",
UpdateManifestECUID: 123456789,
Filename: "filename_value",
URL: "url_value",
FileSize: 1024, // Example file size
Checksum: "checksum_value",
FileType: "type_value",
FileOrder: 1, // Example order
WriteRegion: MemoryRegion{
Offset: 1024, // Example offset
Length: 2048, // Example length
},
EraseRegion: &MemoryRegion{
Offset: 4096, // Example offset
Length: 8192, // Example length
},
FileKey: &FileKeyResponse{
FileID: "file_key_file_id_value",
Key: "file_key_key_value",
Auth: "file_key_auth_value",
Nonce: "file_key_nonce_value",
Error: "file_key_error_value",
},
Parsed: &flag, // Example parsed file value
Signature: "signature_value",
CompatibleTrims: []CompatibleTrim{
EXTREME,
SPORT,
},
CompatibleDriveSides: []CompatibleDriveSide{
LEFT_HAND_DRIVE,
RIGHT_HAND_DRIVE,
},
},
},
},
},
}
expectedUpdate.CreatedAt = &toDate
expectedUpdate.ECUs[0].CreatedAt = &toDate
expectedUpdate.ECUs[0].UpdatedAt = &toDate
expectedUpdate.ECUs[0].Files[0].CreatedAt = &date
expectedUpdate.ECUs[0].Files[0].UpdatedAt = &date
b, _ := json.Marshal(expectedUpdate)
msg := MessageRawJSON{
Handler: "send_manifest",
Data: b,
}
grpcRep := UpdateManifestToGRPC(msg)
if grpcRep == nil {
t.Errorf("Expected not nil reponse from UpdateManifestToGRPC but got: nil")
}
if grpcRep.GetHandler() != msg.Handler {
t.Errorf("Expected handler should equal to %v but got %v:", msg.Handler, grpcRep.Handler)
}
reveseGRPC := GRPCToUpdateManifest(grpcRep)
grpcP, _ := json.Marshal(reveseGRPC)
if string(grpcP) != string(b) {
t.Errorf("Expected data should equal to %v but got %v:", string(b), string(grpcP))
}
}
func TestAttendantSendManifest(t *testing.T) {
pub1 := "9a1a6949d7f8a511df6e2e2771e444dbd6de97e7d98bdecbb5adc4b8965ce3bef353f523dbea123d7882dc043d415cda02810bad1b6f1b8c6202234a424b7d5b"
pub2 := "407f59557fb64ae98bc30b5370fab138f4827e14784d79bcf707dbe35ba2b85d"
priv1 := "9a1a6949d7f8a511df6e2e2771e444dbd6de97e7d98bdecbb5adc4b8965ce3bef353f523dbea123d7882dc043d415cda02810bad1b6f1b8c6202234a424b7d5c"
var pub1B BinaryHex = []byte(pub1)
var pub2B BinaryHex = []byte(pub2)
var priv1B BinaryHex = []byte(priv1)
date := time.Date(2022, 5, 27, 12, 49, 0, 0, time.UTC)
dateString := "2022-05-27T12:49:00Z"
unixMillis := date.UnixMilli()
toDate := time.Unix(0, unixMillis*int64(time.Millisecond))
flag := true
expectedUpdate := &UpdateManifest{
ID: 1,
ECUs: []*UpdateManifestECU{
{
ID: 3,
Mode: "v",
ECCKeys: &ECCKeys{
ECU: "ecu",
Env: "env",
PubKey1: &pub1B,
PrivKey1: &priv1B,
PubKey2: &pub2B,
},
Files: []*UpdateManifestFile{
{
FileID: "file_id_value",
UpdateManifestECUID: 123456789,
Filename: "filename_value",
URL: "url_value",
FileSize: 1024, // Example file size
Checksum: "checksum_value",
FileType: "type_value",
FileOrder: 1, // Example order
WriteRegion: MemoryRegion{
Offset: 1024, // Example offset
Length: 2048, // Example length
},
EraseRegion: &MemoryRegion{
Offset: 4096, // Example offset
Length: 8192, // Example length
},
FileKey: &FileKeyResponse{
FileID: "file_key_file_id_value",
Key: "file_key_key_value",
Auth: "file_key_auth_value",
Nonce: "file_key_nonce_value",
Error: "file_key_error_value",
},
Parsed: &flag, // Example parsed file value
Signature: "signature_value",
CompatibleTrims: []CompatibleTrim{
EXTREME,
SPORT,
},
CompatibleDriveSides: []CompatibleDriveSide{
LEFT_HAND_DRIVE,
RIGHT_HAND_DRIVE,
},
},
},
},
},
}
expectedUpdate.CreatedAt = &toDate
expectedUpdate.ECUs[0].CreatedAt = &toDate
expectedUpdate.ECUs[0].UpdatedAt = &toDate
expectedUpdate.ECUs[0].Files[0].CreatedAt = &date
expectedUpdate.ECUs[0].Files[0].UpdatedAt = &date
tests := []struct {
name string
payload *kafka_grpc.GRPC_AttendantPayload
expectedResult *ConsumerPayload
expectedError error
}{
{
name: "Valid payload with 'send_manifest' handler",
payload: &kafka_grpc.GRPC_AttendantPayload{
Handler: "send_manifest",
Data: &kafka_grpc.GRPC_AttendantPayload_UpdateManifest{
UpdateManifest: &kafka_grpc.UpdateManifest{
Id: 1,
EcuUpdates: []*kafka_grpc.UpdateManifestECU{
{
Created: &unixMillis,
Updated: &unixMillis,
Id: 3,
Mode: "v",
EccKeys: &kafka_grpc.ECCKey{
Ecu: "ecu",
Env: "env",
PubKeyLevel_1: &kafka_grpc.BineryHex{
Data: []byte(pub1),
},
Level_1: &kafka_grpc.BineryHex{
Data: []byte(priv1),
},
PubKeyLevel_2: &kafka_grpc.BineryHex{
Data: []byte(pub2),
},
},
Files: []*kafka_grpc.UpdateManifestFile{
{
FileId: "file_id_value",
ManifestEcuId: 123456789,
Filename: "filename_value",
Url: "url_value",
FileSize: 1024, // Example file size
Checksum: "checksum_value",
Type: "type_value",
Order: 1, // Example order
WriteRegion: &kafka_grpc.MemoryRegion{
Offset: 1024, // Example offset
Length: 2048, // Example length
},
EraseRegion: &kafka_grpc.MemoryRegion{
Offset: 4096, // Example offset
Length: 8192, // Example length
},
FileKey: &kafka_grpc.FileKeyResponse{
FileId: "file_key_file_id_value",
Key: "file_key_key_value",
Auth: "file_key_auth_value",
Nonce: "file_key_nonce_value",
Error: "file_key_error_value",
},
Updated: &dateString,
Created: &dateString,
ParsedFile: &flag, // Example parsed file value
Signature: "signature_value",
CompatibleTrims: []string{
string(EXTREME),
string(SPORT),
},
CompatibleDriveSides: []string{
string(LEFT_HAND_DRIVE),
string(RIGHT_HAND_DRIVE),
},
},
},
},
},
Created: &unixMillis,
},
},
},
expectedResult: &ConsumerPayload{
Handler: "send_manifest",
Data: func() []byte {
data, _ := json.Marshal(expectedUpdate)
return data
}(),
},
expectedError: nil,
},
{
name: "Payload with nil data",
payload: &kafka_grpc.GRPC_AttendantPayload{
Handler: "send_manifest",
Data: nil,
},
expectedResult: &ConsumerPayload{
Handler: "send_manifest",
Data: nil,
},
expectedError: nil,
},
// Add more test cases as needed
}
// Set date
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result, err := AttendantRouteServicePayload(test.payload)
if err != test.expectedError {
t.Errorf("Expected error: %v, got: %v", test.expectedError, err)
}
if result.Handler != test.expectedResult.Handler {
t.Errorf("Handler mismatch. Expected: %s, got: %s", test.expectedResult.Handler, result.Handler)
}
if string(result.Data) != string(test.expectedResult.Data) {
t.Errorf("Data mismatch. Expected: %s, <<><><<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n<<<<<got: %s", test.expectedResult.Data, result.Data)
}
})
}
}
func TestAttendantOrderUpdated(t *testing.T) {
date := time.Date(2022, 5, 27, 12, 49, 0, 0, time.UTC)
unixMillis := date.UnixMilli()
toDate := time.Unix(0, unixMillis*int64(time.Millisecond))
tests := []struct {
name string
payload *kafka_grpc.GRPC_AttendantPayload
expectedResult *ConsumerPayload
expectedError error
}{
{
name: "Valid payload with 'order_updated' handler",
payload: &kafka_grpc.GRPC_AttendantPayload{
Handler: "order_updated",
Data: &kafka_grpc.GRPC_AttendantPayload_Order{
Order: &kafka_grpc.Order{
OrderNo: 123,
MsgIdentifier: "msg123",
SpecId: 456,
VehicleSpecification: &kafka_grpc.VehicleSpecification{
DestCon: "DestinationCountry",
FOrderId: "FleetOrderIndicator",
MfPlant: "ManufacturingPlant",
ModelType: "ModelType",
ModelYear: 2022,
ModelId: 1,
OrderId: "OrderIndicator",
ProductId: "ProductionPhaseIndicator",
Sn: "SequenceNumber",
VehicleId: "VehicleIndicator",
Model: "VehicleModel",
VinPre: "VinPrefix",
Version: 1,
Date: unixMillis,
Feature: []*kafka_grpc.FeatureCodes{
{
FamilyCode: "family1",
FeatureCode: "feature1",
},
{
FamilyCode: "family2",
FeatureCode: "feature2",
},
},
},
},
},
},
expectedResult: &ConsumerPayload{
Handler: "order_updated",
Data: func() []byte {
order := &VehicleOrder{
OrderNumber: 123,
MessageIdentifier: "msg123",
SpecID: 456,
VehicleSpecification: VehicleSpecification{
DestinationCountry: "DestinationCountry",
FleetOrderIndicator: "FleetOrderIndicator",
ManufacturingPlant: "ManufacturingPlant",
ModelType: "ModelType",
ModelYear: 2022,
ModelYearIndicator: 1,
OrderIndicator: "OrderIndicator",
ProductionPhaseIndicator: "ProductionPhaseIndicator",
SequenceNumber: "SequenceNumber",
VehicleIndicator: "VehicleIndicator",
VehicleModel: "VehicleModel",
VinPrefix: "VinPrefix",
VersionDuringModelYear: 1,
ExpectedReferenceDate: ExpectedReferenceDate{toDate},
VehicleFeatures: []FeatureCodes{
{
FamilyCode: "family1",
FeatureCode: "feature1",
},
{
FamilyCode: "family2",
FeatureCode: "feature2",
},
},
},
}
data, _ := json.Marshal(order)
return data
}(),
},
expectedError: nil,
},
{
name: "Payload with nil data",
payload: &kafka_grpc.GRPC_AttendantPayload{
Handler: "order_updated",
Data: nil,
},
expectedResult: &ConsumerPayload{
Handler: "order_updated",
Data: nil,
},
expectedError: nil,
},
// Add more test cases as needed
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result, err := AttendantRouteServicePayload(test.payload)
if err != test.expectedError {
t.Errorf("Expected error: %v, got: %v", test.expectedError, err)
}
if result.Handler != test.expectedResult.Handler {
t.Errorf("Handler mismatch. Expected: %s, got: %s", test.expectedResult.Handler, result.Handler)
}
if string(result.Data) != string(test.expectedResult.Data) {
t.Errorf("Data mismatch. Expected: %s, got: %s", test.expectedResult.Data, result.Data)
}
})
}
}
func TestValetRouteTRexCMDChangedPayload(t *testing.T) {
tests := []struct {
name string
payload *kafka_grpc.GRPC_ValetPayload
expectedResult *ConsumerPayload
expectedError error
}{
{
name: "Valid payload with 'charging_command' handler",
payload: &kafka_grpc.GRPC_ValetPayload{
Handler: "charging_command",
Data: &kafka_grpc.GRPC_ValetPayload_ChCMD{
ChCMD: &kafka_grpc.ChargingCommand{
Action: "start_charging",
},
},
},
expectedResult: &ConsumerPayload{
Handler: "charging_command",
Data: func() []byte {
data, _ := json.Marshal(&kafka_grpc.ChargingCommand{
Action: "start_charging",
})
return data
}(),
},
expectedError: nil,
},
{
name: "Payload with nil data",
payload: &kafka_grpc.GRPC_ValetPayload{
Handler: "charging_command",
Data: nil,
},
expectedResult: &ConsumerPayload{
Handler: "charging_command",
Data: nil,
},
expectedError: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result, err := ValetRouteTRexPayload(test.payload)
if err != test.expectedError {
t.Errorf("Expected error: %v, got: %v", test.expectedError, err)
}
if result.Handler != test.expectedResult.Handler {
t.Errorf("Handler mismatch. Expected: %s, got: %s", test.expectedResult.Handler, result.Handler)
}
if string(result.Data) != string(test.expectedResult.Data) {
t.Errorf("Data mismatch. Expected: %s, got: %s", test.expectedResult.Data, result.Data)
}
})
}
}
func TestValetRouteTRexDepartureSchedulePayload(t *testing.T) {
tests := []struct {
name string
payload *kafka_grpc.GRPC_ValetPayload
expectedResult *ConsumerPayload
expectedError error
}{
{
name: "Valid payload with 'departure_schedule' handler",
payload: &kafka_grpc.GRPC_ValetPayload{
Handler: "departure_schedule",
Data: &kafka_grpc.GRPC_ValetPayload_DepartureSchedule{
DepartureSchedule: &kafka_grpc.DepartureSchedule{
NextDayDeparture: nil,
DepartureDays: []*kafka_grpc.DepartureDay{
{
DayOfWeek: "Monday",
Time: "08:00",
},
{
DayOfWeek: "Wednesday",
Time: "10:00",
},
},
},
},
},
expectedResult: &ConsumerPayload{
Handler: "departure_schedule",
Data: func() []byte {
data, _ := json.Marshal(&DepartureSchedule{
NextDayDeparture: nil,
DepartureDays: []DepartureDay{
{
DayOfWeek: "Monday",
Time: "08:00",
},
{
DayOfWeek: "Wednesday",
Time: "10:00",
},
},
})
return data
}(),
},
expectedError: nil,
},
{
name: "Payload with nil data",
payload: &kafka_grpc.GRPC_ValetPayload{
Handler: "departure_schedule",
Data: nil,
},
expectedResult: &ConsumerPayload{
Handler: "departure_schedule",
Data: nil,
},
expectedError: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result, err := ValetRouteTRexPayload(test.payload)
if err != test.expectedError {
t.Errorf("Expected error: %v, got: %v", test.expectedError, err)
}
if result.Handler != test.expectedResult.Handler {
t.Errorf("Handler mismatch. Expected: %s, got: %s", test.expectedResult.Handler, result.Handler)
}
if string(result.Data) != string(test.expectedResult.Data) {
t.Errorf("Data mismatch. Expected: %s, got: %s", test.expectedResult.Data, result.Data)
}
})
}
}
// Unit tests for CarStateToGRPC function
func TestCarStateToGRPC(t *testing.T) {
// Test case 1: Valid input
ecu := CarECU{
VIN: "123456",
ECU: "ECU1",
Version: "v1",
Epoch_usec: time.Now().Unix(),
}
carState := CarStateUpdate{
ECUs: map[string]CarECU{"ecu1": ecu},
}
data, err := json.Marshal(carState)
if err != nil {
t.Fatalf("Failed to marshal test data: %v", err)
}
input1 := MessageRawJSON{
Handler: "car_state_updated",
Data: data,
}
expected := &kafka_grpc.GRPC_AttendantPayload{
Handler: "car_state_updated",
Data: &kafka_grpc.GRPC_AttendantPayload_CarUpdateStatus{
CarUpdateStatus: &kafka_grpc.CarUpdateStatus{
Ecus: map[string]*kafka_grpc.CarECU{
"ecu1": {
Vin: ecu.VIN,
Ecu: ecu.ECU,
SwVersion: ecu.Version,
EpochUsec: ecu.Epoch_usec,
// Add other fields here as needed
},
},
},
},
}
output := CarStateToGRPC(input1)
expectedJSON, _ := json.Marshal(expected)
actualJSON, _ := json.Marshal(output)
if string(expectedJSON) != string(actualJSON) {
t.Errorf("Test case 1 failed: expected handler %+v, got %+v", expected.Data, output.Data)
}
}
// func TestGRPCToECCKeys(t *testing.T) {
// var pub1, pub2, priv1, priv2 []byte
// pub1 = []byte(`9a1a6949d7f8a511df6e2e2771e444dbd6de97e7d98bdecbb5adc4b8965ce3bef353f523dbea123d7882dc043d415cda02810bad1b6f1b8c6202234a424b7d5b`)
// pub2 = []byte(`9a1a6949d7f8a511df6e2e2771e444dbd6de97e7d98bdecbb5adc4b8965ce3bef353f523dbea123d7882dc043d415cda02810bad1b6f1b8c6202234a424b7d5c`)
// priv1 = []byte(`407f59557fb64ae98bc30b5370fab138f4827e14784d79bcf707dbe35ba2b85b`)
// priv2 = []byte(`407f59557fb64ae98bc30b5370fab138f4827e14784d79bcf707dbe35ba2b85c`)
// var pub11, pub22, priv11, priv22 BinaryHex
// pub11 = []byte(`9a1a6949d7f8a511df6e2e2771e444dbd6de97e7d98bdecbb5adc4b8965ce3bef353f523dbea123d7882dc043d415cda02810bad1b6f1b8c6202234a424b7d5b`)
// pub22 = []byte(`9a1a6949d7f8a511df6e2e2771e444dbd6de97e7d98bdecbb5adc4b8965ce3bef353f523dbea123d7882dc043d415cda02810bad1b6f1b8c6202234a424b7d5c`)
// priv11 = []byte(`407f59557fb64ae98bc30b5370fab138f4827e14784d79bcf707dbe35ba2b85b`)
// priv22 = []byte(`407f59557fb64ae98bc30b5370fab138f4827e14784d79bcf707dbe35ba2b85c`)
// testCases := []struct {
// name string
// payload *kafka_grpc.GRPC_AttendantPayload
// expected []ECCKeys
// }{
// {
// name: "NilPayload",
// payload: &kafka_grpc.GRPC_AttendantPayload{
// Handler: "test",
// },
// expected: nil,
// },
// {
// name: "WithPayload",
// payload: &kafka_grpc.GRPC_AttendantPayload{
// Handler: "test",
// Data: &kafka_grpc.GRPC_AttendantPayload_Keys{
// Keys: &kafka_grpc.ECCKeys{
// EccKeys: []*kafka_grpc.ECCKey{
// {
// Ecu: "ecu",
// Env: "env",
// PubKeyLevel_1: &pub1,
// PubKeyLevel_2: &pub2,
// Level_1: &priv1,
// Level_2: &priv2,
// },
// },
// },
// },
// },
// expected: []ECCKeys{
// {
// ECU: "ecu",
// Env: "env",
// PubKey1: &pub11,
// PubKey2: &pub22,
// PrivKey1: &priv11,
// PrivKey2: &priv22,
// },
// },
// },
// }
// for _, tc := range testCases {
// t.Run(tc.name, func(t *testing.T) {
// result := GRPCToECCKeys(tc.payload)
// // Compare the result with expected value
// if !equal(result, tc.expected) {
// t.Errorf("Test case %s failed. Expected: %v, got: %v", tc.name, tc.expected, result)
// }
// })
// }
// }
// Helper function to check equality of slices of ECCKeys
// func equal(a, b []ECCKeys) bool {
// if len(a) != len(b) {
// return false
// }
// for i := range a {
// if a[i] != b[i] {
// return false
// }
// }
// return true
// }
func TestRemoteCMD(t *testing.T) {
input := []struct {
name string
cmd *RemoteCommandRequest
}{
{
name: "With valid data",
cmd: &RemoteCommandRequest{
VIN: "1F15K3R45N1234567",
RemoteCommandSource: RemoteCommandSource{
Command: "temp_cabin",
Data: elptr.ElPtr("20"),
Start: elptr.ElPtr(time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)),
End: elptr.ElPtr(time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)),
},
},
},
{
name: "With end date null",
cmd: &RemoteCommandRequest{
VIN: "1F15K3R45N1234567",
RemoteCommandSource: RemoteCommandSource{
Command: "temp_cabin",
Data: elptr.ElPtr("20"),
Start: elptr.ElPtr(time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)),
End: nil,
},
},
},
{
name: "With all values with zero",
cmd: &RemoteCommandRequest{},
},
{
name: "With null data",
cmd: nil,
},
{
name: "With zero cmd values",
cmd: &RemoteCommandRequest{
RemoteCommandSource: RemoteCommandSource{},
},
},
}
for _, in := range input {
t.Run(in.name, func(t *testing.T) {
b := toBinaryArray(in.cmd)
rawMsg := MessageRawJSON{
Handler: "remote_command",
Data: b,
}
protoCmd := RemoteCMDToGRPC(rawMsg)
resCMD := GRPCToRemotCMD(protoCmd)
protoCmdInBinary := toBinaryArray(resCMD)
if string(b) != string(protoCmdInBinary) {
t.Errorf("Expected %v, got %v", string(b), string(protoCmdInBinary))
}
})
}
}

View File

@@ -0,0 +1,5 @@
package context
type ContextType string
const ProviderKey ContextType = "auth_provider"

View File

@@ -0,0 +1,22 @@
package dbbasemodel
import "time"
type DBModelBase struct {
CreatedAt *time.Time `pg:"default:now()" json:"created,omitempty" swaggerignore:"true"`
UpdatedAt *time.Time `pg:"default:now()" json:"updated,omitempty" swaggerignore:"true"`
}
func (d *DBModelBase) ClearCreatedAt() {
d.CreatedAt = nil
}
func (d *DBModelBase) ClearDates() {
d.CreatedAt = nil
d.UpdatedAt = nil
}
// Keeping the Scrub name used in other locations so people are more likely to find this function
func (d *DBModelBase) Scrub(){
d.ClearDates()
}

140
pkg/common/dbc_desc.go Normal file
View File

@@ -0,0 +1,140 @@
package common
import (
"fmt"
"strings"
can "github.com/Fisker-Inc/project-ai-can-go/pkg/descriptor"
)
// SignalDesc is copied from project-ai-can-go and modified for local needs.
type SignalDesc struct {
DBCHash string `json:"dbc_hash" ch:"dbc_hash"`
MessageID uint16 `json:"message_id" ch:"message_id"`
// Description of the signal.
Name string `json:"name" ch:"signal_name"`
// Start bit.
Start uint16 `json:"start" ch:"start"`
// Length in bits.
Length uint16 `json:"length" ch:"length"`
// IsBigEndian is true if the signal is big-endian.
IsBigEndian bool `json:"big_endian" ch:"big_endian"`
// IsSigned is true if the signal uses raw signed values.
IsSigned bool `json:"signed" ch:"signed"`
// IsMultiplexer is true if the signal is the multiplexor of a multiplexed message.
IsMultiplexer bool `json:"multiplexer" ch:"multiplexer"`
// IsMultiplexed is true if the signal is multiplexed.
IsMultiplexed bool `json:"multiplexed" ch:"multiplexed"`
// MultiplexerValue is the value of the multiplexer when this signal is present.
MultiplexerValue uint8 `json:"multiplexer_value" ch:"multiplexer_value"`
// Offset for real-world transform.
Offset float64 `json:"offset" ch:"offset"`
// Scale for real-world transform.
Scale float64 `json:"scale" ch:"scale"`
// Min real-world value.
Min float64 `json:"min" ch:"min"`
// Max real-world value.
Max float64 `json:"max" ch:"max"`
// Unit of the signal.
Unit string `json:"unit" ch:"unit"`
// Description of the signal.
Description string `json:"description" ch:"description"`
// ValueDescriptions of the signal.
ValueDescriptions []string `json:"value_descriptions" ch:"value_descriptions"`
// ReceiverNodes is the list of names of the nodes receiving the signal.
ReceiverNodes []string `json:"receiver_nodes" ch:"receiver_nodes"`
// DefaultValue of the signal.
DefaultValue int64 `json:"default_value" ch:"default_value"`
// ECUName is a name of an ECU.
ECUName string
}
func (s *SignalDesc) CopyFromCAN(in *can.Signal) {
s.Name = in.Name
s.Start = in.Start
s.Length = in.Length
s.IsBigEndian = in.IsBigEndian
s.IsSigned = in.IsSigned
s.IsMultiplexer = in.IsMultiplexer
s.IsMultiplexed = in.IsMultiplexed
s.MultiplexerValue = uint8(in.MultiplexerValue)
s.Offset = in.Offset
s.Scale = in.Scale
s.Min = in.Min
s.Max = in.Max
s.Unit = in.Unit
s.Description = in.Description
s.ValueDescriptions = getDescriptions(in.ValueDescriptions)
s.ReceiverNodes = in.ReceiverNodes
s.DefaultValue = int64(in.DefaultValue)
}
func getDescriptions(ds []*can.ValueDescription) []string {
res := make([]string, len(ds))
for k, d := range ds {
if d == nil {
continue
}
res[k] = fmt.Sprintf("%d: %s", d.Value, d.Description)
}
return res
}
type SignalDescWithECU struct {
SignalDesc
ECUName string `json:"ecu_name" ch:"ecu_name"`
}
type MessageDesc struct {
DBCHash string
// Name of the message.
Name string
// ECUName is a name of an ECU.
ECUName string
// ID of the message.
ID uint32
// IsExtended is true if the message is an extended CAN message.
IsExtended bool
// SendType is the message's send type.
SendType string
// Length in bytes.
Length uint16
// Description of the message.
Description string
// SenderNode is the name of the node sending the message.
SenderNode string
// CycleTime is the cycle time (ns) of a cyclic message.
CycleTime int64
// DelayTime is the allowed delay (ns) between cyclic message sends.
DelayTime int64
}
func (m *MessageDesc) CopyFromCAN(in *can.Message) {
m.Name = in.Name
m.ID = in.ID
m.IsExtended = in.IsExtended
m.SendType = in.SendType.String()
m.Length = in.Length
m.Description = in.Description
m.SenderNode = in.SenderNode
m.CycleTime = int64(in.CycleTime)
m.DelayTime = int64(in.DelayTime)
m.ECUName = extractECUName(in.Name)
}
func extractECUName(msgName string) string {
names := strings.Split(msgName, "_")
ecuName := names[0]
if names[0] == "Diag" && len(names) > 1 {
ecuName = names[1]
}
return ecuName
}
type DBCDesc struct {
Hash string
Name string
}

134
pkg/common/dbc_desc_test.go Normal file
View File

@@ -0,0 +1,134 @@
package common_test
import (
"testing"
"fiskerinc.com/modules/common"
"github.com/Fisker-Inc/project-ai-can-go/pkg/descriptor"
"github.com/stretchr/testify/assert"
)
func TestCopySignal(t *testing.T) {
expOut := common.SignalDesc{
Name: "TBOX_RemPassSeatHeatgCmd",
Start: 23,
Length: 3,
IsBigEndian: true,
IsSigned: false,
IsMultiplexer: false,
IsMultiplexed: false,
MultiplexerValue: 0,
Offset: 0,
Scale: 1,
Min: 0,
Max: 7,
Unit: "",
Description: "Remote passenger seat heating command",
ValueDescriptions: []string{
"0: without_remote_control",
"1: Remote_open_passenger_seat_heating_first_gear",
"2: Remote_passenger_seat_heating_second_gear",
"3: Remote_passenger_seat_heating_third_gear",
"4: Remote_closing_passenger_seat_heating",
"5: Reserved",
"6: Reserved",
"7: Reserved",
},
ReceiverNodes: []string{
"GW",
},
DefaultValue: 0,
}
in := descriptor.Signal{
Name: "TBOX_RemPassSeatHeatgCmd",
Start: 23,
Length: 3,
IsBigEndian: true,
IsSigned: false,
IsMultiplexer: false,
IsMultiplexed: false,
MultiplexerValue: 0,
Offset: 0,
Scale: 1,
Min: 0,
Max: 7,
Unit: "",
Description: "Remote passenger seat heating command",
ValueDescriptions: []*descriptor.ValueDescription{
{
Value: 0,
Description: "without_remote_control",
},
{
Value: 1,
Description: "Remote_open_passenger_seat_heating_first_gear",
},
{
Value: 2,
Description: "Remote_passenger_seat_heating_second_gear",
},
{
Value: 3,
Description: "Remote_passenger_seat_heating_third_gear",
},
{
Value: 4,
Description: "Remote_closing_passenger_seat_heating",
},
{
Value: 5,
Description: "Reserved",
},
{
Value: 6,
Description: "Reserved",
},
{
Value: 7,
Description: "Reserved",
},
},
ReceiverNodes: []string{
"GW",
},
DefaultValue: 0,
}
s := common.SignalDesc{}
s.CopyFromCAN(&in)
assert.Equal(t, expOut, s)
}
func TestCopyMessage(t *testing.T) {
in := &descriptor.Message{
Name: "Diag_EPS2_Resp",
ID: 2045,
IsExtended: false,
SendType: 0,
Length: 8,
Description: "",
SenderNode: "GW",
CycleTime: 1000000,
DelayTime: 2000000,
}
expOut := common.MessageDesc{
Name: "Diag_EPS2_Resp",
ID: 2045,
IsExtended: false,
SendType: "None",
Length: 8,
Description: "",
SenderNode: "GW",
ECUName: "EPS2",
CycleTime: 1000000,
DelayTime: 2000000,
}
s := common.MessageDesc{}
s.CopyFromCAN(in)
assert.Equal(t, expOut, s)
}

View File

@@ -0,0 +1,62 @@
package common
import "errors"
var ErrInvalidType = errors.New("invalid type")
type MobileDepartureSchedule struct {
VIN string `json:"vin" validate:"required,vin"`
DepartureSchedule DepartureSchedule `json:"departure_schedule" validate:"required"`
}
func (m *MobileDepartureSchedule) SetVIN(vin string) {
m.VIN = vin
}
func (m *MobileDepartureSchedule) SetPayload(payload interface{}) error {
var ok bool
m.DepartureSchedule, ok = payload.(DepartureSchedule)
if !ok {
return ErrInvalidType
}
return nil
}
func (m *MobileDepartureSchedule) GetVIN() string {
return m.VIN
}
func (m *MobileDepartureSchedule) GetPayload() interface{} {
return m.DepartureSchedule
}
type TRexDepartureSchedule = DepartureSchedule
type DepartureSchedule struct {
NextDayDeparture *string `json:"next_day_departure,omitempty" validate:"required_without=DepartureDays"`
DepartureDays []DepartureDay `json:"departure_days,omitempty"`
}
type DepartureDay struct {
DayOfWeek string `json:"day_of_week" validate:"oneof=Mon Tue Wed Thu Fri Sat Sun"`
Time string `json:"time" validate:"datetime=15:04"`
}
func (m *TRexDepartureSchedule) SetPayload(payload interface{}) error {
var ok bool
*m, ok = payload.(DepartureSchedule)
if !ok {
return ErrInvalidType
}
return nil
}
func (m *TRexDepartureSchedule) GetMessage() interface{} {
return m
}
func (m *TRexDepartureSchedule) GetPayload() interface{} {
return *m
}

53
pkg/common/device.go Normal file
View File

@@ -0,0 +1,53 @@
package common
import (
"fmt"
"strconv"
"strings"
)
type Device int
const (
Unknown Device = iota
TRex
HMI
Mobile
Service
)
// Key returns formatting for websockets, kafka, redis given an ID
// ex: TRex.Key("FISKER123") returns "1:FISKER123"
func (d Device) Key(id string) string {
return fmt.Sprintf("%d:%s", d, id)
}
func (d Device) String() string {
return [...]string{"unknown", "trex", "hmi", "mobile", "service"}[d]
}
func (d Device) EnumIndex() int {
return int(d)
}
// ParseDeviceKey is a generic parser for keys from websockets, kafka, redis
// If unable to parse string correctly, will return device type Unknown
// ex: "1:FISKER123" returns (TRex, "FISKER123")
// ex: "-1:FISKER123" returns (Unknown, "FISKER123")
// ex: "FISKER123" returns (Unknown, "FISKER123")
func ParseDeviceKey(key string) (Device, string) {
keys := strings.Split(key, ":")
if len(keys) == 1 {
return Unknown, keys[0]
} else if len(keys) != 2 {
return Unknown, ""
}
i, err := strconv.Atoi(keys[0])
if err != nil {
return Unknown, keys[1]
}
return Device(i), keys[1]
}

View File

@@ -0,0 +1,58 @@
package common
import "time"
type DigitalTwinRequest struct {
VIN string `json:"vin" validate:"vin"`
}
type JSONDigitalTwin struct {
VIN string `json:"vin"`
Online bool `json:"online"`
OnlineHMI bool `json:"online_hmi"`
VehicleSpeed *VehicleSpeed `json:"vehicle_speed,omitempty"`
Gear *Gear `json:"gear,omitempty"`
Battery *JSONBattery `json:"battery,omitempty"`
Doors *Doors `json:"doors,omitempty"`
Location *Location `json:"location,omitempty"`
Locks *Locks `json:"door_locks,omitempty"`
Windows *JSONWindows `json:"windows,omitempty"`
ClimateControl *JSONClimateControl `json:"climate_control,omitempty"`
TRexVersion string `json:"trex_version,omitempty"`
IP string `json:"ip,omitempty"`
VehicleReadyState *VehicleReadyState `json:"vehicle_ready_state,omitempty"`
ExpandedSignals *ExpandedSignals `json:"expanded_signals,omitempty"`
UpdatedAt *time.Time `json:"updated,omitempty"`
}
type JSONBattery struct {
Percent *int `json:"percent,omitempty"`
MaxMiles *int `json:"max_miles,omitempty"`
AvgCellTemperature *int `json:"avg_cell_temp,omitempty"`
ChargeType *string `json:"charge_type,omitempty"`
RemainingChargingTime *int `json:"remaining_charging_time,omitempty"`
RemainingChargingTimeFull *int `json:"remaining_charging_time_full,omitempty"`
StateOfCharge *int `json:"state_of_charge,omitempty"`
TotalMileageOdometer *int `json:"total_mileage_odometer,omitempty"`
}
type JSONWindows struct {
LeftFront int `json:"left_front"`
LeftRear int `json:"left_rear"`
LeftRearQuarter int `json:"left_rear_quarter"`
RightFront int `json:"right_front"`
RightRear int `json:"right_rear"`
RightRearQuarter int `json:"right_rear_quarter"`
RearWindshield int `json:"rear_windshield"`
Sunroof int `json:"sunroof"`
}
type JSONClimateControl struct {
CabinTemperature int `json:"cabin_temperature"`
RearDefrost bool `json:"rear_defrost"`
DriverSeatHeat int `json:"driver_seat_heat"`
PassengerSeatHeat int `json:"passenger_seat_heat"`
SteeringWheelHeat bool `json:"steering_wheel_heat"`
AmbientTemperature int `json:"ambient_temperature"`
InternalTemperature int `json:"internal_temperature"`
}

31
pkg/common/driver.go Normal file
View File

@@ -0,0 +1,31 @@
package common
import (
"fmt"
"fiskerinc.com/modules/common/dbbasemodel"
)
// DriverAccount model
type Driver struct {
ID string `pg:",pk,unique" json:"id" validate:"required,max=256"`
dbbasemodel.DBModelBase
}
func (da Driver) String() string {
return fmt.Sprintf("Driver<%s>", da.ID)
}
type DriverExternal struct {
ExternalID string `pg:"external_id"`
Source OVSource `pg:"source"`
}
type OVSource string
var (
OV_DEFAULT OVSource = "OVLoop"
OV_DEV OVSource = "OVLoop-dev"
OV_QA OVSource = "OVLoop-qa"
OV_PROD OVSource = "OVLoop-prod"
)

View File

@@ -0,0 +1,9 @@
package common
type DriverEmail struct {
Vin string `json:"vin"`
DriverId string `json:"driver_id"`
Email string `json:"email"`
GivenName string `json:"given_name"`
FamilyName string `json:"family_name"`
}

7
pkg/common/dtc_alert.go Normal file
View File

@@ -0,0 +1,7 @@
package common
type DTCAlert struct {
Timestamp int `json:"epoch_usec"`
ECU string `json:"ecu"`
DTC string `json:"dtc"`
}

50
pkg/common/ecc_keys.go Normal file
View File

@@ -0,0 +1,50 @@
package common
import (
"fiskerinc.com/modules/common/dbbasemodel"
)
type ECCKeys struct {
ECU string `json:"ecu,omitempty" pg:",pk"`
Env string `json:"env,omitempty" pg:",pk"`
PubKey1 *BinaryHex `json:"pub_key_level_1,omitempty" pg:"pub_key_level_1,type:bytea" swaggertype:"string" format:"hex" example:"9a1a6949d7f8a511df6e2e2771e444dbd6de97e7d98bdecbb5adc4b8965ce3bef353f523dbea123d7882dc043d415cda02810bad1b6f1b8c6202234a424b7d5b"`
PrivKey1 *BinaryHex `json:"level_1,omitempty" pg:"priv_key_level_1,type:bytea" swaggertype:"string" format:"hex" example:"407f59557fb64ae98bc30b5370fab138f4827e14784d79bcf707dbe35ba2b85d"`
PubKey2 *BinaryHex `json:"pub_key_level_2,omitempty" pg:"pub_key_level_2,type:bytea" swaggertype:"string" format:"hex" example:"9a1a6949d7f8a511df6e2e2771e444dbd6de97e7d98bdecbb5adc4b8965ce3bef353f523dbea123d7882dc043d415cda02810bad1b6f1b8c6202234a424b7d5b"`
PrivKey2 *BinaryHex `json:"level_2,omitempty" pg:"priv_key_level_2,type:bytea" swaggertype:"string" format:"hex" example:"407f59557fb64ae98bc30b5370fab138f4827e14784d79bcf707dbe35ba2b85d"`
PubKey3 *BinaryHex `json:"pub_key_level_3,omitempty" pg:"pub_key_level_3,type:bytea" swaggertype:"string" format:"hex" example:"9a1a6949d7f8a511df6e2e2771e444dbd6de97e7d98bdecbb5adc4b8965ce3bef353f523dbea123d7882dc043d415cda02810bad1b6f1b8c6202234a424b7d5b"`
PrivKey3 *BinaryHex `json:"level_3,omitempty" pg:"priv_key_level_3,type:bytea" swaggertype:"string" format:"hex" example:"407f59557fb64ae98bc30b5370fab138f4827e14784d79bcf707dbe35ba2b85d"`
dbbasemodel.DBModelBase
}
func (ek *ECCKeys) Init(pub1 *BinaryHex, pub2 *BinaryHex, pub3 *BinaryHex, priv1 *BinaryHex, priv2 *BinaryHex, priv3 *BinaryHex) {
ek.PubKey1 = pub1
ek.PubKey2 = pub2
ek.PubKey3 = pub3
ek.PrivKey1 = priv1
ek.PrivKey2 = priv2
ek.PrivKey3 = priv3
}
// Use this to remove unneeded values when adding to update manifest's ecc_keys
func (ek *ECCKeys) ScrubForManifest() {
ek.ScrubForAftersales()
ek.PubKey1 = nil
ek.PubKey2 = nil
ek.PubKey3 = nil
}
func (ek *ECCKeys) ScrubForAftersales() {
ek.ECU = ""
ek.Env = ""
ek.CreatedAt = nil
ek.UpdatedAt = nil
}
// This transforming nonsense is getting pretty crazy here
// Unsure if this will actually work
func (ek *ECCKeys) TransformECUName() {
replacement, ok := ECUReplacement()[ek.ECU]
if ok {
ek.ECU = replacement
}
}

111
pkg/common/ecu_dtc.go Normal file
View File

@@ -0,0 +1,111 @@
package common
import (
"fmt"
"time"
"fiskerinc.com/modules/common/dbbasemodel"
)
type AddDTCRequest struct {
Data map[string]DTCReq `validate:"required,gt=0,dive,keys,max=1000,endkeys,dive"`
}
type DTCReq struct {
EpochUsec int64 `json:"epoch_usec"`
DTCs [][]byte `json:"dtcs" validate:"required,gt=0,dive,max=10000,required,gt=0,dive,max=1024"`
}
type DTC_ECU struct {
ID int `json:"id,omitempty" pg:",pk"`
VIN string `json:"vin,omitempty" bson:"vin" validate:"vin"`
ECU string `json:"ecu_name" bson:"ecu" validate:"required,ecu"`
TroubleCode int64 `json:"trouble_code" bson:"dtc"`
StatusByte int64 `json:"status_byte" bson:"status"`
Epoch_usec time.Time `json:"epoch_usec" bson:"timestamp"`
Created_at time.Time `json:"created_at" bson:"created_at"`
Speed uint16 `json:"speed" bson:"speed"`
Mileage uint32 `json:"mileage" bson:"mileage"`
Voltage uint16 `json:"voltage" bson:"voltage"`
Snapshot string `json:"snapshot,omitempty" bson:"snapshot,omitempty"`
Information *DTCInformation `json:"trouble_code_information,omitempty" pg:"-"`
StatusByteDecode []DTCStatusByteMeaning `json:"status_byte_meaning,omitempty" pg:"-"`
dbbasemodel.DBModelBase
}
type DTC_ECUQuery struct {
VIN string `pg:"vin" validate:"vin"`
ECU string `pg:"ecu,omit_empty" validate:"required,ecu"`
TroubleCode int64 `pg:"trouble_code,omit_empty" json:"trouble_code"`
StartTime *time.Time
EndTime *time.Time
}
type DTCInformation struct {
ShortName string `bson:"ShortName"`
TroubleCode int64 `bson:"TroubleCode"`
TroubleCodeHex string `bson:"TroubleCodeHex"`
DisplayTroubleCode string `bson:"DisplayTroubleCode"`
ErrorText struct {
Ti string `bson:"TI"`
Text string `bson:"Text"`
} `bson:"ErrorText"`
SplDataGroups struct {
SplDataGroup struct {
SplDataGroupCaption interface{} `bson:"SplDataGroupCaption"`
SplData []struct {
SemanticInformation string `bson:"SemanticInformation"`
Ti string `bson:"TI"`
Text string `bson:"Text"`
} `bson:"SplData"`
} `bson:"SplDataGroup"`
} `bson:"SplDataGroups"`
// ID string `bson:"_id"`
// OID interface{} `bson:"OId"`
}
func (dtc DTC_ECU) DTCStatusByteMeaning() (statuses []DTCStatusByteMeaning) {
statuses = make([]DTCStatusByteMeaning, 0)
if 0b10000000&dtc.StatusByte != 0 {
statuses = append(statuses, Bit0)
}
if 0b01000000&dtc.StatusByte != 0 {
statuses = append(statuses, Bit1)
}
if 0b00100000&dtc.StatusByte != 0 {
statuses = append(statuses, Bit2)
}
if 0b00010000&dtc.StatusByte != 0 {
statuses = append(statuses, Bit3)
}
if 0b00001000&dtc.StatusByte != 0 {
statuses = append(statuses, Bit4)
}
if 0b00000100&dtc.StatusByte != 0 {
statuses = append(statuses, Bit5)
}
if 0b00000010&dtc.StatusByte != 0 {
statuses = append(statuses, Bit6)
}
if 0b00000001&dtc.StatusByte != 0 {
statuses = append(statuses, Bit7)
}
return
}
// Bit meaning from here: https://www.ni.com/docs/en-US/bundle/automotive-diagnostic-command-set-toolkit/page/adcs/udsreportdtcbystatusmaskvi.html#:~:text=Status%20is%20the%20DTC%20status.%20Usually%2C%20this%20is%20a%20bit%20field%20with%20the%20following%20meaning
type DTCStatusByteMeaning string
const (
Bit0 DTCStatusByteMeaning = "testFailed"
Bit1 DTCStatusByteMeaning = "testFailedThisMonitoringCycle" //monitoring or operation
Bit2 DTCStatusByteMeaning = "pendingDTC"
Bit3 DTCStatusByteMeaning = "confirmedDTC"
Bit4 DTCStatusByteMeaning = "testNotCompletedSinceLastClear"
Bit5 DTCStatusByteMeaning = "testFailedSinceLastClear"
Bit6 DTCStatusByteMeaning = "testNotCompletedThisMonitoringCycle" //monitoring or operation
Bit7 DTCStatusByteMeaning = "warningIndicatorRequested"
)
func (dtc *DTC_ECU) CacheKey() string {
return fmt.Sprintf("%s:%s:%d", dtc.VIN, dtc.ECU, dtc.TroubleCode)
}

View File

@@ -0,0 +1,62 @@
package common
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestDTCStatusByte(t *testing.T){
fakeDTC := DTC_ECU{}
fakeDTC.StatusByte = 0x09
dtcArrayCheck(t, fakeDTC, []DTCStatusByteMeaning{Bit4, Bit7})
fakeDTC.StatusByte = 0x00
dtcArrayCheck(t, fakeDTC, []DTCStatusByteMeaning{})
fakeDTC.StatusByte = 0x2F
dtcArrayCheck(t, fakeDTC, []DTCStatusByteMeaning{Bit7, Bit6, Bit5, Bit4, Bit2})
}
func dtcArrayCheck(t *testing.T, fakeDTC DTC_ECU, expected []DTCStatusByteMeaning){
statuses := fakeDTC.DTCStatusByteMeaning()
for _, exp := range expected{
found := false
for _, rec := range statuses{
if rec == exp{
found = true
break
}
}
if !found{
t.Logf("Expected %s but did not find", exp)
t.Fail()
}
}
for _, rec := range statuses{
found := false
for _, exp := range expected{
if rec == exp{
found = true
break
}
}
if !found{
t.Logf("Extra Result %s", rec)
t.Fail()
}
}
}
func TestCacheKey(t *testing.T) {
fakeDTC := DTC_ECU{
VIN: "3FAFP13P71R199432",
ECU: "ACU",
TroubleCode: 8388881,
}
actual := fakeDTC.CacheKey()
expected := "3FAFP13P71R199432:ACU:8388881"
assert.Equal(t, actual, expected)
}

5
pkg/common/ecu_keys.go Normal file
View File

@@ -0,0 +1,5 @@
package common
type ECUKeys struct {
EccKeys []ECCKeys `json:"ecc_keys_map"`
}

170
pkg/common/ecu_list.go Normal file
View File

@@ -0,0 +1,170 @@
package common
import (
"strings"
"fiskerinc.com/modules/utils/envtool"
"fiskerinc.com/modules/utils/whereami"
)
var EcuMap = map[string]string{
"AGS": "Active Grille Shutter",
"ADB": "Adaptive Driving Beam",
"ADAS": "Advanced Driver Assist System",
"ACU": "Airbag Control Unit",
"ACP": "Airconditioning Control Panel",
"AMP": "Amplifier",
"AP_FL": "Anti-Pinch Front Left",
"AP_FR": "Anti-Pinch Front Right",
"AP_RL": "Anti-Pinch Rear Left",
"AP_RR": "Anti-Pinch Rear Right",
"AL": "Atmosphere Lamp",
"BCS": "Battery HV Current Sensor",
"BMS": "Battery Management System",
"BMU": "Battery Management Unit",
"BCM": "Body Control Module",
"CDS": "Center Display Screen",
"CCU": "Charging Control Unit",
"CIM": "Column Integrated Module",
"CVM": "Coolant Valve Module",
"CFM": "Cooling Fan Module",
"CMRR_FL": "Corner Mid Range Radar Front Left",
"CMRR_FR": "Corner Mid Range Radar Front Right",
"CMRR_RL": "Corner Mid Range Radar Rear Left",
"CMRR_RR": "Corner Mid Range Radar Rear Right",
"DVRC": "Digital Video Recorder Camera",
"DC-CHM": "Direct Current Charge Machine",
"DMC": "Driver Monitor Camera",
"DSMC": "Driver Seat Memory Controller",
"DWSG": "Driver Window Switch Group",
"EPS": "Electric Power Steering",
"EPS1": "Electric Power Steering Sub 1",
"EPS2": "Electric Power Steering Sub 2",
"EAS": "Electrical Air Compressor System",
"ECC": "Electrical Climate Controller",
"EWP_B": "Electrical Water Pump Battery",
"EWP_FD": "Electrical Water Pump Front Drive",
"EWP_H": "Electrical Water Pump Heat",
"EWP_RD": "Electrical Water Pump Rear Drive",
"EWM": "Electrical Wiper Motor",
"EXV_B": "Electronic Expansion Value Battery",
"EXV_HP": "Electronic Expansion Valve HPC",
"ESP": "Electronic Stability Program",
"FDHA_FL": "Flush Door Handle Actuator Front Left",
"FDHA_FR": "Flush Door Handle Actuator Front Right",
"FDHA_RL": "Flush Door Handle Actuator Rear Left",
"FDHA_RR": "Flush Door Handle Actuator Rear Right",
"Lumber": "Four-Way Lumber",
"FBM_L": "Front Beam Module Left",
"FBM_R": "Front Beam Module Right",
"FCM": "Front Camera Module",
"GW": "Gateway",
"HUD": "Head-Up Display",
"IDS": "Instrument Display Screen",
"ICC": "Integrated Cockpit Controller",
"IBS": "Intelligent Battery Sensor",
"iBooster": "Intelligent Booster",
"KS": "Kick Sensor",
"LSC": "Left Side Camera",
"MRR": "Mid Range Radar",
"MCU": "TBox Microcontroller",
"MCU_F": "Motor Control Unit Front",
"MCU_R": "Motor Control Unit Rear",
"MDV": "Motorized Deco-Vent",
"MFS": "Multifunction Steering",
"MIS": "Multimedia Interactive Switch",
"MPC": "Multipurpose Camera",
"OBC": "Onboard Charger",
"OMC": "Occupant Monitor Camera",
"OHC": "Overhead Console",
"PAS": "Parking Assistant System",
"PCU": "Parking Control Unit",
"PMS": "Particulate Matter Sensor",
"PSM": "Passenger Seat Module",
"PEPS": "Passive Entry And Passive Start",
"PKC": "Phone Key Controller",
"PKC_ANT_L": "Phone Key Controller Antenna Left",
"PKC_ANT_R": "Phone Key Controller Antenna Right",
"PWC_L": "Phone Wireless Charging Left",
"PWC_R": "Phone Wireless Charging Right",
"PVIU": "Photovoltaic Integration Unit",
"PASC": "Power Adjust Steering Column",
"PDU": "Power Distribution Unit",
"PLGM": "Power Lift Gate Module",
"RLS": "Rain Light Sensor",
"RAC": "Rear Airconditioning Control",
"RVC": "Rear View Camera",
"RSC": "Right Side Camera",
"RCM": "Roof Control Module",
"RSM": "Roof Shade Module",
"SCM": "Suspension Control Module",
"TBOX": "Telematics Box",
"TPMS": "Tire Pressure Monitoring System",
"TDS": "Touch Display Screen",
"TRM": "Trailer Module",
"USB Box": "USB Box",
"VCU": "Vehicle Control Unit",
"VSP": "Vehicle Sound For Pedestrians",
"WTC_B": "Water Thermal Controller Battery",
"WTC_H": "Water Thermal Controller Heat",
}
// Currently used by car configuration sender only
var CurrentECUMap = map[string]struct{}{
"ACU": {}, "ADAS": {}, "AMP": {}, "BCM": {}, "BMS": {},
"CIM": {}, "CMRR_FL": {}, "CMRR_FR": {}, "CMRR_RL": {}, "CMRR_RR": {},
"DSMC": {}, "EAS": {}, "ECC": {}, "EPS1": {}, "EPS2": {},
"ESP": {}, "FCM": {}, "GW": {}, "ICC": {}, "MCU_F": {},
"MCU_R": {}, "MFS": {}, "MRR": {}, "OBC": {}, "OHC": {},
"PKC": {}, "PLGM": {}, "PSM": {}, "PVIU": {}, "PWC_R": {},
"PWC": {}, "RAC": {}, "SCM": {}, "TRM": {}, "MCU": {},
"VCU": {}, "VSP": {}, "WTC_B": {}, "WTC_H": {}, "iBooster": {},
"VOD": {}, // Although not officially a ECU, we don't want to delete it from the list of data we receive
}
// List of ECU's we want to filter out from our configuration
var FilterECUConfigurationMap = map[string]struct{}{}
func init() {
BuildFilterECUConfigurationMap()
}
func BuildFilterECUConfigurationMap() {
ecusToFilter := strings.Split(envtool.GetEnv("NO_CDS_ECUS", ""), ",")
for _, str := range ecusToFilter {
str = strings.TrimSpace(str)
FilterECUConfigurationMap[str] = struct{}{}
}
}
var ECUReplacement = func() map[string]string {
switch whereami.Service {
case whereami.OTA:
return OTAUpdateECUReplacement
case whereami.AFTERSALES:
return AfterSalesECUReplacement
default:
return OTAUpdateECUReplacement
}
}
var AfterSalesECUReplacement = map[string]string{}
var OTAUpdateECUReplacement = map[string]string{
"EPS": "EPS1",
"PDU": "OBC",
"PWC_L": "PWC",
}
// An inverse of the otaUpdateECUReplacement. To map the cars definition to the pdx/odx definitions
// keep this updated
var CarDTCLookupInverse = map[string]string{
"EPS1": "EPS",
"OBC": "PDU",
"PWC": "PWC_L",
}
var FlashpackCalculationECUReplacement = map[string]string{
"MCU": "TBOX",
"OBC": "PDU",
}

32
pkg/common/ecu_stat.go Normal file
View File

@@ -0,0 +1,32 @@
package common
// ECUStat describes ecu stat received from clickhouse.
type ECUStat struct {
ECUName string `json:"ecu_name" ch:"ecu_name"`
IncorrectValues uint64 `json:"signals_w_incorrect_values" ch:"signals_w_incorrect_values"`
AllZero uint64 `json:"signals_all_zero" ch:"signals_all_zero"`
ECUSignalsTotal uint64 `json:"number_of_ecu_signals" ch:"number_of_ecu_signals"`
SignalsTotal uint64 `json:"total_signal_records" ch:"total_signal_records"`
IncorrectPercent float64 `json:"incorrect_val_signal_pct" ch:"incorrect_val_signal_pct"`
ZeroPercent float64 `json:"zero_signals_pct" ch:"zero_signals_pct"`
}
// StatsFilter is expected to be used for stats request.
type StatsFilter struct {
MinOutOfRangePct float32 `validate:"required" json:"min_out_of_range_pct"`
MinZeroPct float32 `validate:"required" json:"min_zero_pct"`
Hours int `validate:"required" json:"hours"`
VINs []string `validate:"required,min=1,dive,vin" json:"vins"`
DBCs []string `validate:"required,min=1" json:"dbcs"`
ECUs []string `validate:"required,min=1" json:"ecus"`
}
// VINStatsFilter is expected to be used for stats request for a specific vehicle.
type VINStatsFilter struct {
MinOutOfRangePct float32 `validate:"required" json:"min_out_of_range_pct"`
MinZeroPct float32 `validate:"required" json:"min_zero_pct"`
Hours int `validate:"required" json:"hours"`
VIN string `validate:"required,vin" json:"-"`
DBC string `validate:"required" json:"-"`
ECUs []string `validate:"required,min=1" json:"ecus"`
}

View File

@@ -0,0 +1,13 @@
package common
type EmptyMessageFromMobile struct {
VIN string `json:"vin" validate:"required"`
}
func (m EmptyMessageFromMobile) GetVIN() string {
return m.VIN
}
func (m EmptyMessageFromMobile) GetPayload() interface{} {
return nil
}

31
pkg/common/event.go Normal file
View File

@@ -0,0 +1,31 @@
package common
import (
"encoding/json"
"github.com/pkg/errors"
)
// Event is the generic structure for events sent over Kafka
type Event struct {
Topic string `json:"topic"`
Key string `json:"key"`
Payload Message `json:"payload"`
}
// EventRawJSON is Event (see above) but keeps payload as raw data
type EventRawJSON struct {
Topic string `json:"topic"`
Key string `json:"key"`
Payload json.RawMessage `json:"payload"`
}
func (e *EventRawJSON) Marshal() ([]byte, error) {
data, err := json.Marshal(*e)
return data, errors.WithStack(err)
}
func (e *EventRawJSON) Unmarshal(data []byte) error {
err := json.Unmarshal(data, e)
return errors.WithStack(err)
}

52
pkg/common/file_key.go Normal file
View File

@@ -0,0 +1,52 @@
package common
import (
b64 "encoding/base64"
"fiskerinc.com/modules/common/dbbasemodel"
)
type FileKey struct {
FileID string `pg:",pk" validate:"required,len=16,hexadecimal"`
Key []byte `pg:",notnull" validate:"required"`
Auth []byte `pg:",notnull" validate:"required"`
Nonce []byte `pg:",notnull" validate:"required"`
Error string `pg:"-"`
dbbasemodel.DBModelBase
}
type FileKeysRequest struct {
FileIDs []string `json:"file_ids" validate:"required,dive,len=16,hexadecimal"`
}
type FileKeyResponse struct {
FileID string `json:"file_id" redis:"file_id" validate:"required,len=16,hexadecimal"`
Key string `json:"key,omitempty" redis:"key,omitempty" validate:"required"`
Auth string `json:"auth,omitempty" redis:"auth,omitempty" validate:"required"`
Nonce string `json:"nonce,omitempty" redis:"nonce,omitempty" validate:"required"`
Error string `json:"error,omitempty" redis:"error,omitempty"`
}
// Turns the FileKey into a FileKeyResponse
func (fkr *FileKeyResponse) Apply(key *FileKey) {
fkr.FileID = key.FileID
fkr.Key = b64.StdEncoding.EncodeToString(key.Key)
fkr.Auth = b64.StdEncoding.EncodeToString(key.Auth)
fkr.Nonce = b64.StdEncoding.EncodeToString(key.Nonce)
}
// Turns a FileKeyResponse into a file key
func (key *FileKey) Apply(fkr *FileKeyResponse) (err error) {
key.FileID = fkr.FileID
key.Error = fkr.Error
key.Key, err = b64.StdEncoding.DecodeString(fkr.Key)
if err != nil {
return err
}
key.Auth, err = b64.StdEncoding.DecodeString(fkr.Auth)
if err != nil {
return err
}
key.Nonce, err = b64.StdEncoding.DecodeString(fkr.Nonce)
return err
}

108
pkg/common/flashpack.go Normal file
View File

@@ -0,0 +1,108 @@
package common
import (
"encoding/xml"
"time"
)
type FlashpackDataRequest struct {
XMLName xml.Name `xml:"SerializationData"`
Header FlashPackHeaderArea `xml:"HeaderArea" validate:"required"`
VehicleData VehicleData `xml:"DataArea>VehicleData" validate:"required"`
SerializationDataIndices SerializationDataIndices `xml:"DataArea>SerializationDataIndices" validate:"required"`
}
type FlashPackHeaderArea struct {
CreationDateTime CreationDateTimeWithSeconds `xml:"CreationDateTime" json:"creation_date_time"`
MessageIdentifier string `xml:"MessageIdentifier" json:"message_identifier"`
InterfaceID int `xml:"InterfaceId" json:"interface_id"`
SourceSystem string `xml:"Sender>SourceSystem" json:"source_system"`
TargetSystem string `xml:"Sender>TargetSystem" json:"target_system"`
}
type CreationDateTimeWithSeconds struct {
time.Time
}
func (t *CreationDateTimeWithSeconds) UnmarshalText(data []byte) error {
// Fractional seconds are handled implicitly by Parse.
var err error
t.Time, err = time.Parse("2006-01-02T15:04:05", string(data))
return err
}
func (t CreationDateTimeWithSeconds) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
s := t.Format("2006-01-02T15:04:05")
return e.EncodeElement(s, start)
}
type VehicleData struct {
VIN string `xml:"VIN"`
FlashPackNumber string `xml:"FlashPackNumber"`
PDXData string `xml:"PDXData"`
}
type SerializationDataIndices struct {
SerializationDataIndices []SerializationDataIndex `xml:"SerializationDataIndex"`
}
type SerializationDataIndex struct {
IndexNr string `xml:"IndexNr"`
IndexName string `xml:"IndexName"`
ECUName string `xml:"ECUName"`
DataType string `xml:"DataType"`
SerialNumber string `xml:"SerialNumber"`
F108_CodingData string `xml:"F108_CodingData"`
F111_VehicleOrderData string `xml:"F111_VehicleOrderData"`
F183_BootloaderVersion string `xml:"F183_BootloaderVersion"`
F187_VehicleManufacturerSparePartNumber string `xml:"F187_VehicleManufacturerSparePartNumber"`
F188_ECUSoftwareNumber string `xml:"F188_ECUSoftwareNumber"`
F191_ECUHardwareNumber string `xml:"F191_ECUHardwareNumber"`
F195_ECUSoftwareVersionNumber string `xml:"F195_ECUSoftwareVersionNumber"`
F18C_ECUSerialNumber string `xml:"F18C_ECUSerialNumber"`
MaterialNr string `xml:"MaterialNr"`
SoftwareBranch string `xml:"SoftwareBranch"`
SourcingType string `xml:"SourcingType"`
SourcingTypeName string `xml:"SourcingTypeName"`
}
// FlashpackDataRequestSwag is used ONLY to represent FlashpackDataRequest in swagger.
type FlashpackDataRequestSwag struct {
Header struct {
CreationDateTime CreationDateTime `json:"CreationDateTime" swaggertype:"string"`
MessageIdentifier string `json:"MessageIdentifier"`
InterfaceID int `json:"InterfaceId"`
Sender struct {
SourceSystem string `json:"SourceSystem"`
TargetSystem string `json:"TargetSystem"`
} `json:"Sender"`
} `json:"HeaderArea" validate:"required"`
DataArea struct {
VehicleData struct {
VIN string `json:"vin"`
FlashpackNumber string `json:"flashpack_number"`
PDXNumber string `json:"pdx_number"`
} `json:"VehicleData" validate:"required"`
SerializationDataIndices struct {
SerializationDataIndices []struct {
IndexNr string `json:"index_nr"`
IndexName string `json:"index_name"`
DataType string `json:"data_type"`
SerialNumber string `json:"serial_number"`
F108_CodingData string `json:"f108_coding_data"`
F111_VehicleOrderData string `json:"f111_vehicle_order_data"`
F183_BootloaderVersion string `json:"f183_boot_loader_version"`
F187_VehicleManufacturerSparePartNumber string `json:"f187_vehicle_manufacturer_spare_part_number"`
F188_ECUSoftwareNumber string `json:"f188_ecu_software_number"`
F191_ECUHardwareNumber string `json:"f191_ecu_hardware_number"`
F195_ECUSoftwareVersionNumber string `json:"f195_ecu_software_version_number"`
F18C_ECUSerialNumber string `json:"f18c_ecu_serial_number"`
MaterialNr string `json:"material_nr"`
SoftwareBranch string `json:"software_branch"`
SourcingType string `json:"sourcing_type"`
SourcingTypeName string `json:"sourcing_type_name"`
}
} `json:"SerializationDataIndices" validate:"required"`
} `json:"DataArea"`
} // @name SerializationData

10
pkg/common/gps_path.go Normal file
View File

@@ -0,0 +1,10 @@
package common
// GpsPaths is a map of VIN:GpsPath
type GpsPaths map[string]GpsPath
// GpsPath is a slice of GpsPoints
type GpsPath []GpsPoint
// GpsPoint is a [latitude, longitude] pair
type GpsPoint []float64

View File

@@ -0,0 +1,10 @@
package handlers
const (
UpdateManifest = "update_manifest"
UpdateManifestInstall = "update_manifest_install"
UpdateManifestCancel = "update_manifest_cancel"
CarUpdateStatus = "car_update_status"
GetFileKeys = "get_filekeys"
FileKeys = "filekeys"
)

295
pkg/common/hmi_rx_test.go Normal file
View File

@@ -0,0 +1,295 @@
package common_test
import (
"testing"
"time"
"fiskerinc.com/modules/common"
"fiskerinc.com/modules/common/dbbasemodel"
"fiskerinc.com/modules/common/handlers"
"fiskerinc.com/modules/testhelper"
)
func TestHMIRXMessages(t *testing.T) {
schema := "file://" + testhelper.GetSchemaDirPath() + "/hmi/RXMessage.json"
now := time.Now()
manifest := common.UpdateManifest{
ID: 1234567890,
Name: "Test Update Manifest",
Version: "1.2.3",
Description: "This is a test manifest",
ReleaseNotes: "https://fiskerinc.com",
Fingerprint: "8fe27f7fb079ebe06db5f88586b98797",
Type: "standard",
Country: "US",
PowerTrain: "MD23",
Restraint: "None",
Model: "Ocean",
Trim: "Sport",
Year: 2022,
BodyType: "truck",
ECUs: []*common.UpdateManifestECU{
{
ECU: "ICC",
Version: "1.2.3",
CurrentVersion: "1.0.0",
HWVersions: []string{"10000"},
ConfigurationMask: "04d923318b8e1517a9cc8b52af11b051",
Files: []*common.UpdateManifestFile{
{
FileID: "XXXXXXXXXX",
URL: "https://fiskerinc.com/ICC.bin",
Checksum: "CHECKSUM",
FileType: common.Software,
FileSize: 90,
WriteRegionID: 100,
WriteRegion: common.MemoryRegion{
Offset: 101,
Length: 102,
},
EraseRegionID: 200,
EraseRegion: &common.MemoryRegion{
Offset: 201,
Length: 202,
},
FileKey: &common.FileKeyResponse{
FileID: "fileid",
Key: "key",
Auth: "auth",
Nonce: "nonce",
Error: "NoError",
},
},
},
Rollback: []*common.UpdateManifestECU{
{
Version: "1.2.3",
ConfigurationMask: "04d923318b8e1517a9cc8b52af11b051",
Files: []*common.UpdateManifestFile{
{
FileID: "XXXXXXXXXX",
URL: "https://fiskerinc.com/settings.config",
FileType: common.Calibration,
Checksum: "CHECKSUM",
FileSize: 100,
WriteRegionID: 100,
WriteRegion: common.MemoryRegion{
Offset: 101,
Length: 102,
},
EraseRegionID: 200,
EraseRegion: &common.MemoryRegion{
Offset: 201,
Length: 202,
},
},
},
},
},
},
},
CarUpdateID: 1,
}
manifest.Scrub(common.HMI)
tests := []testhelper.SchemaTest{
{
Name: "TestHMIUpdateManifest",
Object: common.Message{
Handler: "update_manifest",
Data: manifest,
},
},
{
Name: "TestMobileMapDestinationSource",
Object: common.Message{
Handler: "map_destination",
Data: common.MapDestinationSource{
DriverID: "valid-cognito-id-1",
Name: "Fisker Inc",
Address: &common.TomTomAddress{
StreetNumber: "3030",
StreetName: "17th St",
LocalName: "San Francisco",
PostalCode: "94110",
CountrySubdivisionName: "California",
CountryCodeIso3: "USA",
},
Coordinates: common.MapCoordinates{
Latitude: 37.0,
Longitude: -122.0,
},
},
},
},
{
Name: "TestMobileMapRouteSource",
Object: common.Message{
Handler: "map_route",
Data: common.MapRouteSource{
DriverID: "valid-cognito-id-1",
Waypoints: []common.MapWaypoint{
{
Type: "Restaurant",
Title: "Test Restaurant",
MapCoordinates: common.MapCoordinates{
Latitude: 111,
Longitude: 222,
},
},
{
Type: "Park",
Title: "Test Park",
MapCoordinates: common.MapCoordinates{
Latitude: 333.333,
Longitude: 444.444,
},
},
},
Route: []common.MapCoordinates{
{
Latitude: 123,
Longitude: 456,
},
{
Latitude: 999,
Longitude: 888,
},
},
},
},
},
{
Name: "TestHMIProfiles",
Object: common.Message{
Handler: "profiles",
Data: []common.CarToDriverModel{{
User: common.UserProfile{
FirstName: "First",
LastName: "Last",
Email: "test@test.com",
Phone: "1234567890",
},
DriverID: "valid-cognito-id-1",
Role: "DRIVER",
Settings: []common.CarSetting{{
Name: "TESTSETTING",
Value: "TESTVALUE",
Type: "string",
DBModelBase: dbbasemodel.DBModelBase{
UpdatedAt: &now,
},
}},
Subscriptions: []common.Subscription{{
Name: "test",
Destination: "ICC",
Expires: time.Date(2021, 10, 30, 8, 35, 0, 0, time.UTC),
}},
}},
},
},
{
Name: "TestHMISettingsUpdate",
Object: common.Message{
Handler: "settings_update",
Data: common.HMISettingsUpdate{
DriverID: "valid-cognito-id-1",
Settings: []common.CarSetting{{
Name: "TESTSETTING",
Value: "TESTVALUE",
Type: "string",
DBModelBase: dbbasemodel.DBModelBase{
UpdatedAt: &now,
},
}},
},
},
},
{
Name: "TestHMISubscriptionsUpdate",
Object: common.Message{
Handler: "subscriptions_update",
Data: common.HMISubscriptionUpdate{
DriverID: "valid-cognito-id-1",
Subscriptions: []common.Subscription{{
Name: "test",
Destination: "ICC",
Expires: time.Date(2021, 10, 30, 8, 35, 0, 0, time.UTC),
}},
},
},
},
{
Name: "TestHMIPOIs",
Object: common.Message{
Handler: "user_pois_get",
Data: common.HMIPOIRequestMessage{
DriverID: "valid-cognito-id-1",
},
},
},
{
Name: "TestHMIPOICreate",
Object: common.Message{
Handler: "user_poi_create",
Data: common.HMIPOIMessage{
DriverID: "valid-cognito-id-1",
UserPOI: common.PointOfInterest{
Name: "TestPOI",
Location: common.POILocation{
Latitude: 1.23,
Longitude: 4.56,
},
},
},
},
},
{
Name: "TestHMIPOIEdit",
Object: common.Message{
Handler: "user_poi_edit",
Data: common.HMIPOIMessage{
DriverID: "valid-cognito-id-1",
UserPOI: common.PointOfInterest{
Name: "TestPOI",
Location: common.POILocation{
Latitude: 8.88,
Longitude: 9.99,
},
},
},
},
},
{
Name: "TestHMIPOIDelete",
Object: common.Message{
Handler: "user_poi_delete",
Data: common.HMIPOIDeleteMessage{
DriverID: "valid-cognito-id-1",
Name: "TestPOI",
},
},
},
{
Name: "TestHMIError",
Object: common.Message{
Handler: "error",
Data: common.MessageString{
Message: "disconnected by duplicate ID",
},
},
},
{
Name: "TestCancelUpdateManifest",
Object: common.Message{
Handler: handlers.UpdateManifestCancel,
Data: common.CarUpdateRequest{
CarUpdateID: 1000,
},
},
},
}
testhelper.NewSchemaTestHelper(t, schema).
RunSchemaTests(tests)
}

12
pkg/common/hmi_session.go Normal file
View File

@@ -0,0 +1,12 @@
package common
type HMISessionMessage struct {
Handler string `json:"handler"`
Data HMISessionData `json:"data"`
}
type HMISessionData struct {
Salt string `json:"salt,omitempty"`
SessionID string `json:"session_id,omitempty"`
VIN string `json:"vin,omitempty"` // FOR INSECURE ENDPOINT - DELETE THIS IN FUTURE ITERATIONS
}

88
pkg/common/hmi_tx_test.go Normal file
View File

@@ -0,0 +1,88 @@
package common_test
import (
"testing"
"time"
"fiskerinc.com/modules/common"
"fiskerinc.com/modules/common/dbbasemodel"
"fiskerinc.com/modules/testhelper"
)
func TestHMITXMessages(t *testing.T) {
schema := "file://" + testhelper.GetSchemaDirPath() + "/hmi/TXMessage.json"
now := time.Now()
tests := []testhelper.SchemaTest{
{
Name: "TestHMIVerifySalt",
Object: common.Message{
Handler: "verify",
Data: common.HMISessionData{
Salt: "testsalt",
},
},
},
{
Name: "TestHMIVerifySession",
Object: common.Message{
Handler: "verify",
Data: common.HMISessionData{
SessionID: "test_session_id",
},
},
},
{
Name: "TestHMIProfiles",
Object: common.Message{
Handler: "profiles",
Data: nil,
},
},
{
Name: "TestHMISettingsUpdate",
Object: common.Message{
Handler: "settings_update",
Data: common.HMISettingsUpdate{
DriverID: "valid-cognito-id-1",
Settings: []common.CarSetting{{
Name: "TESTSETTING",
Value: "TESTVALUE",
Type: "string",
DBModelBase: dbbasemodel.DBModelBase{
UpdatedAt: &now,
},
}},
},
},
},
{
Name: "TestHMIPOIs",
Object: common.Message{
Handler: "user_pois",
Data: common.HMIPOIsMessage{
DriverID: "valid-cognito-id-1",
UserPOIs: []common.PointOfInterest{
{
Name: "TestWorkPOI",
Location: common.POILocation{
Latitude: 1.11,
Longitude: 2.22,
},
},
{
Name: "TestHomePOI",
Location: common.POILocation{
Latitude: 8.88,
Longitude: 9.99,
},
},
},
},
},
},
}
testhelper.NewSchemaTestHelper(t, schema).
RunSchemaTests(tests)
}

28
pkg/common/issue.go Normal file
View File

@@ -0,0 +1,28 @@
package common
import "time"
type AddIssueRequest struct {
Issue
Images [][]byte `json:"images"`
}
type Issue struct {
ID int `json:"id" pg:",pk"`
VIN string `json:"vin" pg:"vin" validate:"required,vin"`
Title string `json:"title" pg:"title" validate:"required,max=256"`
Description string `json:"description" pg:"description" validate:"required,max=1024"`
DriverID string `json:"driver_id,omitempty" pg:"driver_id"`
Timestamp time.Time `json:"timestamp" validate:"required" pg:"created_at"`
IssueImages []IssueImage `json:"images,omitempty" pg:"rel:has-many"`
}
type IssueImage struct {
ID int `json:"id" pg:",pk"`
Image []byte `json:"image" pg:"image"`
IssueID int `json:"issue_id" pg:"issue_id"`
}
type IssueSearch struct {
Search string `json:"search" validate:"max=1024"`
}

View File

@@ -0,0 +1,8 @@
package common
type JSONBlobReadResult struct {
Data interface{} `json:"data"`
RealOffset int64 `json:realOffset`
BytesRead int64 `json:"bytesRead"`
BlobSize int64 `json:"blobSize"`
}

View File

@@ -0,0 +1,6 @@
package common
type JSONDBQueryResult struct {
Data interface{} `json:"data"`
Total int `json:"total,omitempty"`
}

18
pkg/common/json_resp.go Normal file
View File

@@ -0,0 +1,18 @@
package common
// JSONLink json response struct
type JSONLink struct {
Link string `json:"link,omitempty"`
Timestamp int64 `json:"timestamp,omitempty"`
}
// JSONError json error struct
type JSONError struct {
Message string `json:"message,omitempty"`
Error string `json:"error,omitempty"`
}
// JSONMessage json error struct
type JSONMessage struct {
Message string `json:"message,omitempty"`
}

53
pkg/common/json_rpc.go Normal file
View File

@@ -0,0 +1,53 @@
package common
const JSONRPCVersion = "2.0"
func NewJSONRPCResponse(req JSONRPCRequest, result map[string]interface{}) JSONRPCResponse {
resp := JSONRPCResponse{Result: result}
resp.Init(req)
return resp
}
func NewJSONRPCErrorResponse(code int, message string) JSONRPCErrorResponse {
err := JSONRPCErrorResponse{}
err.RPCVersion = JSONRPCVersion
err.Err(code, message)
return err
}
type JSONRPCData map[string]interface{}
type JSONRPCBase struct {
RPCVersion string `json:"jsonrpc" validate:"required,max=10"`
Method string `json:"method" validate:"required,max=1000"`
ID int `json:"id" validate:"required,gt=0"`
}
func (b *JSONRPCBase) Init(req JSONRPCRequest) {
b.RPCVersion = req.RPCVersion
b.Method = req.Method
b.ID = req.ID
}
type JSONRPCRequest struct {
JSONRPCBase
Params JSONRPCData `json:"params"`
}
type JSONRPCResponse struct {
JSONRPCBase
Result JSONRPCData `json:"result"`
}
type JSONRPCErrorResponse struct {
JSONRPCBase
Error JSONRPCData `json:"error"`
}
func (e *JSONRPCErrorResponse) Err(code int, message string) {
e.Error = JSONRPCData{
"code": code,
"message": message,
}
}

View File

@@ -0,0 +1,46 @@
package manifestfingerprintparams
import (
"sync"
"time"
"fiskerinc.com/modules/utils/envtool"
)
var fpParams FingerprintParamer
var fpParamsOnce sync.Once
func SetFPParams(fpp FingerprintParamer) {
fpParams = fpp
}
func GetFPParams() FingerprintParamer {
fpParamsOnce.Do(func() {
if fpParams == nil {
fpParams = &FingerprintParams{
serialNum: envtool.GetEnv("OTA_MANIFEST_SERIAL", "00000000000000000"),
}
}
})
return fpParams
}
type FingerprintParams struct {
serialNum string
}
func (p *FingerprintParams) ManifestSerial() string {
return p.serialNum
}
func (p *FingerprintParams) CurTime() time.Time {
return time.Now().UTC()
}
var _ FingerprintParamer = &FingerprintParams{}
type FingerprintParamer interface {
ManifestSerial() string
CurTime() time.Time
}

View File

@@ -0,0 +1,18 @@
package manifestfingerprintparams
import "time"
type MockFingerprintParamer struct{
Time time.Time
ManifestSerialValue string
}
func (m *MockFingerprintParamer) CurTime()(time.Time){
return m.Time
}
func (m *MockFingerprintParamer) ManifestSerial()(string){
return m.ManifestSerialValue
}
var _ FingerprintParamer = &MockFingerprintParamer{}

View File

@@ -0,0 +1,47 @@
package common
type MapDestinationRequest struct {
VIN string `json:"vin" validate:"vin"`
Name string `json:"name"`
Address *TomTomAddress `json:"address,omitempty"`
Coordinates MapCoordinates `json:"coordinates"`
}
type MapDestinationSource struct {
DriverID string `json:"driver_id"`
Name string `json:"name"`
Address *TomTomAddress `json:"address,omitempty"`
Coordinates MapCoordinates `json:"coordinates"`
}
type MapRouteRequest struct {
VIN string `json:"vin" validate:"vin"`
Waypoints []MapWaypoint `json:"waypoints"`
Route []MapCoordinates `json:"route"`
}
type MapRouteSource struct {
DriverID string `json:"driver_id"`
Waypoints []MapWaypoint `json:"waypoints"`
Route []MapCoordinates `json:"route"`
}
type MapCoordinates struct {
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
}
type MapWaypoint struct {
Type string `json:"type"`
Title string `json:"title"`
MapCoordinates
}
type TomTomAddress struct {
StreetNumber string `json:"street_number"`
StreetName string `json:"street_name"`
LocalName string `json:"local_name"`
PostalCode string `json:"postal_code"`
CountrySubdivisionName string `json:"country_subdivision_name"`
CountryCodeIso3 string `json:"country_code_iso3"`
}

View File

@@ -0,0 +1,26 @@
package common
type MapHistory struct {
Name string `json:"name,omitempty"`
Description string `json:"description,omitempty"`
Location *MapCoordinates `json:"location,omitempty"`
}
type MapHistoryAdd struct {
VIN string `json:"vin"`
MapHistory MapHistory `json:"map_history"`
}
type HMIMapHistoryGet struct {
DriverID string `json:"driver_id"`
}
type HMIMapHistoryAdd struct {
DriverID string `json:"driver_id"`
Search MapHistory `json:"search"`
}
type HMIMapHistory struct {
DriverID string `json:"driver_id"`
Searches []MapHistory `json:"searches"`
}

View File

@@ -0,0 +1,7 @@
package common
type MemoryRegion struct {
ID int64 `json:"-"`
Offset uint64 `json:"offset"`
Length uint64 `json:"length" validate:"gt=0"`
}

35
pkg/common/message.go Normal file
View File

@@ -0,0 +1,35 @@
package common
import (
"encoding/json"
"github.com/pkg/errors"
)
// Message is the generic structure for messages sent over redis and kafka
type Message struct {
Handler string `json:"handler"`
Data interface{} `json:"data,omitempty"`
}
type MessageString struct {
Message string `json:"message"`
}
// MessageRawJSON is Message (see above) but keeps payload as raw data
type MessageRawJSON struct {
Handler string `json:"handler"`
Data json.RawMessage `json:"data"`
Version string `json:"version,omitempty"`
Verify string `json:"verify,omitempty"`
}
func (m *MessageRawJSON) Marshal() ([]byte, error) {
data, err := json.Marshal(*m)
return data, errors.WithStack(err)
}
func (m *MessageRawJSON) Unmarshal(data []byte) error {
err := json.Unmarshal(data, m)
return errors.WithStack(err)
}

View File

@@ -0,0 +1,6 @@
package common
type MissingFlashpack struct {
VIN string
FlashPackVersion string // Equivalent to OS Version
}

View File

@@ -0,0 +1,267 @@
package common_test
import (
"testing"
"time"
"fiskerinc.com/modules/common"
"fiskerinc.com/modules/common/dbbasemodel"
"fiskerinc.com/modules/testhelper"
"github.com/google/uuid"
)
func TestMobileRXMessages(t *testing.T) {
schema := "file://" + testhelper.GetSchemaDirPath() + "/mobile/RXMessage.json"
now := time.Now()
percent, maxMiles, temp, chTime, stateOfCharge := 100, 350, -11, 43, 40
typ := "AC"
tests := []testhelper.SchemaTest{
{
Name: "TestMobileCarLocations",
Object: common.Message{
Handler: "car_locations",
Data: []common.JSONCarLocation{
{
VIN: "1F15K3R45N1234567",
Altitude: 1,
Latitude: 2,
Longitude: 3,
},
{
VIN: "1F15K3R45N3456789",
Altitude: 100,
Latitude: 200,
Longitude: 300,
},
},
},
},
{
Name: "TestMobileDigitalTwin",
Object: common.Message{
Handler: "digital_twin",
Data: common.JSONDigitalTwin{
VIN: "1F15K3R45N1234567",
Online: true,
OnlineHMI: true,
VehicleSpeed: &common.VehicleSpeed{
Speed: 123.4,
},
Battery: &common.JSONBattery{
Percent: &percent,
MaxMiles: &maxMiles,
AvgCellTemperature: &temp,
ChargeType: &typ,
RemainingChargingTime: &chTime,
StateOfCharge: &stateOfCharge,
},
Doors: &common.Doors{
LeftFront: true,
LeftRear: true,
RightFront: false,
RightRear: false,
},
Location: &common.Location{
Altitude: 1000,
Longitude: 1,
Latitude: -1,
},
Locks: &common.Locks{
Driver: true,
All: false,
},
UpdatedAt: &time.Time{},
Windows: &common.JSONWindows{
LeftFront: 0,
LeftRear: 25,
LeftRearQuarter: 100,
RightFront: 50,
RightRear: 75,
RightRearQuarter: 100,
RearWindshield: 100,
Sunroof: 100,
},
ClimateControl: &common.JSONClimateControl{
CabinTemperature: 70,
RearDefrost: false,
DriverSeatHeat: 0,
PassengerSeatHeat: 0,
SteeringWheelHeat: false,
},
},
},
},
{
Name: "TestMobileProfiles",
Object: common.Message{
Handler: "profiles",
Data: []common.JSONMobileProfile{
{
VIN: "1F15K3R45N1234567",
DriverRole: "OWNER",
Settings: []common.CarSetting{{
Name: "TESTSETTING",
Value: "TESTVALUE",
Type: "string",
DBModelBase: dbbasemodel.DBModelBase{
UpdatedAt: &now,
},
}},
Subscriptions: []common.Subscription{{
Name: "test",
Destination: "ICC",
Expires: time.Date(2021, 10, 30, 8, 35, 0, 0, time.UTC),
}},
},
{
VIN: "1F15K3R45N3456789",
DriverRole: "DRIVER",
},
},
},
},
{
Name: "TestMobileSettingsUpdate",
Object: common.Message{
Handler: "settings_update",
Data: common.MobileSettingsUpdate{
VIN: "1F15K3R45N1234567",
Settings: []common.CarSetting{{
Name: "TESTSETTING",
Value: "TESTVALUE",
Type: "string",
DBModelBase: dbbasemodel.DBModelBase{
UpdatedAt: &now,
},
}},
},
},
},
{
Name: "TestMobileStoreInventory",
Object: common.Message{
Handler: "store_inventory",
Data: []common.SubscriptionType{{
ID: uuid.New(),
Name: "TESTSUB",
Destination: "ICC",
Price: 100,
Currency: "USD",
Description: "this is a test",
DurationValue: 5,
DurationUnit: "Hours",
}},
},
},
{
Name: "TestMobileSubscriptionsUpdate",
Object: common.Message{
Handler: "subscriptions_update",
Data: common.MobileSubscriptionUpdate{
VIN: "1F15K3R45N1234567",
Subscriptions: []common.Subscription{{
Name: "test",
Destination: "ICC",
Expires: time.Date(2021, 10, 30, 8, 35, 0, 0, time.UTC),
}},
},
},
},
{
Name: "TestMobileStorePurchaseError",
Object: common.Message{
Handler: "store_purchase_error",
Data: common.JSONError{
Error: "test error",
},
},
},
{
Name: "TestMobileStoreInventoryError",
Object: common.Message{
Handler: "store_inventory_error",
Data: common.JSONError{
Error: "test error",
},
},
},
{
Name: "TestUpdates",
Object: common.Message{
Handler: "updates",
Data: []common.ApprovalUpdate{
{
ID: 1000,
VIN: "1F15K3R45N1234567",
Name: "Update name",
Version: "Update version",
Description: "Update description",
ReleaseNotes: "http://releasenotes.com",
},
{
ID: 1001,
VIN: "1F15K3R45N1234567",
Name: "Update name",
Version: "Update version",
Description: "Update description",
ReleaseNotes: "http://releasenotes.com",
},
},
},
},
{
Name: "TestPOIs",
Object: common.Message{
Handler: "user_pois",
Data: []common.PointOfInterest{
{
Name: "TestWorkPOI",
Location: common.POILocation{
Latitude: 1.11,
Longitude: 2.22,
},
},
{
Name: "TestHomePOI",
Location: common.POILocation{
Latitude: 8.88,
Longitude: 9.99,
},
},
},
},
},
{
Name: "TestMobileError",
Object: common.Message{
Handler: "error",
Data: common.MessageString{
Message: "disconnected by duplicate ID",
},
},
},
{
Name: "TestMobileManualIssue",
Object: common.Message{
Handler: "manual_issue",
Data: common.AddIssueRequest{
Issue: common.Issue{
VIN: "1F15K3R45N1234567",
Title: "HMI not working",
Description: "Black screen on HMI",
Timestamp: time.Now(),
},
Images: [][]byte{
{72, 101, 108, 108, 111},
{72, 101, 108, 108, 49},
},
},
},
},
}
testhelper.NewSchemaTestHelper(t, schema).
RunSchemaTests(tests)
}

View File

@@ -0,0 +1,10 @@
package common
type MobileSessionMessage struct {
Handler string `json:"handler" validate:"required"`
Data MobileSessionData `json:"data"`
}
type MobileSessionData struct {
Token string `json:"token" validate:"jwt"`
}

View File

@@ -0,0 +1,204 @@
package common_test
import (
"testing"
"time"
"fiskerinc.com/modules/common"
"fiskerinc.com/modules/common/dbbasemodel"
"fiskerinc.com/modules/testhelper"
"github.com/google/uuid"
"fiskerinc.com/modules/utils/elptr"
)
func TestMobileTXMessages(t *testing.T) {
schema := "file://" + testhelper.GetSchemaDirPath() + "/mobile/TXMessage.json"
now := time.Now()
tests := []testhelper.SchemaTest{
{
Name: "TestTRexRemoteCommand",
Object: common.Message{
Handler: "remote_command",
Data: common.RemoteCommandRequest{
VIN: "1F15K3R45N1234567",
RemoteCommandSource: common.RemoteCommandSource{
Command: "temp_cabin",
Data: elptr.ElPtr("20"),
Start: elptr.ElPtr(time.Date(
2020, 1, 1,
0, 0, 0, 0, time.UTC)),
End: elptr.ElPtr(time.Date(
2020, 1, 1,
0, 0, 0, 0, time.UTC)),
},
},
},
},
{
Name: "TestMobileCarLocationsRequest",
Object: common.Message{
Handler: "car_locations",
Data: nil,
},
},
{
Name: "TestMobileDigitalTwinRequest",
Object: common.Message{
Handler: "digital_twin",
Data: common.DigitalTwinRequest{
VIN: "1F15K3R45N1234567",
},
},
},
{
Name: "TestMobileMapDestinationRequest",
Object: common.Message{
Handler: "map_destination",
Data: common.MapDestinationRequest{
VIN: "1F15K3R45N1234567",
Name: "Fisker Inc",
Address: &common.TomTomAddress{
StreetNumber: "3030",
StreetName: "17th St",
LocalName: "San Francisco",
PostalCode: "94110",
CountrySubdivisionName: "California",
CountryCodeIso3: "USA",
},
Coordinates: common.MapCoordinates{
Latitude: 37.0,
Longitude: -122.0,
},
},
},
},
{
Name: "TestMobileMapRouteRequest",
Object: common.Message{
Handler: "map_route",
Data: common.MapRouteRequest{
VIN: "1F15K3R45N1234567",
Waypoints: []common.MapWaypoint{
{
Type: "Restaurant",
Title: "Test Restaurant",
MapCoordinates: common.MapCoordinates{
Latitude: 111,
Longitude: 222,
},
},
{
Type: "Park",
Title: "Test Park",
MapCoordinates: common.MapCoordinates{
Latitude: 333.333,
Longitude: 444.444,
},
},
},
Route: []common.MapCoordinates{
{
Latitude: 123,
Longitude: 456,
},
{
Latitude: 999,
Longitude: 888,
},
},
},
},
},
{
Name: "TestMobileProfilesRequest",
Object: common.Message{
Handler: "profiles",
Data: nil,
},
},
{
Name: "TestMobileSettingsUpdate",
Object: common.Message{
Handler: "settings_update",
Data: common.MobileSettingsUpdate{
VIN: "1F15K3R45N1234567",
Settings: []common.CarSetting{{
Name: "TESTSETTING",
Value: "TESTVALUE",
Type: "string",
DBModelBase: dbbasemodel.DBModelBase{
UpdatedAt: &now,
},
}},
},
},
},
{
Name: "TestStoreInventoryRequest",
Object: common.Message{
Handler: "store_inventory",
Data: nil,
},
},
{
Name: "TestMobileStorePurchase",
Object: common.Message{
Handler: "store_purchase",
Data: common.StorePurchases{
VIN: "1F15K3R45N1234567",
Purchases: []common.StorePurchaseItem{{
ID: uuid.New(),
}},
},
},
},
{
Name: "TestHMIPOIs",
Object: common.Message{
Handler: "user_pois_get",
},
},
{
Name: "TestHMIPOICreate",
Object: common.Message{
Handler: "user_poi_create",
Data: common.PointOfInterest{
Name: "TestPOI",
Location: common.POILocation{
Latitude: 1.23,
Longitude: 4.56,
},
},
},
},
{
Name: "TestMobilePOIEdit",
Object: common.Message{
Handler: "user_poi_edit",
Data: common.MobilePOIEditMessage{
OldName: "TestPOI",
UserPOI: common.PointOfInterest{
Name: "TestPOI",
Location: common.POILocation{
Latitude: 8.88,
Longitude: 9.99,
},
},
},
},
},
{
Name: "TestMobilePOIDelete",
Object: common.Message{
Handler: "user_poi_delete",
Data: common.MobilePOIDeleteMessage{
Name: "TestPOI",
},
},
},
}
testhelper.NewSchemaTestHelper(t, schema).
RunSchemaTests(tests)
}

View File

@@ -0,0 +1,17 @@
package common
type VODCDSRequest struct {
ModelYear string `json:"modelyear"`
VersionDModelYear string `json:"versionDModelYear"`
FeatureCodes []string `json:"sapCodingStringArray"`
PDXIndexVersion string `json:"pdXindexversion"`
VersionInfo string `json:"versionInfo"` // Hex string representation of something "000000000000000000"
ByteFiller string `json:"bytefield_2"` // Hex string filler to make VOD 255 bytes long "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
VehicleVIN string `json:"vehicleVIN"` // Human readable string for the vehicle's vin
}
func NewVODCDSRequest() (srct VODCDSRequest) {
srct.VersionInfo = "000000000000000000"
srct.ByteFiller = "000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
return
}

231
pkg/common/order_updated.go Normal file
View File

@@ -0,0 +1,231 @@
package common
import (
"encoding/json"
"encoding/xml"
"time"
"fiskerinc.com/modules/grpc/kafka_grpc"
)
type OrderUpdated struct {
XMLName xml.Name `xml:"VehicleOrderReplicate"`
Header HeaderArea `xml:"HeaderArea" validate:"required"`
VehicleOrder VehicleOrder `xml:"DataArea>VehicleOrder" validate:"required"`
}
type HeaderArea struct {
CreationDateTime CreationDateTime `xml:"CreationDateTime" json:"creation_date_time"`
MessageIdentifier string `xml:"MessageIdentifier" json:"message_identifier"`
InterfaceID int `xml:"InterfaceId" json:"interface_id"`
SourceSystem string `xml:"Sender>SourceSystem" json:"source_system"`
TargetSystem string `xml:"Sender>TargetSystem" json:"target_system"`
}
type VehicleOrder struct {
SpecID int `xml:"SpecId" json:"spec_id"`
OrderNumber int `xml:"VehicleOrderNumber" json:"order_number"`
MessageIdentifier string `xml:"-" json:"message_identifier"`
VehicleSpecification VehicleSpecification `xml:"VehicleSpecification" json:"vehicle_specification"`
}
type VehicleSpecification struct {
OrderIndicator string `xml:"OrderIndicator" json:"order_indicator"`
FleetOrderIndicator string `xml:"FleetOrderIndicator" json:"fleet_order_indicator"`
ProductionPhaseIndicator string `xml:"ProductionPhaseIndicator" json:"production_phase_indicator"`
VehicleIndicator string `xml:"VehicleIndicator" json:"vehicle_indicator"`
ManufacturingPlant string `xml:"ManufacturingPlant" json:"manufacturing_plant"`
ExpectedReferenceDate ExpectedReferenceDate `xml:"ExpectedReferenceDate" json:"expected_reference_date"`
ModelType string `xml:"ModelType" json:"model_type"`
ModelYearIndicator int `xml:"ModelYearIndicator" json:"model_year_indicator"`
ModelYear int `xml:"ModelYear" json:"model_year"`
SequenceNumber string `xml:"SequenceNumber" json:"sequence_number"`
VersionDuringModelYear int `xml:"VersionDuringModelYear" json:"version_during_model_year"`
VehicleModel string `xml:"VehicleModel" json:"vehicle_model"`
VinPrefix string `xml:"VinPrefix" json:"vin_prefix"`
VehicleFeatures []FeatureCodes `xml:"VehicleFeatures>FeatureCodes" json:"vehicle_features"`
DestinationCountry string `xml:"DestinationCountry" json:"destination_country"`
}
type FeatureCodes struct {
FamilyCode string `xml:"FamilyCode" json:"family_code"`
FeatureCode string `xml:"FeatureCode" json:"feature_code"`
}
type ExpectedReferenceDate struct {
time.Time
}
func (t *ExpectedReferenceDate) UnmarshalText(data []byte) error {
// Fractional seconds are handled implicitly by Parse.
var err error
t.Time, err = time.Parse("2006-01-02", string(data))
return err
}
func (t ExpectedReferenceDate) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
s := t.Format("2006-01-02")
return e.EncodeElement(s, start)
}
type CreationDateTime struct {
time.Time
}
func (t *CreationDateTime) UnmarshalText(data []byte) error {
// Fractional seconds are handled implicitly by Parse.
var err error
t.Time, err = time.Parse("2006-01-02T15:04", string(data))
return err
}
func (t CreationDateTime) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
s := t.Format("2006-01-02T15:04")
return e.EncodeElement(s, start)
}
// OrderUpdatedSwag is used ONLY to represent OrderUpdated in swagger.
type OrderUpdatedSwag struct {
Header struct {
CreationDateTime CreationDateTime `json:"CreationDateTime" swaggertype:"string"`
MessageIdentifier string `json:"MessageIdentifier"`
InterfaceID int `json:"InterfaceId"`
Sender struct {
SourceSystem string `json:"SourceSystem"`
TargetSystem string `json:"TargetSystem"`
} `json:"Sender"`
} `json:"HeaderArea" validate:"required"`
DataArea struct {
VehicleOrder struct {
SpecID int `json:"SpecId"`
OrderNumber int `json:"VehicleOrderNumber"`
VehicleSpecification struct {
OrderIndicator string `json:"OrderIndicator"`
FleetOrderIndicator string `json:"FleetOrderIndicator"`
ProductionPhaseIndicator string `json:"ProductionPhaseIndicator"`
VehicleIndicator string `json:"VehicleIndicator"`
ManufacturingPlant string `json:"ManufacturingPlant"`
ExpectedReferenceDate ExpectedReferenceDate `json:"ExpectedReferenceDate" swaggertype:"string"`
ModelType string `json:"ModelType"`
ModelYearIndicator int `json:"ModelYearIndicator"`
SequenceNumber string `json:"SequenceNumber"`
VehicleModel string `json:"VehicleModel"`
VinPrefix string `json:"VinPrefix"`
VehicleFeatures []struct {
FeatureCodes struct {
FamilyCode string `json:"FamilyCode" `
FeatureCode string `json:"FeatureCode"`
} `json:"FeatureCodes"`
} `json:"VehicleFeatures"`
DestinationCountry string `json:"DestinationCountry"`
} `json:"VehicleSpecification"`
} `json:"VehicleOrder" validate:"required"`
} `json:"DataArea"`
} // @name VehicleOrderReplicate
func OrderToGRPC(input MessageRawJSON) *kafka_grpc.GRPC_AttendantPayload {
grpcPayload := &kafka_grpc.GRPC_AttendantPayload{
Handler: input.Handler,
}
var order VehicleOrder
err := json.Unmarshal(input.Data, &order)
if err != nil {
return grpcPayload
}
orderObj := &kafka_grpc.Order{
VehicleSpecification: &kafka_grpc.VehicleSpecification{},
}
orderObj.VehicleSpecification.Date = order.VehicleSpecification.ExpectedReferenceDate.UnixMilli()
orderObj.MsgIdentifier = order.MessageIdentifier
orderObj.SpecId = int64(order.SpecID)
orderObj.VehicleSpecification.OrderId = order.VehicleSpecification.OrderIndicator
orderObj.VehicleSpecification.FOrderId = order.VehicleSpecification.FleetOrderIndicator
orderObj.VehicleSpecification.MfPlant = order.VehicleSpecification.ManufacturingPlant
orderObj.VehicleSpecification.DestCon = order.VehicleSpecification.DestinationCountry
orderObj.VehicleSpecification.ModelType = order.VehicleSpecification.ModelType
orderObj.VehicleSpecification.ProductId = order.VehicleSpecification.ProductionPhaseIndicator
orderObj.VehicleSpecification.Sn = order.VehicleSpecification.SequenceNumber
orderObj.VehicleSpecification.VehicleId = order.VehicleSpecification.VehicleIndicator
orderObj.VehicleSpecification.Model = order.VehicleSpecification.VehicleModel
orderObj.VehicleSpecification.VinPre = order.VehicleSpecification.VinPrefix
orderObj.VehicleSpecification.ModelYear = int64(order.VehicleSpecification.ModelYear)
orderObj.VehicleSpecification.ModelId = int64(order.VehicleSpecification.ModelYearIndicator)
orderObj.VehicleSpecification.Version = int64(order.VehicleSpecification.VersionDuringModelYear)
if len(order.VehicleSpecification.VehicleFeatures) > 0 {
var features []*kafka_grpc.FeatureCodes
for _, feature := range order.VehicleSpecification.VehicleFeatures {
features = append(features, &kafka_grpc.FeatureCodes{
FamilyCode: feature.FamilyCode,
FeatureCode: feature.FeatureCode,
})
}
orderObj.VehicleSpecification.Feature = features
}
payload := &kafka_grpc.GRPC_AttendantPayload_Order{
Order: orderObj,
}
grpcPayload.Data = payload
return grpcPayload
}
func GRPCToOrder(payload *kafka_grpc.GRPC_AttendantPayload) *VehicleOrder {
if payload.Data == nil {
return nil
}
data := payload.Data.(*kafka_grpc.GRPC_AttendantPayload_Order)
order := &VehicleOrder{}
order.OrderNumber = int(data.Order.OrderNo)
order.MessageIdentifier = data.Order.MsgIdentifier
order.SpecID = int(data.Order.SpecId)
order.VehicleSpecification = VehicleSpecification{}
if data.Order.VehicleSpecification != nil {
order.VehicleSpecification.DestinationCountry = data.Order.VehicleSpecification.DestCon
order.VehicleSpecification.FleetOrderIndicator = data.Order.VehicleSpecification.FOrderId
order.VehicleSpecification.FleetOrderIndicator = data.Order.VehicleSpecification.FOrderId
order.VehicleSpecification.ManufacturingPlant = data.Order.VehicleSpecification.MfPlant
order.VehicleSpecification.ModelType = data.Order.VehicleSpecification.ModelType
order.VehicleSpecification.ModelYear = int(data.Order.VehicleSpecification.ModelYear)
order.VehicleSpecification.ModelYearIndicator = int(data.Order.VehicleSpecification.ModelId)
order.VehicleSpecification.OrderIndicator = data.Order.VehicleSpecification.OrderId
order.VehicleSpecification.ProductionPhaseIndicator = data.Order.VehicleSpecification.ProductId
order.VehicleSpecification.FleetOrderIndicator = data.Order.VehicleSpecification.FOrderId
order.VehicleSpecification.SequenceNumber = data.Order.VehicleSpecification.Sn
order.VehicleSpecification.VehicleIndicator = data.Order.VehicleSpecification.VehicleId
order.VehicleSpecification.VehicleModel = data.Order.VehicleSpecification.Model
order.VehicleSpecification.VinPrefix = data.Order.VehicleSpecification.VinPre
order.VehicleSpecification.VersionDuringModelYear = int(data.Order.VehicleSpecification.Version)
seconds := data.Order.VehicleSpecification.Date / 1000
nanoseconds := (data.Order.VehicleSpecification.Date % 1000) * int64(time.Millisecond)
order.VehicleSpecification.ExpectedReferenceDate = ExpectedReferenceDate{time.Unix(seconds, nanoseconds)}
if data.Order.VehicleSpecification.Feature != nil && len(data.Order.VehicleSpecification.Feature) > 0 {
var features []FeatureCodes
for _, feature := range data.Order.VehicleSpecification.Feature {
features = append(features, FeatureCodes{
FamilyCode: feature.FamilyCode,
FeatureCode: feature.FeatureCode,
})
}
order.VehicleSpecification.VehicleFeatures = features
}
}
return order
}

View File

@@ -0,0 +1,8 @@
package common
import "time"
type MobileOtaInstall struct {
Time *time.Time `json:"time,omitempty"`
CarUpdate int64 `json:"id" validate:"required"`
}

23
pkg/common/payload.go Normal file
View File

@@ -0,0 +1,23 @@
package common
import (
"encoding/json"
"github.com/pkg/errors"
)
// Payload is the generic structure for payloads sent within Kafka messages
type Payload struct {
Handler string `json:"handler"`
Data json.RawMessage `json:"data"`
}
func (p *Payload) Marshal() ([]byte, error) {
data, err := json.Marshal(*p)
return data, errors.WithStack(err)
}
func (p *Payload) Unmarshal(data []byte) error {
err := json.Unmarshal(data, p)
return errors.WithStack(err)
}

View File

@@ -0,0 +1,49 @@
package common
type PointOfInterest struct {
Name string `json:"name"`
Location POILocation `json:"location"`
}
type POILocation struct {
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
}
type MobilePOIEditMessage struct {
OldName string `json:"old_name"`
UserPOI PointOfInterest `json:"user_poi"`
}
type MobilePOIDeleteMessage struct {
Name string `json:"name"`
}
type MobilePOIsMessage struct {
UserPOI []PointOfInterest `json:"user_pois"`
}
type HMIPOIRequestMessage struct {
DriverID string `json:"driver_id"`
}
type HMIPOIMessage struct {
DriverID string `json:"driver_id"`
UserPOI PointOfInterest `json:"user_poi"`
}
type HMIPOIEditMessage struct {
DriverID string `json:"driver_id"`
OldName string `json:"old_name"`
UserPOI PointOfInterest `json:"user_poi"`
}
type HMIPOIDeleteMessage struct {
DriverID string `json:"driver_id"`
Name string `json:"name"`
}
type HMIPOIsMessage struct {
DriverID string `json:"driver_id"`
UserPOIs []PointOfInterest `json:"user_pois"`
}

17
pkg/common/product.go Normal file
View File

@@ -0,0 +1,17 @@
package common
import (
"fiskerinc.com/modules/common/dbbasemodel"
)
type Product struct {
ID string `pg:",pk,unique" json:"id" validate:"required,max=256"`
Name string `json:"name"`
Price int `json:"price"`
ReservationPrice int `json:"reservation_price"`
MonthlyPaymentPrice int `json:"monthlypayment_price"`
Currency string `json:"currency"`
Locale string `json:"locale"`
Code string `json:"code"`
dbbasemodel.DBModelBase
}

View File

@@ -0,0 +1,6 @@
package common
type ProductSearch struct {
Search string `json:"search" validate:"max=1024"`
Product
}

View File

@@ -0,0 +1,5 @@
package common
type ProfilePhotoUpdate struct {
Photo string `json:"photo" validate:"base64,max=50000"`
}

90
pkg/common/profiles.go Normal file
View File

@@ -0,0 +1,90 @@
package common
type JSONMobileProfile struct {
VIN string `json:"vin"`
DriverRole string `json:"role"`
Consent bool `json:"consent,omitempty"`
BLEKey string `json:"ble_key,omitempty"`
Settings []CarSetting `json:"settings"`
Subscriptions []Subscription `json:"subscriptions"`
}
func (p *JSONMobileProfile) Populate(driver CarToDriver) {
p.VIN = driver.VIN
p.DriverRole = driver.DriverRole
p.BLEKey = driver.BLEKey
if driver.Settings == nil {
p.Settings = make([]CarSetting, 0)
} else {
p.Settings = driver.Settings
for i := range p.Settings {
p.Settings[i].ClearCreatedAt()
}
}
if driver.Subscriptions == nil {
p.Subscriptions = make([]Subscription, 0)
} else {
p.Subscriptions = driver.Subscriptions
for i := range p.Subscriptions {
p.Subscriptions[i].ClearDates()
}
}
}
type JSONHMIProfile struct {
DriverID string `json:"driver_id"`
DriverRole string `json:"role"`
User UserProfile `json:"user"`
Settings []CarSetting `json:"settings"`
Subscriptions []Subscription `json:"subscriptions"`
}
func (p *JSONHMIProfile) Populate(user JSONUserProfile, driver CarToDriver) {
p.User = UserProfile{
FirstName: user.FirstName,
LastName: user.LastName,
Email: user.Email,
Phone: user.Phone,
}
p.DriverID = driver.DriverID
p.DriverRole = driver.DriverRole
if driver.Settings == nil {
p.Settings = make([]CarSetting, 0)
} else {
p.Settings = driver.Settings
for i := range p.Settings {
p.Settings[i].ClearCreatedAt()
}
}
if driver.Subscriptions == nil {
p.Subscriptions = make([]Subscription, 0)
} else {
p.Subscriptions = driver.Subscriptions
for i := range p.Subscriptions {
p.Subscriptions[i].ClearDates()
}
}
}
type JSONHMIUpdateProfile struct {
DriverID string `json:"driver_id"`
DriverRole string `json:"role"`
User UserProfile `json:"user"`
}
type JSONHMIDeleteProfile struct {
DriverID string `json:"driver_id" validate:"required,max=100"`
}
type JSONUserProfile struct {
UserName string `json:"username"`
FirstName string `json:"firstname"`
LastName string `json:"lastname"`
Email string `json:"email"`
Phone string `json:"phonenumber"`
}

View File

@@ -0,0 +1,15 @@
package common
import (
"fiskerinc.com/modules/common/dbbasemodel"
)
type RatePlanTMobile struct {
tableName struct{} `pg:"rate_plan_tmobile,alias:r"`
Country string `json:"country" pg:"country,pk"`
ProductID string `json:"product_id" pg:"product_id"`
PlanName string `json:"plan_name" pg:"plan_name"`
dbbasemodel.DBModelBase
}

View File

@@ -0,0 +1,79 @@
package common
import "time"
const (
PositionLeftFront string = "left_front"
PositionRightFront string = "right_front"
PositionLeftRear string = "left_rear"
PositionRightRear string = "right_rear"
PositionLeftRearQuarter string = "left_rear_quarter"
PositionRightRearQuarter string = "right_rear_quarter"
PositionRearWindshield string = "rear_windshield"
PositionTrunk string = "trunk"
)
type RemoteCommandSource struct {
Command string `json:"command" validate:"required,max=100"`
Data *string `json:"data,omitempty"`
Start *time.Time `json:"start,omitempty"`
End *time.Time `json:"end,omitempty"`
}
type RemoteReadVersionsCommandArgs struct {
ECUName string `json:"ecu_name" validate:"required,max=100"`
}
type RemoteResetDiagnosticCommandArgs struct {
ECUName string `json:"ecu_name" validate:"required,max=100"`
UDSKeys *ECCKeys `json:"uds_keys,omitempty"`
}
type RemoteCANNetworkCommandArgs struct {
Action string `json:"action" validate:"required,oneof=on off"`
Timeout int32 `json:"timeout"`
}
type RemoteIgnitionCommandArgs struct {
Action string `json:"action" validate:"required,oneof=on off"`
Timeout int32 `json:"timeout"`
}
type RemoteUpdateSecOCCommandArgs struct {
ECUs []string `json:"ecus" validate:"required"`
UDSKeys []ECCKeys `json:"uds_keys" validate:"required"`
KeyBase64 string `json:"key_base64" validate:"required"`
}
type RemoteDiagnosticCommandRequest struct {
VINs []string `json:"vins,omitempty" validate:"required,max=1000,dive,vin"`
Command string `json:"command" validate:"required,oneof=remote_reset can_network remote_ignition read_ecu_versions write_secoc_key"`
ECU string `json:"ecu_name,omitempty"`
CANNetAction string `json:"can_net_action,omitempty"`
IgnitionAction string `json:"ignition_action,omitempty"`
Timeout int32 `json:"timeout"`
}
type RemoteCommandRequest struct {
VIN string `json:"vin"`
Source string `json:"source"`
SentAt *time.Time `json:"time,omitempty"`
WaitDuration int `json:"wait_dur"`
RemoteCommandSource
}
func (r *RemoteCommandRequest) IsExpired(commandExpiration time.Duration) bool {
if r.SentAt == nil {
return false
}
expiresAt := r.SentAt.Add(commandExpiration * time.Second)
return expiresAt.Before(time.Now())
}
func (r *RemoteCommandRequest) GetSentAt() *time.Time {
return r.SentAt
}
func (r *RemoteCommandRequest) SetSentAt(sentAt time.Time) {
r.SentAt = &sentAt
}

18
pkg/common/sap.go Normal file
View File

@@ -0,0 +1,18 @@
package common
type SAPResponse struct {
ModelYear int `json:"modelYear"`
VersionDuringModelYear string `json:"versionDuringModelYear"`
ModelType string `json:"modelType"`
Features []SAPFeature `json:"features"`
}
type SAPFeature struct {
FamilyCode string `json:"familyCode"`
FeatureCode string `json:"featureCode"`
}
type CarConfigData struct {
Vin string `json:"vin"`
ConfigData string `json:"config_data"`
}

View File

@@ -0,0 +1,11 @@
package common
import (
"fiskerinc.com/modules/common/dbbasemodel"
)
type SignedImage struct {
Signature *BinaryHex `json:"signature" pg:"signature,pk,type:bytea" swaggertype:"string" format:"hex" example:"ecba97e8c5a064e333ae35728f2147f41e64734f381826f55d1a9261962d704e"`
Email string `json:"email,omitempty" pg:"email"`
dbbasemodel.DBModelBase
}

View File

@@ -0,0 +1,8 @@
package common
type AlgoHash string
const (
AlogHashNone AlgoHash = "none"
AlgoHashSHA256 AlgoHash = "sha256"
)

View File

@@ -0,0 +1,15 @@
package common
type AlgoSignType string
const (
AlgoSignRSAX509Raw AlgoSignType = "raw"
AlgoSignRSAPkcs1 AlgoSignType = "pkcs1"
AlgoSignECDSA AlgoSignType = "prime256v1"
)
var AlgoMapper map[AlgoSignType]int = map[AlgoSignType]int{
AlgoSignRSAX509Raw: 0,
AlgoSignRSAPkcs1: 1,
AlgoSignECDSA: 2,
}

View File

@@ -0,0 +1,10 @@
package common
type KeyType string
const (
KeySBC4096 KeyType = "sbc_key_4096"
KeyVerified4096 KeyType = "verified_rsa4096_key"
KeySBCRoot KeyType = "sbc_root_key"
KeyBundle KeyType = "bundle_key"
)

View File

@@ -0,0 +1,9 @@
package common
type JSONRPCSignImageRequest struct {
Supplier string `validate:"required"`
KeyCert KeyType `validate:"required,oneof=bundle_key sbc_key_4096 sbc_root_key verified_rsa4096_key"`
AlgoHash AlgoHash `validate:"required,oneof=none sha256"`
AlgoSign AlgoSignType `validate:"required,oneof=pkcs1 prime256v1 raw"`
Data []byte `validate:"required"`
}

33
pkg/common/sold_status.go Normal file
View File

@@ -0,0 +1,33 @@
package common
import "encoding/xml"
type SoldStatusDataRequest struct {
XMLName xml.Name `xml:"VehicleSoldStatusDataRequestReplicate"`
Header HeaderArea `xml:"HeaderArea" validate:"required"`
SoldStatusData SoldStatusData `xml:"DataArea>SoldStatusData" validate:"required"`
}
type SoldStatusData struct {
VIN string `xml:"VIN"`
SoldStatus string `xml:"SoldStatus"`
}
// SoldStatusDataRequestSwag is used ONLY to represent SoldStatusDataRequest in swagger.
type SoldStatusDataRequestSwag struct {
Header struct {
CreationDateTime CreationDateTime `json:"CreationDateTime" swaggertype:"string"`
MessageIdentifier string `json:"MessageIdentifier"`
InterfaceID int `json:"InterfaceId"`
Sender struct {
SourceSystem string `json:"SourceSystem"`
TargetSystem string `json:"TargetSystem"`
} `json:"Sender"`
} `json:"HeaderArea" validate:"required"`
DataArea struct {
SoldStatusData struct {
VIN string `json:"vin"`
SoldStatus string `json:"sold_status"`
} `json:"SoldStatusData" validate:"required"`
} `json:"DataArea"`
} // @name VehicleSoldStatusDataRequestReplicate

View File

@@ -0,0 +1,3 @@
A place to store any static errors we are creating
This will allow us to specifically handle them inside the logger without import cycles
Maybe it will also let us keep track of our error messaging better?

View File

@@ -0,0 +1,3 @@
package staticerrors
var ErrorSAPFailedCall = "calling SAP failed"

View File

@@ -0,0 +1,11 @@
package common
import "time"
type StatusManifest struct {
tableName struct{} `pg:"update_manifests,discard_unknown_columns"`
UpdateManifest
Status string `json:"status,omitempty" pg:"status"`
StatusUpdatedAt *time.Time `json:"status_updated,omitempty" pg:"status_updated"`
ManifestCreatedAt *time.Time `json:"manifest_created,omitempty" pg:"manifest_created"`
}

Some files were not shown because too many files have changed in this diff Show More