From c323ab8fde212120d8d1914d453afeb55b3576e5 Mon Sep 17 00:00:00 2001 From: Roger Zanoni Date: Thu, 28 Dec 2023 20:07:05 -0300 Subject: Update HVAC app to use grpc-web instead of websockets Adapt the HTML5 applications to use kuksa.val service Bug-AGL: SPEC-4599 Signed-off-by: Roger Zanoni Change-Id: I3e36a6c08041db8fb59fd7f20497c1c156bbb2f7 --- .gitignore | 1 + package.json | 8 +- proto/types.proto | 288 ++++++++++++++++++++++++++++++++++++++++++++++++++ proto/val.proto | 115 ++++++++++++++++++++ src/index.html | 2 +- src/js/buttons.js | 33 +++--- src/js/fan_speed.js | 7 +- src/js/kuksa.js | 232 +++++++++++++++++++++++++++++----------- src/js/paths.js | 12 +-- src/js/seats.js | 7 +- src/js/temperature.js | 9 +- webpack.config.js | 3 + 12 files changed, 619 insertions(+), 98 deletions(-) create mode 100644 proto/types.proto create mode 100644 proto/val.proto diff --git a/.gitignore b/.gitignore index b887236..cbad92c 100644 --- a/.gitignore +++ b/.gitignore @@ -35,6 +35,7 @@ $RECYCLE.BIN/ .sourcemaps/ package-lock.json build +src/generated .DS_Store Thumbs.db diff --git a/package.json b/package.json index 06006e6..7a171b4 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,8 @@ "scripts": { "webpack": "webpack", "build": "webpack", - "start": "webpack-dev-server" + "start": "webpack-dev-server", + "generate-grpc": "mkdirp ./src/generated && protoc -I./proto types.proto val.proto --js_out=import_style=commonjs:./src/generated --grpc-web_out=import_style=commonjs,mode=grpcwebtext:./src/generated" }, "homepage": "https://git.automotivelinux.org/apps/html5-hvac/", "repository": { @@ -32,6 +33,11 @@ "webpack-dev-server": "^4.8.1" }, "dependencies": { + "google-protobuf": "^3.21.2", + "grpc-web": "^1.5.0", + "mkdirp": "^3.0.1", + "protoc-gen-grpc-web": "^1.4.2", + "protoc-gen-js": "^3.21.2", "uuid": "^9.0.0" } } diff --git a/proto/types.proto b/proto/types.proto new file mode 100644 index 0000000..8914e7a --- /dev/null +++ b/proto/types.proto @@ -0,0 +1,288 @@ +/******************************************************************************** + * Copyright (c) 2022 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License 2.0 which is available at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +syntax = "proto3"; + +// I added V1 as in databroker. Is this good practice? +package kuksa.val.v1; +import "google/protobuf/timestamp.proto"; + +option go_package = "kuksa/val/v1"; + +// Describes a VSS entry +// When requesting an entry, the amount of information returned can +// be controlled by specifying either a `View` or a set of `Field`s. +message DataEntry { + // Defines the full VSS path of the entry. + string path = 1; // [field: FIELD_PATH] + + // The value (datapoint) + Datapoint value = 2; // [field: FIELD_VALUE] + + // Actuator target (only used if the entry is an actuator) + Datapoint actuator_target = 3; // [field: FIELD_ACTUATOR_TARGET] + + // Metadata for this entry + Metadata metadata = 10; // [field: FIELD_METADATA] +} + +message Datapoint { + google.protobuf.Timestamp timestamp = 1; + + oneof value { + string string = 11; + bool bool = 12; + sint32 int32 = 13; + sint64 int64 = 14; + uint32 uint32 = 15; + uint64 uint64 = 16; + float float = 17; + double double = 18; + StringArray string_array = 21; + BoolArray bool_array = 22; + Int32Array int32_array = 23; + Int64Array int64_array = 24; + Uint32Array uint32_array = 25; + Uint64Array uint64_array = 26; + FloatArray float_array = 27; + DoubleArray double_array = 28; + } +} + +message Metadata { + // Data type + // The VSS data type of the entry (i.e. the value, min, max etc). + // + // NOTE: protobuf doesn't have int8, int16, uint8 or uint16 which means + // that these values must be serialized as int32 and uint32 respectively. + DataType data_type = 11; // [field: FIELD_METADATA_DATA_TYPE] + + // Entry type + EntryType entry_type = 12; // [field: FIELD_METADATA_ENTRY_TYPE] + + // Description + // Describes the meaning and content of the entry. + optional string description = 13; // [field: FIELD_METADATA_DESCRIPTION] + + // Comment [optional] + // A comment can be used to provide additional informal information + // on a entry. + optional string comment = 14; // [field: FIELD_METADATA_COMMENT] + + // Deprecation [optional] + // Whether this entry is deprecated. Can contain recommendations of what + // to use instead. + optional string deprecation = 15; // [field: FIELD_METADATA_DEPRECATION] + + // Unit [optional] + // The unit of measurement + optional string unit = 16; // [field: FIELD_METADATA_UNIT] + + // Value restrictions [optional] + // Restrict which values are allowed. + // Only restrictions matching the DataType {datatype} above are valid. + ValueRestriction value_restriction = 17; // [field: FIELD_METADATA_VALUE_RESTRICTION] + + // Entry type specific metadata + oneof entry_specific { + Actuator actuator = 20; // [field: FIELD_METADATA_ACTUATOR] + Sensor sensor = 30; // [field: FIELD_METADATA_SENSOR] + Attribute attribute = 40; // [field: FIELD_METADATA_ATTRIBUTE] + } +} + +/////////////////////// +// Actuator specific fields +message Actuator { + // Nothing for now +} + +//////////////////////// +// Sensor specific +message Sensor { + // Nothing for now +} + +//////////////////////// +// Attribute specific +message Attribute { + // Nothing for now. +} + +// Value restriction +// +// One ValueRestriction{type} for each type, since +// they don't make sense unless the types match +// +message ValueRestriction { + oneof type { + ValueRestrictionString string = 21; + // For signed VSS integers + ValueRestrictionInt signed = 22; + // For unsigned VSS integers + ValueRestrictionUint unsigned = 23; + // For floating point VSS values (float and double) + ValueRestrictionFloat floating_point = 24; + } +} + +message ValueRestrictionInt { + optional sint64 min = 1; + optional sint64 max = 2; + repeated sint64 allowed_values = 3; +} + +message ValueRestrictionUint { + optional uint64 min = 1; + optional uint64 max = 2; + repeated uint64 allowed_values = 3; +} + +message ValueRestrictionFloat { + optional double min = 1; + optional double max = 2; + + // allowed for doubles/floats not recommended + repeated double allowed_values = 3; +} + +// min, max doesn't make much sense for a string +message ValueRestrictionString { + repeated string allowed_values = 3; +} + +// VSS Data type of a signal +// +// Protobuf doesn't support int8, int16, uint8 or uint16. +// These are mapped to int32 and uint32 respectively. +// +enum DataType { + DATA_TYPE_UNSPECIFIED = 0; + DATA_TYPE_STRING = 1; + DATA_TYPE_BOOLEAN = 2; + DATA_TYPE_INT8 = 3; + DATA_TYPE_INT16 = 4; + DATA_TYPE_INT32 = 5; + DATA_TYPE_INT64 = 6; + DATA_TYPE_UINT8 = 7; + DATA_TYPE_UINT16 = 8; + DATA_TYPE_UINT32 = 9; + DATA_TYPE_UINT64 = 10; + DATA_TYPE_FLOAT = 11; + DATA_TYPE_DOUBLE = 12; + DATA_TYPE_TIMESTAMP = 13; + DATA_TYPE_STRING_ARRAY = 20; + DATA_TYPE_BOOLEAN_ARRAY = 21; + DATA_TYPE_INT8_ARRAY = 22; + DATA_TYPE_INT16_ARRAY = 23; + DATA_TYPE_INT32_ARRAY = 24; + DATA_TYPE_INT64_ARRAY = 25; + DATA_TYPE_UINT8_ARRAY = 26; + DATA_TYPE_UINT16_ARRAY = 27; + DATA_TYPE_UINT32_ARRAY = 28; + DATA_TYPE_UINT64_ARRAY = 29; + DATA_TYPE_FLOAT_ARRAY = 30; + DATA_TYPE_DOUBLE_ARRAY = 31; + DATA_TYPE_TIMESTAMP_ARRAY = 32; +} + +// Entry type +enum EntryType { + ENTRY_TYPE_UNSPECIFIED = 0; + ENTRY_TYPE_ATTRIBUTE = 1; + ENTRY_TYPE_SENSOR = 2; + ENTRY_TYPE_ACTUATOR = 3; +} + +// A `View` specifies a set of fields which should +// be populated in a `DataEntry` (in a response message) +enum View { + VIEW_UNSPECIFIED = 0; // Unspecified. Equivalent to VIEW_CURRENT_VALUE unless `fields` are explicitly set. + VIEW_CURRENT_VALUE = 1; // Populate DataEntry with value. + VIEW_TARGET_VALUE = 2; // Populate DataEntry with actuator target. + VIEW_METADATA = 3; // Populate DataEntry with metadata. + VIEW_FIELDS = 10; // Populate DataEntry only with requested fields. + VIEW_ALL = 20; // Populate DataEntry with everything. +} + +// A `Field` corresponds to a specific field of a `DataEntry`. +// +// It can be used to: +// * populate only specific fields of a `DataEntry` response. +// * specify which fields of a `DataEntry` should be set as +// part of a `Set` request. +// * subscribe to only specific fields of a data entry. +// * convey which fields of an updated `DataEntry` have changed. +enum Field { + FIELD_UNSPECIFIED = 0; // "*" i.e. everything + FIELD_PATH = 1; // path + FIELD_VALUE = 2; // value + FIELD_ACTUATOR_TARGET = 3; // actuator_target + FIELD_METADATA = 10; // metadata.* + FIELD_METADATA_DATA_TYPE = 11; // metadata.data_type + FIELD_METADATA_DESCRIPTION = 12; // metadata.description + FIELD_METADATA_ENTRY_TYPE = 13; // metadata.entry_type + FIELD_METADATA_COMMENT = 14; // metadata.comment + FIELD_METADATA_DEPRECATION = 15; // metadata.deprecation + FIELD_METADATA_UNIT = 16; // metadata.unit + FIELD_METADATA_VALUE_RESTRICTION = 17; // metadata.value_restriction.* + FIELD_METADATA_ACTUATOR = 20; // metadata.actuator.* + FIELD_METADATA_SENSOR = 30; // metadata.sensor.* + FIELD_METADATA_ATTRIBUTE = 40; // metadata.attribute.* +} + +// Error response shall be an HTTP-like code. +// Should follow https://www.w3.org/TR/viss2-transport/#status-codes. +message Error { + uint32 code = 1; + string reason = 2; + string message = 3; +} + +// Used in get/set requests to report errors for specific entries +message DataEntryError { + string path = 1; // vss path + Error error = 2; +} + +message StringArray { + repeated string values = 1; +} + +message BoolArray { + repeated bool values = 1; +} + +message Int32Array { + repeated sint32 values = 1; +} + +message Int64Array { + repeated sint64 values = 1; +} + +message Uint32Array { + repeated uint32 values = 1; +} + +message Uint64Array { + repeated uint64 values = 1; +} + +message FloatArray { + repeated float values = 1; +} + +message DoubleArray { + repeated double values = 1; +} diff --git a/proto/val.proto b/proto/val.proto new file mode 100644 index 0000000..2d65b7c --- /dev/null +++ b/proto/val.proto @@ -0,0 +1,115 @@ +/******************************************************************************** + * Copyright (c) 2022 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License 2.0 which is available at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +syntax = "proto3"; + +package kuksa.val.v1; + +option go_package = "kuksa/val/v1"; + +import "types.proto"; + +// Note on authorization: +// Tokens (auth-token or auth-uuid) are sent as (GRPC / http2) metadata. +// +// The auth-token is a JWT compliant token as the examples found here: +// https://github.com/eclipse/kuksa.val/tree/master/kuksa_certificates/jwt +// +// See also https://github.com/eclipse/kuksa.val/blob/master/doc/jwt.md +// +// Upon reception of auth-token, server shall generate an auth-uuid in metadata +// that the client can use instead of auth-token in subsequent calls. + +service VAL { + // Get entries + rpc Get(GetRequest) returns (GetResponse); + + // Set entries + rpc Set(SetRequest) returns (SetResponse); + + // Subscribe to a set of entries + // + // Returns a stream of notifications. + // + // InvalidArgument is returned if the request is malformed. + rpc Subscribe(SubscribeRequest) returns (stream SubscribeResponse); + + // Shall return information that allows the client to determine + // what server/server implementation/version it is talking to + // eg. kuksa-databroker 0.5.1 + rpc GetServerInfo(GetServerInfoRequest) returns (GetServerInfoResponse); +} + +// Define which data we want +message EntryRequest { + string path = 1; + View view = 2; + repeated Field fields = 3; +} + +// Request a set of entries. +message GetRequest { + repeated EntryRequest entries = 1; +} + +// Global errors are specified in `error`. +// Errors for individual entries are specified in `errors`. +message GetResponse { + repeated DataEntry entries = 1; + repeated DataEntryError errors = 2; + Error error = 3; +} + +// Define the data we want to set +message EntryUpdate { + DataEntry entry = 1; + repeated Field fields = 2; +} + +// A list of entries to be updated +message SetRequest { + repeated EntryUpdate updates = 1; +} + +// Global errors are specified in `error`. +// Errors for individual entries are specified in `errors`. +message SetResponse { + Error error = 1; + repeated DataEntryError errors = 2; +} + +// Define what to subscribe totime +message SubscribeEntry { + string path = 1; + View view = 2; + repeated Field fields = 3; +} + +// Subscribe to changes in datapoints. +message SubscribeRequest { + repeated SubscribeEntry entries = 1; +} + +// A subscription response +message SubscribeResponse { + repeated EntryUpdate updates = 1; +} + +message GetServerInfoRequest { + // Nothing yet +} + +message GetServerInfoResponse { + string name = 1; + string version = 2; +} diff --git a/src/index.html b/src/index.html index c8a5eed..ce61ba0 100644 --- a/src/index.html +++ b/src/index.html @@ -27,7 +27,7 @@
- +
diff --git a/src/js/buttons.js b/src/js/buttons.js index 76d2143..c593445 100644 --- a/src/js/buttons.js +++ b/src/js/buttons.js @@ -14,15 +14,6 @@ * limitations under the License. */ - - -var values = { - ac: false, - recirculation: false, - rear: false, - front: false -}; - var paths = { ac: PATHS.airConditioning, recirculation: PATHS.recirculation, @@ -30,7 +21,7 @@ var paths = { front: PATHS.frontDefroster, }; -var nodes = {} +var nodes = new Map(); export function init() { nodes[PATHS.airConditioning] = document.getElementById('ac'); @@ -40,10 +31,15 @@ export function init() { nodes['up'] = document.getElementById('up'); nodes['down'] = document.getElementById('down'); nodes['right'] = document.getElementById('right'); + + nodes.forEach(function(node, path) { + node.setAttribute('value', false); + }); } -export function update(path, value) { +export function update(path, dp) { if (path == PATHS.leftAirDistribution) { + var value = dp.getString(); if (value == 'UP') { nodes['up'].setAttribute('value', true); nodes['down'].setAttribute('value', false); @@ -60,20 +56,25 @@ export function update(path, value) { } } else { var node = nodes[path]; + var value = dp.getBool(); node.setAttribute('value', value); } } export function toggle(node) { var key = node.getAttribute('key'); - values[key] = !values[key]; + if (KUKSA.isLocked(paths[key])) { + return; + } + if (key == 'up') { - KUKSA.set(PATHS.leftAirDistribution, 'UP'); + KUKSA.setString(PATHS.leftAirDistribution, 'UP'); } else if (key == 'down') { - KUKSA.set(PATHS.leftAirDistribution, 'DOWN'); + KUKSA.setString(PATHS.leftAirDistribution, 'DOWN'); } else if (key == 'right') { - KUKSA.set(PATHS.leftAirDistribution, 'MIDDLE'); + KUKSA.setString(PATHS.leftAirDistribution, 'MIDDLE'); } else { - KUKSA.set(paths[key], values[key]); + var value = node.getAttribute('value').toLowerCase() == 'true'; + KUKSA.setBool(paths[key], !value); } } diff --git a/src/js/fan_speed.js b/src/js/fan_speed.js index fa0ed01..74e4748 100644 --- a/src/js/fan_speed.js +++ b/src/js/fan_speed.js @@ -17,7 +17,8 @@ var value = 0; var node = null; -export function update(path, value) { +export function update(path, dp) { + var value = dp.getUint32(); node.value = value; node.parentNode.getElementsByTagName('progress')[0].value = value; } @@ -26,6 +27,6 @@ export function init() { node = document.getElementById('FanSpeed'); } -export function set() { - KUKSA.set(PATHS.leftFanSpeed, node.value); +export function set(value) { + KUKSA.setUInt32(PATHS.leftFanSpeed, value); } diff --git a/src/js/kuksa.js b/src/js/kuksa.js index 9e9cc22..8542dcb 100644 --- a/src/js/kuksa.js +++ b/src/js/kuksa.js @@ -16,108 +16,214 @@ import { v4 as uuidv4 } from 'uuid'; -const DEFAULT_TARGET = "wss://localhost:8090"; +const DEFAULT_TARGET = "https://localhost:8888"; // TODO: use an application token when needed // currently using https://github.com/eclipse/kuksa.val/blob/master/kuksa_certificates/jwt/super-admin.json.token const TEST_TOKEN = - 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJrdWtzYS52YWwiLCJpc3MiOiJFY2xpcHNlIEtVS1NBIERldiIsImFkbWluIjp0cnVlLCJtb2RpZnlUcmVlIjp0cnVlLCJpYXQiOjE1MTYyMzkwMjIsImV4cCI6MTc2NzIyNTU5OSwia3Vrc2EtdnNzIjp7IioiOiJydyJ9fQ.p2cnFGH16QoQ14l6ljPVKggFXZKmD-vrw8G6Vs6DvAokjsUG8FHh-F53cMsE-GDjyZH_1_CrlDCnbGlqjsFbgAylqA7IAJWp9_N6dL5p8DHZTwlZ4IV8L1CtCALs7XVqvcQKHCCzB63Y8PgVDCAqpQSRb79JPVD4pZwkBKpOknfEY5y9wfbswZiRKdgz7o61_oFnd-yywpse-23HD6v0htThVF1SuGL1PuvGJ8p334nt9bpkZO3gaTh1xVD_uJMwHzbuBCF33_f-I5QMZO6bVooXqGfe1zvl3nDrPEjq1aPulvtP8RgREYEqE6b2hB8jouTiC_WpE3qrdMw9sfWGFbm04qC-2Zjoa1yYSXoxmYd0SnliSYHAad9aXoEmFENezQV-of7sc-NX1-2nAXRAEhaqh0IRuJwB4_sG7SvQmnanwkz-sBYxKqkoFpOsZ6hblgPDOPYY2NAsZlYkjvAL2mpiInrsmY_GzGsfwPeAx31iozImX75rao8rm-XucAmCIkRlpBz6MYKCjQgyRz3UtZCJ2DYF4lKqTjphEAgclbYZ7KiCuTn9HualwtEmVzHHFneHMKl7KnRQk-9wjgiyQ5nlsVpCCblg6JKr9of4utuPO3cBvbjhB4_ueQ40cpWVOICcOLS7_w0i3pCq1ZKDEMrYDJfz87r2sU9kw1zeFQk'; + 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJsb2NhbCBkZXYiLCJpc3MiOiJjcmVhdGVUb2tlbi5weSIsImF1ZCI6WyJrdWtzYS52YWwiXSwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE3NjcyMjU1OTksInNjb3BlIjoiYWN0dWF0ZSBwcm92aWRlIn0.x-bUZwDCC663wGYrWCYjQZwQWhN1CMuKgxuIN5dUF_izwMutiqF6Xc-tnXgZa93BbT3I74WOMk4awKHBUSTWekGs3-qF6gajorbat6n5180TOqvNu4CXuIPZN5zpngf4id3smMkKOT699tPnSEbmlkj4vk-mIjeOAU-FcYA-VbkKBTsjvfFgKa2OdB5h9uZARBg5Rx7uBN3JsH1I6j9zoLid184Ewa6bhU2qniFt5iPsGJniNsKsRrrndN1KzthO13My44s56yvwSHIOrgDGbXdja_eLuOVOq9pHCjCtorPScgEuUUE4aldIuML-_j397taNP9Y3VZYVvofEK7AuiePTbzwxrZ1RAjK74h1-4ued3A2gUTjr5BsRlc9b7eLZzxLJkrqdfGAzBh_rtrB7p32TbvpjeFP30NW6bB9JS43XACUUm_S_RcyI7BLuUdnFyQDQr6l6sRz9XayYXceilHdCxbAVN0HVnBeui5Bb0mUZYIRZeY8k6zcssmokANTD8ZviDMpKlOU3t5AlXJ0nLkgyMhV9IUTwPUv6F8BTPc-CquJCUNbTyo4ywTSoODWbm3PmQ3Y46gWF06xqnB4wehLscBdVk3iAihQp3tckGhMnx5PI_Oy7utIncr4pRCMos63TnBkfrl7d43cHQTuK0kO76EWtv4ODEHgLvEAv4HA'; var kuksa_context = { authToken: TEST_TOKEN, - socket: undefined, + client: undefined, target: DEFAULT_TARGET, + subscribe_entries: [], + subscribe_stream: null, + locks: new Map() } var pathToHandler = null; -function logReceivedMessage(data) { - console.log('Received message:', data) +function updateVehicleInfo(path, dp) { + if (!pathToHandler.has(path)) { + console.log('Handler not found for', path); + return; + } + + var handler = pathToHandler.get(path); + handler.update(path, dp) } -function logConnectionStatus(status, event) { - console.log('Connection status:', status); +function send(action, values) { } -function kuksaws_onopen(event) { - logConnectionStatus('Connected.', event); - - authorize(); - subscribe(PATHS.leftAirDistribution); - subscribe(PATHS.leftFanSpeed); - subscribe(PATHS.leftSeatWarmer); - subscribe(PATHS.rightSeatWarmer); - subscribe(PATHS.leftSeatTemperature); - subscribe(PATHS.rightSeatTemperature); - subscribe(PATHS.airConditioning); - subscribe(PATHS.recirculation); - subscribe(PATHS.frontDefroster); - subscribe(PATHS.rearDefroster); +function add_subscribe_entry(path) { + console.log('Adding subscribe entry for ' + path + '...') + var entry = new VAL.SubscribeEntry(); + entry.setPath(path); + entry.setView(TYPES.View.VIEW_ALL); + entry.addFields(TYPES.Field.FIELD_PATH); + entry.addFields(TYPES.Field.FIELD_VALUE); + kuksa_context.subscribe_entries.push(entry); } -function kuksaws_onerror(event) { - logConnectionStatus('Failed to connect:', event); +function subscribe() { + if (kuksa_context.client == undefined) { + console.log("Client not initialized."); + return; + } + var metadata = {'authorization': 'Bearer ' + kuksa_context.authToken }; + + var request = new VAL.SubscribeRequest(); + for (var i in kuksa_context.subscribe_entries) { + var entry = kuksa_context.subscribe_entries[i]; + entry.setView(TYPES.View.VIEW_CURRENT_VALUE); + request.addEntries(entry); + } + + kuksa_context.subscribe_stream = kuksa_context.client.subscribe(request, metadata); + + kuksa_context.subscribe_stream.on('data', function(response) { + var updates = response.getUpdatesList(); + for (var i in updates) { + var update = updates[i]; + var entry = update.getEntry(); + var path = entry.getPath(); + var dp = entry.getValue(); + + if (!(entry && path && dp)) { + continue; + } + + lock(path); + updateVehicleInfo(path, dp); + unlock(path); + } + }); + + kuksa_context.subscribe_stream.on('error', function(error) { + console.log("Error code: " + error.code + " message: " + error.message); + // if an error happens here, the databroker will drop the subscriber, so + // we need to subscribe again + subscribe(); + }); } -function kuksaws_onmessage(event) { - logReceivedMessage(event.data); +// TODO: investigate why an empty response is always being returned on the +// response +export function get(path) { + if (kuksa_context.client == undefined) { + console.log("Client not initialized."); + return; + } + if (isLocked(path)) { + return; + } - var jsonData = JSON.parse(event.data); + var metadata = {'authorization': 'Bearer ' + kuksa_context.authToken }; + + var request = new VAL.GetRequest(); + var entry = new VAL.EntryRequest(); + entry.setPath(path); + entry.setView(TYPES.View.VIEW_ALL); + entry.addFields(TYPES.Field.FIELD_METADATA); + entry.addFields(TYPES.Field.FIELD_VALUE); + request.addEntries(entry); + + kuksa_context.client.get(request, metadata, function(error, response) { + if (error) { + console.log("Get error, code: " + error.code); + return; + } + if (!response) { + return; + } + var entries = response.getEntriesList(); + for (var i in entries) { + var entry = entries[i]; + var path = entry.getPath(); + var dp = entry.getValue(); + + if (!(entry && path && dp)) { + continue; + } + lock(path); + updateVehicleInfo(path, dp); + unlock(path); + } + }); +} - if (jsonData.action == 'get' || - jsonData.action =='subscription') { - updateVehicleInfo(jsonData); +export function set(path, dp) { + if (kuksa_context.client == undefined) { + console.log("Client not initialized."); + return; + } + if (isLocked(path)) { + return; } + lock(path); + + var metadata = {'authorization': 'Bearer ' + kuksa_context.authToken }; + + var entry = new TYPES.DataEntry(); + entry.setPath(path); + entry.setValue(dp); + + var update = new VAL.EntryUpdate(); + update.addFields(TYPES.Field.FIELD_PATH); + update.addFields(TYPES.Field.FIELD_VALUE); + update.setEntry(entry); + + var request = new VAL.SetRequest(); + request.addUpdates(update); + + kuksa_context.client.set(request, metadata, function(error, response) { + // don't unlock updates here, only when we get a value from + // the subscription updates + }); } -function updateVehicleInfo(message) { - var value = message.data.dp.value; - var path = message.data.path; +export function init(handlers) { + console.log("Initializing kuka-val module..."); + pathToHandler = new Map(handlers); + pathToHandler.forEach(function(handler, path) { + add_subscribe_entry(path); + }); - if (!pathToHandler.has(path)) { - console.log('Handler not found for', path); - return; + if (kuksa_context.client == undefined) { + console.log("Creating kuksa-val client..."); + kuksa_context.client = new VAL_WEB.VALClient(kuksa_context.target); + + subscribe(); } +} - var handler = pathToHandler.get(path); - handler.update(path, value) +export function setInt32(path, value) { + var dp = new TYPES.Datapoint(); + dp.setInt32(value); + set(path, dp); } -function send(action, values) { - var uuid = uuidv4(); - var data = Object.assign({ - action: action, - tokens: kuksa_context.authToken, - requestId: uuid, - }, values); - var message = JSON.stringify(data); - console.log('Sent message:', message); - kuksa_context.socket.send(message); - return uuid; +export function setUInt32(path, value) { + var dp = new TYPES.Datapoint(); + dp.setUint32(value); + set(path, dp); } -function subscribe(path) { - return send('subscribe', { path: path }); +export function setBool(path, value) { + var dp = new TYPES.Datapoint(); + dp.setBool(value); + set(path, dp); } -function authorize() { - return send('authorize'); +export function setString(path, value) { + var dp = new TYPES.Datapoint(); + dp.setString(value); + set(path, dp); } -export function get(path) { - return send('get', { path: path }); +export function lock(path) { + kuksa_context.locks[path] = true; } -export function set(path, value) { - return send('set', { path: path, value: value }); +export function unlock(path) { + kuksa_context.locks[path] = false; } -export function init(handlers) { - pathToHandler = new Map(handlers); - if (kuksa_context.socket == undefined) { - kuksa_context.socket = new WebSocket(kuksa_context.target); - kuksa_context.socket.onopen = kuksaws_onopen; - kuksa_context.socket.onerror = kuksaws_onerror; - kuksa_context.socket.onmessage = kuksaws_onmessage; +export function isLocked(path) { + if (!kuksa_context.locks.has(path)) { + kuksa_context.locks[path] = false; } + return kuksa_context.locks[path]; } diff --git a/src/js/paths.js b/src/js/paths.js index 4f3c5d3..3001b51 100644 --- a/src/js/paths.js +++ b/src/js/paths.js @@ -15,14 +15,14 @@ */ // https://github.com/COVESA/vehicle_signal_specification/blob/master/spec/Cabin/SingleHVACStation.vspec -export const leftAirDistribution = 'Vehicle.Cabin.HVAC.Station.Row1.Left.AirDistribution'; -export const leftFanSpeed = 'Vehicle.Cabin.HVAC.Station.Row1.Left.FanSpeed'; -export const leftSeatTemperature = 'Vehicle.Cabin.Seat.Row1.Pos1.Heating'; -export const rightSeatTemperature = 'Vehicle.Cabin.Seat.Row1.Pos2.Heating'; +export const leftAirDistribution = 'Vehicle.Cabin.HVAC.Station.Row1.Driver.AirDistribution'; +export const leftFanSpeed = 'Vehicle.Cabin.HVAC.Station.Row1.Driver.FanSpeed'; +export const leftSeatTemperature = 'Vehicle.Cabin.Seat.Row1.DriverSide.Heating'; +export const rightSeatTemperature = 'Vehicle.Cabin.Seat.Row1.PassengerSide.Heating'; // https://github.com/COVESA/vehicle_signal_specification/blob/master/spec/Cabin/SingleSeat.vspec -export const leftSeatWarmer = 'Vehicle.Cabin.Seat.Row1.Pos1.Switch.IsWarmerEngaged'; -export const rightSeatWarmer = 'Vehicle.Cabin.Seat.Row1.Pos2.Switch.IsWarmerEngaged'; +export const leftSeatWarmer = 'Vehicle.Cabin.Seat.Row1.DriverSide.Switch.IsWarmerEngaged'; +export const rightSeatWarmer = 'Vehicle.Cabin.Seat.Row1.PassengerSide.Switch.IsWarmerEngaged'; // https://github.com/COVESA/vehicle_signal_specification/blob/master/spec/Cabin/HVAC.vspec export const recirculation = 'Vehicle.Cabin.HVAC.IsRecirculationActive'; diff --git a/src/js/seats.js b/src/js/seats.js index 3afc481..91b45f6 100644 --- a/src/js/seats.js +++ b/src/js/seats.js @@ -30,7 +30,8 @@ export function init() { controls.rightSeatWarmer = document.getElementById('RightChair'); } -export function update(path, value) { +export function update(path, dp) { + var value = dp.getBool(); switch (path) { case PATHS.leftSeatWarmer: values.leftSeatWarmer = value; @@ -44,8 +45,8 @@ export function update(path, value) { } export function left() { - KUKSA.set(PATHS.leftSeatWarmer, !values.leftSeatWarmer); + KUKSA.setBool(PATHS.leftSeatWarmer, !values.leftSeatWarmer); } export function right() { - KUKSA.set(PATHS.rightSeatWarmer, !values.rightSeatWarmer); + KUKSA.setBool(PATHS.rightSeatWarmer, !values.rightSeatWarmer); } diff --git a/src/js/temperature.js b/src/js/temperature.js index 1e35ddf..2d29b7c 100644 --- a/src/js/temperature.js +++ b/src/js/temperature.js @@ -40,7 +40,8 @@ function createTemperatureElement() { return element; } -export function update(path, value) { +export function update(path, dp) { + var value = dp.getInt32(); var temperature, node; if (path == PATHS.rightSeatTemperature) { values.rightTemperature = value; @@ -69,8 +70,7 @@ export function left() { isScrolling = setTimeout(function() { var index = Math.round(controls.leftTemperatureNode.scrollTop / elementHeight); values.leftTemperature = temperatures[index]; - console.log('LEFT', values.leftTemperature); - KUKSA.set(PATHS.leftSeatTemperature, values.leftTemperature); + KUKSA.setInt32(PATHS.leftSeatTemperature, values.leftTemperature); }, 100); } @@ -80,8 +80,7 @@ export function right() { isScrolling = setTimeout(function() { var index = Math.round(controls.rightTemperatureNode.scrollTop / elementHeight); values.rightTemperature = temperatures[index]; - console.log('RIGHT', values.rightTemperature); - KUKSA.set(PATHS.rightSeatTemperature, values.rightTemperature); + KUKSA.setInt32(PATHS.rightSeatTemperature, values.rightTemperature); }, 100); } diff --git a/webpack.config.js b/webpack.config.js index 43bc54c..9a4ca38 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -13,6 +13,9 @@ module.exports = { SEATS: './src/js/seats.js', FANSPEED: './src/js/fan_speed.js', KUKSA: './src/js/kuksa.js', + VAL_WEB: './src/generated/val_grpc_web_pb.js', + VAL: './src/generated/val_pb.js', + TYPES: './src/generated/types_pb.js', BUTTONS: './src/js/buttons.js', TEMPERATURE: './src/js/temperature.js' }, -- cgit 1.2.3-korg