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,28 @@
package usecase_helpers
import (
"fiskerinc.com/modules/common"
"fiskerinc.com/modules/db/queries"
)
func NewECUKeys(eccKeys queries.EccKeysInterface) *EcuKeys {
return &EcuKeys{
eccKeys: eccKeys,
}
}
type EcuKeys struct {
eccKeys queries.EccKeysInterface
}
func (e EcuKeys) AddECUECCKeys(manifest *common.UpdateManifest) error {
ecus := manifest.GetECUs()
keys, err := e.eccKeys.SelectPrivateKeysByECUsEnv(ecus, manifest.Env)
if err != nil {
return err
}
manifest.AddECUECCKeys(keys)
return nil
}

View File

@@ -0,0 +1,34 @@
package usecase_helpers
import (
"fiskerinc.com/modules/common"
"fiskerinc.com/modules/db/queries"
)
// PopulateECUsCurrentVersion adds CurrentVersion field in provided ecus list of
// a specific vehicle, defined by vin.
// Since the ecus amount doesn't change, we don't need returning ecus and
// caller can use the same variable he provided in the function.
func PopulateECUsCurrentVersion(cars queries.CarsInterface, vin string, ecus []*common.UpdateManifestECU) error {
ids := make([]string, 0, len(ecus))
for _, ecu := range ecus {
ids = append(ids, ecu.ECU)
}
carECUs, err := cars.GetCarECUs(common.CarECUFilter{VIN: vin, ECUs: ids, Unique: true}, nil)
if err != nil {
return err
}
carECUsMap := make(map[string]string, len(carECUs))
for _, ecu := range carECUs {
carECUsMap[ecu.ECU] = ecu.Version
}
for index, ecu := range ecus {
ecu.CurrentVersion = carECUsMap[ecu.ECU]
ecus[index] = ecu
}
return nil
}

View File

@@ -0,0 +1,78 @@
package usecase_helpers
import (
"testing"
m "fiskerinc.com/modules/common"
"fiskerinc.com/modules/db/queries"
"fiskerinc.com/modules/db/queries/mocks"
"github.com/pkg/errors"
"github.com/stretchr/testify/assert"
)
var someErr = errors.New("some err")
func TestPopulateECUsCurrentVersion(t *testing.T) {
mockVIN := ""
tests := map[string]struct {
cars queries.CarsInterface
ecus []*m.UpdateManifestECU
expEcus []*m.UpdateManifestECU
expErr error
}{
"correct": {
cars: &mocks.MockCars{
SelectCarECUs: []m.CarECU{
{ECU: "TREX", Version: " S213C213"},
{ECU: "PKC", Version: " S213C222"},
},
},
ecus: []*m.UpdateManifestECU{
{ECU: "TREX"},
{ECU: "PKC"},
{ECU: "MPT"},
},
expEcus: []*m.UpdateManifestECU{
{ECU: "TREX", CurrentVersion: " S213C213"},
{ECU: "PKC", CurrentVersion: " S213C222"},
{ECU: "MPT"},
},
},
"empty_car_ecus": {
cars: &mocks.MockCars{},
ecus: []*m.UpdateManifestECU{
{ECU: "TREX"},
{ECU: "PKC"},
{ECU: "MPT"},
},
expEcus: []*m.UpdateManifestECU{
{ECU: "TREX"},
{ECU: "PKC"},
{ECU: "MPT"},
},
},
"get_car_ecus_error": {
cars: &mocks.MockCars{
DBMockHelper: mocks.DBMockHelper{Error: someErr},
},
ecus: []*m.UpdateManifestECU{
{ECU: "TREX"},
{ECU: "PKC"},
{ECU: "MPT"},
},
expErr: someErr,
},
}
for tname, tt := range tests {
t.Run(tname, func(t *testing.T) {
err := PopulateECUsCurrentVersion(tt.cars, mockVIN, tt.ecus)
if err != nil && tt.expErr != nil {
assert.Equal(t, tt.expErr.Error(), err.Error())
return
}
assert.Equal(t, tt.expErr, err)
assert.Equal(t, tt.expEcus, tt.ecus)
})
}
}

