519 lines
15 KiB
Go
519 lines
15 KiB
Go
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
|
|
}
|