Add depot, attendant, jetfire, optimus, ota services with kustomize overlays

This commit is contained in:
Chris Rai
2026-01-31 15:35:07 -05:00
parent a0ec642ca1
commit 9a5cb2f547
404 changed files with 38817 additions and 16 deletions

View File

@@ -0,0 +1,518 @@
package controllers
import (
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"github.com/fiskerinc/cloud-services/services/attendant/services"
"github.com/fiskerinc/cloud-services/pkg/cache"
"github.com/fiskerinc/cloud-services/pkg/common"
s "github.com/fiskerinc/cloud-services/pkg/common/carupdatestatus"
"github.com/fiskerinc/cloud-services/pkg/db/queries"
"github.com/fiskerinc/cloud-services/pkg/grpc/sms"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/fiskerinc/cloud-services/pkg/manifestsender"
"github.com/fiskerinc/cloud-services/pkg/redis"
vconfig "github.com/fiskerinc/cloud-services/pkg/vehicleconfig"
"github.com/fiskerinc/cloud-services/pkg/hwversion"
"github.com/go-pg/pg/v10"
r "github.com/gomodule/redigo/redis"
"github.com/pkg/errors"
)
const redisObjectExpire = 3600
const (
PackageDownloadStart = "package_download_start"
PackageDownloadComplete = "package_download_complete"
PackageInstallStart = "package_install_start"
PackageInstallComplete = "package_install_complete"
InstallError = "install_error"
)
var RepeatedStatus = errors.New("RepeatedStatus")
// CarUpdateProgress takes in a car update message and saves it to our database
// This includes setting the status of a car update, and telling the car and SAP that the update is done
func NewCarUpdateProgress(clientPool redis.ClientPoolInterface, ka *services.KeepAwake, db *services.DB, device common.Device) CarUpdateProgressInterface {
if device == common.TRex {
return &CarUpdateProgress{
RedisClientPool: clientPool,
DB: db,
ka: ka,
}
}
if device == common.HMI {
return &HMICarUpdateProgress{
conf: services.GetVehicleConfig(),
sms: services.GetSMSClient(),
ka: ka,
CarUpdateProgress: CarUpdateProgress{
RedisClientPool: clientPool,
DB: db,
ka: ka,
},
}
}
return nil
}
type CarUpdateProgressInterface interface {
Process(vin string, data []byte) error
ProcessStatus(vin string, status common.CarUpdateProgress) error
Dispose()
}
type CarUpdateProgress struct {
RedisClientPool redis.ClientPoolInterface
DB *services.DB
ka *services.KeepAwake
}
func (cu *CarUpdateProgress) Process(vin string, data []byte) error {
var status common.CarUpdateProgress
err := json.Unmarshal(data, &status)
if err != nil {
return err
}
return cu.ProcessStatus(vin, status)
}
func (cu *CarUpdateProgress) ProcessStatus(vin string, status common.CarUpdateProgress) (err error) {
if cu.transformDBCarUpdateProgress(&status) {
err = cu.logStatusDB(status)
// If the error is the repeated, we can just exit early
if err != nil {
if errors.Is(err, queries.RepeatedStatus) {
err = nil
}
return
}
}
cu.cancelTheCANAwake(vin, status)
batch := redis.NewRedisBatchCommands()
cu.transformRedisCarUpdateProgress(&status)
cu.BatchCacheRedis(batch, redis.CarUpdateStatusHashKey(status.CarUpdateID), &status)
client := cu.RedisClientPool.GetFromPool()
defer client.Close()
_, err = client.ExecuteBatch(batch)
if err != nil {
return err
}
// do not send car update status for internal cloud statuses
if cu.isInternalStatus(status) {
return nil
}
msg := cu.getMessage(&status)
err = cu.publishStatusHMI(vin, &msg)
if err != nil {
return err
}
msgMobile := cu.getMessageForMobile(&status, vin)
err = cu.publishStatusMobile(vin, &msgMobile)
if err != nil {
return err
}
err = cu.onUpdateManifestComplete(&status, vin)
return err
}
// We will try and cancel sending CAN status stuff
func (cu *CarUpdateProgress) cancelTheCANAwake(vin string, status common.CarUpdateProgress) {
switch status.Status {
case s.DownloadFailed, s.InstallFailed, s.ManifestCancelAccepted, s.ManifestCancelRejected,
s.ManifestError, s.ManifestRejected, s.ManifestValidationFailed, s.RequirementsFailed, s.ManifestCanceled:
logger.Info().Msgf("canceling CAN Awake for %s because %s", vin, status.Status)
cu.ka.RemoveKeepAwakeMessage(vin)
}
}
func (cu *CarUpdateProgress) isInternalStatus(status common.CarUpdateProgress) bool {
return status.Status == s.Sent || status.Status == s.Pending
}
func (cu *CarUpdateProgress) transformDBCarUpdateProgress(status *common.CarUpdateProgress) bool {
switch status.Status {
case s.DownloadStarted:
if status.PackageCurrent == 0 {
status.Status = PackageDownloadStart
}
return true
case s.DownloadCompleted:
if status.PackageCurrent == status.PackageTotal {
status.Status = PackageDownloadComplete
}
return true
case s.InstallStarted:
if status.InstalledFiles == 0 && status.TotalFiles > 0 {
status.Status = PackageInstallStart
}
return true
case s.InstallSucceeded:
if status.InstalledFiles == status.TotalFiles && status.TotalFiles > 0 {
status.Status = PackageInstallComplete
}
return true
case InstallError:
status.Status = s.InstallFailed
return true
case s.Installing:
return false
case s.Downloading:
// these status updates do not need to be saved in the database
return false
}
return true
}
func (cu *CarUpdateProgress) transformRedisCarUpdateProgress(status *common.CarUpdateProgress) {
switch status.Status {
case s.DownloadStarted, s.DownloadCompleted, PackageDownloadStart:
status.Status = s.Downloading
case s.InstallStarted, s.InstallSucceeded, PackageInstallStart:
status.Status = s.Installing
}
}
func (cu *CarUpdateProgress) logStatusDB(status common.CarUpdateProgress) (err error) {
// If we are one of these status's we want to ignore, then we need to do some extra database steps, otherwise insert normally
carUpdate := common.CarUpdate{
ID: status.CarUpdateID,
Status: status.Status,
ErrorCode: status.ErrorCode,
Info: strings.TrimSpace(fmt.Sprintf("%s %s", status.ECU, status.Info)),
}
if _, ok := s.NoRepeatUpdateStatus[status.Status]; ok {
_, err = cu.DB.GetCarUpdates().UpdateStatusIfNotRepeat(&carUpdate)
return
}
_, err = cu.DB.GetCarUpdates().UpdateStatus(&carUpdate)
return err
}
func (cu *CarUpdateProgress) GetCache(key string) (*common.CarUpdateProgress, error) {
client := cu.RedisClientPool.GetFromPool()
defer client.Close()
status := common.CarUpdateProgress{}
err := client.GetObject(key, &status)
return &status, err
}
func (cu *CarUpdateProgress) BatchCacheRedis(batch *redis.RedisBatchCommands, key string, status *common.CarUpdateProgress) {
batch.Add(r.Args{}.Add("HSET").Add(key).AddFlat(status)...)
batch.Add("EXPIRE", key, redisObjectExpire)
}
func (cu *CarUpdateProgress) getMessage(status *common.CarUpdateProgress) common.Message {
return common.Message{
Handler: "car_update_status",
Data: status,
}
}
func (cu *CarUpdateProgress) getMessageForMobile(status *common.CarUpdateProgress, vin string) common.Message {
type mobileData struct {
VIN string `json:"vin"`
*common.CarUpdateProgress
}
return common.Message{
Handler: "car_update_status",
Data: mobileData{vin, status},
}
}
func (cu *CarUpdateProgress) publishStatusHMI(vin string, msg *common.Message) error {
client := cu.RedisClientPool.GetFromPool()
defer client.Close()
// redis publish to HMI
hmiKey := common.HMI.Key(vin)
// Add VIN
err := client.SafePublishMessage(hmiKey, msg)
return err
}
func (cu *CarUpdateProgress) publishStatusMobile(vin string, msg *common.Message) error {
drivers := cache.NewDriversCache(cu.RedisClientPool, cu.DB.GetCars())
// redis publish to mobile devices
driverIDs, err := drivers.RetrieveDriverIDs(vin)
if err != nil {
return err
}
// Change thos for loop to isntead create a batch and execute it all at once
client := cu.RedisClientPool.GetFromPool()
defer client.Close()
for _, d := range driverIDs {
mobileKey := common.Mobile.Key(d)
err = client.SafePublishMessage(mobileKey, msg)
if err != nil {
return err
}
}
return nil
}
func (cu *CarUpdateProgress) Dispose() {
cu.DB = nil
}
type HMICarUpdateProgress struct {
conf vconfig.ConfigServiceInterface
sms sms.SMSServiceClient
ka *services.KeepAwake
CarUpdateProgress
}
func (h *HMICarUpdateProgress) Process(vin string, data []byte) error {
var status common.CarUpdateProgress
err := json.Unmarshal(data, &status)
if err != nil {
return err
}
if h.downloadComplete(&status) {
// stop calling the sendKeepAwakeMessage
h.ka.RemoveKeepAwakeMessage(vin)
_, err = h.sendManifestToTRex(vin, &status)
if err != nil {
return err
}
h.logStatusDB(common.CarUpdateProgress{
CarUpdateID: status.CarUpdateID,
Status: s.Sent,
Info: "TBOX",
})
}
return h.ProcessStatus(vin, status)
}
func (h *HMICarUpdateProgress) downloadComplete(status *common.CarUpdateProgress) bool {
return status.Status == s.DownloadCompleted
}
func (h *HMICarUpdateProgress) getManifest(status *common.CarUpdateProgress) (*common.UpdateManifest, error) {
update := common.CarUpdate{ID: status.CarUpdateID}
err := h.DB.GetCarUpdates().Load(&update)
if err != nil {
return nil, err
}
update.UpdateManifest.CarUpdateID = status.CarUpdateID
return update.UpdateManifest, nil
}
func (h *HMICarUpdateProgress) sendManifestToTRex(vin string, status *common.CarUpdateProgress) (msgID string, err error) {
logger.Info().Msgf("HMICarUpdateProgress sendManifestToTRex car_update_id %d", status.CarUpdateID)
manifest, err := h.getManifest(status)
if err != nil {
return
}
if !manifest.HasSelfDownload() {
logger.Error().Msgf("%s download_completed for non-self-download manifest", vin)
return
}
err = hwversion.SetHWVersion(manifest, vin, services.GetDB().GetCars())
if err != nil {
// An error here is very unexpected. The hw versioning should have been confirmed earlier before ICC was updated
err = errors.WithStack(err)
logger.Err(err).Str("VIN", vin).Int64("UpdateID", status.CarUpdateID).Msg("failed to set hw versions for a manifest after ICC complete update")
err = nil
}
manifest.SortECUs()
manifest.FilterCompatibleECUs(vin)
// This code is going to be removed by mny other PR so not going to mess with it for now
client := h.RedisClientPool.GetFromPool()
defer client.Close()
trex := manifestsender.NewTBOXManifestSender(client, h.conf, h.DB, h.sms, nil)
defer trex.Close()
msgID, err = trex.ProcessSoftwareUpdate(vin, manifest, services.GetDB().GetCarConfigData())
return
}
func (h *HMICarUpdateProgress) GetRedisHashKey(status *common.CarUpdateProgress) string {
return redis.CarUpdateStatusHMIHashKey(status.CarUpdateID)
}
// Car Update Done
func (cu *CarUpdateProgress) onUpdateManifestComplete(status *common.CarUpdateProgress, vin string) (err error) {
success := false
final := false
submitSAP := false
switch status.Status {
case s.ManifestSucceeded:
success = true
submitSAP = true
final = true
case s.ManifestCanceled, s.ManifestError, s.ManifestRejected:
success = false
submitSAP = true
final = true
case s.DownloadFailed, s.ManifestCancelPending, s.RollbackSucceeded, s.RollbackFailed, s.CleanupSucceeded:
final = true
default:
return nil
}
carUpdatesDB := cu.DB.GetCarUpdates()
carUpdate, err := carUpdatesDB.SelectByID(status.CarUpdateID)
if err != nil {
err = errors.WithStack(err)
return
}
if carUpdate != nil {
// Notify car user of in progress update through FOA API
fs := services.GetFoaService()
foaResp, err := fs.OtaUpdateStatus(vin, carUpdate, status)
if err != nil || (foaResp != nil && foaResp.StatusCode != http.StatusOK) {
bodyBytes, _ := io.ReadAll(foaResp.Body)
bodyString := string(bodyBytes)
logger.Err(err).Msgf("notify FOA for update manifest %d final state %s for %s failed with http status %d and message %s", carUpdate.UpdateManifestID, status.Status, vin, foaResp.StatusCode, bodyString)
err = nil
}
}
logger.Info().Msgf("Manifest update completed for %s with status of %s", vin, status.Status)
if submitSAP {
logger.Info().Msg("SAP: No Longer Submit Updates")
// sap := services.GetSapService()
// err = sap.SubmitResult(vin, success)
// if err != nil {
// requestBody := struct {
// VIN string
// Success bool
// CarUpdateProgress common.CarUpdateProgress
// }{VIN: vin, Success: success, CarUpdateProgress: *status}
// logger.Err(err).Interface("body", requestBody).Msgf("failed to call sap submit result")
// err = nil
// }
}
if success {
// If we are successful, we want to possibly update the cars sums version
// Need to pull the manifest to check it has a sums version, and then update the car
err = cu.updateCarsSUMSVersion(status)
if err != nil {
logger.Err(err).Msgf("failed to update car sums version for manifest with CarUpdateID %d", status.CarUpdateID)
err = nil
}
// Send the read_ecu_versions remote command so that the ECU data is updated in postgres ASAP
client := services.RedisClientPool().GetFromPool()
defer client.Close()
err = client.SafePublishMessage(
common.TRex.Key(vin),
common.Message{
Handler: "read_ecu_versions",
Data: common.RemoteReadVersionsCommandArgs{
ECUName: "*",
},
},
)
if err != nil {
logger.Err(err).Msgf("failed to send read_ecu_versions command to vin %s", vin)
err = nil
}
}
if final {
// if the manifest is in a final state
// then delete the redundant requirements_await rows from car_update_statuses, to avoid overcrowding the table
err = cu.truncateRequirementsAwaitForUpdate(status)
if err != nil {
logger.Err(err).Msgf("failed to delete redundant requirements_await rows from car_update_statuses for manifest with CarUpdateID %d", status.CarUpdateID)
err = nil
}
}
return err
}
func (cu *CarUpdateProgress) truncateRequirementsAwaitForUpdate(status *common.CarUpdateProgress) error {
logger.Info().Msgf("Manifest with CarUpdateID %d successful with status %s. Deleting redundant requirements_await rows from car_update_statuses", status.CarUpdateID, status.Status)
_, err := cu.DB.GetCarUpdates().TruncateRequirementsAwaitForUpdate(status.CarUpdateID)
if err != nil && !errors.Is(err, pg.ErrNoRows) {
return err
}
return nil
}
// Find the car update, and it gives the update manifest
// If the manifest has a sums version, apply it to the car
func (cu *CarUpdateProgress) updateCarsSUMSVersion(status *common.CarUpdateProgress) (err error) {
carUpdatesDB := cu.DB.GetCarUpdates()
carUpdate, err := carUpdatesDB.SelectByID(status.CarUpdateID)
if err != nil {
err = errors.WithStack(err)
return
}
if carUpdate.UpdateManifest == nil {
err = errors.New("failed to pull car updates update manifest")
return
}
um := carUpdate.UpdateManifest
// So if we have have a sums version we want to update
if um.SUMS == "" {
return
}
carsDB := cu.DB.GetCars()
filter := common.Car{
VIN: carUpdate.VIN,
}
cars, err := carsDB.Select(&filter, nil)
if err != nil {
err = errors.WithStack(err)
return
}
if len(cars) != 1 {
err = fmt.Errorf("did not receive only one car, received: %d", len(cars))
err = errors.WithStack(err)
return
}
car := cars[0]
car.SUMSVersion = um.SUMS
_, err = carsDB.Update(&car)
if err != nil {
err = errors.WithStack(err)
}
return
}