View File

@@ -0,0 +1,174 @@
package usecase_helpers
import (
"encoding/hex"
"encoding/json"
"fmt"
"strings"
"fiskerinc.com/modules/common"
"fiskerinc.com/modules/db/queries"
"fiskerinc.com/modules/logger"
"fiskerinc.com/modules/utils/envtool"
"fiskerinc.com/modules/utils/vod"
vconfig "fiskerinc.com/modules/vehicleconfig"
"github.com/go-pg/pg/v10"
"github.com/pkg/errors"
)
var noCDSResult = map[string]string{}
func GetCDS(config vconfig.ConfigServiceInterface, ccd queries.CarConfigDataInterface, vin string) (map[string]string, error) {
// if SAP is not available, get the stored feature codes instead
conf, err := getStoredFeatureCodes(vin, ccd)
if err != nil {
logger.At(logger.Error(), common.TRex.Key(vin), "update").
Err(err).Send()
return nil, err
}
if len(conf.Features) == 0 {
return noCDSResult, nil
}
featureCodes := make([]string, 0, len(conf.Features))
for _, f := range conf.Features {
featureCodes = append(featureCodes, f.FeatureCode)
}
sYear := fmt.Sprint(conf.ModelYear)
if len(sYear) > 2 {
sYear = sYear[2:]
}
pdxVersion := getPDXVersion(vin)
vodcdsReq := common.NewVODCDSRequest()
vodcdsReq.ModelYear = sYear
vodcdsReq.VersionDModelYear = conf.VersionDuringModelYear
vodcdsReq.FeatureCodes = featureCodes
vodcdsReq.PDXIndexVersion = pdxVersion
vodcdsReq.VehicleVIN = vin
cdsMap, err := config.GetCDS(vodcdsReq)
if err != nil {
logger.At(logger.Error(), common.TRex.Key(vin), "update").
Str("request", fmt.Sprintf("%v", vodcdsReq)).
Err(err).Send()
return nil, err
}
// Lazy way to fix some tests instead of fixing the tests
bcm, ok := cdsMap["BCM"]
if ok {
bcm = overwriteBCMBeep(bcm)
bcm = overwriteBCMSequentialLighting(bcm)
cdsMap["BCM"] = bcm
}
icc, ok := cdsMap["ICC"]
if ok {
icc = overwriteICCHotSpot(icc)
cdsMap["ICC"] = icc
}
cdsMap = cdsNameTransform(cdsMap)
cdsMap = cdsRemoval(cdsMap)
return cdsMap, nil
}
func overwriteBCMBeep(bcm string) string {
// COD_Sirene_Master_beep_type adding rule 161300 -> no beep.
bcmR := []rune(bcm)
bcmR[39] = '0'
bcm = string(bcmR)
return overwriteCalc(bcm)
}
func overwriteBCMSequentialLighting(bcm string) string {
bcmR := []rune(bcm)
bcmR[25] = '1'
bcm = string(bcmR)
return overwriteCalc(bcm)
}
func overwriteICCHotSpot(icc string) string {
// COD_Car_as_Hotspot removal of rule 130100 -> without. !130100 is with hotspot
iccR := []rune(icc)
iccR[127] = '1'
icc = string(iccR)
return overwriteCalc(icc)
}
// Given the modified coding string, we will calculate the hex values and such
func overwriteCalc(ecu string) string {
vodHelper := vod.NewVODHelper(false, false, false)
hexECU, err := hex.DecodeString(ecu)
if err != nil {
panic(err)
}
// chop off CRC
hexECU = hexECU[:len(hexECU)-1]
// CRC does not include short vin value
steinway := hexECU[9:]
crc := vodHelper.GenerateCRC(steinway)
//crc := crc8.Checksum(hexBCMData, v.table)
hexECU = append(hexECU, crc)
ecu = hex.EncodeToString(hexECU)
ecu = strings.ToUpper(ecu)
return ecu
}
func getStoredFeatureCodes(vin string, db queries.CarConfigDataInterface) (common.SAPResponse, error) {
config, err := db.SelectByVIN(vin)
if err != nil {
if errors.Is(err, pg.ErrNoRows) {
// Default config if none is available in db
config = common.CarConfigData{
ConfigData: `{"modelYear":0,"versionDuringModelYear":"","modelType":"","features":[]}`,
}
err = nil
} else {
return common.SAPResponse{}, err
}
}
var parsedConfig common.SAPResponse
err = json.Unmarshal([]byte(config.ConfigData), &parsedConfig)
return parsedConfig, err
}
func getPDXVersion(vin string) (pdxVersion string) {
return envtool.GetEnv("PDX_FILE_VERSION", "V3.20.0")
}
// Need to replace certain names to match up with new ones, and remove ECU's that arn't being coded
func cdsNameTransform(cdsMap map[string]string) map[string]string {
for ecuName, ecuReplacement := range common.ECUReplacement() {
cdsString, ok := cdsMap[ecuName]
if !ok {
continue
}
delete(cdsMap, ecuName)
cdsMap[ecuReplacement] = cdsString
}
return cdsMap
}
// Run this after the name transform. If the ecu is not in the list to send, don't send it
func cdsRemoval(cdsMap map[string]string) map[string]string {
for ecuName := range cdsMap {
_, ok := common.FilterECUConfigurationMap[ecuName]
if ok {
delete(cdsMap, ecuName)
}
}
return cdsMap
}

