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

View File

@@ -0,0 +1,235 @@
package immobilizerditto
import (
"bytes"
"context"
"encoding/json"
"io"
"net/http"
"sync"
"time"
"fiskerinc.com/modules/common"
"fiskerinc.com/modules/dbc/state"
"fiskerinc.com/modules/immobilizer/immobilizershared"
"fiskerinc.com/modules/logger"
"fiskerinc.com/modules/redisv2"
)
// I should put these parked values as a constant somewhere TODO
func (id *ImmobilizeDitto) VCUGearSig(vin string, newState int) {
//0 "Undefined_initial_value" 1 "gear_P" 2 "gear_N" 3 "R_gear" 4 "D_gear" 5 "Reserved" 6 "gear_E" 7 "gear_S" 8 "Reserved" 9 "Reserved" 10 "Reserved" 11 "Reserved" 12 "Reserved" 13 "Reserved" 14 "Reserved" 15 "Reserved" ;
id.WatchListSync.RLock()
track, ok := id.WatchList[vin]
id.WatchListSync.RUnlock()
if !ok {
return
}
switch track.State {
case immobilizershared.STATE_IMMOBILIZED:
// If we are immobilized, but we are out of park, thats an issue
if newState == 1 || newState == 2 {
// Car is parked, and possible already immobilized, but we are going to enforce in case we were wrong
// id.sendLockCommand(vin)
} else if newState > 2 {
logger.Error().Str("VIN", vin).Int("New Gear", newState).Str("MSG", "Marked as Immobilized, but received driving gear").Msg("VCUGearSig")
go alertAmericanLeaseMovingVehicle(vin)
}
case immobilizershared.STATE_IMMOBILIZING:
if newState == 1 || newState == 2 {
// Car is parked, send that lock command. We know the car is awake, so no need to send a wake up sms
id.sendLockCommand(vin)
}
case immobilizershared.STATE_MOBILIZING:
if newState > 2 {
// unsure if we should accept this as done
immobilizershared.UpdateCarMobilized(vin, id.RedisClient)
}
}
}
func (id *ImmobilizeDitto) sendLockCommand(vin string) {
msg := common.RemoteCommandSource{}
msg.Command = "doors_lock"
// 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,
})
res := id.RedisClient.RPush(context.Background(), redisv2.QueueKey(common.TRex.Key(vin)), data)
err := res.Err()
if err != nil {
logger.Err(err).Msg("Failed to send lock command")
}
}
func alertAmericanLeaseMovingVehicle(vin string) {
url := "https://mobile.americanlease.net/api/v1/fisker/command/failure"
token := "Bearer f3d795d3-5325-42d7-8dd8-ccb416c52ae2"
type AmericanLeaseBody struct {
VIN string `json:"vin"`
UTCMobileAt time.Time `json:"utcMobileAt"` //"utcMobileAt":"2025-03-07T09:00:00",
UUID string `json:"uuid"` //"uuid": "db6e407d-791b-4d00-a742-da856a42c4ba"
}
body := AmericanLeaseBody{
VIN: vin,
UTCMobileAt: time.Now().UTC(),
UUID: "00000000-0000-0000-0000-000000000000",
}
b, _ := json.Marshal(body)
// Create the HTTP request
req, err := http.NewRequest("POST", url, bytes.NewBuffer(b))
if err != nil {
logger.Err(err).Msg("failed to make request to american lease")
return
}
// Set headers
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", token)
// Execute the request
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
logger.Err(err).Msg("failed to do request to american lease")
return
}
defer resp.Body.Close()
if resp.StatusCode >= 300 {
respBody, _ := io.ReadAll(resp.Body)
logger.Error().Str("response body", string(respBody)).Str("response header", resp.Status).Msg("failed to contact american lease about moving car")
}
}
// So we got the VCU Immo sts, most likely a new value compared to what was had before
func (id *ImmobilizeDitto) VCUIMMOSts(vin string, newState string) {
// Possible values for newState
// "Immo_Active",
// "Immo_Inactive",
// "Reserved",
// "Invalid",
// Check if we are tracking this vehicle
id.WatchListSync.RLock()
track, ok := id.WatchList[vin]
id.WatchListSync.RUnlock()
if !ok {
return
}
logger.Debug().Str("New State", newState).Str("Tracked", track.State.String()).Str("VIN", vin).Msg("VCUIMMOSts signal")
switch track.State {
case immobilizershared.STATE_IMMOBILIZED:
// Car is currently marked as immobilized
if newState == "Immo_Inactive" {
// This is an issue, we have the car as being immobilized, but it just got moving. Now this could possible be a timing issue between a different ditto
// If this actually happens, we need to set the vehicle to Immobilizing
logger.Error().Str("VIN", vin).Str("MSG", "Marked as Immobilized, but received Immo_Inactive").Msg("Ditto Immobilizer")
}
// if newState == "Immo_Active" {
// // This is can be ignored
// }
case immobilizershared.STATE_IMMOBILIZING:
// We are trying to immobilize the vehicle
if newState == "Immo_Active" {
immobilizershared.UpdateCarImmobilized(vin, id.RedisClient)
// Send lock command some more
id.sendLockCommand(vin)
}
case immobilizershared.STATE_MOBILIZING:
// Hey great, we know that the car is now active
if newState == "Immo_Inactive" {
immobilizershared.UpdateCarMobilized(vin, id.RedisClient)
}
}
}
func (id *ImmobilizeDitto) PKC_KeyStsMod(vin string, newState string) {
id.WatchListSync.RLock()
track, ok := id.WatchList[vin]
id.WatchListSync.RUnlock()
if !ok {
return
}
logger.Debug().Str("New State", newState).Str("Tracked", track.State.String()).Str("VIN", vin).Msg("PKC_KeyStsMode signal")
switch track.State {
case immobilizershared.STATE_IMMOBILIZED:
// Car is currently marked as immobilized
if newState == state.PKC_KeyStsMod_disabled {
// we thought the car was immobilized, but we just got a signal saying its been mobilized
logger.Error().Str("VIN", vin).Str("MSG", "Marked as Immobilized, but received PKC_KeyStsMod: disabled").Msg("Ditto Immobilizer")
}
case immobilizershared.STATE_IMMOBILIZING:
// We were trying to immobilize, and we got confirmation that it is
if newState == state.PKC_KeyStsMod_enabled {
immobilizershared.UpdateCarImmobilized(vin, id.RedisClient)
}
case immobilizershared.STATE_MOBILIZING:
// trying to get car moving, and got confirmation it is
if newState == state.PKC_KeyStsMod_disabled {
immobilizershared.UpdateCarMobilized(vin, id.RedisClient)
}
}
}
// Ongoing process, needs to be run in da background
func (id *ImmobilizeDitto) ListenToRedisChanges() {
subscription := id.RedisClient.Subscribe(context.Background(), immobilizershared.IMMOBILIZER_PUBSUB_CHANNEL)
subChannel := subscription.Channel()
for msg := range subChannel {
rim := immobilizershared.RedisImmobilizerMsg{}
err := json.Unmarshal([]byte(msg.Payload), &rim)
if err != nil {
logger.Err(err).Str("Payload", msg.Payload).Msg("ListenToRedisChanges: Failed to parse payload")
continue
}
id.WatchListSync.Lock()
if rim.State == immobilizershared.STATE_MOBILIZED {
delete(id.WatchList, rim.VIN)
} else {
id.WatchList[rim.VIN] = immobilizershared.ImmobilizeTrack{State: rim.State}
}
id.WatchListSync.Unlock()
}
}
// Read in redis to get out initial state
func (id *ImmobilizeDitto) SeedLocalData() {
res, err := immobilizershared.ReadVehicleStatuses(id.RedisClient)
if err != nil {
return
}
id.WatchListSync.Lock()
id.WatchList = res
id.WatchListSync.Unlock()
}
// Don't want to become de-synced, so will seed once an hour
func (id *ImmobilizeDitto) SeedHourly() {
id.SeedLocalData()
time.AfterFunc(time.Hour, id.SeedHourly)
}
func InitImmobilizerDitto(connection *redisv2.Connection) (id *ImmobilizeDitto) {
id = &ImmobilizeDitto{}
id.RedisClient = connection
go id.SeedHourly()
go id.ListenToRedisChanges()
return id
}
// Wether a car is to start mobilizing or immobilizing is a decision by the server, not from a cars messaging
type ImmobilizeDitto struct {
RedisClient *redisv2.Connection
WatchList map[string]immobilizershared.ImmobilizeTrack
WatchListSync sync.RWMutex
}