View File

@@ -0,0 +1,184 @@
package controllers
import (
"github.com/fiskerinc/cloud-services/services/attendant/services"
"encoding/base64"
"encoding/json"
"errors"
"sync"
"time"
"github.com/fiskerinc/cloud-services/pkg/grpc/kafka_grpc"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/fiskerinc/cloud-services/pkg/can-go/pkg/descriptor"
)
type DTCEntry struct {
CreatedAt time.Time `bson:"created_at"`
VIN string `bson:"vin"`
ECU string `json:"ecu" bson:"ecu"`
DTC uint64 `json:"dtc" bson:"dtc"`
Status uint8 `json:"status" bson:"status"`
Timestamp time.Time `json:"timestamp" bson:"timestamp"`
Speed uint16 `json:"speed" bson:"speed"`
Mileage uint32 `json:"mileage" bson:"mileage"`
Voltage uint16 `json:"voltage" bson:"voltage"`
SnapshotBase64 string `json:"snapshot,omitempty" bson:"snapshot,omitempty"`
}
func GRPCToDTCEntry(payload *kafka_grpc.GRPC_AttendantPayload) []byte {
if payload.Data == nil {
return nil
}
data := payload.Data.(*kafka_grpc.GRPC_AttendantPayload_DtcEntry)
if data == nil {
return nil
}
dtc := &DTCEntry{
VIN: data.DtcEntry.Vin,
ECU: data.DtcEntry.Ecu,
CreatedAt: milliToDate(data.DtcEntry.CreatedAt),
DTC: data.DtcEntry.Dtc,
Status: uint8(data.DtcEntry.Status),
Timestamp: milliToDate(data.DtcEntry.Timestamp),
Speed: uint16(data.DtcEntry.Speed),
Mileage: data.DtcEntry.Mileage,
Voltage: uint16(data.DtcEntry.Volt),
SnapshotBase64: data.DtcEntry.SnapshotBase64,
}
bytes, _ := json.Marshal(dtc)
return bytes
}
func milliToDate(timestamp int64) time.Time {
seconds := timestamp / 1000
nanoseconds := (timestamp % 1000) * int64(time.Millisecond)
return time.Unix(seconds, nanoseconds)
}
const SNAPSHOT_LEN = 21
func (entry *DTCEntry) ParseSnapshot() error {
data, errc := base64.StdEncoding.DecodeString(entry.SnapshotBase64)
if errc != nil {
return errc
}
payloadLen := len(data) - 2
if payloadLen < SNAPSHOT_LEN {
logger.Debug().Msgf("DTC snapshot payload is too small. Required length=%d, actual=%d, ECU=%s\n", SNAPSHOT_LEN, len(data), entry.ECU)
return errors.New("Snapshot too small")
}
// Our "Basic Diagnostic" spec defines 4 mandatory DIDs that must be
// stored inside DTCStapshotRecord:
// 1. EF F6 - Timestamp (6 bytes).
// 2. EF F7 - Vehicle Speed (2 bytes).
// 3. EF F8 - Milage (idk, our spec doesn't specify exact number.
// Empirically it's 4 bytes (same as "ICC_0x531::TotMilg_ODO").
// 4. EF F9 - Battery Voltage (2 bytes).
///entry.Speed = int16(speed.ToPhysical(float64(uint16(decodedBytes[0])<<8 | uint16(decodedBytes[1]))))
//entry.Voltage = int16(voltage.ToPhysical(float64(uint16(decodedBytes[0])<<8 | uint16(decodedBytes[1]))))
for idx := 2; idx < len(data)-1; {
if data[idx] != 0xEF {
idx++
continue
}
if data[idx+1] == 0xF6 && idx+7 < len(data) {
idx += 2
entry.Timestamp, idx = consumeTimestamp(data, idx)
} else if data[idx+1] == 0xF7 && idx+3 < len(data) {
idx += 2
entry.Speed, idx = consumeSpeed(data, idx)
} else if data[idx+1] == 0xF8 && idx+5 < len(data) {
idx += 2
entry.Mileage, idx = consumeMileage(data, idx)
} else if data[idx+1] == 0xF9 && idx+3 < len(data) {
idx += 2
entry.Voltage, idx = consumeVoltage(data, idx)
} else {
idx++
}
}
return nil
}
var onceTimestamp sync.Once
var (
day *descriptor.Signal
hr *descriptor.Signal
mins *descriptor.Signal
yr *descriptor.Signal
month *descriptor.Signal
sec *descriptor.Signal
)
func consumeTimestamp(data []byte, idx int) (res time.Time, idx_ret int) {
onceTimestamp.Do(func() {
day, _ = services.GetDBC().Signal(0x62F, "TBOX_CrtTi_Day")
hr, _ = services.GetDBC().Signal(0x62F, "TBOX_CrtTi_Hr")
mins, _ = services.GetDBC().Signal(0x62F, "TBOX_CrtTi_Mins")
yr, _ = services.GetDBC().Signal(0x62F, "TBOX_CrtTi_Yr")
month, _ = services.GetDBC().Signal(0x62F, "TBOX_CrtTi_Mth")
sec, _ = services.GetDBC().Signal(0x62F, "TBOX_CrtTi_Sec")
})
res = time.Date(int(yr.ToPhysical(float64(data[idx]))),
time.Month(month.ToPhysical(float64(data[idx+1]))),
int(day.ToPhysical(float64(data[idx+2]))),
int(hr.ToPhysical(float64(data[idx+3]))),
int(mins.ToPhysical(float64(data[idx+4]))),
int(sec.ToPhysical(float64(data[idx+5]))), 0, time.UTC)
idx_ret = idx + 6
return res, idx_ret
}
var onceSpeed sync.Once
var speed *descriptor.Signal
func consumeSpeed(data []byte, idx int) (res uint16, idx_ret int) {
onceSpeed.Do(func() {
speed, _ = services.GetDBC().Signal(0x318, "ESP_VehSpd")
})
res = uint16(speed.ToPhysical(float64(uint16(data[idx])<<8 | uint16(data[idx+1]))))
idx_ret = idx + 2
return res, idx_ret
}
var onceMileage sync.Once
var mileage *descriptor.Signal
func consumeMileage(data []byte, idx int) (res uint32, idx_ret int) {
onceMileage.Do(func() {
mileage, _ = services.GetDBC().Signal(0x531, "ICC_TotMilg_ODO")
})
res = uint32(mileage.ToPhysical(
float64(
uint64(data[idx])<<24 |
uint64(data[idx+1])<<16 |
uint64(data[idx+2])<<8 | uint64(data[idx+3]))))
idx_ret = idx + 4
return res, idx_ret
}
var onceVoltage sync.Once
var voltage *descriptor.Signal
func consumeVoltage(data []byte, idx int) (res uint16, idx_ret int) {
onceVoltage.Do(func() {
voltage, _ = services.GetDBC().Signal(0x507, "VCU_BattVolt")
})
res = uint16(voltage.ToPhysical(float64(uint16(data[idx])<<8 | uint16(data[idx+1]))))
idx_ret = idx + 2
return res, idx_ret
}

