Initial cloud-services repo - gateway service + pkg modules

This commit is contained in:
Chris Rai
2026-01-30 23:14:52 -05:00
commit fbb820d7b3
1037 changed files with 171318 additions and 0 deletions

6
pkg/cachev2/constants.go Normal file
View File

@@ -0,0 +1,6 @@
package cachev2
import "time"
const redisObjectExpire = time.Hour
const redisObjectExpireDay = 24 * time.Hour

192
pkg/cachev2/digital_twin.go Normal file
View File

@@ -0,0 +1,192 @@
package cachev2
import (
"fmt"
"sort"
"strconv"
"strings"
"time"
"fiskerinc.com/modules/dbc/state"
"fiskerinc.com/modules/logger"
"fiskerinc.com/modules/redis"
"fiskerinc.com/modules/utils/querystring"
redigo "github.com/gomodule/redigo/redis"
"github.com/pkg/errors"
)
const (
pattern = "car:*:state"
)
type DigitalTwinTimestampState struct {
redisClient redis.Client
}
func NewDigitalTwinTimestampState(redisClient redis.Client) *DigitalTwinTimestampState {
return &DigitalTwinTimestampState{
redisClient: redisClient,
}
}
// getStateKeys retrieves car state keys from Redis based on the specified pattern
// and returns a sliced list of keys according to the provided offset and limit.
//
// Parameters:
// - offset: An integer indicating the starting index of the slice.
// - limit: An integer specifying the maximum number of elements in the sliced list.
//
// Returns:
// - []string: A sliced list of car state keys based on the given offset and limit.
// - error: An error, if any, encountered during the Redis operation or slicing process.
func (dtts *DigitalTwinTimestampState) getStateKeys(offset, limit int) ([]string, error) {
keys, err := redigo.Strings(dtts.redisClient.Execute("KEYS", pattern))
if err != nil {
return nil, err
}
totalKeys := len(keys)
if totalKeys <= offset {
return nil, nil
}
if (offset + limit) > totalKeys {
limit = totalKeys - offset
}
sort.Strings(keys)
keys = keys[offset : offset+limit]
return keys, nil
}
// readCarStateByKey retrieves data from Redis based on the specified key using the HGETALL command.
// It iterates over all keys and values returned by the command, sets them in a response map,
// and returns the populated map along with any encountered errors.
//
// Parameters:
// - key: A string representing the key to retrieve data from in Redis.
//
// Returns:
// - map[string]interface{}: A map containing keys and values retrieved from Redis.
// - error: An error, if any, encountered during the Redis HGETALL operation or mapping process.
func (dtts *DigitalTwinTimestampState) readCarStateByKey(key string) (map[string]interface{}, error) {
keyval := make(map[string]interface{})
batch := redis.NewRedisBatchCommands()
batch.Add("HGETALL", key)
payload, err := redigo.Values(dtts.redisClient.ExecuteBatch(batch))
if err != nil {
return keyval, err
}
stateValues, err := redigo.Values(payload[0], nil)
if err != nil {
return keyval, err
}
for i := 0; i < len(stateValues); i += 2 {
key, okKey := stateValues[i].([]byte)
value, okValue := stateValues[i+1].([]byte)
if !okKey || !okValue {
continue
}
err = dtts.parseCarState(string(key), value, keyval)
// log error, do not return error so we can read other properties for digital twin
if err != nil {
logger.Warn().Err(err).Send()
continue
}
}
return keyval, nil
}
// GetDigitalTwinSignals retrieves digital twin signals from Redis based on the specified offset and limit.
// It reads all signals from Redis and returns a list of maps, where each map represents a cars signal with its properties.
//
// Parameters:
// - offset: An integer indicating the starting index of the signals to retrieve.
// - limit: An integer specifying the maximum number of signals to retrieve.
//
// Returns:
// - []map[string]interface{}: A list of maps representing digital twin signals.
func (dtts *DigitalTwinTimestampState) GetDigitalTwinSignals(offset, limit int) (resp []map[string]interface{}) {
keys, err := dtts.getStateKeys(offset, limit)
if err != nil {
logger.Warn().Err(err).Send()
return
}
for _, key := range keys {
keyval, err := dtts.readCarStateByKey(key)
if err != nil {
logger.Warn().Err(err).Send()
continue
}
if len(keyval) > 0 {
keySlice := strings.Split(key, ":")
keyval["VIN"] = keySlice[1]
resp = append(resp, keyval)
}
}
return
}
// timestampKey generates a timestamp key based on the provided key by appending ":updated" to it.
// It formats the key in a way suitable for storing timestamps associated with the original key in data storage systems.
//
// Parameters:
// - key: A string representing the original key for which the timestamp key is generated.
//
// Returns:
// - string: A formatted string representing the timestamp key.
func (dtts *DigitalTwinTimestampState) timestampKey(key string) string {
return fmt.Sprintf("%s:%s", key, "updated")
}
// timestampVal parses a byte slice containing a JSON-encoded timestamp and returns a pointer to a time.Time
// representing the parsed timestamp. It uses the UnmarshalJSON method of the time.Time type for decoding.
//
// Parameters:
// - val: A byte slice containing the JSON-encoded timestamp to be parsed.
//
// Returns:
// - time.Time: A time representing the parsed timestamp.
// - error: An error, if any, encountered during the parsing process.
func (dtts *DigitalTwinTimestampState) timestampVal(val []byte) (time.Time, error) {
t := &time.Time{}
err := t.UnmarshalJSON(val)
return *t, err
}
// parseCarState checks if the provided key is needed and, if so, sets the key and value in the given map.
//
// Parameters:
// - key: A string representing the key to check and potentially set in the map.
// - value: A byte slice containing the value associated with the key.
// - keyval: A map[string]interface{} where the key and valueset if the key is needed.
//
// Returns:
// - error: An error, if any, encountered during the parsing and mapping process.
func (dtts *DigitalTwinTimestampState) parseCarState(key string, value []byte, keyval map[string]interface{}) error {
var err error
val := string(value)
switch key {
case state.BMS_PwrBattRmngCpSOC, state.BMS_RmChrgTi_FullChrg, state.BCM_PwrMod, state.PWC_ChrgSts, state.VCU_DCChrgRmngTi, state.BMS_RmChrgTi_TrgtSoC:
keyval[key], err = strconv.Atoi(val)
case state.ICC_TotMilg_ODO:
keyval[key], err = querystring.ConvertStringToInt(val)
case state.IBS_BatteryVoltage:
keyval[key], err = strconv.ParseFloat(val, 64)
// updated timestamps
case dtts.timestampKey(state.BMS_PwrBattRmngCpSOC), dtts.timestampKey(state.ICC_TotMilg_ODO), dtts.timestampKey(state.VCU_DCChrgRmngTi), dtts.timestampKey(state.BMS_RmChrgTi_TrgtSoC), dtts.timestampKey(state.IBS_BatteryVoltage),
dtts.timestampKey(state.BMS_RmChrgTi_FullChrg), dtts.timestampKey(state.BCM_PwrMod), dtts.timestampKey(state.PWC_ChrgSts):
keyval[key], err = dtts.timestampVal(value)
}
return errors.WithStack(err)
}

136
pkg/cachev2/drivers.go Normal file
View File