View File

@@ -0,0 +1,142 @@
package usecase_helpers
import (
"encoding/json"
"os"
"testing"
"fiskerinc.com/modules/common"
"fiskerinc.com/modules/db"
"fiskerinc.com/modules/db/queries"
"github.com/stretchr/testify/assert"
)
func init() {
os.Setenv("NO_CDS_ECUS", "VCU, NTN")
}
func TestCDSRenameAndRemoval(t *testing.T) {
common.BuildFilterECUConfigurationMap()
cdsMap := map[string]string{
"GW": "000547303031313632FF7CFF7F0040",
"VCU": "0003473030313136320102010A",
"PDU": "00024730303131363202768F",
"EPS": "000147303031313632011D",
"EPS2": "000147303031313632011D",
"ESP": "000F47303031313632000001012301027600000101000002B0",
}
expectedMap := map[string]string{
"GW": "000547303031313632FF7CFF7F0040",
"EPS1": "000147303031313632011D",
"EPS2": "000147303031313632011D",
"ESP": "000F47303031313632000001012301027600000101000002B0",
"OBC": "00024730303131363202768F",
}
cdsMap = cdsNameTransform(cdsMap)
cdsMap = cdsRemoval(cdsMap)
if !assert.Equal(t, expectedMap, cdsMap) {
t.Fail()
}
}
func TestGetCDSFromDB(t *testing.T) {
os.Setenv("DB_USER", "fiskercloud")
os.Setenv("DB_PASSWORD", "ywo5ea8OPN1E4V9&")
os.Setenv("DB_HOST", "cec-prd-cloud-2.postgres.database.azure.com")
os.Setenv("DB_NAME", "postgres")
os.Setenv("DB_SSLMODE", "require")
client := &db.DBClient{}
client.RegisterManyToManyRel([]interface{}{
(*common.CarToDriver)(nil),
})
err := client.InitSchema([]interface{}{
(*common.UpdateManifest)(nil),
(*common.Car)(nil),
(*common.CarToDriver)(nil),
(*common.CarUpdateStatus)(nil),
(*common.CarUpdate)(nil),
(*common.FileKey)(nil),
(*common.RatePlanTMobile)(nil),
})
if err != nil {
t.Fatal(err)
}
carConfigData := &queries.CarConfigData{}
carConfigData.SetClient(client)
res, err := getStoredFeatureCodes("VCF1ZBU28PG002114", carConfigData)
if err != nil {
t.Fatal(err)
}
featureCodes := make([]string, 0, len(res.Features))
for _, c := range res.Features {
featureCodes = append(featureCodes, c.FeatureCode)
}
featureCodesString, _ := json.Marshal(featureCodes)
_ = featureCodesString
t.Logf("%s\n", featureCodesString)
}
func TestBCMBeepOverwrite(t *testing.T){
// Generated on local test ODX.
withBeep := "001247303032313134000100010001010101000201010101010073"
noBeep := "0012473030323131340001000100010101010000010101010100C9"
expectNoChange := overwriteBCMBeep(noBeep)
if expectNoChange != noBeep {
t.Errorf("expected No Change between \n%s\n%s\n", noBeep, expectNoChange)
}
expectChange := overwriteBCMBeep(withBeep)
if expectChange != noBeep {
t.Errorf("expected Change to match between \n%s\n%s\n", noBeep, expectChange)
}
}
// Should change these tests to ignore the headers, and only check the actual byteage
func TestBCMSequentialLightingOverwrite(t *testing.T){
noLight := "001247303032313732000100000201010101020001010001010071"
withLight := "0012473030323137320001000102010101010200010100010100F0"
expectNoChange := overwriteBCMSequentialLighting(withLight)
if expectNoChange != withLight {
t.Errorf("expected No Change between \n%s\n%s\n", withLight, expectNoChange)
}
expectChange := overwriteBCMSequentialLighting(noLight)
if expectChange != withLight {
t.Errorf("expected Change to match between \n%s\n%s\n", withLight, expectChange)
}
}
func TestICCOverwrite(t *testing.T){
withoutHotSpot := "010047303032313134230108400000020401010001000000010000000001010000000001020000000001000000010000000100000000000000000001010100000000000001010101000100000000010000000000000000000001000101000102010101010101000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000E5" // Ivans with 130100
withHotSpot := "0100473030323131342301084000000204010100010000000100000000010100000000010200000000010000000100000001000000000000000000010101000100000000010101010001000000000100000000000000000000010001010001020101010101010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009D" // Ivans -> MID_BLUE_MATTE from COL 57 the value 4
expectedChange := overwriteICCHotSpot(withoutHotSpot)
if expectedChange == withoutHotSpot {
t.Error("my life is meanignless")
}
if expectedChange != withHotSpot {
t.Errorf("Expected {2} to become {1} \n%s\n%s\n", withHotSpot, expectedChange)
}
expectNoChange := overwriteICCHotSpot(withHotSpot)
if expectNoChange != withHotSpot {
t.Errorf("expected No Change to match between \n%s\n%s\n", withHotSpot, expectNoChange)
}
}
// Give a input, and check if the bit is correct
func TestHasICCHotSpot(t *testing.T) {
input := "0100473030323131342301084000000204010100010000000100000000010100000000010200000000010000000100000001000000000000000000010101000100000000010101010001000000000100000000000000000000010001010001020101010101010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009D"
out := overwriteICCHotSpot(input)
if input != out {
t.Fail()
}
}