View File

@@ -0,0 +1,6 @@
So need to listen to redis updates to know when a new car is added to the system.
Take that signal and add it to our map thing
Ditto should special listen for the parked signal, the immobilize signal, and possible another if we need
When we receive the immobilize signal, we can turn it off. Signal is continues sent

View File

@@ -0,0 +1,207 @@
package immobilizershared
import (
"context"
"encoding/json"
"time"
"fiskerinc.com/modules/logger"
"fiskerinc.com/modules/redisv2"
)
// Grabs all the vehicle statuses from redis
func ReadVehicleStatuses(connection *redisv2.Connection) (results map[string]ImmobilizeTrack, err error) {
res := connection.HGetAll(context.Background(), IMMOBILIZER_HASHSET_NAME)
tempMap, err := res.Result()
if err != nil {
logger.Err(err).Stack().Str("function", "ReadVehicleStatuses").Msg("Immobilizer Error")
return
}
results = make(map[string]ImmobilizeTrack)
for key, value := range tempMap {
tempTrack := ImmobilizeTrack{}
err = json.Unmarshal([]byte(value), &tempTrack)
if err != nil {
logger.Err(err).Stack().Str("function", "ReadVehicleStatuses").Msg("Immobilizer Error")
continue
}
results[key] = tempTrack
}
return
}
// WE want to update redis with the fact a car is immobilized, so start with modifying redis, then sending a publish event
// This kind of specification per function is sort of silly, probably will want to change in the future
func UpdateCarImmobilized(vin string, connection *redisv2.Connection) (err error) {
err = SetVehicleImmobilized(vin, connection)
if err != nil {
logger.Err(err).Stack().Str("function", "UpdateCarImmobilized").Msg("Immobilizer Error")
return
}
err = PublishVehicleImmobilized(vin, connection)
if err != nil {
logger.Err(err).Stack().Str("function", "UpdateCarImmobilized").Msg("Immobilizer Error")
return
}
return
}
func UpdateCarImmobilizing(vin string, connection *redisv2.Connection) (err error) {
err = SetVehicleImmobilizing(vin, connection)
if err != nil {
logger.Err(err).Stack().Str("function", "UpdateCarImmobilizing").Msg("Immobilizer Error")
return
}
err = PublishVehicleImmobilizing(vin, connection)
if err != nil {
logger.Err(err).Stack().Str("function", "UpdateCarImmobilizing").Msg("Immobilizer Error")
return
}
return
}
func UpdateCarMobilized(vin string, connection *redisv2.Connection) (err error) {
err = SetVehicleMobilized(vin, connection)
if err != nil {
logger.Err(err).Stack().Str("function", "UpdateCarMobilized").Msg("Immobilizer Error")
return
}
err = PublishVehicleMobilized(vin, connection)
if err != nil {
logger.Err(err).Stack().Str("function", "UpdateCarMobilized").Msg("Immobilizer Error")
return
}
return
}
func UpdateCarMobilizing(vin string, connection *redisv2.Connection) (err error) {
err = SetVehicleMobilizing(vin, connection)
if err != nil {
logger.Err(err).Stack().Str("function", "UpdateCarMobilizing").Msg("Immobilizer Error")
return
}
err = PublishVehicleMobilizing(vin, connection)
if err != nil {
logger.Err(err).Stack().Str("function", "UpdateCarMobilizing").Msg("Immobilizer Error")
return
}
return
}
// Might not need to call these directly
// Publishing the fact the car is now mobilized. Ditto's will remove the car from their containers
func PublishVehicleMobilized(vin string, connection *redisv2.Connection) (err error) {
return PublishVehicleMobilizationChange(RedisImmobilizerMsg{VIN: vin, State: STATE_MOBILIZED}, connection)
}
func PublishVehicleMobilizing(vin string, connection *redisv2.Connection) (err error) {
return PublishVehicleMobilizationChange(RedisImmobilizerMsg{VIN: vin, State: STATE_MOBILIZING}, connection)
}
func PublishVehicleImmobilized(vin string, connection *redisv2.Connection) (err error) {
return PublishVehicleMobilizationChange(RedisImmobilizerMsg{VIN: vin, State: STATE_IMMOBILIZED}, connection)
}
// Publish the fact we are trying to immobile the vehicle
func PublishVehicleImmobilizing(vin string, connection *redisv2.Connection) (err error) {
return PublishVehicleMobilizationChange(RedisImmobilizerMsg{VIN: vin, State: STATE_IMMOBILIZING}, connection)
}
func PublishVehicleMobilizationChange(msg RedisImmobilizerMsg, connection *redisv2.Connection) (err error) {
// I think the int that is returned is the number of subscribers
res := connection.Publish(context.Background(), IMMOBILIZER_PUBSUB_CHANNEL, msg)
err = res.Err()
return
}
// These all modify the redis data store, which acts as a database for the cars that are immobilized
func SetVehicleMobilized(vin string, connection *redisv2.Connection) (err error) {
// Instead of using HSET, we are deleting the car from the immobilized stats line. Keeps the redis store smaller
res := connection.HDel(context.Background(), IMMOBILIZER_HASHSET_NAME, vin)
err = res.Err()
if err != nil {
logger.Err(err).Stack().Str("function", "SetVehicleMobilized").Msg("Immobilizer Error")
}
return
}
func SetVehicleMobilizing(vin string, connection *redisv2.Connection) (err error) {
return setVehicleMobilizationChange(vin, ImmobilizeTrack{State: STATE_MOBILIZING}, connection)
}
func SetVehicleImmobilized(vin string, connection *redisv2.Connection) (err error) {
return setVehicleMobilizationChange(vin, ImmobilizeTrack{State: STATE_IMMOBILIZED}, connection)
}
// Set the fact we are trying to immobile the vehicle
func SetVehicleImmobilizing(vin string, connection *redisv2.Connection) (err error) {
return setVehicleMobilizationChange(vin, ImmobilizeTrack{State: STATE_IMMOBILIZING}, connection)
}
// CHANGE
func setVehicleMobilizationChange(vin string, msg ImmobilizeTrack, connection *redisv2.Connection) (err error) {
// I think the int that is returned is the number of subscribers
res := connection.HSet(context.Background(), IMMOBILIZER_HASHSET_NAME, vin, msg)
err = res.Err()
if err != nil {
logger.Err(err).Stack().Str("function", "setVehicleMobilizationChange").Msg("Immobilizer Error")
return
}
return
}
const IMMOBILIZER_PUBSUB_CHANNEL = "immobilizer"
const IMMOBILIZER_HASHSET_NAME = "immobilizer"
type ImmobilizeTrack struct {
State ImmobilizeState
// IDK, do we need the time we tried? We probably need a timer thing
CreatedAt *time.Time
UpdatedAt *time.Time
}
// For redis serialization
func (it ImmobilizeTrack) MarshalBinary() ([]byte, error) {
return json.Marshal(it)
}
func (it ImmobilizeTrack) UnmarshalBinary(data []byte) error {
return json.Unmarshal(data, &it)
}
type ImmobilizeState int
var (
STATE_IMMOBILIZED ImmobilizeState = 0 // Car officially immobilized
STATE_IMMOBILIZING ImmobilizeState = 1 // Trying to immobilize car
STATE_MOBILIZING ImmobilizeState = 2 // Trying to mobilize the car
STATE_MOBILIZED ImmobilizeState = 3 // Car is mobilized, although not stored in redis or ditto, is used to communicate the change has been made
)
func (i ImmobilizeState) String()(string){
switch i{
case STATE_IMMOBILIZED:
return "STATE_IMMOBILIZED"
case STATE_IMMOBILIZING:
return "STATE_IMMOBILIZING"
case STATE_MOBILIZING:
return "STATE_MOBILIZING"
case STATE_MOBILIZED:
return "STATE_MOBILIZED"
}
return "missing"
}
type RedisImmobilizerMsg struct {
VIN string // VIN that we are modifying
State ImmobilizeState // How we are changing said state, which will affect how listening ditto's will manage
}
func (rim RedisImmobilizerMsg) MarshalBinary() ([]byte, error) {
return json.Marshal(rim)
}
func (rim RedisImmobilizerMsg) UnmarshalBinary(data []byte) error {
return json.Unmarshal(data, &rim)
}

View File

@@ -0,0 +1,2 @@
Controller side will manage adding vehicles to the redis maps
It will also have a fetch to get the list of vehicles and what their status is in the immobilize/mobilize pathway