@@ -0,0 +1,136 @@
package cachev2
import (
"fiskerinc.com/modules/common"
"fiskerinc.com/modules/db/queries"
"fiskerinc.com/modules/logger"
redis "fiskerinc.com/modules/redisv2"
"github.com/pkg/errors"
)
func NewDriversCache(redisClient redis.ClientInterface, cars queries.CarsInterface) *DriversCache {
return &DriversCache{
redisClient: redisClient,
cars: cars,
}
}
type DriversCache struct {
redisClient redis.ClientInterface
cars queries.CarsInterface
}
func (dc *DriversCache) RedisClientPool() redis.ClientInterface {
return dc.redisClient
}
func (dc *DriversCache) Cars() queries.CarsInterface {
return dc.cars
}
func (dc *DriversCache) hasCachedNoDrivers(drivers []string) bool {
// Redis will return []string{""} for no drivers
return len(drivers) == 1 && len(drivers[0]) == 0
}
func (dc *DriversCache) cacheDrivers(key string, drivers []string) error {
// cache driver IDs
if len(drivers) > 0 {
return dc.redisClient.NewSet(key, drivers, redisObjectExpire)
}
// Redis will not take an empty array as an arg
return nil
}
// RetrieveDriverIDs retrieves IDs from redis or from DB and proceeds to cache both the drivers and IDs
// redis keys:
//
// car:<VIN>:drivers
func (dc *DriversCache) RetrieveDriverIDs(vin string) ([]string, error) {
var driverIDs []string
driverIDsKey := redis.CarToAllDriversKey(vin)
// retrieve IDs from redis
err := dc.redisClient.GetSet(driverIDsKey, &driverIDs)
if err != nil {
logger.Warn().Err(err).Send()
return []string{}, err
}
if dc.hasCachedNoDrivers(driverIDs) {
return []string{}, nil
}
if len(driverIDs) > 0 {
return driverIDs, nil
}
// if IDs not present in redis perform DB lookup
var drivers []common.CarToDriver
drivers, err = dc.cars.GetDrivers(vin)
if err != nil {
return nil, err
}
for _, driver := range drivers {
driverIDs = append(driverIDs, driver.DriverID)
}
err = dc.cacheDrivers(driverIDsKey, driverIDs)
if err != nil {
return driverIDs, err
}
return driverIDs, nil
}
// RetrieveDriverIDsAsSet retrieves IDs from redis or from DB and proceeds to cache both the drivers and IDs
// redis keys:
//
// car:<VIN>:drivers
func (dc *DriversCache) RetrieveDriverIDsAsSet(vin string) (map[string]struct{}, error) {
driverIDs, err := dc.RetrieveDriverIDs(vin)
if err != nil {
return nil, err
}
var dIDsSet = make(map[string]struct{})
for _, did := range driverIDs {
dIDsSet[did] = struct{}{}
}
return dIDsSet, nil
}
func (dc *DriversCache) IsDriverOfVIN(vin string, driverid string) (bool, error) {
ids, err := dc.RetrieveDriverIDs(vin)
if err != nil {
return false, err
}
for _, id := range ids {
if id == driverid {
return true, nil
}
}
return false, dc.NotDriverError(vin, driverid)
}
// Add driver to database and cache
func (dc *DriversCache) AddDriver(car *common.Car, driver *common.Driver, role string) (*common.CarToDriver, error) {
relation, err := dc.cars.AddDriver(car, driver, role)
if err != nil {
return nil, err
}
driverIDsKey := redis.CarToAllDriversKey(car.VIN)
dc.redisClient.AddToSet(driverIDsKey, driver.ID, redisObjectExpire)
return relation, nil
}
func (dc DriversCache) NotDriverError(vin string, driverid string) error {
return errors.Errorf("id %s is not a driver for vin %v", driverid, vin)
}

107
pkg/cachev2/drivers_test.go Normal file
View File

@@ -0,0 +1,107 @@
package cachev2_test
import (
"encoding/json"
"testing"
cache "fiskerinc.com/modules/cachev2"
"fiskerinc.com/modules/db/queries"
"fiskerinc.com/modules/db/queries/mocks"
"fiskerinc.com/modules/redis/tester"
redis "fiskerinc.com/modules/redisv2"
"fiskerinc.com/modules/testhelper"
)
var mockRedis redis.Client
var mockDB queries.CarsInterface
func setupRedisMock() {
redis.MockRedisConnection()
}
func setupDBMock() {
mockDB = &mocks.MockCars{}
}
type mockRedisCache struct {
redis.Connection
}
func (c *mockRedisCache) GetSet(id string, data interface{}) error {
drivers := []string{"valid-id-1", "valid-id-2", "valid-id-3"}
dataBytes, err := json.Marshal(drivers)
if err != nil {
return err
}
err = json.Unmarshal(dataBytes, data)
if err != nil {
return err
}
return nil
}
type mockRedisEmptyCache struct {
redis.Connection
}
func (c *mockRedisEmptyCache) GetSet(id string, data interface{}) error {
drivers := []string{}
dataBytes, err := json.Marshal(drivers)
if err != nil {
return err
}
err = json.Unmarshal(dataBytes, data)
if err != nil {
return err
}
return nil
}
func (c *mockRedisEmptyCache) SetObjects(id []string, data []interface{}, expire int) error {
return nil
}
func TestRetrieveAndCacheDriverIDs(t *testing.T) {
setupRedisMock()
setupDBMock()
mockRedis = &mockRedisCache{}
redisPool := tester.NewMockClientPool(mockRedis)
drivers := cache.NewDriversCache(redisPool, mockDB)
_, err := drivers.RetrieveDriverIDs("FISKER123")
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestRetrieveAndCacheDriverIDs", "no error", err)
}
mockRedis = &mockRedisEmptyCache{}
_, err = drivers.RetrieveDriverIDs("FISKER456")
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestRetrieveAndCacheDriverIDs", "no error", err)
}
}
func TestRetrieveAndCacheDriverIDsAsSet(t *testing.T) {
setupRedisMock()
setupDBMock()
mockRedis = &mockRedisCache{}
redisPool := tester.NewMockClientPool(mockRedis)
drivers := cache.NewDriversCache(redisPool, mockDB)
_, err := drivers.RetrieveDriverIDs("FISKER123")
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestRetrieveAndCacheDriverIDs", "no error", err)
}
mockRedis = &mockRedisEmptyCache{}
_, err = drivers.RetrieveDriverIDsAsSet("FISKER456")
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestRetrieveAndCacheDriverIDs", "no error", err)
}
}

11
pkg/cachev2/errors.go Normal file
View File

@@ -0,0 +1,11 @@
package cachev2
import "github.com/pkg/errors"
func ErrInvalidCarToDriverAssociation(vin string, driverID string) error {
return errors.Errorf("no relationship found between vin %s and driver %s", vin, driverID)
}
func ErrCarHasNoDrivers(vin string) error {
return errors.Errorf("car %s has no drivers", vin)
}

View File

