Files
cloud-services/services/ota_update_go/background/carImmobilizer.go

194 lines
6.2 KiB
Go

package background
import (
"encoding/json"
"maps"
"otaupdate/services"
"sync"
"time"
"github.com/fiskerinc/cloud-services/pkg/cache"
"github.com/fiskerinc/cloud-services/pkg/carcommand"
"github.com/fiskerinc/cloud-services/pkg/common"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/fiskerinc/cloud-services/pkg/redis"
)
// Anywhere a comment says lock, its probably actually lock/unlock
// Have a local list we check and run against
// and have a redis cache list that we will modify
// keep track of time started, last updated time, number of times sent, and if its a lock or unlock
var (
immobilizerOnce sync.Once
immobilizer *Immobilizer
)
// Message will stay alive in redis for an hour before it is removed
const CAR_CHECK_INTERVAL time.Duration = time.Duration(5 * time.Minute) // Minutes between checking car status and sending command
const REDIS_UPDATE_INTERVAL time.Duration = time.Duration(time.Hour * 2) // Minutes between updating redis with the current cache list. // need to keep track of last updated incase our service goes down
const REDIS_EXPIRATION_DURATION time.Duration = time.Duration(24 * time.Hour) // how old a time should be before we pick it up for us to process
const CAR_LOCK_ATTEMPT_COUNT = 15 // how many times to send the lock command
type CarTrack struct {
TimesModified int `json:"times_modified"`
Immobilize bool `json:"immobilize"` // 0: unlock, 1: lock
}
type Immobilizer struct {
VINs map[string]*CarTrack // By having a pointer here, should be able to modify timesModified without write lock
sync.RWMutex // mutex for handling the reading and writing of the VIN's map
}
func GetImmobilizer() *Immobilizer {
immobilizerOnce.Do(func() {
if immobilizer != nil {
return
}
logger.Info().Msg("Init Immobilizer instance")
immobilizer = InitiateImmobilizer()
})
return immobilizer
}
func InitiateImmobilizer() *Immobilizer {
imm := &Immobilizer{}
imm.VINs = make(map[string]*CarTrack)
go time.AfterFunc(CAR_CHECK_INTERVAL, imm.CheckCars)
// go time.AfterFunc(REDIS_UPDATE_INTERVAL, imm.UpdateRedis)
// Have a random offset so multiple ota's starting at same time don't look and claim at the same time
// go time.AfterFunc(REDIS_EXPIRATION_DURATION+(time.Duration(rand.IntN(20))*time.Minute), imm.ClaimRedis)
return imm
}
func (imm *Immobilizer) GetVINList() (vinList []string) {
imm.RLock()
defer imm.RUnlock()
for vin := range imm.VINs {
vinList = append(vinList, vin)
}
return
}
func (imm *Immobilizer) CheckCars() {
clientPool := services.RedisClientPool()
twins, _ := cache.GetVINListDigitalTwin(imm.GetVINList(), clientPool)
parkedVINs := []string{} // parkedVINs are the cars we are going to send the lock command to
for vin, twin := range twins {
gear := twin.Gear
if gear == nil {
continue
}
if gear.InPark {
parkedVINs = append(parkedVINs, vin)
}
}
// Have a list of vins we can now modify
// Send the lock/unlock command, send wake up command
hopefulImmob := imm.sendRemoteCommands(parkedVINs)
// Not on hopeful immob, we do not check if the car ever wakes up from its sms, so its possible if parked deep in a garage it will not
// Possible fix: remove redis timeout for car lock commands
go imm.RemoveVINs(hopefulImmob)
time.AfterFunc(CAR_CHECK_INTERVAL, imm.CheckCars)
}
func (imm *Immobilizer) sendRemoteCommands(parkedVINs []string) (removableVins []string) {
// First send wake up message
// Send lock or unlock command
// for _, vin := range request.VINs {
// Action logger should get added to when the user adds car to the list
// go func() {
// actionLog := actionlogger.ActionLog{
// VIN: vin,
// Action: actionlogger.RemoteCommand,
// UserIdentifier: httphandlers.GetClientID(r),
// CallLocation: "github.com/fiskerinc/cloud-services/services/ota_update_go/handlers/vehicle_command.go",
// Description: string(description),
// }
// err = alDB.Insert(actionLog)
// if err != nil {
// logger.Err(err).Msg("failed to insert action log inside HandleVehicleCommand")
// }
// }()
// vehicle_command.go has an extremely convoluted way to get remote commands to car. It pushed it to a kafka queue to to be picked up
// then kafka does some delivery re-trying stuff in some way, wakes up the car and then puts the message into redis.
// we are going straight to the redis section
// remoteCommands := make([]string, 0, len(parkedVINs))
imm.RLock()
batch := redis.NewRedisBatchCommands()
smsClient := services.GetSMSClient()
wake := carcommand.NewCarWakeUp(services.GetDB().GetCars(), smsClient)
for _, vin := range parkedVINs {
temp := imm.VINs[vin]
temp.TimesModified -= 1
if temp.TimesModified < 0 {
removableVins = append(removableVins, vin)
continue
}
// probably faster to create the list of things to push and batch, but fine for now
msg := common.RemoteCommandSource{}
if temp.Immobilize {
msg.Command = "doors_lock"
} else {
msg.Command = "doors_unlock"
}
// SafeQueueMessage auto deletes after one hour, which I do not want with this lock command, rather it sat around indefinitely
data, _ := json.Marshal(common.Message{
Handler: "remote_command",
Data: msg,
})
batch.Add("RPUSH", redis.QueueKey(common.TRex.Key(vin)), data)
// try to wake up car
wake.WakeUp(vin, false)
}
imm.RUnlock()
redisClient := services.RedisClientPool().GetFromPool()
defer redisClient.Close()
_, err := redisClient.ExecuteBatch(batch)
if err != nil {
logger.Err(err).Msg("failed to push car immobilizer commands to redis")
}
return
}
func (imm *Immobilizer) RemoveVINs(vins []string) {
imm.Lock()
defer imm.Unlock()
for _, v := range vins {
delete(imm.VINs, v)
}
}
func (imm *Immobilizer) AddVINs(vins []string, immobilize bool) {
imm.Lock()
defer imm.Unlock()
for _, v := range vins {
imm.VINs[v] = &CarTrack{TimesModified: CAR_LOCK_ATTEMPT_COUNT, Immobilize: immobilize}
}
}
// Just returns the information about the vins we are currently tracking
// not sure about memory safety on this
func (imm *Immobilizer) GetVINInformation() (vinInfo map[string]*CarTrack) {
imm.RLock()
defer imm.RUnlock()
vinInfo = maps.Clone(imm.VINs)
return
}
func (imm *Immobilizer) UpdateRedis() {
}
func (imm *Immobilizer) ClaimRedis() {
}