View File

@@ -0,0 +1,60 @@
package controllers_test
import (
"github.com/fiskerinc/cloud-services/services/attendant/controllers"
"encoding/json"
"fmt"
"testing"
)
func TestParsing(t *testing.T) {
ecc := []byte(`{"ecu":"ECC","dtc":1719200,"status":9,"snapshot":"AQTv9ucIARIKNO/3AADv+AAAAADv+TLI"}`)
icc := []byte(`{"ecu":"ICC","dtc":14302599,"status":9,"snapshot":"AQUFAAHv+AAAAADv+S2W7/cAAO/2B+cIChAB"}`)
gw := []byte(`{"ecu":"GW","dtc":10718486,"status":8,"snapshot":"AQTv+QLA7/cAAO/2CAcHFAMs7/gAAAAA"}`)
mcu := []byte(`{"ecu":"MCU","dtc":14123795,"status":47,"snapshot":"AQTv9ggHCRcBD+/3AADv+AAAAADv+TLI"}`)
check := func(ecu []byte) {
var entry controllers.DTCEntry
err := json.Unmarshal(ecu, &entry)
if err != nil {
t.Error(err)
}
entry.ParseSnapshot()
if entry.Mileage != 0 {
t.Errorf("Incorrect Mileage %d", entry.Mileage)
}
fmt.Println(entry.Timestamp.Unix())
switch entry.ECU {
case "ECC":
if entry.Timestamp.Unix() != 8730871852 {
t.Errorf("Incorrect Timestamp %d", entry.Timestamp.Unix())
}
if entry.Voltage != 13 {
t.Errorf("Incorrect Voltage %d", entry.Voltage)
}
case "ICC":
if entry.Timestamp.Unix() != 1670580961 {
t.Errorf("Incorrect Timestamp %d", entry.Timestamp.Unix())
}
if entry.Voltage != 11 {
t.Errorf("Incorrect Voltage %d", entry.Voltage)
}
case "GW":
if entry.Timestamp.Unix() != 1691525024 {
t.Errorf("Incorrect Timestamp %d", entry.Timestamp.Unix())
}
case "MCU":
if entry.Timestamp.Unix() != 1691708475 {
t.Errorf("Incorrect Timestamp %d", entry.Timestamp.Unix())
}
}
}
check(ecc)
check(icc)
check(gw)
check(mcu)
}