@@ -0,0 +1,197 @@
package cachev2
import (
"encoding/json"
"fiskerinc.com/modules/common"
"fiskerinc.com/modules/logger"
"fiskerinc.com/modules/mongo"
"fiskerinc.com/modules/redis"
"fiskerinc.com/modules/utils/envtool"
"fiskerinc.com/modules/utils/elptr"
redigo "github.com/gomodule/redigo/redis"
"github.com/pkg/errors"
)
const (
ENABLE_DBG_MASK_EV_NAME = "ENABLE_DEBUGMASK"
ENABLE_DBG_MASK_VAL_FALSE = "0"
ENABLE_DBG_MASK_VAL_TRUE = "1"
ENABLE_DBG_MASK_VAL_DEFAULT = ENABLE_DBG_MASK_VAL_FALSE
)
// This flag is to decide whether retrieved value of DebugMask is to be passed to TrexCfg or not.
// When the flag is true, the retrieved value is passed; else no value is passed.
// The value of flag is fetched from the specific environmental variable. If that environmental
// variable is not present / not defined, we assume the flag itself to be FALSE. That is the
// default value (FALSE) of the environmental variable. When user/developer has set this evironmental
// variable correctly, the flag can become TRUE in which case the value is passed to TrexCfg.
var ENABLE_DEBUG_MASK = DbgMaskEnabled()
// method introduced so as unit testing is easier otherwise not necessary since environment variables
// can't be changed so easily subsequent to a process start (meaning revaluation at runtime of no much use).
func DbgMaskEnabled() bool {
return envtool.GetEnv(ENABLE_DBG_MASK_EV_NAME, ENABLE_DBG_MASK_VAL_DEFAULT) == ENABLE_DBG_MASK_VAL_TRUE
}
func RetrieveVehicleConfig(r redis.Client, m mongo.Client, id string) (*common.TRexConfigResponse, error) {
config := &common.TRexConfigResponse{}
reply, err := checkCacheForVehicleConfig(r, id)
if err != nil {
return nil, errors.WithStack(err)
}
if reply != nil {
err = json.Unmarshal(reply, config)
if err != nil {
return nil, errors.WithStack(err)
}
if config.CANBus.DTCEnabled == nil {
config.CANBus.DTCEnabled = elptr.ElPtr(false)
}
return config, nil
}
config.LogLevel = common.Critical
// config.Log = &common.LogConfig{
// Matches: []common.LogConfigChannel{
// {
// Channel: common.ChannelCMD,
// Level: common.Trace,
// },
// },
// }
config.CANBus.Enabled = true
config.CANBus.DataLogger = true
filters := make(FiltersMap)
f, err := checkFleetsDBForVehicleConfig(m, id)
if err != nil {
logger.Warn().Err(err).Send()
}
if f != nil {
config.CANBus = f.CANBus
config.LogLevel = f.LogLevel
filters.AppendFilters(f.CANBus.Filters)
}
v, err := checkVehiclesDBForVehicleConfig(m, id)
if err != nil {
logger.Warn().Err(err).Send()
}
if v != nil {
config.CANBus = v.CANBus
config.LogLevel = v.LogLevel
config.DLTEnabled = v.DLTEnabled
config.DLTLevel = v.DLTLevel
// we should evaluate at run-time, not just at start-up time
if ENABLE_DEBUG_MASK {
config.DebugMask = v.DebugMask
}
config.IDPSEnabled = v.IDPSEnabled
filters.AppendFilters(v.CANBus.Filters)
}
config.CANBus.Filters = filters.ToSlice()
if config.CANBus.DTCEnabled == nil {
config.CANBus.DTCEnabled = elptr.ElPtr(false)
}
err = setCacheForVehicleConfig(r, id, config)
return config, err
}
func checkCacheForVehicleConfig(r redis.Client, id string) ([]byte, error) {
key := redis.CarConfigKey(id)
reply, err := redigo.Bytes(r.Execute("GET", key))
if err != nil {
if errors.Is(err, redigo.ErrNil) {
return nil, nil
}
return nil, err
}
return reply, nil
}
func checkVehiclesDBForVehicleConfig(m mongo.Client, id string) (*mongo.Vehicle, error) {
return m.GetVehicles().FindVehicle(&mongo.Vehicle{VIN: id})
}
func checkFleetsDBForVehicleConfig(m mongo.Client, id string) (*mongo.Fleet, error) {
return m.GetFleets().GetCANBusForVehicle(id)
}
func setCacheForVehicleConfig(r redis.Client, id string, config *common.TRexConfigResponse) error {
key := redis.CarConfigKey(id)
data, err := json.Marshal(config)
if err != nil {
return errors.WithStack(err)
}
batch := redis.NewRedisBatchCommands()
batch.Add("SET", key, data)
batch.Add("EXPIRE", key, redisObjectExpire.Seconds())
_, err = r.ExecuteBatch(batch)
if err != nil {
return errors.WithStack(err)
}
return nil
}
func RemoveCacheConfigForVehicles(r redis.Client, vins []string) error {
batch := redis.NewRedisBatchCommands()
for _, vin := range vins {
batch.Add("DEL", redis.CarConfigKey(vin))
}
_, err := r.ExecuteBatch(batch)
if err != nil {
return errors.WithStack(err)
}
return nil
}
type IntervalEdgeMask struct {
Interval *int
EdgeMask *common.BinaryHex
}
type FiltersMap map[string]IntervalEdgeMask
func (f FiltersMap) AppendFilters(filters []common.CANFilter) {
for _, filter := range filters {
if filter.EdgeMask != nil && filter.EdgeMask.String() != "" {
f[filter.CANID] = IntervalEdgeMask{
EdgeMask: filter.EdgeMask,
}
} else if filter.Interval != nil {
f[filter.CANID] = IntervalEdgeMask{
Interval: filter.Interval,
}
}
}
}
func (f FiltersMap) ToSlice() []common.CANFilter {
filters := make([]common.CANFilter, 0, len(f))
for k, v := range f {
filters = append(filters, common.CANFilter{
CANID: k,
Interval: v.Interval,
EdgeMask: v.EdgeMask,
})
}
return filters
}

View File

@@ -0,0 +1,203 @@
package cachev2_test
import (
"encoding/json"
"sort"
"testing"
cache "fiskerinc.com/modules/cachev2"
"fiskerinc.com/modules/common"
"fiskerinc.com/modules/mongo"
"fiskerinc.com/modules/redis"
"fiskerinc.com/modules/testhelper"
"fiskerinc.com/modules/utils/elptr"
redigo "github.com/gomodule/redigo/redis"
"github.com/stretchr/testify/assert"
)
func TestRetrieveVehicleConfig(t *testing.T) {
setupRedisMock()
id := "TESTVIN1234567"
mockRedis = &mockRedisVehicleConfig{}
config, err := cache.RetrieveVehicleConfig(mockRedis, mongo.NewMockClient(), id)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestRetrieveVehicleConfig", nil, err)
}
data, err := json.Marshal(&config)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestRetrieveVehicleConfig", nil, err)
}
assert.Equal(t, `{"canbus":{"enabled":false,"data_logger_enabled":false,"dtc_enabled":false},"log_level":"trace"}`, string(data))
mockRedis = &mockRedisNoVehicleConfig{}
config, err = cache.RetrieveVehicleConfig(mockRedis, mongo.NewMockClient(), id)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestRetrieveVehicleConfig", nil, err)
}
data, err = json.Marshal(&config)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestRetrieveVehicleConfig", nil, err)
}
assert.Equal(t, `{"canbus":{"enabled":false,"data_logger_enabled":false,"dtc_enabled":false},"log_level":"trace","log":{"matches":[{"channel":"cmd","level":"trace"}]}}`, string(data))
}
func TestRetrieveVehicleConfigDbgMask(t *testing.T) {
setupRedisMock()
id := "TESTVIN1234567"
mockVehicle := mongo.Vehicle{VIN: id}
mockRedis = &mockRedisNoVehicleConfig{}
// validate that by default, retrieved debug value IS NOT passed to trxCfg
trxCfg, err := cache.RetrieveVehicleConfig(mockRedis, mongo.NewMockClient(), id)
existingValue := trxCfg.DebugMask
assert.Nil(t, err)
assert.NotNil(t, trxCfg)
// assert that trxCfg value is unchanged
assert.Equal(t, trxCfg.DebugMask, existingValue)
// let us try to enable
// the mock for redis is with no data so that code will fall through to the DB part
// we ensure that what we get from DB has speific debug mask which should be
// passed to Trex when the flag is true
t.Setenv(cache.ENABLE_DBG_MASK_EV_NAME, cache.ENABLE_DBG_MASK_VAL_TRUE)
cache.ENABLE_DEBUG_MASK = cache.DbgMaskEnabled()
mmc := mongo.NewMockMongoClient()
mockVehicle.DebugMask = "test"
mmc.GetVehicles().AddVehicle(&mockVehicle)
trxCfg, _ = cache.RetrieveVehicleConfig(mockRedis, mmc, id)
// now validate that Trex config got the value as set in the mocked vehicle
// (presumed as retrieved)
assert.Equal(t, trxCfg.DebugMask, mockVehicle.DebugMask)
// now set back the env variable so new values don't flow to trex
t.Setenv(cache.ENABLE_DBG_MASK_EV_NAME, cache.ENABLE_DBG_MASK_VAL_FALSE)
cache.ENABLE_DEBUG_MASK = cache.DbgMaskEnabled()
oldMask := mockVehicle.DebugMask
mockVehicle.DebugMask = "new-value"
// skipping adding to the cache/DB as we still had the valid reference
trxCfg, _ = cache.RetrieveVehicleConfig(mockRedis, mmc, id)
// assert that trex does not have new value
assert.NotEqual(t, trxCfg.DebugMask, oldMask)
}
func TestFiltersMap(t *testing.T) {
filters := make(cache.FiltersMap)
if len(filters) != 0 {
t.Errorf(testhelper.TestErrorTemplate, "TestFiltersMap", 0, len(filters))
return
}
emptyHex := common.NewBinaryHex([]byte{})
bhex := common.BinaryHex("123")
filters.AppendFilters(
[]common.CANFilter{
{CANID: "123", Interval: elptr.ElPtr(123)},
{CANID: "456", Interval: elptr.ElPtr(456)},
{CANID: "789", EdgeMask: &emptyHex},
{CANID: "901", EdgeMask: &bhex},
{CANID: "222", Interval: elptr.ElPtr(123), EdgeMask: &bhex},
{CANID: "333", Interval: elptr.ElPtr(0)},
},
)
if len(filters) != 5 {
t.Errorf(testhelper.TestErrorTemplate, "TestFiltersMap", 5, len(filters))
return
}
interval, ok := filters["123"]
if !ok || *interval.Interval != 123 && interval.EdgeMask != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestFiltersMap", 123, "error")
return
}
interval, ok = filters["456"]
if !ok || *interval.Interval != 456 && interval.EdgeMask != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestFiltersMap", 456, "error")
return
}
interval, ok = filters["789"]
if ok || interval.EdgeMask != nil || interval.Interval != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestFiltersMap", emptyHex, "error")
return
}
interval, ok = filters["901"]
if !ok || interval.EdgeMask.String() != bhex.String() && interval.Interval != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestFiltersMap", bhex, "error")
return
}
interval, ok = filters["222"]
if !ok || interval.EdgeMask.String() != bhex.String() && interval.Interval != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestFiltersMap", bhex, "error")
return
}
interval, ok = filters["333"]
if !ok || interval.EdgeMask != nil && *interval.Interval != 0 {
t.Errorf(testhelper.TestErrorTemplate, "TestFiltersMap", nil, "error")
return
}
slice := filters.ToSlice()
if len(slice) != 5 {
t.Errorf(testhelper.TestErrorTemplate, "TestFiltersMap", 5, len(slice))
return
}
sort.Slice(slice, func(i, j int) bool {
return slice[i].CANID < slice[j].CANID
})
if slice[0].CANID != "123" {
t.Errorf(testhelper.TestErrorTemplate, "TestFiltersMap", "123", slice[0].CANID)
return
}
if slice[1].CANID != "222" {
t.Errorf(testhelper.TestErrorTemplate, "TestFiltersMap", "222", slice[1].CANID)
return
}
if slice[2].CANID != "333" {
t.Errorf(testhelper.TestErrorTemplate, "TestFiltersMap", "333", slice[2].CANID)
return
}
if slice[3].CANID != "456" {
t.Errorf(testhelper.TestErrorTemplate, "TestFiltersMap", "456", slice[0].CANID)
return
}
if slice[4].CANID != "901" {
t.Errorf(testhelper.TestErrorTemplate, "TestFiltersMap", "901", slice[0].CANID)
return
}
}
type mockRedisVehicleConfig struct {
redis.Connection
}
func (c *mockRedisVehicleConfig) Execute(command ...interface{}) (interface{}, error) {
config := common.TRexConfigResponse{}
data, _ := json.Marshal(config)
return data, nil
}
type mockRedisNoVehicleConfig struct {
redis.Connection
}
func (c *mockRedisNoVehicleConfig) Execute(command ...interface{}) (interface{}, error) {
return nil, redigo.ErrNil
}
func (c *mockRedisNoVehicleConfig) ExecuteBatch(batch *redis.RedisBatchCommands) (interface{}, error) {
return nil, nil
}