View File

@@ -0,0 +1,136 @@
package usecase_helpers
import (
"fmt"
"net/http"
"strings"
"fiskerinc.com/modules/common"
"fiskerinc.com/modules/common/carupdatestatus"
"fiskerinc.com/modules/db/queries"
e "fiskerinc.com/modules/errors"
"fiskerinc.com/modules/grpc/kafka_grpc"
"fiskerinc.com/modules/kafka"
"github.com/pkg/errors"
"google.golang.org/protobuf/proto"
)
type UpdateNotifierInterface interface {
Send(vins []string, manifest common.UpdateManifest, username string) ([]common.CarUpdate, error)
}
func NewUpdateNotifier(
cu queries.CarUpdatesInterface,
pr kafka.ProducerInterface,
) UpdateNotifierInterface {
return &updateNotifier{
carUpdates: cu,
producer: pr,
}
}
type updateNotifier struct {
carUpdates queries.CarUpdatesInterface
producer kafka.ProducerInterface
targetService string
}
// Expect only ota or another service that actually sends the update to the car to call this
func (un *updateNotifier) Send(vins []string, manifest common.UpdateManifest, username string) ([]common.CarUpdate, error) {
carUpdateList := make([]common.CarUpdate, len(vins))
pendingVins := make([]string, 0)
for _, vin := range vins {
pending, err := un.carUpdates.HasPendingUpdates(manifest.ID, vin)
if err != nil {
return nil, err
}
if pending {
pendingVins = append(pendingVins, vin)
}
}
if len(pendingVins) > 0 {
err := e.NewCustomError(fmt.Sprintf("pending update exists for %s", strings.Join(pendingVins, ", ")), http.StatusForbidden)
return carUpdateList, errors.WithStack(err)
}
// For each car, we create a new car_update entry, and then try to send the manifest to the car
for i, vin := range vins {
carUpdate, err := un.insertCarUpdate(un.carUpdates, manifest.ID, vin, username)
if err != nil {
return carUpdateList, err
}
err = un.sendManifest(vin, username, carUpdate.ID)
if err != nil {
un.cancel(un.carUpdates, carUpdate.ID, vin, "Failed to queue message to Kafka, error: "+err.Error())
return carUpdateList, err
}
carUpdateList[i] = carUpdate
}
return carUpdateList, nil
}
func (un *updateNotifier) sendManifest(vin string, username string, carUpdateID int64) error {
data := &kafka_grpc.GRPC_AttendantPayload_UpdateManifest{
UpdateManifest: &kafka_grpc.UpdateManifest{
CarUpdateId: carUpdateID,
},
}
kafkaMSG := kafka_grpc.GRPC_AttendantPayload{
Handler: "send_manifest",
Data: data,
}
binaryPayload, _ := proto.Marshal(&kafkaMSG)
return un.producer.ProduceBinary(kafka.AttendantServiceGRPCKafka, common.Service.Key(vin), binaryPayload, map[string][]byte{
"id": []byte(username),
})
}
func (un *updateNotifier) insertCarUpdate(db queries.CarUpdatesInterface, manifestID int64, vin string, username string) (common.CarUpdate, error) {
up := common.CarUpdate{
UpdateManifestID: manifestID,
VIN: vin,
Status: "pending",
Username: username,
UpdateSource: common.UPDATE_SOURCE_OTA,
}
_, err := db.Insert(&up)
if err != nil {
return up, err
}
_, err = db.LogStatus(&up)
return up, err
}
func (un *updateNotifier) cancel(db queries.CarUpdatesInterface, updateID int64, vin string, info string) {
up := common.CarUpdate{
ID: updateID,
Status: carupdatestatus.ManifestCanceled,
Info: info,
}
db.UpdateStatus(&up)
}
type JSONCarUpdatesRequest struct {
UpdateManifestID int64 `json:"manifest_id" validate:"required"`
VINs []string `json:"vins" validate:"required,gte=1,lte=1000,dive,vin"`
}
type JSONOneCarUpdatesRequest struct {
UpdateManifestID int64 `json:"manifest_id" validate:"required"`
VIN string `json:"vin" validate:"required,vin"`
}
type JSONFleetUpdatesRequest struct {
UpdateManifestID int64 `json:"manifest_id" validate:"required"`
FleetNames []string `json:"fleet_names" validate:"required,gte=1,lte=1000,dive,fleet"`
}