View File

@@ -0,0 +1,81 @@
package controllers
import (
"encoding/json"
"github.com/fiskerinc/cloud-services/services/attendant/services"
"github.com/fiskerinc/cloud-services/pkg/cache"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/fiskerinc/cloud-services/pkg/redis"
"github.com/fiskerinc/cloud-services/pkg/validator"
"github.com/pkg/errors"
)
// Mega ducky repeat of /handlers/get_file_keys. Need a code re-org
func GetFileKeys(db *services.DB, device common.Device, id string, data []byte) error {
logger.Debug().Msgf("GetFileKeys %v %s", device, id)
var err error
var req *common.FileKeysRequest
client := services.RedisClientPool().GetFromPool()
defer client.Close()
req, err = parseGetFileKeysRequest(data)
if err != nil {
notifyFileKeysGeneralError(client, device, id, err)
return err
}
keys, err := cache.RetrieveFileEncryptionParams(client, db.GetFileKeys(), req.FileIDs)
if err != nil {
notifyFileKeysGeneralError(client, device, id, err)
return err
}
err = client.SafePublishMessage(device.Key(id), common.Message{
Handler: "filekeys",
Data: keys,
})
if err != nil {
return err
}
logger.Debug().Msgf("GetFileKeys sent %v %s", device, id)
return nil
}
func parseGetFileKeysRequest(data []byte) (*common.FileKeysRequest, error) {
var status common.FileKeysRequest
err := json.Unmarshal(data, &status)
if err != nil {
return nil, errors.WithStack(err)
}
err = validator.ValidateStruct(status)
if err != nil {
return &status, errors.WithStack(err)
}
return &status, nil
}
func notifyFileKeysGeneralError(client redis.Client, device common.Device, id string, err error) {
e := client.SafePublishMessage(device.Key(id), common.Message{
Handler: "filekeys",
Data: []common.FileKeyResponse{
{
FileID: "0",
Error: err.Error(),
},
},
})
if e != nil {
logger.Error().Err(errors.WithStack(e)).Send()
}
}