View File

@@ -0,0 +1,591 @@
package cachev2
import (
"context"
"strconv"
"strings"
"time"
"fiskerinc.com/modules/common"
dt "fiskerinc.com/modules/dbc/state"
"fiskerinc.com/modules/logger"
redis "fiskerinc.com/modules/redisv2"
"fiskerinc.com/modules/utils/querystring"
redispkg "github.com/redis/go-redis/v9"
"github.com/pkg/errors"
)
const UPDATED_TIME_FORMAT = "2006-01-02T15:04:05Z"
type stateParser func(state *common.CarState, key string, value []byte) (found bool, err error)
func NewVehicleState(client redis.ClientInterface) *VehicleState {
return &VehicleState{redisClient: client}
}
type VehicleState struct {
redisClient redis.ClientInterface
}
func (v *VehicleState) Get(vin string) (common.CarState, error) {
var state common.CarState
values, err := v.queryVehicleState(vin)
if err != nil {
return state, err
}
state, err = v.ParsePayloadForVehicleState(values)
if err != nil {
return state, err
}
return state, nil
}
type QueryVehicleStateResponse struct {
CarSessionExists bool
HMISessionExists bool
CarState map[string]string
}
type QueryVehicleStatePreResponse struct {
CarSessionExists *redispkg.BoolCmd
HMISessionExists *redispkg.BoolCmd
CarState *redispkg.MapStringStringCmd
}
func (qvspr *QueryVehicleStatePreResponse) Resolve() (qvsr *QueryVehicleStateResponse, errR error) {
var err error
qvsr = &QueryVehicleStateResponse{}
qvsr.CarSessionExists, err = qvspr.CarSessionExists.Result()
if err != nil {
errR = errors.Wrap(errR, err.Error())
}
qvsr.HMISessionExists, err = qvspr.HMISessionExists.Result()
if err != nil {
errR = errors.Wrap(errR, err.Error())
}
qvsr.CarState, err = qvspr.CarState.Result()
if err != nil {
errR = errors.Wrap(errR, err.Error())
}
return
}
func (v *VehicleState) queryVehicleState(vin string) (QueryVehicleStateResponse, error) {
payload := QueryVehicleStateResponse{}
pipe := v.redisClient.GetClient().TxPipeline()
carSessionKey := pipe.SIsMember(context.Background(), redis.CarSessionsKey(), vin)
hmiSessionKey := pipe.SIsMember(context.Background(), redis.HMISessionsKey(), vin)
carStateHash := pipe.HGetAll(context.Background(), redis.CarStateHashKey(vin))
_, err := pipe.Exec(context.Background())
if err != nil {
return payload, errors.WithStack(err)
}
payload.CarSessionExists = carSessionKey.Val()
payload.HMISessionExists = hmiSessionKey.Val()
payload.CarState = carStateHash.Val()
return payload, nil
}
func (v *VehicleState) ParsePayloadForVehicleState(payload QueryVehicleStateResponse) (common.CarState, error) {
var state common.CarState
state.Online = payload.CarSessionExists
state.OnlineHMI = payload.HMISessionExists
var err error
err = v.parseStateValues(&state, payload.CarState, v.parseCarState)
return state, err
}
func ParsePayloadForVehicleState(payload *QueryVehicleStateResponse) (state *common.CarState, err error) {
state = &common.CarState{}
state.Online = payload.CarSessionExists
state.OnlineHMI = payload.HMISessionExists
err = parseStateValues(state, payload.CarState, ParseCarState)
return
}
func ParsePayloadForALVehicleState(payload *QueryVehicleStateResponse) (alState *common.CarStateAL, err error) {
alState.CarState = &common.CarState{}
alState.Online = payload.CarSessionExists
alState.OnlineHMI = payload.HMISessionExists
err = parseStateValues(alState.CarState, payload.CarState, ParseCarState)
return
}
func (v *VehicleState) parseStateValues(state *common.CarState, stateValues map[string]string, parser stateParser) error {
for key, value := range stateValues {
_, err := parser(state, string(key), []byte(value))
// log error, do not return error so we can read other properties for digital twin
if err != nil {
// strconv.Atoi: parsing "127.5": invalid syntax, track down. Add better info
logger.Err(err).Send()
}
}
return nil
}
func parseStateValues(state *common.CarState, stateValues map[string]string, parser stateParser) error {
for key, value := range stateValues {
_, err := parser(state, string(key), []byte(value))
// log error, do not return error so we can read other properties for digital twin
if err != nil {
logger.Err(err).Send()
}
}
return nil
}
func (v *VehicleState) parseCarState(state *common.CarState, key string, value []byte) (bool, error) {
var err error
val := string(value)
switch key {
case dt.VCU_VehChrgDchgMod:
state.GetVCU0x260().ChargeType = val
case dt.BMS_Bat_SoC_usable:
state.GetStateOfCharge().Usable, err = strconv.Atoi(val)
case dt.BMS_Bat_SOH:
state.GetStateOfCharge().Health, err = strconv.Atoi(val)
case dt.BCM_AP_FL_LeFrntWinPosnInfo:
state.GetWindows().LeftFront, err = strconv.Atoi(val)
case dt.BCM_AP_FL_RiFrntWinPosnInfo:
state.GetWindows().RightFront, err = strconv.Atoi(val)
case dt.BCM_AP_FL_LeReWinPosnInfo:
state.GetWindows().LeftRear, err = strconv.Atoi(val)
case dt.BCM_AP_FL_RiReWinPosnInfo:
state.GetWindows().RightRear, err = strconv.Atoi(val)
case dt.BMS_PwrBattRmngCpSOC:
state.GetBattery().Percent, err = strconv.Atoi(val)
case dt.BCM_ReDefrstHeatgCmd:
state.GetRearDefrost().On, err = strconv.ParseBool(val)
case dt.BCM_PasFrntDoorSts:
state.GetDoors().RightFront, err = strconv.ParseBool(val)
case dt.BCM_DrFrntDoorSts:
state.GetDoors().LeftFront, err = strconv.ParseBool(val)
case dt.BCM_FrntDrDoorLockSts:
state.GetLocks().Driver, err = notValue(strconv.ParseBool(val))
case dt.BCM_CenLockSwtSts:
state.GetLocks().All = (val == "2")
case dt.BCM_RiReDoorSts:
state.GetDoors().RightRear, err = strconv.ParseBool(val)
case dt.BCM_LeReDoorSts:
state.GetDoors().LeftRear, err = strconv.ParseBool(val)
case dt.BCM_FrntHoodLidSts:
state.GetDoors().Hood, err = strconv.ParseBool(val)
case dt.PLGM_TrSts:
state.GetDoors().Trunk, err = strconv.ParseBool(val)
case dt.BCM_SunroofPosnInfo:
state.GetSunroof().Sunroof, err = strconv.Atoi(val)
case dt.BCM_AP_TL_LeReWinPosnInfo:
state.GetMiscWindows().LeftRearQuarter, err = strconv.Atoi(val)
case dt.BCM_AP_TL_RiReWinPosnInfo:
state.GetMiscWindows().RightRearQuarter, err = strconv.Atoi(val)
case dt.BCM_AP_RW_WinPosnInfo:
state.GetMiscWindows().RearWindshield, err = strconv.Atoi(val)
case dt.BMS_BattAvrgT:
state.GetCellTemperature().AvgBatteryTemp, err = strconv.Atoi(val)
case dt.ECC_OutdT:
state.GetAmbientTemperature().Temperature, err = strconv.Atoi(val)
case dt.BCM_HeatedSteerWhlSt:
state.GetSteeringWheelHeat().On, err = strconv.ParseBool(val)
case dt.ESP_VehSpd:
state.GetVehicleSpeed().Speed, err = strconv.ParseFloat(val, 64)
case dt.VCU_DrvgMilg:
state.GetMaxRange().MaxMiles, err = strconv.Atoi(val)
case dt.PSM_PassSeatHeatgSts:
state.GetPassengerSeatHeat().Level, err = strconv.Atoi(val)
case dt.DSMC_DrvrSeatHeatgSts:
state.GetDriverSeatHeat().Level, err = strconv.Atoi(val)
case dt.ICC_TotMilg_ODO:
state.GetBattery().TotalMileageOdometer, err = querystring.ConvertStringToInt(val)
case dt.VCU_DCChrgRmngTi, dt.BMS_RmChrgTi_TrgtSoC:
state.GetChargingMetrics().RemainingChargingTime, err = strconv.Atoi(val)
case dt.IBS_BatteryVoltage:
state.GetBattery().BatteryVoltage, err = strconv.ParseFloat(val, 64)
state.GetBattery12V().IBS_BatteryVoltage = ref(state.GetBattery().BatteryVoltage)
case dt.VCU_GearSig:
var gear int
gear, err = strconv.Atoi(val)
state.GetGear().InPark = (gear <= 2)
case dt.BMS_RmChrgTi_FullChrg:
state.GetChargingMetrics().RemainingChargingTimeFull, err = strconv.Atoi(val)
case dt.ECC_InsdT:
state.GetCabinClimate().InternalTemperature, err = strconv.Atoi(val)
case dt.ECC_RemTSetSts:
state.GetCabinClimate().CabinTemperature, err = strconv.Atoi(val)
case dt.TBOX_GPSHei:
state.GetLocation().Altitude, err = strconv.ParseFloat(val, 64)
case dt.TBOX_GPSLongi:
state.GetLocation().Longitude, err = strconv.ParseFloat(val, 64)
case dt.TBOX_GPSLati:
state.GetLocation().Latitude, err = strconv.ParseFloat(val, 64)
case dt.DBC_VERSION:
state.DBCVersion = val
case dt.TREX_VERSION:
state.TRexVersion = val
case dt.TREX_IP:
state.IP = val
case dt.UPDATED_AT:
var t time.Time
t, err = time.Parse(UPDATED_TIME_FORMAT, strings.Trim(val, "\""))
if !t.IsZero() {
state.UpdatedAt = ref(t)
}
case dt.VCU_VehSt:
state.GetSafeState().VehicleSafeState = val == dt.VCU_VehSt_Safestate
case dt.VCU_VcuState:
state.GetSafeState().VCUSafeState = val == dt.VCU_VcuState_Safestate
case dt.MCU_F_ActSafeSt:
state.GetSafeState().MCUFrontSafeState = val == dt.MCU_F_ActSafeSt_AS0 || val == dt.MCU_F_ActSafeSt_ASC || val == dt.MCU_F_ActSafeSt_ASC_Emergency
case dt.MCU_R_ActSafeSt:
state.GetSafeState().MCURearSafeState = val == dt.MCU_R_ActSafeSt_AS0 || val == dt.MCU_R_ActSafeSt_ASC || val == dt.MCU_R_ActSafeSt_ASC_Emergency
case dt.MCU_R_Decoup_State:
state.GetSafeState().MCURearDecoupState = val == dt.MCU_R_Decoup_State_Connected
case dt.MCU_F_CrtMod:
state.GetSafeState().MCUFrontInverterError = val == dt.MCU_F_CrtMod_Internal_inverter_error || val == dt.MCU_F_CrtMod_Invalid
case dt.MCU_R_CrtMod:
state.GetSafeState().MCURearInverterError = val == dt.MCU_R_CrtMod_Internal_inverter_error || val == dt.MCU_R_CrtMod_Invalid
case dt.ACU_Drvr_Occpt_St:
var vi int
vi, err = strconv.Atoi(val)
state.DriverOccupySeatState = ref(vi)
case dt.BCM_PwrMod:
var vi int
vi, err = strconv.Atoi(val)
state.PowerMode = ref(vi)
case dt.PWC_ChrgSts:
var vi int
vi, err = strconv.Atoi(val)
state.ChargingStatus = ref(vi)
case dt.VCU_RdyLamp:
state.GetVehicleReadyState().IsVehicleReady, err = strconv.ParseBool(val)
// New untested signals
// case dt.IBS_SOCUpperTolerance:
// var vi float64
// vi, err = strconv.ParseFloat(val, 64)
// state.GetExpandedSignals().IBS_SOCUpperTolerance = ref(vi)
// case dt.IBS_SOCLowerTolerance:
// var vi float64
// vi, err = strconv.ParseFloat(val, 64)
// state.GetExpandedSignals().IBS_SOCLowerTolerance = ref(vi)
case dt.IBS_StateOfCharge:
var vi float64
vi, err = strconv.ParseFloat(val, 64)
state.GetBattery12V().IBS_StateOfCharge = ref(vi)
case dt.IBS_StateOfHealth:
var vi int
vi, err = strconv.Atoi(val)
state.GetBattery12V().IBS_StateOfHealth = ref(vi)
case dt.IBS_NominalCapacity:
var vi int
vi, err = strconv.Atoi(val)
state.GetExpandedSignals().IBS_NominalCapacity = ref(vi)
case dt.IBS_AvailableCapacity:
var vi int
vi, err = strconv.Atoi(val)
state.GetExpandedSignals().IBS_AvailableCapacity = ref(vi)
case dt.BCM_TotMilg_ODO:
var vi float64
vi, err = strconv.ParseFloat(val, 64)
state.GetExpandedSignals().BCM_TotMilg_ODO = ref(vi)
case dt.BMS_SwVersS:
var vi int
vi, err = strconv.Atoi(val)
state.GetExpandedSignals().BMS_SwVersS = ref(vi)
case dt.BMS_SwVersM:
var vi int
vi, err = strconv.Atoi(val)
state.GetExpandedSignals().BMS_SwVersM = ref(vi)
case dt.BMS_SwVers:
var vi int
vi, err = strconv.Atoi(val)
state.GetExpandedSignals().BMS_SwVers = ref(vi)
case dt.BMS_AccueDchaTotAh:
var vi int
vi, err = strconv.Atoi(val)
state.GetExpandedSignals().BMS_AccueDchaTotAh = ref(vi)
case dt.BMS_AccueChrgTotAh:
var vi int
vi, err = strconv.Atoi(val)
state.GetExpandedSignals().BMS_AccueChrgTotAh = ref(vi)
case dt.TBOX_Heading:
state.GetLocation().Heading, err = strconv.ParseFloat(val, 64)
case dt.PKC_KeyStsMod:
state.GetGear().Immobilizer = val
}
return true, errors.WithStack(err)
}
func ParseCarState(state *common.CarState, key string, value []byte) (found bool, err error) {
val := string(value)
switch key {
case dt.VCU_VehChrgDchgMod:
found = true
state.GetVCU0x260().ChargeType = val
case dt.BMS_Bat_SoC_usable:
found = true
state.GetStateOfCharge().Usable, err = strconv.Atoi(val)
case dt.BMS_Bat_SOH:
found = true
state.GetStateOfCharge().Health, err = strconv.Atoi(val)
case dt.BCM_AP_FL_LeFrntWinPosnInfo:
found = true
state.GetWindows().LeftFront, err = strconv.Atoi(val)
case dt.BCM_AP_FL_RiFrntWinPosnInfo:
found = true
state.GetWindows().RightFront, err = strconv.Atoi(val)
case dt.BCM_AP_FL_LeReWinPosnInfo:
found = true
state.GetWindows().LeftRear, err = strconv.Atoi(val)
case dt.BCM_AP_FL_RiReWinPosnInfo:
found = true
state.GetWindows().RightRear, err = strconv.Atoi(val)
case dt.BMS_PwrBattRmngCpSOC:
found = true
state.GetBattery().Percent, err = strconv.Atoi(val)
case dt.BCM_ReDefrstHeatgCmd:
found = true
state.GetRearDefrost().On, err = strconv.ParseBool(val)
case dt.BCM_PasFrntDoorSts:
found = true
state.GetDoors().RightFront, err = strconv.ParseBool(val)
case dt.BCM_DrFrntDoorSts:
found = true
state.GetDoors().LeftFront, err = strconv.ParseBool(val)
case dt.BCM_FrntDrDoorLockSts:
found = true
state.GetLocks().Driver, err = notValue(strconv.ParseBool(val))
case dt.BCM_CenLockSwtSts:
found = true
state.GetLocks().All = (val == "2")
case dt.BCM_RiReDoorSts:
found = true
state.GetDoors().RightRear, err = strconv.ParseBool(val)
case dt.BCM_LeReDoorSts:
found = true
state.GetDoors().LeftRear, err = strconv.ParseBool(val)
case dt.BCM_FrntHoodLidSts:
found = true
state.GetDoors().Hood, err = strconv.ParseBool(val)
case dt.PLGM_TrSts:
found = true
state.GetDoors().Trunk, err = strconv.ParseBool(val)
case dt.BCM_SunroofPosnInfo:
found = true
state.GetSunroof().Sunroof, err = strconv.Atoi(val)
case dt.BCM_AP_TL_LeReWinPosnInfo:
found = true
state.GetMiscWindows().LeftRearQuarter, err = strconv.Atoi(val)
case dt.BCM_AP_TL_RiReWinPosnInfo:
found = true
state.GetMiscWindows().RightRearQuarter, err = strconv.Atoi(val)
case dt.BCM_AP_RW_WinPosnInfo:
found = true
state.GetMiscWindows().RearWindshield, err = strconv.Atoi(val)
case dt.BMS_BattAvrgT:
found = true
state.GetCellTemperature().AvgBatteryTemp, err = strconv.Atoi(val)
case dt.ECC_OutdT:
found = true
state.GetAmbientTemperature().Temperature, err = strconv.Atoi(val)
case dt.BCM_HeatedSteerWhlSt:
found = true
state.GetSteeringWheelHeat().On, err = strconv.ParseBool(val)
case dt.ESP_VehSpd:
found = true
state.GetVehicleSpeed().Speed, err = strconv.ParseFloat(val, 64)
case dt.VCU_DrvgMilg:
found = true
state.GetMaxRange().MaxMiles, err = strconv.Atoi(val)
case dt.PSM_PassSeatHeatgSts:
found = true
state.GetPassengerSeatHeat().Level, err = strconv.Atoi(val)
case dt.DSMC_DrvrSeatHeatgSts:
found = true
state.GetDriverSeatHeat().Level, err = strconv.Atoi(val)
case dt.ICC_TotMilg_ODO:
found = true
state.GetBattery().TotalMileageOdometer, err = querystring.ConvertStringToInt(val)
case dt.VCU_DCChrgRmngTi, dt.BMS_RmChrgTi_TrgtSoC:
found = true
state.GetChargingMetrics().RemainingChargingTime, err = strconv.Atoi(val)
case dt.IBS_BatteryVoltage:
found = true
state.GetBattery().BatteryVoltage, err = strconv.ParseFloat(val, 64)
state.GetBattery12V().IBS_BatteryVoltage = ref(state.GetBattery().BatteryVoltage)
case dt.VCU_GearSig:
found = true
var gear int
gear, err = strconv.Atoi(val)
state.GetGear().InPark = (gear <= 2)
case dt.BMS_RmChrgTi_FullChrg:
found = true
state.GetChargingMetrics().RemainingChargingTimeFull, err = strconv.Atoi(val)
case dt.ECC_InsdT:
found = true
state.GetCabinClimate().InternalTemperature, err = strconv.Atoi(val)
case dt.ECC_RemTSetSts:
found = true
state.GetCabinClimate().CabinTemperature, err = strconv.Atoi(val)
case dt.TBOX_GPSHei:
found = true
state.GetLocation().Altitude, err = strconv.ParseFloat(val, 64)
case dt.TBOX_GPSLongi:
found = true
state.GetLocation().Longitude, err = strconv.ParseFloat(val, 64)
case dt.TBOX_GPSLati:
found = true
state.GetLocation().Latitude, err = strconv.ParseFloat(val, 64)
case dt.DBC_VERSION:
found = true
state.DBCVersion = val
case dt.TREX_VERSION:
found = true
state.TRexVersion = val
case dt.TREX_IP:
found = true
state.IP = val
case dt.UPDATED_AT:
found = true
var t time.Time
t, err = time.Parse(UPDATED_TIME_FORMAT, strings.Trim(val, "\""))
if !t.IsZero() {
state.UpdatedAt = ref(t)
}
case dt.VCU_VehSt:
found = true
state.GetSafeState().VehicleSafeState = val == dt.VCU_VehSt_Safestate
case dt.VCU_VcuState:
found = true
state.GetSafeState().VCUSafeState = val == dt.VCU_VcuState_Safestate
case dt.MCU_F_ActSafeSt:
found = true
state.GetSafeState().MCUFrontSafeState = val == dt.MCU_F_ActSafeSt_AS0 || val == dt.MCU_F_ActSafeSt_ASC || val == dt.MCU_F_ActSafeSt_ASC_Emergency
case dt.MCU_R_ActSafeSt:
found = true
state.GetSafeState().MCURearSafeState = val == dt.MCU_R_ActSafeSt_AS0 || val == dt.MCU_R_ActSafeSt_ASC || val == dt.MCU_R_ActSafeSt_ASC_Emergency
case dt.MCU_R_Decoup_State:
found = true
state.GetSafeState().MCURearDecoupState = val == dt.MCU_R_Decoup_State_Connected
case dt.MCU_F_CrtMod:
found = true
state.GetSafeState().MCUFrontInverterError = val == dt.MCU_F_CrtMod_Internal_inverter_error || val == dt.MCU_F_CrtMod_Invalid
case dt.MCU_R_CrtMod:
found = true
state.GetSafeState().MCURearInverterError = val == dt.MCU_R_CrtMod_Internal_inverter_error || val == dt.MCU_R_CrtMod_Invalid
case dt.ACU_Drvr_Occpt_St:
found = true
var vi int
vi, err = strconv.Atoi(val)
state.DriverOccupySeatState = ref(vi)
case dt.BCM_PwrMod:
found = true
var vi int
vi, err = strconv.Atoi(val)
state.PowerMode = ref(vi)
case dt.PWC_ChrgSts:
found = true
var vi int
vi, err = strconv.Atoi(val)
state.ChargingStatus = ref(vi)
case dt.VCU_RdyLamp:
found = true
state.GetVehicleReadyState().IsVehicleReady, err = strconv.ParseBool(val)
// New untested signals
// case dt.IBS_SOCUpperTolerance:
found = true
// var vi float64
// vi, err = strconv.ParseFloat(val, 64)
// state.GetExpandedSignals().IBS_SOCUpperTolerance = ref(vi)
// case dt.IBS_SOCLowerTolerance:
found = true
// var vi float64
// vi, err = strconv.ParseFloat(val, 64)
// state.GetExpandedSignals().IBS_SOCLowerTolerance = ref(vi)
case dt.IBS_StateOfCharge:
found = true
var vi float64
vi, err = strconv.ParseFloat(val, 64)
state.GetBattery12V().IBS_StateOfCharge = ref(vi)
case dt.IBS_StateOfHealth:
found = true
var vi int
vi, err = strconv.Atoi(val)
state.GetBattery12V().IBS_StateOfHealth = ref(vi)
case dt.IBS_NominalCapacity:
found = true
var vi int
vi, err = strconv.Atoi(val)
state.GetExpandedSignals().IBS_NominalCapacity = ref(vi)
case dt.IBS_AvailableCapacity:
found = true
var vi int
vi, err = strconv.Atoi(val)
state.GetExpandedSignals().IBS_AvailableCapacity = ref(vi)
case dt.BCM_TotMilg_ODO:
found = true
var vi float64
vi, err = strconv.ParseFloat(val, 64)
state.GetExpandedSignals().BCM_TotMilg_ODO = ref(vi)
case dt.BMS_SwVersS:
found = true
var vi int
vi, err = strconv.Atoi(val)
state.GetExpandedSignals().BMS_SwVersS = ref(vi)
case dt.BMS_SwVersM:
found = true
var vi int
vi, err = strconv.Atoi(val)
state.GetExpandedSignals().BMS_SwVersM = ref(vi)
case dt.BMS_SwVers:
found = true
var vi int
vi, err = strconv.Atoi(val)
state.GetExpandedSignals().BMS_SwVers = ref(vi)
case dt.BMS_AccueDchaTotAh:
found = true
var vi int
vi, err = strconv.Atoi(val)
state.GetExpandedSignals().BMS_AccueDchaTotAh = ref(vi)
case dt.BMS_AccueChrgTotAh:
found = true
var vi int
vi, err = strconv.Atoi(val)
state.GetExpandedSignals().BMS_AccueChrgTotAh = ref(vi)
case dt.TBOX_Heading:
found = true
state.GetLocation().Heading, err = strconv.ParseFloat(val, 64)
case dt.PKC_KeyStsMod:
found = true
state.GetGear().Immobilizer = val
}
return found, errors.WithStack(err)
}
func ref[T any](v T) *T {
return &v
}
func IsCarOnline(client redis.ClientInterface, vin string) (bool, error) {
res := client.GetClient().SIsMember(context.Background(), redis.CarSessionsKey(), vin)
return res.Result()
}
func notValue(value bool, err error) (bool, error) {
return !value, err
}

