Files
cloud-services/pkg/common/updatemanifest.go

535 lines
15 KiB
Go

package common
import (
"encoding/hex"
"encoding/json"
"fmt"
"sort"
"strconv"
"strings"
"time"
v "github.com/fiskerinc/cloud-services/pkg/utils/vod"
"github.com/fiskerinc/cloud-services/pkg/vindecoder"
"github.com/fiskerinc/cloud-services/pkg/vod_decoder"
"github.com/fiskerinc/cloud-services/pkg/common/dbbasemodel"
"github.com/fiskerinc/cloud-services/pkg/utils/envtool"
"github.com/albenik/bcd"
"github.com/jinzhu/copier"
"github.com/pkg/errors"
)
// Had to copy getenv to stop import cycle
var defaultVOD string = envtool.GetEnv("DEFAULT_VOD", "11111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111")
var defaultUpdateDuration int = getDefaultUpdateDuration()
const (
ecuICCName string = "ICC"
ManifestTypeStandard string = "standard"
ManifestTypeForced string = "forced"
ManufacturerName string = "FISKER"
EnvCurrent string = "current"
)
// If a change here will mess with a manifest environment migration, increment MIGRATION_VERSION in
// ota_update_go/handlers/update_manifest_migrate.go and create an adapter or return an error if it
// is incompatable
type UpdateManifest struct {
ID int64 `json:"id,omitempty"`
Name string `json:"name,omitempty" pg:",unique:name_version" validate:"required,max=255"`
Version string `json:"version,omitempty" pg:",unique:name_version" validate:"max=255"`
Description string `json:"description,omitempty" validate:"max=5120"`
ReleaseNotes string `json:"release_notes,omitempty" validate:"omitempty,max=32768,url"`
ECUList string `json:"ecu_list,omitempty"`
ECUs []*UpdateManifestECU `json:"ecu_updates,omitempty" pg:"rel:has-many" validate:"omitempty,min=1,dive"`
Fingerprint string `json:"fingerprint,omitempty" validate:"max=5000"`
CarUpdateID int64 `json:"car_update_id,omitempty" pg:"-"`
RollbackEnabled bool `json:"rollback" pg:",use_zero"`
Type string `json:"type,omitempty" validate:"max=100"`
VOD string `json:"vod,omitempty" pg:"vod"`
ManifestType UpdateManifestType `json:"manifest_type,omitempty" validate:"oneof=0 1 2 3 4" swaggerignore:"true"`
Active *bool `json:"active,omitempty"` // Use a pointer so we know when we want to change the value
Country string `json:"country,omitempty" pg:"country"`
PowerTrain string `json:"powertrain,omitempty" pg:"powertrain"`
Restraint string `json:"restraint,omitempty" pg:"restraint"`
Model string `json:"model,omitempty" pg:"model"`
Trim string `json:"trim,omitempty" pg:"trim"`
Year int `json:"year,omitempty" pg:"year"`
BodyType string `json:"body_type,omitempty" pg:"body_type"`
SUMS string `json:"sums,omitempty"` // Software Update Management System
Env string `json:"env,omitempty"` // Environment of ECC keys to include
UpdateDuration int `json:"update_duration,omitempty" pg:"update_duration"` // Duration of update in minutes
MaxAttempts int `json:"max_attempts,omitempty" pg:"max_attempts"`
dbbasemodel.DBModelBase
}
func (um *UpdateManifest) String() string {
return fmt.Sprintf("UpdateManifest<%d %s %s>", um.ID, um.Name, um.Version)
}
// Scrub cleans data for sending to car
func (um *UpdateManifest) Scrub(ecuType Device) {
um.ID = 0
um.Env = ""
um.ECUList = ""
um.CreatedAt = nil
um.UpdatedAt = nil
um.ManifestType = 0
um.Active = nil
um.Country = ""
um.PowerTrain = ""
um.Restraint = ""
um.Model = ""
um.Trim = ""
um.Year = 0
um.BodyType = ""
um.SUMS = ""
// If the update has no duration, we can default it to 30
if um.UpdateDuration == 0 {
um.UpdateDuration = 30
}
vodIndex := -1
for x, ecu := range um.ECUs {
ecu.Scrub(ecuType, true)
if ecu.ECU == "VOD" {
vodIndex = x
}
}
if vodIndex != -1 {
um.ECUs = append(um.ECUs[:vodIndex], um.ECUs[vodIndex+1:]..., )
}
if ecuType != HMI {
um.Name = ""
um.Version = ""
um.Description = ""
um.ReleaseNotes = ""
}
}
func (um *UpdateManifest) RemoveParsedS19HexFiles() {
for _, ecu := range um.ECUs {
ecu.RemoveParsedS19HexFiles()
for _, rollback := range ecu.Rollback {
rollback.RemoveParsedS19HexFiles()
}
}
}
func (um *UpdateManifest) RemoveOriginalS19HexFiles() {
for _, ecu := range um.ECUs {
ecu.RemoveOriginalS19HexFiles()
for _, rollback := range ecu.Rollback {
rollback.RemoveOriginalS19HexFiles()
}
}
}
func (um *UpdateManifest) RemoveECCKeysFromECUs() {
for _, ecu := range um.ECUs {
ecu.ECCKeys = nil
}
}
func (um *UpdateManifest) RemoveParsedS19HexFilesRollbacks() {
for i := range um.ECUs {
for j := range um.ECUs[i].Rollback {
um.ECUs[i].Rollback[j].RemoveParsedS19HexFiles()
}
}
}
func (um *UpdateManifest) RemoveOriginalS19HexFilesRollbacks() {
for i := range um.ECUs {
for j := range um.ECUs[i].Rollback {
um.ECUs[i].Rollback[j].RemoveOriginalS19HexFiles()
}
}
}
func (um *UpdateManifest) SortECUs() {
// If this is an old update manifest without sorting info, we will fall back to the old sort
nonZero := false
for _, ecu := range um.ECUs {
if ecu.InstallPriority > 0 {
nonZero = true
break
}
}
// Have a non-zero value, sort by install priority
if nonZero {
// Sort in Ascending order
sort.Slice(um.ECUs, func(i, j int) bool {
return um.ECUs[i].InstallPriority < um.ECUs[j].InstallPriority
})
} else {
// We do not have any install priorities, so fall back to the old sort method
um.sortECUByMode()
}
}
func (um *UpdateManifest) sortECUByMode() {
sort.Slice(um.ECUs, func(i, j int) bool {
return um.modePosition(um.ECUs[i]) < um.modePosition(um.ECUs[j])
})
}
func (um *UpdateManifest) modePosition(ecu *UpdateManifestECU) int {
if ecu.SelfDownload {
return 4
}
if ecu.ECU == "TREX" {
return 0
}
// Easy catch all case as PDU and OBC are interswappable
if ecu.ECU == "PDU" || ecu.ECU == "OBC" {
return 3
}
if ecu.Mode == "D" {
return 2
}
return 1
}
func (um *UpdateManifest) HasComponent(name string) bool {
match := strings.ToUpper(name)
for _, ecu := range um.ECUs {
if ecu.ECU == match {
return true
}
}
return false
}
func (um *UpdateManifest) HasSelfDownload() bool {
for _, ecu := range um.ECUs {
if ecu.SelfDownload {
return true
}
}
return false
}
func (um *UpdateManifest) Copy() *UpdateManifest {
clone := *um
ecus := make([]*UpdateManifestECU, len(um.ECUs))
copier.CopyWithOption(&ecus, &um.ECUs, copier.Option{DeepCopy: true})
clone.ECUs = ecus
return &clone
}
func (um *UpdateManifest) GenerateFingerprint(curDate time.Time, serialSfx string) {
if um.Fingerprint == "" {
y, m, d := curDate.Date()
y = y % 100
um.Fingerprint = fmt.Sprintf("%02d%02d%02d%s%s", y, m, d, ManufacturerName, serialSfx)
// 29 is the max length of the fingerprint
um.Fingerprint = string([]byte(um.Fingerprint)[:29])
}
}
// Pulls the vod field from cds, and then applies it to the updateManifest
// If the vod is not found, we insert the default vod
// We also remove the VOD field from the cds map
func (um *UpdateManifest) AddVOD(cds map[string]string) {
if um.VOD == "" {
if vod, ok := cds["VOD"]; ok && vod != "" {
um.VOD = cds["VOD"]
} else {
um.VOD = defaultVOD
}
}
delete(cds, "VOD")
}
// Applies the cds coding strings to the VOD field along with each
// ECU that is inside the update manifest
// Any extra cds does not create the ecu field
func (um *UpdateManifest) AddCDSToECUs(cds map[string]string) {
um.AddVOD(cds)
for key := range um.ECUs {
um.ECUs[key].Configuration = cds[um.ECUs[key].ECU]
}
}
// Will take the UpdateManifest's ECU's, and transform their names to send to the car
func (um *UpdateManifest) TransformECUNames() {
for _, ecu := range um.ECUs {
ecu.TransformECUName()
}
}
// Will add the ecu's the set of ECU's if there are no ecu's already in the manifest
func (um *UpdateManifest) AddECUsFromCDSList(cds map[string]string) {
if len(um.ECUs) == 0 {
um.ECUs = make([]*UpdateManifestECU, 0)
for key, value := range cds {
ecu := UpdateManifestECU{
UpdateManifestID: um.ID,
ECU: key,
Configuration: value,
}
um.ECUs = append(um.ECUs, &ecu)
}
}
}
// Fill out um.ECUList from um.ECUS
func (um *UpdateManifest) FillECUList() {
str := ""
for _, ecu := range um.ECUs {
str = str + ecu.ECU + ","
}
str = strings.TrimRight(str, ",")
um.ECUList = str
}
func (um *UpdateManifest) GetFileIDs() []string {
ids := []string{}
if len(um.ECUs) == 0 {
return ids
}
for _, ecu := range um.ECUs {
result := ecu.GetFileIDs()
if len(result) > 0 {
ids = append(ids, result...)
}
}
return ids
}
// Becaue of the differences in the new hardware versioning, we still need to be able to migrate, so going to send filled old hw_version
// This should be deleted as soon as hardware versions list reaches all the way to production
func (um *UpdateManifest) MigratePrepareHardwareVersion() {
for _, ecu := range um.ECUs {
if len(ecu.HWVersions) >= 1 {
ecu.HWVersion = ecu.HWVersions[0]
}
}
}
var dataLength int = 255
var vodHasCRCToRemovce bool = true // If the given VOD already has a CRC code at the end, remove it
var lengthInCRC bool = false // True if the CRC includes the length bytes in its calculation
var crcInLength bool = false // True if the length includes the extra 1 bit for crc
var lengthInLength bool = true // True if the length includes its own two bytes
var choppedLengthOffset int = 2 // Because we are cutting off the front two bytes that would be the length
func (um *UpdateManifest) AddSUMSToVOD() (err error) {
if um.VOD == "" {
um.VOD = defaultVOD
}
vod, err := hex.DecodeString(um.VOD)
if err != nil {
return errors.WithStack(err)
}
//Removing the previous length
vod = vod[2:]
// Remove the previous CRC
if vodHasCRCToRemovce {
vod = vod[:len(vod)-1]
}
// Ensure that the vod byte's is long enough. Just incase it was truncated at the end
// 2 byte length, 255 byte data, 1 byte CRC
new_vod := make([]byte, dataLength)
// copy vod excluding CRC last byte
copy(new_vod, vod)
// Now we will modify the bytes as so
// I did double check this uint16 -> uint8 takes the last 8 bits correctly
versionInfo, err := um.parseVersionNumber()
if err != nil {
return errors.WithStack(err)
}
//Field Strt Byte StrtBit EndBit byteLength Encoding
//Cld. Mnfst.ID 200 bit 0 bit 15 2 bytes BCD
bcdEncoded := bcd.FromUint16(uint16(um.ID))
new_vod[200-choppedLengthOffset] = bcdEncoded[0]
new_vod[201-choppedLengthOffset] = bcdEncoded[1]
//Year 202 bit 0 bit 15 2 bytes BCD
bcdEncoded = bcd.FromUint16(uint16(versionInfo.Year))
new_vod[202-choppedLengthOffset] = bcdEncoded[0]
new_vod[203-choppedLengthOffset] = bcdEncoded[1]
//Month 204 bit 0 bit 7 1 byte BCD
var bcdByte byte
bcdByte = bcd.FromUint8(uint8(versionInfo.Month))
new_vod[204-choppedLengthOffset] = bcdByte
//Major V. 205 bit 0 bit 7 1 byte BCD
bcdByte = bcd.FromUint8(uint8(versionInfo.MajorVersion))
new_vod[205-choppedLengthOffset] = bcdByte
//Minor V. 206 bit 0 bit 7 1 byte BCD
bcdByte = bcd.FromUint8(uint8(versionInfo.MinorVersion))
new_vod[206-choppedLengthOffset] = bcdByte
//Eng. Rls. 207 bit 0 bit 7 1 byte binary: 0x00 Formal, 0x01 Engineering
if versionInfo.EngineeringRelease {
new_vod[207-choppedLengthOffset] = 0x01
}
helper := v.NewVODHelper(lengthInCRC,
crcInLength,
lengthInLength)
new_vod = helper.AddLengthAndCRC(new_vod)
um.VOD = hex.EncodeToString(new_vod)
return
}
// Given 2023.05.01.02.E parses that up.
// No ID in beginning
type version struct {
Year int
Month int
MajorVersion int
MinorVersion int
EngineeringRelease bool // True if .E is present
}
func (um *UpdateManifest) parseVersionNumber() (v version, err error) {
vals := strings.Split(um.SUMS, ".")
if len(vals) < 4 {
err = errors.Errorf("invalid version %s", um.Version)
return
}
v.Year, err = strconv.Atoi(vals[0])
if err != nil {
return
}
v.Month, err = strconv.Atoi(vals[1])
if err != nil {
return
}
v.MajorVersion, err = strconv.Atoi(vals[2])
if err != nil {
return
}
v.MinorVersion, err = strconv.Atoi(vals[3])
if err != nil {
return
}
if len(vals) > 4 && vals[4] == "E" {
v.EngineeringRelease = true
}
return
}
func (um *UpdateManifest) GetECUs() []string {
ecus := make([]string, len(um.ECUs))
for i, ecu := range um.ECUs {
ecus[i] = ecu.ECU
}
return ecus
}
func (um *UpdateManifest) getECU(name string) *UpdateManifestECU {
for i := range um.ECUs {
if um.ECUs[i].ECU == name {
return um.ECUs[i]
}
}
return nil
}
func (um *UpdateManifest) AddECUECCKeys(keys []ECCKeys) {
for i, key := range keys {
ecu := um.getECU(key.ECU)
if ecu != nil {
ecu.ECCKeys = &keys[i]
}
}
}
func (um *UpdateManifest) Clone() UpdateManifest {
copy := UpdateManifest{}
js, _ := json.Marshal(um)
json.Unmarshal(js, &copy)
return copy
}
// I think this code is a bit silly
func (um *UpdateManifest) ToUpdateConfigManifest() (configManifest UpdateConfigManifest) {
configManifest = UpdateConfigManifest{
CarUpdateID: um.CarUpdateID,
ECUs: []*UpdateConfigManifestECU{},
Type: um.Type,
VOD: um.VOD,
}
for _, ecu := range um.ECUs {
v := ecu.ToUpdateConfigManifestECU()
configManifest.ECUs = append(configManifest.ECUs, &v)
}
return
}
type UpdateManifestSearch struct {
Search string `json:"search" validate:"max=1024"`
UpdateManifest
}
func getDefaultUpdateDuration() int {
i, err := strconv.Atoi(envtool.GetEnv("DEFAULT_UPDATE_DURATION", "30"))
if err != nil {
return 30
}
return i
}
// FilterCompatibleECUs Expects the UpdateManifest to have a filled VOD field
// If the VOD field is not filled, this will return improper results
func (um *UpdateManifest)FilterCompatibleECUs(vin string){
fileFilter := ECUFileFilter{}
info, ok := vindecoder.DecodeVIN(vin)
if !ok {
// Hanlde that we are not okay
return
}
fileFilter.Trim, ok = vinTrimToFileTrim(info.Trim)
if !ok {
// Hanlde that we are not okay
return
}
side, ok := vod_decoder.GetDriverSideFromVOD(um.VOD)
if !ok {
return
}
fileFilter.DriveSide, ok = vodDriveSideToFileDriveSide(side)
if !ok {
return
}
for _, ecu := range um.ECUs {
ecu.FilterECUFiles(fileFilter)
}
}