View File

@@ -0,0 +1,59 @@
package controllers
import (
"time"
"github.com/fiskerinc/cloud-services/services/attendant/services"
"github.com/fiskerinc/cloud-services/pkg/health"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/pkg/errors"
)
var mismatchTypeError = errors.New("mismatch type error")
func HealthCheck() {
redis := health.NewRedisHealth(services.RedisClientPool())
server := health.HealthCheckServer{}
err := server.Serve([]health.Config{
{
Name: "db",
Check: health.NewPostgresCheck(services.GetDB().GetDBClient().GetConn()),
Timeout: time.Second * 1,
},
{
Name: "redis",
Check: redis.Check,
Timeout: time.Second * 1,
},
{
Name: "kafka",
Check: health.NewKafkaMultiCheck(getKafkaConsumer),
Timeout: time.Second * 1,
Vital: true,
},
})
if err != nil {
logger.Error().Err(err).Send()
}
}
func getKafkaConsumer() (connections []health.KafkaConnCheckInterface, err error) {
client, oldClient, err := services.GetKafkaConsumer()
if err != nil {
return connections, err
}
conn, ok := client.(health.KafkaConnCheckInterface)
if !ok {
return nil, errors.WithStack(mismatchTypeError)
}
connections = append(connections, conn)
oldConn, ok := oldClient.(health.KafkaConnCheckInterface)
if !ok {
return connections, errors.WithStack(mismatchTypeError)
}
connections = append(connections, oldConn)
return connections, nil
}