View File

@@ -0,0 +1,101 @@
package cachev2
import (
"context"
"fiskerinc.com/modules/common"
"fiskerinc.com/modules/logger"
redis "fiskerinc.com/modules/redisv2"
"github.com/pkg/errors"
"fiskerinc.com/modules/db/queries"
)
func GetVINListDigitalTwin(vins []string, redisClient *redis.Connection) (digitalTwins map[string]common.CarState, errorList []error) {
digitalTwins = make(map[string]common.CarState)
pipe := redisClient.TxPipeline()
responses := make([]QueryVehicleStatePreResponse, 0, len(vins))
for _, vin := range vins {
rr := QueryVehicleStatePreResponse{}
rr.CarSessionExists = pipe.SIsMember(context.Background(), redis.CarSessionsKey(), vin)
rr.HMISessionExists = pipe.SIsMember(context.Background(), redis.HMISessionsKey(), vin)
rr.CarState = pipe.HGetAll(context.Background(), redis.CarStateHashKey(vin))
responses = append(responses, rr)
}
_, err := pipe.Exec(context.Background())
if err != nil {
errorList = append(errorList, err)
return
}
for index, resStruct := range responses {
bb, err := resStruct.Resolve()
if err != nil {
err = errors.Wrapf(err, "VIN: %s", vins[index])
errorList = append(errorList, err)
continue
}
state, err := ParsePayloadForVehicleState(bb)
if err != nil {
err = errors.Wrapf(err, "VIN: %s", vins[index])
errorList = append(errorList, err)
}
if state != nil {
digitalTwins[vins[index]] = *state
}
}
return
}
type getALDigitalTwinDBFieldsResults struct {
results []common.CarPKCOSVersion
err error
}
func GetVINListALDigitalTwin(vins []string, redisClient *redis.Connection, carsDB queries.CarsInterface) (digitalTwinsAL map[string]common.CarStateAL, errorList []error) {
digitalTwinsAL = make(map[string]common.CarStateAL)
dbResultsChan := make(chan getALDigitalTwinDBFieldsResults)
// While the redis is fetching its stored info, we should go out to the database and fetch the database knowledge we need
// May want to put this information into a cache
go getALDigitalTwinDBFields(vins, carsDB, dbResultsChan)
digitalTwins, errorList := GetVINListDigitalTwin(vins, redisClient)
dbResults := <-dbResultsChan
if dbResults.err != nil {
errorList = append(errorList, dbResults.err)
return
}
for _, dbRes := range dbResults.results {
temp := common.CarStateAL{}
dt, ok := digitalTwins[dbRes.Vin]
if !ok {
logger.Warn().Str("VIN", dbRes.Vin).Msg("AL Digital Twin Missing Redis")
// Think I need to initiate it so we don't null memory maybe?
dt = common.CarState{}
}
temp.CarState = &dt
temp.OSVersion = dbRes.OSVersion
temp.PKCVersion = dbRes.PKCVersion
temp.SumsVersion = dbRes.SumsVersion
digitalTwinsAL[dbRes.Vin] = temp
}
return
}
// IDK how I feel about having this database functionality inside /cache
func getALDigitalTwinDBFields(vins []string, carsDB queries.CarsInterface, out chan getALDigitalTwinDBFieldsResults) {
results, err := carsDB.GetSoftwareAndPKCVersions(vins)
out <- getALDigitalTwinDBFieldsResults{
results: results,
err: err,
}
}
type ALDB interface {
GetCars() queries.CarsInterface
}

