Files
cloud-services/services/jetfire/models/vehicle.go

238 lines
6.8 KiB
Go

package models
import (
"fmt"
"time"
"github.com/fiskerinc/cloud-services/services/jetfire/utils"
"github.com/fiskerinc/cloud-services/pkg/grpc/kafka_grpc"
"github.com/fiskerinc/cloud-services/pkg/logger"
"github.com/fiskerinc/cloud-services/pkg/utils/envtool"
)
var (
maxVinCount = envtool.GetEnvInt("JETFIRE_MAX_VINS", 10000)
timestampThreshold = 500 * time.Millisecond
)
// Vehicle State stores latest state for a particular VIN.
type VehicleState struct {
VIN string
Timestamp time.Time //data timestamp of last data sample
TripStart time.Time //data timestamp of start of trip
TripID string //trip id
StateValues map[string]float64 //map of values in state. Keys correspond to CAN signal name
StateTimes map[string]time.Time //map of timestamps in state. Keys correspond to CAN signal name
InsertTime time.Time // local timestamp of last received data. Used only for removing old vehicle states from cache
pollingMap map[uint]time.Time //map of last polling times. Key matches update flag
// for linked list
Next *VehicleState
prev *VehicleState
}
func (v *VehicleState) TimeSincePolled(updateIndex uint) time.Duration {
pollTime, ok := v.pollingMap[updateIndex]
if !ok {
pollTime = time.Unix(0, 0)
}
return v.Timestamp.Sub(pollTime)
}
func (v *VehicleState) SetPollTime(updateIndex uint) {
v.pollingMap[updateIndex] = v.Timestamp
}
func (v *VehicleState) Clear(newVIN string) {
v.VIN = newVIN
cacheSize := len(v.StateValues)
v.StateValues = make(map[string]float64, cacheSize)
v.StateTimes = make(map[string]time.Time, cacheSize)
v.Timestamp = time.Unix(0, 0)
v.TripStart = v.Timestamp
v.TripID = ""
v.InsertTime = time.Now().UTC()
v.pollingMap = make(map[uint]time.Time)
}
// VehicleCache is a table of vehicle states.
type VehicleCache struct {
Cache map[string]*VehicleState
SignalsSet *map[string]bool
LastTimestamp time.Time
// linked list LIFO of States in order of update.
StatesListHead *VehicleState //oldest update
StatesListTail *VehicleState //newest update
}
func (cache *VehicleCache) Clear() {
clear(cache.Cache)
// disconnect linked list
node := cache.StatesListHead
for node != nil {
next := node.Next
node.Next = nil
node.prev = nil
node = next
}
cache.StatesListHead = nil
cache.StatesListTail = nil
}
// removes the node from linked list and appends it to the right end
func (cache *VehicleCache) ReinsertRight(node *VehicleState) {
if cache.StatesListTail == node {
// node is already the tail. don't do anything
return
}
// move head ptr if we are moving the first node
if cache.StatesListHead == node {
cache.StatesListHead = node.Next
}
// remove from list
if node.prev != nil && node.Next != nil {
p := node.prev
n := node.Next
n.prev = p
p.Next = n
} else if node.prev != nil {
node.prev.Next = nil
} else if node.Next != nil {
node.Next.prev = nil
}
node.prev = nil
node.Next = nil
// append to tail
if cache.StatesListTail != nil {
cache.StatesListTail.Next = node
}
node.prev = cache.StatesListTail
cache.StatesListTail = node
if cache.StatesListHead == nil {
cache.StatesListHead = node
}
}
// removes left node from the linked list AND from the cache map
func (cache *VehicleCache) PopLeft() *VehicleState {
if cache.StatesListHead == nil {
return nil
}
node := cache.StatesListHead
cache.StatesListHead = node.Next
delete(cache.Cache, node.VIN)
return node
}
// Insert CANSignal into vehicle cache. Allocates new vehicle state as necessary.
func (cache *VehicleCache) UpdateSignal(signal *kafka_grpc.GRPC_CANSignal, updateFlag uint) error {
if len(*cache.SignalsSet) > 0 {
// check if signal is in signals set, skip if not in signals set
_, contains := (*cache.SignalsSet)[signal.Name]
if !contains {
return nil
}
}
// if VIN is not in cache, allocate new vehicle state for VIN and add to cache
_, contains := cache.Cache[signal.Vin]
if !contains {
if len(cache.Cache) > maxVinCount {
oldState := cache.PopLeft()
logger.Debug().Msgf("repurposing state %s -> %s", oldState.VIN, signal.Vin)
oldState.Clear(signal.Vin)
cache.Cache[signal.Vin] = oldState
} else {
cache.Cache[signal.Vin] = NewVehicleState(signal.Vin, len(*cache.SignalsSet))
logger.Debug().Msgf("new vehicle state %s", signal.Vin)
}
}
//update value
cache.Cache[signal.Vin].UpdateSignal(signal, updateFlag)
cache.ReinsertRight(cache.Cache[signal.Vin]) //move state to end of orderly linkedlist
cache.LastTimestamp = time.Now().UTC()
return nil
}
// constructs a new VehicleState
func NewVehicleState(VIN string, cacheSize int) *VehicleState {
newState := new(VehicleState)
newState.VIN = VIN
newState.StateValues = make(map[string]float64, cacheSize)
newState.StateTimes = make(map[string]time.Time, cacheSize)
newState.Timestamp = time.Unix(0, 0)
newState.TripStart = newState.Timestamp
newState.TripID = ""
newState.InsertTime = time.Now().UTC()
newState.pollingMap = make(map[uint]time.Time)
return newState
}
// constructs a new VehicleState
func NewVehicleStateDefault(VIN string) *VehicleState {
return NewVehicleState(VIN, 10)
}
// UpdateSignal() updates the vehicle state cache
func (state *VehicleState) UpdateSignal(signal *kafka_grpc.GRPC_CANSignal, updateFlag uint) {
// Mark start of new trip if too much time has elapsed between updates
signalTime := utils.FloatToTime(signal.Timestamp)
ignitionTriggered := false
// check for vehicle ignition rising edge as a trigger for a new trip.
if signal.Name == "BCM_PwrMod" && signal.Value >= 2 && signal.Value <= 4 && !signalTime.Before(state.Timestamp) {
pwrMod, ok := state.StateValues["BCM_PwrMod"]
ignitionTriggered = !ok || (ok && pwrMod < 2)
}
if signalTime.Sub(state.Timestamp) >= utils.TripTimeout || ignitionTriggered {
logger.Debug().Msgf("%s New TripStart: %d, old %d, delta %d", state.VIN, signalTime.Unix(), state.Timestamp.Unix(), signalTime.Sub(state.Timestamp))
state.TripStart = signalTime
state.TripID = fmt.Sprintf("%s_%d", state.VIN, state.TripStart.Unix())
}
// Update the vehicle timestamp
oldTime, ok := state.StateTimes[signal.Name]
if !ok {
oldTime = state.Timestamp
}
if !signalTime.Add(timestampThreshold).Before(oldTime) {
if !signalTime.Before(state.Timestamp) {
state.Timestamp = signalTime
}
// Insert new signal value
state.StateValues[signal.Name] = signal.Value
state.StateTimes[signal.Name] = signalTime
state.InsertTime = time.Now()
} else {
// if receiving message out of timestamp order, only accept new signal value if
// cached value is empty for signal name
_, hasSignal := state.StateValues[signal.Name]
if !hasSignal {
state.StateValues[signal.Name] = signal.Value
state.StateTimes[signal.Name] = signalTime
state.InsertTime = time.Now()
}
utils.LogOutOfOrderMsg(signal.Name, signal.Vin)
}
}