View File

@@ -0,0 +1,403 @@
package controllers
import (
"encoding/json"
"fmt"
"github.com/fiskerinc/cloud-services/pkg/grpc/kafka_grpc"
"github.com/fiskerinc/cloud-services/services/attendant/services"
"github.com/fiskerinc/cloud-services/pkg/carcommand"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/common/carupdatestatus"
"github.com/fiskerinc/cloud-services/pkg/common/manifestfingerprintparams"
"github.com/fiskerinc/cloud-services/pkg/grpc/sms"
"github.com/fiskerinc/cloud-services/pkg/hwversion"
"github.com/fiskerinc/cloud-services/pkg/kafka"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/fiskerinc/cloud-services/pkg/manifestsender"
"github.com/fiskerinc/cloud-services/pkg/redis"
"github.com/fiskerinc/cloud-services/pkg/tmobile"
uhelpers "github.com/fiskerinc/cloud-services/pkg/usecase_helpers"
"github.com/fiskerinc/cloud-services/pkg/utils/envtool"
"github.com/fiskerinc/cloud-services/pkg/utils/randomvalues"
"github.com/fiskerinc/cloud-services/pkg/utils/whereami"
"github.com/fiskerinc/cloud-services/pkg/validator"
vconfig "github.com/fiskerinc/cloud-services/pkg/vehicleconfig"
"google.golang.org/protobuf/proto"
"sync"
"time"
"github.com/pkg/errors"
)
// 0100084000000101012200010101010001010101000000000000000000ff7eff7f000101010101000101010100010001010101000101010100000000000000000000000000010101000100000100010101000201010101000101020101000101010200010101010101010101000101000100010001010101010101010101010000000000000100010101020101010101010000000000000000ffffff00010102010102020001010000000000000000000000000000000000000000000000000000000000000000000100202310010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f6
var defaultVOD string = envtool.GetEnv("DEFAULT_VOD", "00FF111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111114A")
var fileKeyBypass map[int64]string
var FileKeyBypassManifest_US map[int64]string = map[int64]string{
1189: "fe4f19fbc940ed58",
1380: "6a44fd71c940716d",
}
var FileKeyBypassManifest_EU map[int64]string = map[int64]string{
893: "fe4f19fbc940ed58",
1002: "6a44fd71c940716d",
1012: "d70e1d32c940a221",
}
func init() {
if whereami.Environment == whereami.PRODUCTION_EU {
fileKeyBypass = FileKeyBypassManifest_EU
} else {
fileKeyBypass = FileKeyBypassManifest_US
}
}
func NewManifestSender(
r redis.ClientPoolInterface,
db *services.DB,
conf vconfig.ConfigServiceInterface,
device common.Device,
ka *services.KeepAwake,
seed int64, //This seed is used only for testing
) *ManifestSender {
randomGenerator := randomvalues.NewNonCryptoGenerator("ABCDEFGHIJKLMNOPQRSTUVWXYZ", seed)
return &ManifestSender{
Redis: r,
db: db,
conf: conf,
Device: device,
sms: services.GetSMSClient(),
ka: ka,
gen: randomGenerator,
}
}
type ManifestSender struct {
Redis redis.ClientPoolInterface
db *services.DB
conf vconfig.ConfigServiceInterface
sms sms.SMSServiceClient
Device common.Device
ka *services.KeepAwake
gen randomvalues.NonCryptoGenerator
}
func (m *ManifestSender) Process(id string, data []byte) error {
// id string parameter is unused except in the following log
// the data might not be used either, just for its single carUpdateID
u, err := m.parseRequest(data)
if err != nil {
logger.Err(err).Msgf("Manifest sender, unable to process update %s", id)
return err
}
carUpdate := common.CarUpdate{ID: u.CarUpdateID}
err = m.loadCarUpdate(&carUpdate)
if err != nil {
return err
}
logger.Info().Msgf("ManifestSender car_update_id %d", carUpdate.ID)
carUpdate.UpdateManifest.CarUpdateID = carUpdate.ID
helper := uhelpers.NewECUKeys(m.db.GetECCKeys())
err = helper.AddECUECCKeys(carUpdate.UpdateManifest)
if err != nil {
logger.Err(err).Msgf("Unable to decrypt keys for %d", carUpdate.ID)
return err //comment out for local debugging
}
// If we fail to deliver the sms, then we don't believe the car has awoken
// then set the manifest as failed
err = m.send(carUpdate.VIN, carUpdate.UpdateManifest)
if err != nil {
update := NewCarUpdateProgress(m.Redis, m.ka, m.db, common.TRex)
err = update.ProcessStatus(carUpdate.VIN, common.CarUpdateProgress{
CarUpdateID: u.CarUpdateID,
Status: carupdatestatus.ManifestCanceled,
Info: " Underlying error: " + err.Error(),
})
}
return err
}
func (m *ManifestSender) destination(manifest *common.UpdateManifest) string {
if manifest.HasSelfDownload() {
return "ICC"
} else {
return "ICC/TBOX"
}
}
func (m *ManifestSender) parseRequest(data []byte) (*common.UpdateManifest, error) {
var req common.UpdateManifest
err := json.Unmarshal(data, &req)
if err != nil {
return nil, errors.WithStack(err)
}
err = validator.ValidateIDField(req.CarUpdateID)
if err != nil {
return &req, errors.WithStack(err)
}
return &req, nil
}
func (m *ManifestSender) loadCarUpdate(cu *common.CarUpdate) error {
err := m.db.GetCarUpdates().Load(cu)
if err != nil {
return errors.WithStack(err)
}
if cu.UpdateManifest == nil {
return nil
}
return nil
}
func (m *ManifestSender) queueManifest(vin string, manifest *common.UpdateManifest) error {
loggedManifest := manifest.Copy()
loggedManifest.RemoveECCKeysFromECUs()
logger.At(logger.Info(), common.HMI.Key(vin), "update").Interface("manifest.json", loggedManifest).Msgf("HMI ManifestSender sent %v %s %d", common.HMI, vin, manifest.CarUpdateID)
client := m.Redis.GetFromPool()
defer client.Close()
err := client.SafeQueueMessage(common.HMI.Key(vin), common.Message{
Handler: "update_manifest",
Data: manifest,
})
if err != nil {
logger.Err(err).Msgf("failed to queue manifest in redis vin %s ", vin)
}
return err
}
func (m *ManifestSender) send(vin string, manifest *common.UpdateManifest) error {
updateManifestID := manifest.ID
manifest.TransformECUNames()
err := hwversion.SetHWVersion(manifest, vin, m.db.GetCars())
if err != nil {
logger.Err(err).Msgf("manifest sender failed at SetHwVersion for vin %s", vin)
return err //comment out for local debugging
}
manifest.SortECUs()
manifest.RemoveOriginalS19HexFiles()
manifest.RemoveOriginalS19HexFilesRollbacks()
manifest.FilterCompatibleECUs(vin)
err = uhelpers.PopulateECUsCurrentVersion(m.db.GetCars(), vin, manifest.ECUs)
if err != nil {
logger.Err(err).Msgf("manifest sender failed at PupulateECUsCurrentVersion for vin %s", vin)
return err
}
fpparams := manifestfingerprintparams.GetFPParams()
manifest.GenerateFingerprint(fpparams.CurTime(), fpparams.ManifestSerial())
hmiManifest := manifest.Copy()
cds, err := m.addVOD(hmiManifest, vin)
if err != nil {
logger.Err(err).Msgf("manifest sender failed at adding the vod for vin %s", vin)
return err // comment out for local debugging
}
err = validator.ValidateField(hmiManifest.SUMS, "sums_version")
if err == nil {
err = hmiManifest.AddSUMSToVOD()
if err != nil {
logger.Err(err).Msgf("manifest sender failed at AddSUMSToVOD for vin %s", vin)
return err
}
} else {
logger.Err(err).Msgf("manifest sender failed at Validation for vin %s", vin)
return err
}
hmiManifest.Scrub(common.HMI)
client := m.Redis.GetFromPool()
defer client.Close()
// If HasSelfDownload, we are not yet ready to send to t box, so we wake the car if the update is forced
if manifest.HasSelfDownload() && m.sms != nil {
// The function that calls this one handles canceling the manifest
var msgID string
res, err := carcommand.QueueSMSWakeUp(vin, true, client, m.db.GetCars(), m.sms)
if err != nil || res == nil || !res.SentSuccessful {
logger.Err(err).Msgf("Attendant:manifest sender failed at sendSMSWakeUp for vin %s", vin)
msgID = m.gen.GetString(10)
defer m.selfKafkaCallback(msgID)
} else {
msgID = res.SmsMsgID
}
err = m.ka.SendFirstKeepAwakeMessage(vin)
if err != nil {
logger.Err(err).Msgf("failed to SendFirstKeepAwakeMessage on msgID %s", msgID)
}
m.queueManifest(vin, hmiManifest)
fileID, ok := fileKeyBypass[updateManifestID]
if ok {
logger.Info().Str("VIN", vin).Msg("Queueing SendFileKeys")
time.AfterFunc(time.Second*3, func() { sendFileKeys(vin, fileID) })
}
// Do not send manifest to TBOX, wait until ICC has finished downloading
return err
}
trex := manifestsender.NewTBOXManifestSender(client, m.conf, m.db, services.GetSMSClient(), cds)
defer trex.Close()
//smsID, err := trex.ProcessSoftwareUpdate(vin, manifest)
_, err = trex.ProcessSoftwareUpdate(vin, manifest, services.GetDB().GetCarConfigData())
logger.Debug().Msgf("Send HMI manifest to %s ", vin)
m.queueManifest(vin, hmiManifest)
// We managed to send an sms, so we want to continue with an sms delivered check
// if smsID != "" {
// err = m.setManifestCache(smsID, ManifestCachedPoint{
// VIN: vin,
// ManifestID: manifest.ID,
// Destination: m.destination(manifest),
// })
// if err != nil {
// return err
// }
// }
return err
}
// So this flow of continue will be activated at two different times, possible multiple times.
// 1) When a new car_update comes through and the .hasdownload is true, and then again when the manifest is being sent to the tbox
func (m *ManifestSender) ContinueTBOXSend(id string, data []byte) (err error) {
// After a text has been succesfully delivered, we can now continue the update
// Check the status and make sure that is is success, otherwise fail and set the update status as failed
var msgStatus tmobile.MessageStatus
err = json.Unmarshal(data, &msgStatus)
if err != nil {
return
}
return nil
}
func (m *ManifestSender) setManifestCache(msgID string, cache ManifestCachedPoint) (err error) {
client := m.Redis.GetFromPool()
defer client.Close()
return client.Set(msgIDToRedisKey(msgID), cache)
}
type ManifestCachedPoint struct {
VIN string
ManifestID int64
Destination string
}
func msgIDToRedisKey(msgID string) (redisKey string) {
return fmt.Sprintf("manifest_tbox_send_cache:%s", msgID)
}
func (m *ManifestSender) addVOD(manifest *common.UpdateManifest, vin string) (map[string]string, error) {
cds, err := uhelpers.GetCDS(m.conf, services.GetDB().GetCarConfigData(), vin)
if err != nil {
return nil, err
}
if vod, ok := cds["VOD"]; ok && vod != "" {
manifest.VOD = vod
} else {
manifest.VOD = defaultVOD
}
return cds, nil
}
// When car is a virtual trex, we are going to produce to the kafka message queue as if the sms service did it
func (m *ManifestSender) selfKafkaCallback(messageID string) {
producer, err := services.GetKafkaProducer()
if err != nil {
logger.Err(err).Send()
return
}
messageStatus := &kafka_grpc.GRPC_AttendantPayload_MessageStatus{
MessageStatus: &kafka_grpc.MessageStatus{
MessageId: messageID,
Status: kafka_grpc.EmumStatus_DELIVERED,
},
}
kafkaMSG := kafka_grpc.GRPC_AttendantPayload{
Handler: "sms_delivery_status_manifest",
Data: messageStatus,
}
binaryPayload, _ := proto.Marshal(&kafkaMSG)
err = producer.ProduceBinary(kafka.AttendantServiceGRPCKafka, "4:Service", binaryPayload, nil)
if err != nil {
err = errors.WithStack(err)
logger.Err(err).Msgf("failed to produce kafka message for sms id %s", messageID)
}
}
func (m *ManifestSender) Release() {
m.Redis = nil
m.db = nil
m.conf = nil
}
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()
}
type FingerprintParamer interface {
ManifestSerial() string
CurTime() time.Time
}
// Mega hack mode
func sendFileKeys(vin string, fileID string) {
logger.Info().Str("VIN", vin).Msg("Sending SendFileKeys")
db := services.GetDB()
err := GetFileKeys(db, common.HMI, vin, []byte(`{"file_ids": ["`+fileID+`"]}`))
if err != nil {
logger.Err(err).Str("VIN", vin).Str("file ID", fileID).Msg("Failed to SendFileKeys")
}
}