View File

@@ -0,0 +1,199 @@
package cachev2_test
import (
"fmt"
"testing"
"time"
cache "fiskerinc.com/modules/cachev2"
"fiskerinc.com/modules/common"
redis "fiskerinc.com/modules/redisv2"
"github.com/go-redis/redismock/v9"
"github.com/stretchr/testify/assert"
)
//HERE
func TestConnGetVehicleState(t *testing.T) {
var updateTime = time.Date(2020, time.October, 3, 12, 10, 0, 0, time.UTC)
vin := "TESTVIN123"
// redisMock := tester.NewRedisMock()
// redisPool := tester.NewMockClientPool(redisMock)
redisClientFakeConnection, clientMock := redismock.NewClientMock()
_ = clientMock
redisClient := redis.NewClient(redisClientFakeConnection)
testCases := map[string]struct {
sismemberResults map[string]map[string]interface{}
hgetallResults map[string][]interface{}
expResp common.CarState
expErr error
}{
"correct": {
sismemberResults: map[string]map[string]interface{}{
redis.CarSessionsKey(): {
vin: int64(1),
},
redis.HMISessionsKey(): {
vin: int64(1),
},
},
hgetallResults: map[string][]interface{}{
fmt.Sprintf("car:%s:state", vin): {
[]byte("DSMC_DrvrSeatHeatgSts"), []byte("2"),
[]byte("ESP_VehSpd"), []byte("123.4"),
[]byte("BMS_RmChrgTi_TrgtSoC"), []byte("5000"),
[]byte("BMS_RmChrgTi_FullChrg"), []byte("6000"),
[]byte("VCU_VehChrgDchgMod"), []byte("DC_charging"),
[]byte("BCM_AP_FL_LeReWinPosnInfo"), []byte("30"),
[]byte("BCM_ReDefrstHeatgCmd"), []byte("1"),
[]byte("BCM_FrntHoodLidSts"), []byte("1"),
[]byte("BMS_Bat_SOH"), []byte("20"),
[]byte("ICC_TotMilg_ODO"), []byte("2345"),
[]byte("IBS_BatteryVoltage"), []byte("12.3"),
[]byte("TBOX_GPSHei"), []byte("16"),
[]byte("ECC_OutdT"), []byte("30"),
[]byte("PSM_PassSeatHeatgSts"), []byte("4"),
[]byte("TBOX_GPSLati"), []byte("35.831"),
[]byte("BCM_PasFrntDoorSts"), []byte("0"),
[]byte("BCM_CenLockSwtSts"), []byte("3"),
[]byte("BCM_RiReDoorSts"), []byte("1"),
[]byte("BCM_LeReDoorSts"), []byte("1"),
[]byte("VCU_DrvgMilg"), []byte("1234"),
[]byte("TBOX_GPSLongi"), []byte("-120.398"),
[]byte("BCM_AP_FL_RiReWinPosnInfo"), []byte("40"),
[]byte("BCM_FrntDrDoorLockSts"), []byte("1"),
[]byte("BCM_DrFrntDoorSts"), []byte("0"),
[]byte("BCM_AP_TL_LeReWinPosnInfo"), []byte("60"),
[]byte("ECC_RemTSetSts"), []byte("120"),
[]byte("BCM_AP_FL_RiFrntWinPosnInfo"), []byte("20"),
[]byte("BMS_PwrBattRmngCpSOC"), []byte("50"),
[]byte("BCM_AP_TL_RiReWinPosnInfo"), []byte("70"),
[]byte("BCM_HeatedSteerWhlSt"), []byte("1"),
[]byte("BCM_AP_RW_WinPosnInfo"), []byte("80"),
[]byte("ECC_InsdT"), []byte("30"),
[]byte("updated"), []byte(`"2020-10-03T12:10:00Z"`),
[]byte("BMS_Bat_SoC_usable"), []byte("10"),
[]byte("BCM_AP_FL_LeFrntWinPosnInfo"), []byte("10"),
[]byte("BCM_SunroofPosnInfo"), []byte("50"),
[]byte("BMS_BattAvrgT"), []byte("90"),
[]byte("dbc_version"), []byte("hash"),
[]byte("VCU_VehSt"), []byte("12"),
[]byte("VCU_VcuState"), []byte("18"),
[]byte("MCU_F_ActSafeSt"), []byte("4"),
[]byte("MCU_R_ActSafeSt"), []byte("2"),
[]byte("MCU_R_Decoup_State"), []byte("3"),
[]byte("MCU_F_CrtMod"), []byte("7"),
[]byte("MCU_R_CrtMod"), []byte("8"),
[]byte("VCU_RdyLamp"), []byte("1"),
},
},
expResp: common.CarState{
Online: true,
OnlineHMI: true,
VehicleSpeed: &common.VehicleSpeed{
Speed: 123.4,
},
Battery: &common.Battery{
Percent: 50,
TotalMileageOdometer: 2345,
BatteryVoltage: 12.3,
},
MaxRange: &common.MaxRange{
MaxMiles: 1234,
},
Doors: &common.Doors{
Hood: true,
LeftFront: false,
LeftRear: true,
RightFront: false,
RightRear: true,
},
Location: &common.Location{
Altitude: 16,
Longitude: -120.398,
Latitude: 35.831,
},
Locks: &common.Locks{
Driver: false,
All: false,
},
Windows: &common.Windows{
LeftFront: 10,
LeftRear: 30,
RightFront: 20,
RightRear: 40,
},
MiscWindows: &common.MiscWindows{
LeftRearQuarter: 60,
RightRearQuarter: 70,
RearWindshield: 80,
},
Sunroof: &common.Sunroof{
Sunroof: 50,
},
CabinClimate: &common.CabinClimate{
CabinTemperature: 120,
InternalTemperature: 30,
},
RearDefrost: &common.RearDefrost{
On: true,
},
DriverSeatHeat: &common.DriverSeatHeat{
Level: 2,
},
PassengerSeatHeat: &common.PassengerSeatHeat{
Level: 4,
},
CellTemperature: &common.CellTemperature{
AvgBatteryTemp: 90,
},
ChargingMetrics: &common.VCUChargingMetrics{
RemainingChargingTime: 5000,
RemainingChargingTimeFull: 6000,
},
SteeringWheelHeat: &common.SteeringWheelHeat{
On: true,
},
AmbientTemperature: &common.AmbientTemperature{
Temperature: 30,
},
VCU0x260: &common.VCU0x260Descriptor{
ChargeType: "DC_charging",
},
StateOfCharge: &common.StateOfCharge{
Usable: 10,
Health: 20,
},
DBCVersion: "hash",
UpdatedAt: &updateTime,
SafeState: &common.SafeState{
VehicleSafeState: false,
VCUSafeState: true,
MCUFrontSafeState: false,
MCURearSafeState: true,
MCURearDecoupState: false,
MCUFrontInverterError: true,
MCURearInverterError: false,
},
VehicleReadyState: &common.VehicleReadyState{
IsVehicleReady: true,
},
},
expErr: nil,
},
}
parser := cache.NewVehicleState(redisClient)
for tName, tt := range testCases {
t.Run(tName, func(t *testing.T) {
// clientMock.ExpectSIsMember()
// redisMock.SISMEMBEResults = tt.sismemberResults
// redisMock.HGETALLResults = tt.hgetallResults
state, err := parser.Get(vin)
assert.Equal(t, tt.expErr, err)
assert.Equal(t, tt.expResp, state)
})
}
}

