Add depot, attendant, jetfire, optimus, ota services with kustomize overlays
This commit is contained in:
518
services/attendant/controllers/car_update_progress.go
Normal file
518
services/attendant/controllers/car_update_progress.go
Normal 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
|
||||
}
|
||||
184
services/attendant/controllers/dtc_request.go
Normal file
184
services/attendant/controllers/dtc_request.go
Normal 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
|
||||
|
||||
}
|
||||
60
services/attendant/controllers/dtc_request_test.go
Normal file
60
services/attendant/controllers/dtc_request_test.go
Normal 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)
|
||||
|
||||
}
|
||||
81
services/attendant/controllers/get_filekeys.go
Normal file
81
services/attendant/controllers/get_filekeys.go
Normal 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()
|
||||
}
|
||||
}
|
||||
59
services/attendant/controllers/health_check.go
Normal file
59
services/attendant/controllers/health_check.go
Normal 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
|
||||
}
|
||||
403
services/attendant/controllers/send_manifest.go
Normal file
403
services/attendant/controllers/send_manifest.go
Normal 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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user