-
Notifications
You must be signed in to change notification settings - Fork 1
/
device_template_service.go
402 lines (355 loc) · 14.9 KB
/
device_template_service.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
package services
import (
"context"
"fmt"
"strings"
"github.com/DIMO-Network/shared"
"github.com/DIMO-Network/vehicle-signal-decoding/internal/gateways"
"github.com/DIMO-Network/vehicle-signal-decoding/internal/core/appmodels"
common2 "github.com/ethereum/go-ethereum/common"
"github.com/volatiletech/null/v8"
pb "github.com/DIMO-Network/devices-api/pkg/grpc"
"github.com/DIMO-Network/vehicle-signal-decoding/internal/config"
"github.com/gofiber/fiber/v2"
"github.com/rs/zerolog"
"github.com/volatiletech/sqlboiler/v4/queries/qm"
"github.com/volatiletech/sqlboiler/v4/types"
"github.com/pkg/errors"
"github.com/volatiletech/sqlboiler/v4/boil"
"database/sql"
"github.com/DIMO-Network/vehicle-signal-decoding/internal/infrastructure/db/models"
)
//go:generate mockgen -source device_template_service.go -destination mocks/device_template_service_mock.go
type DeviceTemplateService interface {
StoreDeviceConfigUsed(ctx context.Context, address common2.Address, dbcURL, pidURL, settingURL, firmwareVersion *string) (*models.DeviceTemplateStatus, error)
ResolveDeviceConfiguration(c *fiber.Ctx, ud *pb.UserDevice, vehicle *gateways.VehicleInfo) (*appmodels.DeviceConfigResponse, string, error)
// todo: pass in a ResolveConfigRequest instead of pb.UserDevice - this is not tied to a user device
FindDirectDeviceToTemplateConfig(ctx context.Context, address common2.Address) *appmodels.DeviceConfigResponse
}
type deviceTemplateService struct {
db *sql.DB
log zerolog.Logger
settings *config.Settings
deviceDefSvc DeviceDefinitionsService
}
func NewDeviceTemplateService(database *sql.DB, deviceDefSvc DeviceDefinitionsService, log zerolog.Logger, settings *config.Settings) DeviceTemplateService {
return &deviceTemplateService{
db: database,
log: log,
settings: settings,
deviceDefSvc: deviceDefSvc,
}
}
// StoreDeviceConfigUsed stores the configurations that were used by the mobile app to apply onto the device
func (dts *deviceTemplateService) StoreDeviceConfigUsed(ctx context.Context, address common2.Address, dbcURL, pidURL, settingURL, firmwareVersion *string) (*models.DeviceTemplateStatus, error) {
dt, err := models.DeviceTemplateStatuses(models.DeviceTemplateStatusWhere.DeviceEthAddr.EQ(address.Bytes())).
One(ctx, dts.db)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return nil, err
}
if dt != nil {
// update - only set if not nil
if settingURL != nil {
dt.TemplateSettingsURL = null.StringFromPtr(settingURL)
}
if dbcURL != nil {
dt.TemplateDBCURL = null.StringFromPtr(dbcURL)
}
if pidURL != nil {
dt.TemplatePidURL = null.StringFromPtr(pidURL)
}
if firmwareVersion != nil {
fwv := *firmwareVersion
if len(fwv) > 1 {
if fwv[0:1] != "v" {
fwv = "v" + fwv
}
dt.FirmwareVersion = null.StringFrom(fwv)
}
}
if _, err = dt.Update(ctx, dts.db, boil.Infer()); err != nil {
return nil, err
}
} else {
// create
dt = &models.DeviceTemplateStatus{
DeviceEthAddr: address.Bytes(),
TemplateDBCURL: null.StringFromPtr(dbcURL),
TemplatePidURL: null.StringFromPtr(pidURL),
TemplateSettingsURL: null.StringFromPtr(settingURL),
FirmwareVersion: null.StringFromPtr(firmwareVersion),
}
if err = dt.Insert(ctx, dts.db, boil.Infer()); err != nil {
return nil, err
}
}
return dt, nil
}
// FindDirectDeviceToTemplateConfig retrieves the device configuration for a specific device address
func (dts *deviceTemplateService) FindDirectDeviceToTemplateConfig(ctx context.Context, address common2.Address) *appmodels.DeviceConfigResponse {
deviceToTemplate, err := models.AftermarketDeviceToTemplates(
models.AftermarketDeviceToTemplateWhere.AftermarketDeviceEthereumAddress.EQ(address.Bytes()),
qm.Load(models.AftermarketDeviceToTemplateRels.TemplateNameTemplate),
).One(ctx, dts.db)
if err != nil || deviceToTemplate == nil {
return nil
}
response := appmodels.DeviceConfigResponse{
PidURL: dts.buildConfigRoute(PIDs, deviceToTemplate.TemplateName, deviceToTemplate.R.TemplateNameTemplate.Version),
}
// only set dbc url if we have dbc
dbcFile, _ := models.FindDBCFile(ctx, dts.db, deviceToTemplate.TemplateName)
if dbcFile != nil {
response.DbcURL = dts.buildConfigRoute(DBC, deviceToTemplate.TemplateName, deviceToTemplate.R.TemplateNameTemplate.Version)
}
// use specific settings otherwise use fallback to first one
deviceSetting, _ := models.DeviceSettings(models.DeviceSettingWhere.TemplateName.EQ(null.StringFrom(deviceToTemplate.TemplateName))).One(ctx, dts.db)
if deviceSetting != nil {
response.DeviceSettingURL = dts.buildConfigRoute(Setting, deviceSetting.Name, deviceSetting.Version)
} else {
// fallback jic
deviceSetting, err = models.DeviceSettings().One(ctx, dts.db)
if err != nil {
dts.log.Error().Err(err).Msg("Failed to retrieve device settings for FindDirectDeviceToTemplateConfig")
} else if deviceSetting != nil {
response.DeviceSettingURL = dts.buildConfigRoute(Setting, deviceSetting.Name, deviceSetting.Version)
}
}
return &response
}
// ResolveDeviceConfiguration figures out what template to return based on protocol, powertrain, vehicle or definition (vehicle could be nil)
func (dts *deviceTemplateService) ResolveDeviceConfiguration(c *fiber.Ctx, ud *pb.UserDevice, vehicle *gateways.VehicleInfo) (*appmodels.DeviceConfigResponse, string, error) {
canProtocl := convertCANProtocol(dts.log, ud.CANProtocol)
// todo (jreate): what about powertrain at the style level... But ideally it is stored at vehicle level. this could come from oracle?
powertrain, err := dts.retrievePowertrain(c.Context(), ud.DeviceDefinitionId)
if err != nil {
return nil, "", errors.Wrap(err, fmt.Sprintf("Failed to retrieve powertrain for ddid: %s", ud.DeviceDefinitionId))
}
matchedTemplate, strategy, err := dts.selectAndFetchTemplate(c.Context(), canProtocl, powertrain, ud.DeviceDefinitionId, vehicle)
if err != nil {
return nil, strategy, err
}
if matchedTemplate == nil {
return nil, strategy, errors.New("matched template is nil")
}
response := appmodels.DeviceConfigResponse{
PidURL: dts.buildConfigRoute(PIDs, matchedTemplate.TemplateName, matchedTemplate.Version),
}
// only set dbc url if we have dbc
if matchedTemplate.R.TemplateNameDBCFile != nil && len(matchedTemplate.R.TemplateNameDBCFile.DBCFile) > 0 {
response.DbcURL = dts.buildConfigRoute(DBC, matchedTemplate.TemplateName, matchedTemplate.Version)
}
// set device settings from template, or based on powertrain default
if len(matchedTemplate.R.TemplateNameDeviceSettings) > 0 {
ds := matchedTemplate.R.TemplateNameDeviceSettings[0]
response.DeviceSettingURL = dts.buildConfigRoute(Setting, ds.Name, ds.Version)
} else {
var deviceSetting *models.DeviceSetting
var dbErr error
if matchedTemplate.ParentTemplateName.Valid {
deviceSetting, dbErr = models.DeviceSettings(models.DeviceSettingWhere.TemplateName.EQ(matchedTemplate.ParentTemplateName),
qm.OrderBy(models.DeviceSettingColumns.Name)).One(c.Context(), dts.db)
if dbErr != nil && !errors.Is(dbErr, sql.ErrNoRows) {
return nil, strategy, errors.Wrap(dbErr, "Failed to retrieve device setting for parent template")
}
}
if deviceSetting == nil {
var pt string
if ud.PowerTrainType != "" {
pt = ud.PowerTrainType
} else {
pt = matchedTemplate.Powertrain
}
// default will be whatever gets ordered first
deviceSetting, dbErr = models.DeviceSettings(models.DeviceSettingWhere.Powertrain.EQ(pt),
qm.OrderBy(models.DeviceSettingColumns.Name)).One(c.Context(), dts.db)
if errors.Is(dbErr, sql.ErrNoRows) {
// grab the first record in db
deviceSetting, dbErr = models.DeviceSettings(qm.OrderBy(models.DeviceSettingColumns.Name)).One(c.Context(), dts.db)
}
if dbErr != nil {
return nil, strategy, errors.Wrap(err, fmt.Sprintf("Failed to retrieve device setting. Powertrain: %s", pt))
}
}
// device settings have a name key separate from templateName since simpler setup
response.DeviceSettingURL = dts.buildConfigRoute(Setting, deviceSetting.Name, deviceSetting.Version)
}
return &response, strategy, nil
}
type configType string
const (
PIDs = "pids"
Setting = "settings"
DBC = "dbc"
)
func (dts *deviceTemplateService) buildConfigRoute(ct configType, name, version string) string {
return fmt.Sprintf("%s/v1/device-config/%s/%s@%s", dts.settings.DeploymentURL, ct, name, version)
}
// retrievePowertrain gets the powertrain for the device definition id from attributes, if empty defaults to ICE
func (dts *deviceTemplateService) retrievePowertrain(ctx context.Context, deviceDefinitionID string) (string, error) {
ddResponse, err := dts.deviceDefSvc.GetDeviceDefinitionByID(ctx, deviceDefinitionID)
if err != nil {
return "", fmt.Errorf("failed to retrieve device definition for deviceDefinitionId %s: %w", deviceDefinitionID, err)
}
var powerTrainType string
for _, attribute := range ddResponse.DeviceAttributes {
if attribute.Name == "powertrain_type" {
powerTrainType = attribute.Value
break
}
}
if powerTrainType == "" {
powerTrainType = "ICE"
}
return powerTrainType, nil
}
// selectAndFetchTemplate figures out the right template to use based on the protocol, powertrain, year range, make, and /or model.
// Returns default template if nothing found. Requirees ud.CANProtocol and Powertrain to be set to something
func (dts *deviceTemplateService) selectAndFetchTemplate(ctx context.Context, canProtocol, powertrain, definitionID string, vehicle *gateways.VehicleInfo) (*models.Template, string, error) {
strategy := "" // strategy used to find right template
// guard
if canProtocol == "" {
return nil, strategy, fmt.Errorf("CANProtocol is required in the user device")
}
if powertrain == "" {
return nil, strategy, fmt.Errorf("PowerTrainType is required in the user device")
}
var matchedTemplateName string
// First, try to find a template based on device definitions
deviceDefinitions, err := models.TemplateDeviceDefinitions(
models.TemplateDeviceDefinitionWhere.DeviceDefinitionID.EQ(definitionID),
).All(ctx, dts.db)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return nil, strategy, fmt.Errorf("failed to query template device definitions: %w", err)
}
if len(deviceDefinitions) > 0 {
matchedTemplateName = deviceDefinitions[0].TemplateName
strategy = "definition mapping"
}
year := 0
mk := ""
model := ""
if vehicle == nil {
definition, err := dts.deviceDefSvc.GetDeviceDefinitionByID(ctx, definitionID)
if err != nil {
return nil, strategy, errors.Wrapf(err, "failed to query device definition %s", definitionID)
}
year = int(definition.Type.Year)
mk = definition.Type.Make
model = definition.Type.Model
} else {
year = vehicle.Definition.Year
mk = vehicle.Definition.Make
model = vehicle.Definition.Model
}
// Second, try to find a template based on Year, then Make & Model
if matchedTemplateName == "" {
// compare by year first, then in memory below we'll look for make and/or model
templateVehicles, err := models.TemplateVehicles(
models.TemplateVehicleWhere.YearStart.LTE(year),
models.TemplateVehicleWhere.YearEnd.GTE(year),
qm.Load(models.TemplateVehicleRels.TemplateNameTemplate),
).All(ctx, dts.db)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return nil, strategy, fmt.Errorf("failed to query templates for vehicle: %s: %w", fmt.Sprintf("%d %s %s", year, mk, model), err)
}
if len(templateVehicles) > 0 {
strategy = "vehicle and year mapping"
}
// try finding a match by make and/or model
for _, tv := range templateVehicles {
// match by make and/or model
if tv.MakeSlug.String == shared.SlugString(mk) {
matchedTemplateName = tv.TemplateName
strategy += ", makeSlug match"
// now see if there is also a model slug match
if modelMatch(tv.ModelWhitelist, shared.SlugString(model)) {
strategy += ", model match"
break
}
}
}
// if no matches, try casting a wider net matching by protocol, but only for templates that don't have a make assigned
if matchedTemplateName == "" {
for _, tv := range templateVehicles {
if tv.MakeSlug.IsZero() {
// any matches for same protocol if nothing make or model specific
if tv.R.TemplateNameTemplate.Protocol == canProtocol {
matchedTemplateName = tv.TemplateName
strategy += ", protocol match"
}
}
}
}
}
// Third, default templates come into play: fallback to query by protocol, 'default' as first word, and powertrain
if matchedTemplateName == "" {
templates, err := models.Templates(
models.TemplateWhere.Protocol.EQ(canProtocol),
models.TemplateWhere.Powertrain.EQ(powertrain),
qm.Where("template_name like 'default%'"),
).All(ctx, dts.db)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return nil, strategy, fmt.Errorf("failed to query templates for protocol: %s and powertrain: %s: %w", canProtocol, powertrain, err)
}
if len(templates) == 0 {
return nil, strategy, fmt.Errorf("configuration error - no default template found for protocol: %s and powertrain: %s", canProtocol, powertrain)
}
if len(templates) > 0 {
matchedTemplateName = templates[0].TemplateName
strategy = "protocol and powertrain match, default"
}
if len(templates) > 1 {
dts.log.Warn().Msgf("more than one default template found for protocol: %s and powertrain: %s (%d templates found)", canProtocol, powertrain, len(templates))
}
}
// Fetch the template object if a name was found
matchedTemplate, err := models.Templates(
models.TemplateWhere.TemplateName.EQ(matchedTemplateName),
qm.Load(models.TemplateRels.TemplateNameDBCFile),
qm.Load(models.TemplateRels.TemplateNameDeviceSettings),
).One(ctx, dts.db)
if err != nil {
return nil, strategy, fmt.Errorf("failed to fetch template by name %s: %w", matchedTemplateName, err)
}
return matchedTemplate, strategy, nil
}
// modelMatch simply returns if the modelName is in the model List
func modelMatch(modelList types.StringArray, modelSlug string) bool {
for _, m := range modelList {
if strings.EqualFold(m, modelSlug) {
return true
}
}
return false
}
// convertCANProtocol converts autopi/macaron style Protocol (6 or 7) to our VSD style protocol (db enum), but always returning a default if nothing found
func convertCANProtocol(logger zerolog.Logger, canProtocolSimple string) string {
switch canProtocolSimple {
case "6":
return models.CanProtocolTypeCAN11_500
case "7":
return models.CanProtocolTypeCAN29_500
case "8":
return models.CanProtocolTypeCAN11_250
case "9":
return models.CanProtocolTypeCAN29_250
case "66":
// car supports UDS vin query
return models.CanProtocolTypeCAN11_500
case "77":
// car supports UDS vin query
return models.CanProtocolTypeCAN29_500
case "88":
// car supports UDS vin query
return models.CanProtocolTypeCAN11_250
case "99":
// car supports UDS vin query
return models.CanProtocolTypeCAN29_250
case "":
return models.CanProtocolTypeCAN11_500
default:
logger.Warn().Msgf("invalid protocol detected: %s", canProtocolSimple)
return models.CanProtocolTypeCAN11_500
}
}