View File

@@ -0,0 +1,40 @@
package cachev2
import (
"context"
"errors"
"fiskerinc.com/modules/common"
redis "fiskerinc.com/modules/redisv2"
"fiskerinc.com/modules/utils/elptr"
)
func GetTowManDigitalTwin(vin string, redisClient *redis.Connection)(tdt common.TowmanDigitalTwin, err error){
// TODO: Make this more efficient with specific gets
dTwins, errorList := GetVINListDigitalTwin([]string{vin}, redisClient)
if len(errorList) > 0 {
err = errorList[0]
}
if err != nil {
return
}
digitalTwin, ok := dTwins[vin]
if !ok {
err = errors.New("digital twin not found")
return
}
tdt.Gear = digitalTwin.GetGear()
tdt.Location = digitalTwin.GetLocation()
tdt.Online = digitalTwin.Online
tdt.Charging = elptr.ElPtr((digitalTwin.GetVCU0x260().ChargeType != "initial_value") && (digitalTwin.GetVCU0x260().ChargeType != ""))
return
}
func GetVehicleLocation(vin string, redisClient *redis.Connection)(location common.Location, err error){
res := redisClient.HGet(context.Background(), redis.CarLocationsKey(), vin)
err = res.Scan(&location)
return
}

60
pkg/cachev2/verify.go Normal file
View File

