Skip to content

Commit

Permalink
Use sync.Map to avoid concurrent read/iterate and write errors (#32)
Browse files Browse the repository at this point in the history
* Use sync.Map
* Formatting
* Handling ABRP defaults
* Drop main binary
  • Loading branch information
tma authored Aug 8, 2022
1 parent bd3eab2 commit 7486216
Show file tree
Hide file tree
Showing 4 changed files with 97 additions and 67 deletions.
13 changes: 10 additions & 3 deletions internal/app/abrp.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package app
import (
"bytes"
"encoding/json"
"fmt"
"log"
"net/http"
"time"
Expand Down Expand Up @@ -48,10 +49,10 @@ func abrpSendLoop(car *Car, endTime time.Time) {
timer = 30
}

car.abrpData["utc"] = int(time.Now().UTC().Unix())
car.abrpData.Store("utc", int(time.Now().UTC().Unix()))

if car.state == "parked" || car.state == "online" || car.state == "suspended" || car.state == "asleep" {
delete(car.abrpData, "kwh_charged")
car.abrpData.Delete("kwh_charged")
if timer%30 == 0 || timer > 30 {
// parked, update every 30 seconds (avoid being offline for ABRP)
abrpSend(car)
Expand Down Expand Up @@ -82,7 +83,13 @@ const abrpUrl = "https://api.iternio.com/1/tlm/send"
func abrpSend(car *Car) {
log.Println("Sending to ABRP...")

data, error := json.Marshal(map[string]interface{}{"tlm": car.abrpData})
m := map[string]interface{}{}
car.abrpData.Range(func(key, value interface{}) bool {
m[fmt.Sprint(key)] = value
return true
})

data, error := json.Marshal(map[string]interface{}{"tlm": m})
if error != nil {
log.Println("Error marshalling data: " + error.Error())
return
Expand Down
135 changes: 73 additions & 62 deletions internal/app/car.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,139 +2,150 @@ package app

import (
"strconv"
"sync"
"time"
)

type Car struct {
number string
state string
previousState string
tmData map[string]interface{}
abrpData map[string]interface{}
tmData *sync.Map
abrpData *sync.Map
abrpSendActive bool
abrpUpdatesEndTime time.Time
abrpToken string
abrpApiKey string
}

func NewCar(number string, carModel string, abrpToken string, abrpApiKey string) *Car {
return &Car{
number: number,
tmData: map[string]interface{}{},
abrpData: map[string]interface{}{
"car_model": carModel,
"utc": 0,
"soc": 0,
"power": 0,
"speed": 0,
"lat": "",
"lon": "",
"elevation": "",
"heading": "",
"is_charging": 0,
"is_dcfc": 0,
"is_parked": 0,
"est_battery_range": "",
"ideal_battery_range": "",
"ext_temp": "",
"tlm_type": "api",
"voltage": 0,
"current": 0,
"kwh_charged": 0,
"odometer": "",
},
car := &Car{
number: number,
tmData: new(sync.Map),
abrpData: new(sync.Map),
abrpSendActive: false,
abrpToken: abrpToken,
abrpApiKey: abrpApiKey,
}

abrpDataDefaults := map[string]interface{}{
"car_model": carModel,
"utc": 0,
"soc": 0,
"power": 0,
"speed": 0,
"lat": "",
"lon": "",
"elevation": "",
"heading": "",
"is_charging": 0,
"is_dcfc": 0,
"is_parked": 0,
"est_battery_range": "",
"ideal_battery_range": "",
"ext_temp": "",
"tlm_type": "api",
"voltage": 0,
"current": 0,
"kwh_charged": 0,
"odometer": "",
}

for key, value := range abrpDataDefaults {
car.abrpData.Store(key, value)
}

return car
}

func updateCarTmData(car *Car, topic string, payload string) {
car.tmData[topic] = payload
car.tmData.Store(topic, payload)
}

func updateCarAbrpData(car *Car, topic string, payload string) {
switch topic {
case "latitude":
car.abrpData["lat"] = payload
car.abrpData.Store("lat", payload)
case "longitude":
car.abrpData["lon"] = payload
car.abrpData.Store("lon", payload)
case "elevation":
car.abrpData["elevation"] = payload
car.abrpData.Store("elevation", payload)
case "heading":
car.abrpData["heading"] = payload
car.abrpData.Store("heading", payload)
case "speed":
car.abrpData["speed"], _ = strconv.Atoi(payload)
value, _ := strconv.Atoi(payload)
car.abrpData.Store("speed", value)
case "outside_temp":
car.abrpData["ext_temp"] = payload
car.abrpData.Store("ext_temp", payload)
case "odometer":
car.abrpData["odometer"] = payload
car.abrpData.Store("odometer", payload)
case "ideal_battery_range_km":
car.abrpData["ideal_battery_range"] = payload
car.abrpData.Store("ideal_battery_range", payload)
case "est_battery_range_km":
car.abrpData["est_battery_range"] = payload
car.abrpData.Store("est_battery_range", payload)
case "usable_battery_level":
car.abrpData["soc"] = payload
car.abrpData.Store("soc", payload)
case "charge_energy_added":
car.abrpData["kwh_charged"] = payload
car.abrpData.Store("kwh_charged", payload)
case "power":
power, _ := strconv.Atoi(payload)
car.abrpData["power"] = power
car.abrpData.Store("power", power)

if (car.abrpData["is_charging"] == 1) && (power < -22) {
car.abrpData["is_dcfc"] = 1
isCharging, _ := car.abrpData.Load("is_charging")
if (isCharging == 1) && (power < -22) {
car.abrpData.Store("is_dcfc", 1)
}
case "charger_power":
if payload != "" && payload != "0" {
car.abrpData["is_charging"] = 1
car.abrpData.Store("is_charging", 1)

chargerPower, _ := strconv.Atoi(payload)
if chargerPower > 22 {
car.abrpData["is_dcfc"] = 1
car.abrpData.Store("is_dcfc", 1)
}
}
case "charger_actual_current":
if payload != "" {
current, _ := strconv.Atoi(payload)
if current > 0 {
car.abrpData["current"] = payload
car.abrpData.Store("current", payload)
} else {
delete(car.abrpData, "current")
car.abrpData.Delete("current")
}
}
case "charger_voltage":
if payload != "" {
voltage, _ := strconv.Atoi(payload)
if voltage > 4 {
car.abrpData["voltage"] = payload
car.abrpData.Store("voltage", payload)
} else {
delete(car.abrpData, "voltage")
car.abrpData.Delete("voltage")
}
}
case "state":
car.state = payload
if car.state == "driving" {
car.abrpData["is_parked"] = 0
car.abrpData["is_charging"] = 0
car.abrpData["is_dcfc"] = 0
car.abrpData.Store("is_parked", 0)
car.abrpData.Store("is_charging", 0)
car.abrpData.Store("is_dcfc", 0)
} else if car.state == "charging" {
car.abrpData["is_parked"] = 1
car.abrpData["is_charging"] = 1
car.abrpData["is_dcfc"] = 0
car.abrpData.Store("is_parked", 1)
car.abrpData.Store("is_charging", 1)
car.abrpData.Store("is_dcfc", 0)
} else if car.state == "supercharging" {
car.abrpData["is_parked"] = 1
car.abrpData["is_charging"] = 1
car.abrpData["is_dcfc"] = 1
car.abrpData.Store("is_parked", 1)
car.abrpData.Store("is_charging", 1)
car.abrpData.Store("is_dcfc", 1)
} else if car.state == "online" || car.state == "suspended" || car.state == "asleep" {
car.abrpData["is_parked"] = 1
car.abrpData["is_charging"] = 0
car.abrpData["is_dcfc"] = 0
car.abrpData.Store("is_parked", 1)
car.abrpData.Store("is_charging", 0)
car.abrpData.Store("is_dcfc", 0)
}
case "shift_state":
if payload == "P" {
car.abrpData["is_parked"] = 1
car.abrpData.Store("is_parked", 1)
} else if payload == "D" || payload == "R" {
car.abrpData["is_parked"] = 0
car.abrpData.Store("is_parked", 0)
}
}
}
16 changes: 14 additions & 2 deletions internal/app/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package app

import (
"encoding/json"
"fmt"
"log"
"net/http"
"os"
Expand All @@ -15,8 +16,19 @@ var indexTemplate = template.Must(template.ParseFiles("web/templates/index.html"

func indexHandler(car *Car) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
carTmData, _ := json.MarshalIndent(car.tmData, "", " ")
carAbrpData, _ := json.MarshalIndent(car.abrpData, "", " ")
tmDataMap := map[string]interface{}{}
car.tmData.Range(func(key, value interface{}) bool {
tmDataMap[fmt.Sprint(key)] = value
return true
})
carTmData, _ := json.MarshalIndent(tmDataMap, "", " ")

abrpDataMap := map[string]interface{}{}
car.abrpData.Range(func(key, value interface{}) bool {
abrpDataMap[fmt.Sprint(key)] = value
return true
})
carAbrpData, _ := json.MarshalIndent(abrpDataMap, "", " ")

var carAbrpSendContinuousButtonText string
if car.abrpSendActive {
Expand Down
Binary file removed main
Binary file not shown.

0 comments on commit 7486216

Please sign in to comment.