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() { }