@@ -0,0 +1,60 @@
package cachev2
import (
"fmt"
"fiskerinc.com/modules/common"
"fiskerinc.com/modules/db/queries"
redis "fiskerinc.com/modules/redisv2"
"github.com/pkg/errors"
)
// VerifyCarToDriver checks cache and DB for car to driver relationship.
// If relationship exists and not in cache, will cache value.
//
// car:<VIN>:driver:<DRIVER_ID>
func VerifyCarToDriver(clientPool redis.ClientInterface, db queries.CarsInterface, vin string, driverID string) (bool, error) {
key := redis.CarToDriverKey(vin, driverID)
ok, err := redisCheckGet(clientPool, key)
if err != nil {
return ok, err
}
if ok {
return ok, err
}
carToDrivers, err := db.SelectCarToDriver(&common.CarToDriver{VIN: vin, DriverID: driverID})
if err != nil {
return false, err
}
verified := len(carToDrivers) == 1
redisPlaceDriverCache(clientPool, key, verified)
return verified, err
}
func redisCheckGet(redisClient redis.ClientInterface, key string) (bool, error) {
value, err := redisClient.Get(key)
if err != nil {
if errors.Is(err, redis.ErrNil) {
err = nil
}
return false, err
}
v, ok := value.(bool)
if !ok {
err = fmt.Errorf("failed to convert %v interface to bool", value)
}
return v, err
}
func redisPlaceDriverCache(redisClient redis.ClientInterface, key string, verified bool) (err error) {
batch := redis.NewRedisBatchCommands()
batch.Add(redis.Command{Command: "SET", Arguments: []interface{}{key, verified}})
batch.Add(redis.Command{Command: "EXPIRE", Arguments: []interface{}{key, redisObjectExpire.Seconds()}})
_, err = redisClient.ExecuteBatch(batch)
return
}

View File

@@ -0,0 +1,55 @@
package cachev2_test
import (
"testing"
"fiskerinc.com/modules/cache"
"fiskerinc.com/modules/common"
"fiskerinc.com/modules/db/queries/mocks"
"fiskerinc.com/modules/redis"
"fiskerinc.com/modules/redis/tester"
"fiskerinc.com/modules/testhelper"
redigo "github.com/gomodule/redigo/redis"
)
type mockRedisCacheDriverToCars struct {
redis.Connection
}
func (c *mockRedisCacheDriverToCars) Execute(command ...interface{}) (interface{}, error) {
return []byte("1"), nil
}
type mockRedisEmptyCacheDriverToCars struct {
redis.Connection
}
func (c *mockRedisEmptyCacheDriverToCars) Execute(command ...interface{}) (interface{}, error) {
return nil, redigo.ErrNil
}
func (c *mockRedisEmptyCacheDriverToCars) ExecuteBatch(batch *redis.RedisBatchCommands) (interface{}, error) {
return nil, nil
}
func TestVerifyCarToDriver(t *testing.T) {
setupRedisMock()
mockDB := &mocks.MockCars{
SelectCarsForDrivers: []common.CarToDriver{{}},
}
mockRedis = &mockRedisCacheDriverToCars{}
redisPool := tester.NewMockClientPool(mockRedis)
_, err := cache.VerifyCarToDriver(redisPool, mockDB, "VALID_VIN", "VALID_ID")
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestRetrieveAndCacheDriverIDs", nil, err)
}
mockRedis = &mockRedisEmptyCacheDriverToCars{}
redisPool = tester.NewMockClientPool(mockRedis)
_, err = cache.VerifyCarToDriver(redisPool, mockDB, "VALID_VIN", "VALID_ID")
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestRetrieveAndCacheDriverIDs", nil, err)
}
}