From 13b637356a92d68e5dfed6990443e6937f1ed9c6 Mon Sep 17 00:00:00 2001 From: Romain Forlot Date: Mon, 5 Jun 2017 19:16:27 +0200 Subject: Initial Commit Change-Id: I4a832208f3db9f5fece82f44cc957c6c1bb91d6c Signed-off-by: Romain Forlot --- .gitignore | 17 + .gitmodules | 4 + CMakeLists.txt | 31 ++ README.md | 111 ++++++ conf.d/config.cmake | 126 +++++++ conf.d/templates | 1 + high-can-binding/CMakeLists.txt | 43 +++ high-can-binding/high-can-binding-hat.cpp | 47 +++ high-can-binding/high-can-binding-hat.hpp | 17 + high-can-binding/high-can-binding.cpp | 74 ++++ high-can-binding/high.cpp | 543 ++++++++++++++++++++++++++++++ high-can-binding/high.hpp | 57 ++++ high.json | 115 +++++++ 13 files changed, 1186 insertions(+) create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 CMakeLists.txt create mode 100644 README.md create mode 100644 conf.d/config.cmake create mode 160000 conf.d/templates create mode 100644 high-can-binding/CMakeLists.txt create mode 100644 high-can-binding/high-can-binding-hat.cpp create mode 100644 high-can-binding/high-can-binding-hat.hpp create mode 100644 high-can-binding/high-can-binding.cpp create mode 100644 high-can-binding/high.cpp create mode 100644 high-can-binding/high.hpp create mode 100644 high.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..df894fc --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +.vscode +.kdev4 +*.kdev4 +*.swp +*.kate-swp +.vs +*.sln +*.vcxproj +*.user +obj +build +bin +src/configuration-generated.cpp +docs_doxygen +node_modules/ +packaging/ +_book/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..c87a462 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "conf.d/templates"] + path = conf.d/templates + url = https://gerrit.automotivelinux.org/gerrit/apps/app-templates + branch = reference diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..1902466 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,31 @@ +########################################################################### +# Copyright 2015, 2016, 2017 IoT.bzh +# +# author: Romain Forlot +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +########################################################################### + +CMAKE_MINIMUM_REQUIRED(VERSION 3.3) + +include(${CMAKE_CURRENT_SOURCE_DIR}/conf.d/config.cmake) +include(${CMAKE_CURRENT_SOURCE_DIR}/conf.d/templates/cmake/common.cmake) + +# Bindings to compile +# -------------------- +project_subdirs_add() + +project_targets_populate() +project_package_build() + +project_closing_msg() diff --git a/README.md b/README.md new file mode 100644 index 0000000..c007d2e --- /dev/null +++ b/README.md @@ -0,0 +1,111 @@ +# High level binding + +CAN binder, based upon ViWi definition. + +This binding is intended to act between the low-level binding and clients. It collects resources as defined in a json configuration file, and +implements subscribe/unsubscribe/get verbs for the clients. + +# build +```bash +mkdir build;cd build; cmake ..; make +``` +# launching +natively under linux you can launch afb-daemon with the low-level and high-level bindings with a command like: + +```bash +afb-daemon --rootdir=/CAN-binder/build/package --ldpaths=/CAN-binder/build/package/lib:/build/high-can-binding --port=1234 --tracereq=common --token=1 --verbose --verbose --verbose +``` + +#json configuration file +json configuration file (high.json) must be placed in the directory where you will launch afb-dameon +The json configuration file consists in 2 sections: + +## definitions section +This section describes each resources defined in the high-level binding. Each resource is composed with different properties having a name, a type and a description. +Type can be boolean, double, string, or int. Properties "id", "uri" and "name" are compulsory. +For instance: +```json +{ + "name": "/car/demoboard/", + "properties": { + "id": { + "type": "string", + "description": "identifier" + }, + "uri": { + "type": "string", + "description": "object uri" + }, + "name": { + "type": "string", + "description": "name" + }, + "unit": { + "type": "string", + "description": "units" + }, + "speed": { + "type": "double", + "description": "vehicle centerpoint speed as shown by the instrument cluster" + }, + "rpm": { + "type": "double", + "description": "engine rotations per minute" + }, + "level": { + "type": "double", + "description": "level of tankage" + }, + "load": { + "type": "double", + "description": "engine load" + } + } +} +``` +## resources section +This section defines which values should be assigned to resource's properties as defined in the definitions section. +The link to the definitions section is made through the name of the resource. +Some values are static, some are linked to low-level requests. +In case a value is linked to a low-level request, the value will start with "${" and end with "}". In that case the value will consist in the name of the low-level signal, followed +with the frequency of the signal in ms. -1 in the frequency means that high level binding should subscribe to low level binding for all changes, without specifying a frequency. +For instance: +```json +{ + "name": "/car/demoboard/", + "values": [{ + "name": "vehicleSpeed", + "unit": "km/h", + "speed": "${diagnostic_messages.vehicle.speed,1000}" + }, { + "name": "engineSpeed", + "unit": "rpm", + "rpm": "${diagnostic_messages.engine.speed,1000}" + }, { + "name": "fuelLevel", + "unit": "litre", + "level": "${diagnostic_messages.fuel.level,1000}" + }, { + "name": "engineLoad", + "unit": "Nm", + "load": "${diagnostic_messages.engine.load,1000}" + }] +} +``` +# Running and testing +You can use afb-client-demo to test high level binding. +For instance, once daemon has been launched with the 2 bindings: +```bash +afb-client-demo ws://localhost:1234/api?token=1 +``` +You can then use commands such as: +```bash +high-can subscribe {"name":"/car/doors/","interval":10000} +high-can unsubscribe {"name":"/car/doors/","interval":10000} +high-can get {"name":"/car/demoboard/"} +high-can get {"name":"/car/demoboard/","fields":["fuelLevel","engineLoad"]} +``` +You can also inject some data in CAN bus using canplayer (example of data can be find in low-level binding example directory) +```bash +canplayer -I highwaycomplete +``` diff --git a/conf.d/config.cmake b/conf.d/config.cmake new file mode 100644 index 0000000..6d7ea03 --- /dev/null +++ b/conf.d/config.cmake @@ -0,0 +1,126 @@ +########################################################################### +# Copyright 2015, 2016, 2017 IoT.bzh +# +# author: Fulup Ar Foll +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +########################################################################### + +# Project Info +# ------------------ +set(NAME high-can-project) +set(VERSION "1.0") +set(PRETTY_NAME "High level CAN binding") +set(DESCRIPTION "Expose CAN Low Level APIs through AGL Framework") +set(URL "https://github.com/iotbzh/CAN_signaling") +set(PROJECT_ICON "icon.png") +set(PROJECT_AUTHOR "Last Name, First Name") +set(PROJECT_AUTHOR_MAIL "example.man@bigouden.bzh") +set(PROJECT_LICENCE "APL2.0") +set(PROJECT_LANGUAGES,"C") + +# Where are stored default templates files from submodule or subtree app-templates in your project tree +# relative to the root project directory +set(PROJECT_APP_TEMPLATES_DIR "conf.d/templates") + +# Where are stored config.xml.in and icon.png.in files. Template available at : +# https://gerrit.automotivelinux.org/gerrit/#/admin/projects/apps/app-templates +# set(PROJECT_WGT_DIR "packaging/wgt") + +# Where are stored your external libraries for your project. This is 3rd party library that you don't maintain +# but used and must be built and linked. +# set(PROJECT_LIBDIR "libs") + +# Where are stored data for your application. Pictures, static resources must be placed in that folder. +# set(PROJECT_RESOURCES "data") + +# Compilation Mode (DEBUG, RELEASE) +# ---------------------------------- +set(CMAKE_BUILD_TYPE "DEBUG") + +# Compiler selection if needed. Impose a minimal version. +# ----------------------------------------------- +set (gcc_minimal_version 4.9) + +# PKG_CONFIG required packages +# ----------------------------- +set (PKG_REQUIRED_LIST + json-c + libsystemd + afb-daemon +) + +# Static constante definition +# ----------------------------- +add_compile_options() + +# LANG Specific compile flags set for all build types +set(CMAKE_C_FLAGS "") +set(CMAKE_CXX_FLAGS "-std=c++11") + +# Print a helper message when every thing is finished +# ---------------------------------------------------- +#set(CLOSING_MESSAGE "") +#set(WIDGET_MESSAGE "Install widget file using in the target : afm-util install ${PROJECT_NAME}.wgt") + +# (BUG!!!) as PKG_CONFIG_PATH does not work [should be an env variable] +# --------------------------------------------------------------------- +set(CMAKE_INSTALL_PREFIX $ENV{HOME}/opt) +set(CMAKE_PREFIX_PATH ${CMAKE_INSTALL_PREFIX}/lib64/pkgconfig ${CMAKE_INSTALL_PREFIX}/lib/pkgconfig) +set(LD_LIBRARY_PATH ${CMAKE_INSTALL_PREFIX}/lib64 ${CMAKE_INSTALL_PREFIX}/lib) + +# Optional dependencies order +# --------------------------- +#set(EXTRA_DEPENDENCIES_ORDER) + +# Optional Extra global include path +# ----------------------------------- +#set(EXTRA_INCLUDE_DIRS) + +# Optional extra libraries +# ------------------------- +#set(EXTRA_LINK_LIBRARIES boost_system) + +# Optional force binding installation +# ------------------------------------ +# set(BINDINGS_INSTALL_PREFIX PrefixPath ) + +# Optional force widget prefix generation +# ------------------------------------------------ +# set(WIDGET_PREFIX DestinationPath) + +# Optional Widget entry point file. +# --------------------------------------------------------- + # This is the file that will be executed, loaded,... +# at launch time by the application framework + +# set(WIDGET_ENTRY_POINT EntryPoint_Path) + +# Optional Widget Mimetype specification +# -------------------------------------------------- +# Choose between : +# - application/x-executable +# - application/vnd.agl.url +# - application/vnd.agl.service +# - application/vnd.agl.native +# - text/vnd.qt.qml +# - text/html +# - application/vnd.agl.qml +# - application/vnd.agl.qml.hybrid +# - application/vnd.agl.html.hybrid +# +# set(WIDGET_TYPE MimeType) + +# Optional force binding Linking flag +# ------------------------------------ +# set(BINDINGS_LINK_FLAG LinkOptions ) diff --git a/conf.d/templates b/conf.d/templates new file mode 160000 index 0000000..d63a072 --- /dev/null +++ b/conf.d/templates @@ -0,0 +1 @@ +Subproject commit d63a072def64647022cff067aff65e957281bf43 diff --git a/high-can-binding/CMakeLists.txt b/high-can-binding/CMakeLists.txt new file mode 100644 index 0000000..e607bfe --- /dev/null +++ b/high-can-binding/CMakeLists.txt @@ -0,0 +1,43 @@ +########################################################################### +# Copyright 2015, 2016, 2017 IoT.bzh +# +# author: Fulup Ar Foll +# contrib: Romain Forlot +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +########################################################################### + +# Add target to project dependency list +PROJECT_TARGET_ADD(high-can-binding) + + # Define project Targets + add_library(${TARGET_NAME} MODULE ${TARGET_NAME}.cpp high-can-binding-hat.cpp high.cpp) + + # Binder exposes a unique public entry point + SET_TARGET_PROPERTIES(${TARGET_NAME} PROPERTIES + PREFIX "" + LABELS "BINDING" + LINK_FLAGS ${BINDINGS_LINK_FLAG} + OUTPUT_NAME ${TARGET_NAME} + ) + + # Library dependencies (include updates automatically) + TARGET_LINK_LIBRARIES(${TARGET_NAME} + ${link_libraries} + ) + + # installation directory + INSTALL(TARGETS ${TARGET_NAME} + LIBRARY DESTINATION ${BINDINGS_INSTALL_DIR}) + + #build_widget("BINDING") diff --git a/high-can-binding/high-can-binding-hat.cpp b/high-can-binding/high-can-binding-hat.cpp new file mode 100644 index 0000000..01f6ab1 --- /dev/null +++ b/high-can-binding/high-can-binding-hat.cpp @@ -0,0 +1,47 @@ +#include "high-can-binding-hat.hpp" +#include +/// Interface between the daemon and the binding +const struct afb_binding_interface *binder_interface; +extern "C" +{ + #include + struct afb_service srvitf; +}; +static const struct afb_verb_desc_v1 verbs[]= +{ + { .name= "subscribe", .session= AFB_SESSION_NONE, .callback= subscribe, .info= "subscribe to notification of CAN bus messages." }, + { .name= "unsubscribe", .session= AFB_SESSION_NONE, .callback= unsubscribe, .info= "unsubscribe a previous subscription." }, + { .name= "get", .session= AFB_SESSION_NONE, .callback= get, .info= "high can get viwi request." }, +}; + +static const struct afb_binding binding_desc { + AFB_BINDING_VERSION_1, + { + "High level CAN bus service", + "high-can", + verbs + } +}; + +const struct afb_binding *afbBindingV1Register (const struct afb_binding_interface *itf) +{ + binder_interface = itf; + NOTICE(binder_interface, "high level afbBindingV1Register"); + + return &binding_desc; +} +/// @brief Initialize the binding. +/// +/// @param[in] service Structure which represent the Application Framework Binder. +/// +/// @return Exit code, zero if success. +int afbBindingV1ServiceInit(struct afb_service service) +{ + srvitf = service; + //NOTICE(binder_interface, "before afb_daemon_require_api"); + //afb_daemon_require_api(binder_interface->daemon, "low-can", 1); + NOTICE(binder_interface, "high level binding is initializing"); + initHigh(service); + NOTICE(binder_interface, "high level binding is initialized and running"); + return 0; +} diff --git a/high-can-binding/high-can-binding-hat.hpp b/high-can-binding/high-can-binding-hat.hpp new file mode 100644 index 0000000..b91f083 --- /dev/null +++ b/high-can-binding/high-can-binding-hat.hpp @@ -0,0 +1,17 @@ +#pragma once +#include +#include +extern "C" +{ + #define AFB_BINDING_VERSION 1 + #include +}; + +extern "C" struct afb_binding_interface; +extern "C" struct afb_service srvitf; +extern const struct afb_binding_interface *binder_interface; + void subscribe(struct afb_req request); + void unsubscribe(struct afb_req request); + void get(struct afb_req request); + void initHigh(afb_service service); + int ticked(sd_event_source *source, unsigned long t, void *data); diff --git a/high-can-binding/high-can-binding.cpp b/high-can-binding/high-can-binding.cpp new file mode 100644 index 0000000..bbcc941 --- /dev/null +++ b/high-can-binding/high-can-binding.cpp @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2015, 2016 "IoT.bzh" + * Author "Romain Forlot" + * Author "Loic Collignon" + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "high-can-binding-hat.hpp" +#include +#include "high.hpp" +High high; + +/// @brief callback for receiving message from low binding. Treatment itself is made in High class. +extern void afbBindingV1ServiceEvent(const char *event, struct json_object *object) +{ + high.treatMessage(object); +} +/// @brief entry point for client subscription request. Treatment itself is made in High class. +void subscribe(struct afb_req request) +{ + if(high.subscribe(request)) + afb_req_success(request, NULL, NULL); + else + afb_req_fail(request, "error", NULL); +} + +/// @brief entry point for client un-subscription request. Treatment itself is made in High class. +void unsubscribe(struct afb_req request) +{ + if(high.unsubscribe(request)) + afb_req_success(request, NULL, NULL); + else + afb_req_fail(request, "error", NULL); +} + +/// @brief entry point for get requests. Treatment itself is made in High class. +void get(struct afb_req request) +{ + json_object *jobj; + if(high.get(request, &jobj)) { + afb_req_success(request, jobj, NULL); + } else { + afb_req_fail(request, "error", NULL); + } +} + +/// @brief entry point for systemD timers. Treatment itself is made in High class. +/// @param[in] source: systemD timer, t: time of tick, data: interval (ms). +int ticked(sd_event_source *source, long unsigned int t, void* data) +{ + high.tick(source, t, data); + return 0; +} + +/// @brief Initialize the binding. +/// +/// @param[in] service Structure which represent the Application Framework Binder. +void initHigh(struct afb_service service) +{ + high.parseConfigAndSubscribe(service); +} + + diff --git a/high-can-binding/high.cpp b/high-can-binding/high.cpp new file mode 100644 index 0000000..82f95cb --- /dev/null +++ b/high-can-binding/high.cpp @@ -0,0 +1,543 @@ +/** +TYPICAL COMMANDS: +high-can start +high-can get {"name":"/car/doors/"} +high-can get {"name":"/car/doors/6078d4-23b89b-8faaa9-8bd0f3"} +high-can get {"name":"/car/doors/","fields":["isDoorOpen"]} +high-can subscribe {"name":"/car/doors/6078d4-23b89b-8faaa9-8bd0f3"} note: fields parameter on subscribe is not implemented yet +high-can unsubscribe {"name":"/car/doors/6078d4-23b89b-8faaa9-8bd0f3"} +high-can subscribe {"name":"/car/doors/"} +high-can subscribe {"name":"/car/doors/6078d4-23b89b-8faaa9-8bd0f3","interval":5000} +high-can subscribe {"name":"/car/doors/","interval":5000} +high-can unsubscribe {"name":"/car/doors/","interval":5000} +*/ + +#include +#include +#include "high.hpp" +#include "high-can-binding-hat.hpp" +#include +#include +#include +/// @brief Split a std::string in several string, based on a delimeter +/// +/// @param[in] string: the string to be splitted, delim: the delimeter to use for splitting. +/// +/// @return std::vector : a vector containing each individual string after the split. +using namespace std; +template +void split(const std::string &s, char delim, Out result) { + std::stringstream ss; + ss.str(s); + std::string item; + while (std::getline(ss, item, delim)) { + *(result++) = item; + } +} +std::vector split(const std::string &s, char delim) { + std::vector elems; + split(s, delim, std::back_inserter(elems)); + return elems; +} + +/// @brief Main high binding class: maintains resources status, subcriptions and timers +High::High() +{ +} + +/// @brief Reads the json configuration and generates accordingly the resources container. An UID is generated for each resource. +/// Makes necessary subscriptions to low-level, eventually with a frequency. +/// +/// @param[in] afb_service: the service to call for subscriptions. +/// +void High::parseConfigAndSubscribe(struct afb_service service) +{ + json_object *config = json_object_from_file("high.json"); + json_object *jvalue, *jarray1, *jarray2, *obj; + std::map> properties; + + json_object_object_get_ex(config, "definitions", &jarray1); + int arraylen1 = json_object_array_length(jarray1); + for(int n = 0; n < arraylen1; ++n) { + obj = json_object_array_get_idx(jarray1, n); + json_object_object_get_ex(obj, "name", &jvalue); + const std::string name = json_object_get_string(jvalue); + json_object_object_get_ex(obj, "properties", &jarray2); + std::map props; + json_object_object_foreach(jarray2, key, val) { + Property p; + json_object_object_get_ex(val, "type", &jvalue); + p.type = json_object_get_string(jvalue); + json_object_object_get_ex(val, "description", &jvalue); + p.description = json_object_get_string(jvalue); + props[key] = p; + } + properties[name] = props; + } + json_object_object_get_ex(config, "resources", &jarray1); + arraylen1 = json_object_array_length(jarray1); + std::map toSubscribe; + for(int n = 0; n < arraylen1; ++n) { + obj = json_object_array_get_idx(jarray1, n); + json_object_object_get_ex(obj, "name", &jvalue); + const std::string name = json_object_get_string(jvalue); + json_object_object_get_ex(obj, "values", &jarray2); + const int arraylen2 = json_object_array_length(jarray2); + for(int i = 0; i < arraylen2; ++i) { + const std::string id = generateId(); + const std::string uri = name + id; + jvalue = json_object_array_get_idx(jarray2, i); + if(properties.find(name) == properties.end()) { + NOTICE(binder_interface, "Unable to find name %s in properties", name.c_str()); + continue; + } + const std::map props = properties[name]; + std::map localProps; //note that local props can have less members than defined. + localProps["id"] = props.at("id"); + localProps["name"] = props.at("name"); + localProps["uri"] = props.at("uri"); + localProps["id"].value_string = std::string(id); + localProps["uri"].value_string = std::string(uri); + json_object_object_foreach(jvalue, key, val) { + const std::string value = json_object_get_string(val); + if(props.find(key) == props.end()) { + NOTICE(binder_interface, "Unable to find key %s in properties", value.c_str()); + continue; + } + Property prop = props.at(key); + if(startsWith(value, "${")) { + const std::string canMessage = value.substr(2, value.size() - 1); + const std::vector params = split(canMessage, ','); + if(params.size() != 2) { + NOTICE(binder_interface, "Invalid CAN message definition %s", value.c_str()); + continue; + } + prop.lowMessageName = params.at(0); + prop.interval = stoi(params.at(1)); + if(toSubscribe.find(prop.lowMessageName) != toSubscribe.end()) { + if(toSubscribe.at(prop.lowMessageName) > prop.interval) + toSubscribe[prop.lowMessageName] = prop.interval; + } else { + toSubscribe[prop.lowMessageName] = prop.interval; + } + if(prop.type == "string") + prop.value_string = std::string("nul"); + else if(prop.type == "boolean") + prop.value_bool = false; + else if(prop.type == "double") + prop.value_double = 0.0; + else if(prop.type == "int") + prop.value_int = 0; + else + NOTICE(binder_interface, "ERROR 2! unexpected type in parseConfig %s %s", prop.description.c_str(), prop.type.c_str()); + } else { + prop.value_string= std::string(value); + } + localProps[key] = prop; + } + registeredObjects[uri] = localProps; + for(const auto &p : localProps) { + if(p.second.lowMessageName.size() > 0) { + std::set objectList; + if(lowMessagesToObjects.find(p.second.lowMessageName) != lowMessagesToObjects.end()) + objectList = lowMessagesToObjects.at(p.second.lowMessageName); + objectList.insert(uri); + lowMessagesToObjects[p.second.lowMessageName] = objectList; + } + } + } + } + for(const auto &p : toSubscribe) { + json_object *jobj = json_object_new_object(); + json_object_object_add(jobj,"event", json_object_new_string(p.first.c_str())); + if(p.second > 0) { + json_object *filter = json_object_new_object(); + json_object_object_add(filter, "frequency", json_object_new_double(1000.0 / (double)p.second)); + json_object_object_add(jobj, "filter", filter); + } + json_object *dummy; + const std::string js = json_object_get_string(jobj); + if(afb_service_call_sync(service, "low-can", "subscribe", jobj, &dummy) != 1) + NOTICE(binder_interface, "high-can subscription to low-can FAILED %s", js.c_str()); + else + NOTICE(binder_interface, "high-can subscribed to low-can %s", js.c_str()); + json_object_put(dummy); + } + json_object_put(config); + NOTICE(binder_interface, "configuration loaded"); +} + +/// @brief Create and start a systemD timer. Only one timer is created per frequency. +/// +/// @param[in] t: interval in ms. +/// +void High::startTimer(const int &t) +{ + if(timers.find(t) != timers.end()) + return; + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + sd_event_add_time(afb_daemon_get_event_loop(binder_interface->daemon), NULL, CLOCK_MONOTONIC, (ts.tv_sec + 1) * 1000000, 0, &ticked, new int(t)); +} +High::~High() +{ + timers.clear(); +} + +/// @brief callback called after subscription to low-level binding. +/// +void High::callBackFromSubscribe(void *handle, int iserror, struct json_object *result) +{ + NOTICE(binder_interface, "high level callBackFromSubscribe method called %s", json_object_get_string(result)); +} + +/// @brief Entry point for all timer events. Treats all requests linked to the specific timer frequency. +/// Restarts the timer, or cancels it if no requests are anymore linked to it. +/// +/// @param[in] source: systemD timer, now: tick timestamp, interv: specific timer interval in ms. +/// +void High::tick(sd_event_source *source, const long &now, void *interv) +{ + const int interval = *(int*)interv; + NOTICE(binder_interface, "tick! %d %ld", interval, now); + bool hasEvents = false; + if(timedEvents.find(interval) != timedEvents.end()) { + std::vector evts = timedEvents[interval]; + for(int i = (int)evts.size() - 1; i >= 0; --i) { + const TimedEvent e = evts.at(i); + std::map jsons; + for(const auto &pp : registeredObjects) { + if(startsWith(pp.first, e.name)) { + jsons[pp.first] = generateJson(pp.first); + } + } + json_object *j; + if(jsons.size() == 1) { + j = jsons[0]; + } else if(jsons.size() > 1) { + j = json_object_new_object(); + for(const auto &pp : jsons) + json_object_object_add(j, pp.first.c_str(), pp.second); + } + const int nbSubscribers = afb_event_push(e.event, j); + if(nbSubscribers == 0) { + afb_event_drop(e.event); + evts.erase(evts.begin() + i); + timedEvents[interval] = evts; + } + //NOTICE(binder_interface, "%s event pushed to %d subscribers", e.eventName.c_str(), nbSubscribers); + } + if(evts.size() > 0) + hasEvents = true; + } + if(hasEvents) { + sd_event_source_set_time(source, now + interval * 1000); + sd_event_source_set_enabled(source, SD_EVENT_ON); + } else { + //NOTICE(binder_interface, "timer removed %d", interval); + delete (int*)interv; + if(timers.find(interval) != timers.end()) { + timers.erase(interval); + } + sd_event_source_unref(source); + } +} + +/// @brief Entry point for low-binding events. Updates all resources linked to this event and eventually +/// sends back events to subscribers, if any. +/// +/// @param[in] message: json low-level message. +/// +void High::treatMessage(json_object *message) +{ + json_object *nameJson, *jvalue; + json_object_object_get_ex(message, "name", &nameJson); + json_object_object_get_ex(message, "value", &jvalue); + const std::string messageName(json_object_get_string(nameJson)); + if(lowMessagesToObjects.find(messageName) == lowMessagesToObjects.end()) { + NOTICE(binder_interface, "message not linked to any object %s", json_object_get_string(message)); + return; + } +// NOTICE(binder_interface, "message received %s", json_object_get_string(message)); + const std::set objects = lowMessagesToObjects.at(messageName); + std::vector candidateMessages; + for(const std::string &uri : objects) { + std::map properties = registeredObjects.at(uri); + std::string foundProperty; + for(const auto &p : properties) { + if(p.second.lowMessageName != messageName) + continue; + foundProperty = p.first; + candidateMessages.push_back(uri); + break; + } + + if(foundProperty.size() > 0) { + Property property = properties.at(foundProperty); + if(property.type == "boolean") + property.value_bool = json_object_get_boolean(jvalue); + else if(property.type == "string") + property.value_string = std::string(json_object_get_string(jvalue)); + else if(property.type == "double") + property.value_double = json_object_get_double(jvalue); + else if(property.type == "int") + property.value_int = json_object_get_int(jvalue); + else + NOTICE(binder_interface, "ERROR 3! unexpected type %s %s", property.description.c_str(), property.type.c_str()); + properties[foundProperty] = property; + registeredObjects[uri] = properties; + } + } +/** at that point all objects have been updated. Now lets see if we should also send back messages to our subscribers. */ + for(const std::string &m : candidateMessages) { + for(const auto &p : events) { + if(startsWith(m, p.first)) { + std::map jsons; + for(const auto &pp : registeredObjects) { + if(startsWith(pp.first, p.first)) { + jsons[pp.first] = generateJson(pp.first); + } + } + json_object *j; + if(jsons.size() == 1) { + j = jsons[0]; + } else if(jsons.size() > 1) { + j = json_object_new_object(); + for(const auto &pp : jsons) + json_object_object_add(j, pp.first.c_str(), pp.second); + } + const int nbSubscribers = afb_event_push(p.second, j); + if(nbSubscribers == 0) { + afb_event_drop(p.second); + events.erase(p.first); + } + } + } + } +} + +/// @brief Generate json message for a resource, in ViWi format. Based on resource definition extracted from json +/// configuration file. If vector "fields" is not empty, will included only properties present in the vector. +/// +/// @param[in] messageObject: resource's name, fields: list of properties to be included (NULL = all). +/// +/// @return jsonObject containing the resource status for this resource's name. +json_object *High::generateJson(const std::string &messageObject, std::vector *fields) +{ + json_object *json = json_object_new_object(); + const std::map props = registeredObjects.at(messageObject); + for(const auto &p : props) { + if(fields && fields->size() > 0 && p.first != "id" && p.first != "uri" && p.first != "name") { + if(std::find(fields->begin(), fields->end(), p.first) == fields->end()) + continue; + } + if(p.second.type == "string") { + const std::string value = p.second.value_string; + json_object_object_add(json, p.first.c_str(), json_object_new_string(value.c_str())); + } else if(p.second.type == "boolean") { + const bool value = p.second.value_bool; + json_object_object_add(json, p.first.c_str(), json_object_new_boolean(value)); + } else if(p.second.type == "double") { + const double value = p.second.value_double; + json_object_object_add(json, p.first.c_str(), json_object_new_double(value)); + } else if(p.second.type == "int") { + const int value = p.second.value_int; + json_object_object_add(json, p.first.c_str(), json_object_new_int(value)); + } else { + NOTICE(binder_interface, "ERROR 1! unexpected type %s %s %s", p.first.c_str(), p.second.description.c_str(), p.second.type.c_str()); + } + } + return json; +} + +/// @brief Generates a random UID +/// +/// @return string containing the generated UID. +std::string High::generateId() const +{ + char id[50]; + sprintf(id, "%x-%x-%x-%x", (rand()%(int)1e7 + 1), (rand()%(int)1e7 + 1), (rand()%(int)1e7 + 1), (rand()%(int)1e7 + 1)); + return std::string(id); +} + +/// @brief Entry point for subscribing to a resource. Can optionnally include a time interval in ms. +/// +/// @param[in] request: afb-request containing a json request, for instance {"name":"/car/demoboard/", "interval":1000} +/// +/// @return true if subscribed succeeded, false otherwise. +bool High::subscribe(afb_req request) +{ + /** /car/doors/3901a278-ba17-44d6-9aef-f7ca67c04840 */ + bool ok = false; + json_object *args = afb_req_json(request); + json_object *nameJson = NULL; + json_object *intervalJson = NULL; + if(!json_object_object_get_ex(args, "name", &nameJson)) + return false; + json_object_object_get_ex(args, "interval", &intervalJson); + int ms = -1; + if(intervalJson) + ms = json_object_get_int(intervalJson); + std::string message(json_object_get_string(nameJson)); + if(message.size() == 0) + return ok; + for(const auto &p : registeredObjects) { + if(startsWith(p.first, message)) { + afb_event event; + if(ms <= 0) { + if(events.find(message) != events.end()) { + event = events.at(message); + } else { + event = afb_daemon_make_event(binder_interface->daemon, p.first.c_str()); + events[message] = event; + } + if (afb_event_is_valid(event) && afb_req_subscribe(request, event) == 0) { + ok = true; + } + } else { + std::vector evts; + if(timedEvents.find(ms) != timedEvents.end()) + evts = timedEvents.at(ms); + afb_event afbEvent; + bool found = false; + for(const auto & e : evts) { + if(e.name == message) { + afbEvent = e.event; + found = true; + break; + } + } + if(!found) { + char ext[20]; + sprintf(ext, "_%d", ms); + std::string messageName = message + std::string(ext); + //NOTICE(binder_interface, "subscribe with interval %s", messageName.c_str()); + afbEvent = afb_daemon_make_event(binder_interface->daemon, messageName.c_str()); + if (!afb_event_is_valid(afbEvent)) { + NOTICE(binder_interface, "unable to create event"); + return false; + } + TimedEvent e; + e.name = message; + e.eventName = messageName; + e.event = afbEvent; + e.interval = ms; + evts.push_back(e); + timedEvents[ms] = evts; + } + if(afb_req_subscribe(request, afbEvent) == 0) { + ok = true; + } else { + if(!found) { + evts.erase(evts.end() - 1); + timedEvents[ms] = evts; + } + } + if(timedEvents.size() == 0) { + timers.clear(); + } else if(ok) { + startTimer(ms); + } + } + break; + } + } + return ok; +} + +/// @brief Entry point for unsubscribing to a resource. Can optionnally include a time interval in ms. +/// +/// @param[in] request: afb-request containing a json request, for instance {"name":"/car/demoboard/", "interval":1000} +/// +/// @return true if unsubscription succeeded, false otherwise. +bool High::unsubscribe(afb_req request) +{ + json_object *args = afb_req_json(request); + json_object *nameJson = NULL; + json_object *intervalJson = NULL; + if(!json_object_object_get_ex(args, "name", &nameJson)) + return false; + json_object_object_get_ex(args, "interval", &intervalJson); + int ms = -1; + if(intervalJson) + ms = json_object_get_int(intervalJson); + std::string message(json_object_get_string(nameJson)); + if(message.size() == 0) + return false; + if(ms <= 0) { + if(events.find(message) != events.end()) { + if(afb_req_unsubscribe(request, events.at(message)) == 0) + return true; + } + } else { + if(timedEvents.find(ms) == timedEvents.end()) + return false; + const auto evts = timedEvents.at(ms); + afb_event afbEvent; + bool found = false; + for(const auto & e : evts) { + if(e.name == message) { + afbEvent = e.event; + found = true; + break; + } + } + if(!found) + return false; + if(afb_req_unsubscribe(request, afbEvent) == 0) + return true; + } + return false; +} + +/// @brief entry point for get requests. Accepts an optional list of properties to be included. +/// +/// @param[in] request: afb-request containing a json request, for instance {"name":"/car/demoboard/", "fields":["vehicleSpeed"]}, +/// **json: a pointer to a json object to be used for the reply. +/// +/// @return true if get succeeded, false otherwise, and **json object generated with the reply. +bool High::get(afb_req request, json_object **json) +{ + json_object *args = afb_req_json(request); + json_object *nameJson; + json_object *fieldsJson; + if(!json_object_object_get_ex(args, "name", &nameJson)) + return false; + bool hasFields = json_object_object_get_ex(args, "fields", &fieldsJson); + std::vector fields; + if(hasFields) { + int arraylen = json_object_array_length(fieldsJson); + json_object * jvalue; + for(int i = 0; i < arraylen; ++i) { + jvalue = json_object_array_get_idx(fieldsJson, i); + fields.push_back(json_object_get_string(jvalue)); + } + } + const std::string name(json_object_get_string(nameJson)); + std::map jsons; + for(const auto &p : registeredObjects) { + if(startsWith(p.first, name)) + jsons[p.first] = generateJson(p.first, &fields); + } + if(jsons.size() == 0) { + return false; + } + json_object *j = json_object_new_object(); + for(const auto &p : jsons) { + json_object_object_add(j, p.first.c_str(), p.second); + } + *json = j; + return true; +} + +/// @brief Sub-routine (static) to check whether a string starts with another string +/// +/// @param[in] s: string to scan, val: string to start with. +/// +/// @return true if s starts with val, false otherwise. +bool High::startsWith(const std::string &s, const std::string &val) +{ + if(val.size() > s.size()) + return false; + return s.substr(0, val.size()) == val; +} diff --git a/high-can-binding/high.hpp b/high-can-binding/high.hpp new file mode 100644 index 0000000..c015f4a --- /dev/null +++ b/high-can-binding/high.hpp @@ -0,0 +1,57 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +extern "C" +{ + #include +}; + +struct TimedEvent { + int interval; + afb_event event; + std::string name; + std::string eventName; +}; +struct Property { + /** + * alternatively, instead of a value per type, we could use boost::any, or in c++17 variant. + */ + std::string type; + std::string description; + std::string lowMessageName; + int interval; + bool value_bool; + std::string value_string; + double value_double; + int value_int; +}; + +class High +{ +public: + High(); + void treatMessage(json_object *message); + bool subscribe(afb_req request); + bool unsubscribe(afb_req request); + bool get(afb_req request, json_object **json); + void tick(sd_event_source *source, const long &now, void *interv); + void startTimer(const int &t); + ~High(); + void parseConfigAndSubscribe(afb_service service); + static bool startsWith(const std::string &s, const std::string &val); + static void callBackFromSubscribe(void *handle, int iserror, json_object *result); +private: + std::map events; + std::map> timedEvents; + std::map> registeredObjects; + std::map> lowMessagesToObjects; + std::set timers; + std::string generateId() const; + json_object *generateJson(const std::string &messageObject, std::vector *fields = NULL); +}; diff --git a/high.json b/high.json new file mode 100644 index 0000000..fe91cc5 --- /dev/null +++ b/high.json @@ -0,0 +1,115 @@ +{ + "resources": [{ + "name": "/car/doors/", + "values": [{ + "name": "front_left", + "position": "front_left", + "isDoorOpen": "${messages.doors.front_left.open,-1}", + "isWindowOpen": "${messages.windows.front_left.open,-1}" + }, + { + "name": "front_right", + "position": "front_right", + "isDoorOpen": "${messages.doors.front_right.open,-1}", + "isWindowOpen": "${messages.windows.front_right.open,-1}" + }, + { + "name": "rear_left", + "position": "rear_left", + "isDoorOpen": "${messages.doors.rear_left.open,-1}", + "isWindowOpen": "${messages.windows.rear_left.open,-1}" + }, + { + "name": "rear_right", + "position": "rear_right", + "isDoorOpen": "${messages.doors.rear_right.open,-1}", + "isWindowOpen": "${messages.windows.rear_right.open,-1}" + } + ] + }, { + "name": "/car/demoboard/", + "values": [{ + "name": "vehicleSpeed", + "unit": "km/h", + "speed": "${diagnostic_messages.vehicle.speed,10000}" + }, { + "name": "engineSpeed", + "unit": "rpm", + "rpm": "${diagnostic_messages.engine.speed,10000}" + }, { + "name": "fuelLevel", + "unit": "litre", + "level": "${diagnostic_messages.fuel.level,10000}" + }, { + "name": "engineLoad", + "unit": "Nm", + "load": "${diagnostic_messages.engine.load,10000}" + }] + }], + + "definitions": [{ + "name": "/car/doors/", + "properties": { + "id": { + "type": "string", + "description": "identifier" + }, + "uri": { + "type": "string", + "description": "object uri" + }, + "name": { + "type": "string", + "description": "name" + }, + "position": { + "type": "string", + "description": "the vehicle door position" + }, + "isDoorOpen": { + "type": "boolean", + "description": "the door state; set to 'true' if the door is open" + }, + "isWindowOpen": { + "type": "boolean", + "description": "the window state of this door; set to 'true' if window is open; set to JSON-undefined if window state is unknown or invalid" + } + } + }, { + "name": "/car/demoboard/", + "properties": { + "id": { + "type": "string", + "description": "identifier" + }, + "uri": { + "type": "string", + "description": "object uri" + }, + "name": { + "type": "string", + "description": "name" + }, + "unit": { + "type": "string", + "description": "units" + }, + "speed": { + "type": "double", + "description": "vehicle centerpoint speed as shown by the instrument cluster" + }, + "rpm": { + "type": "double", + "description": "engine rotations per minute" + }, + "level": { + "type": "double", + "description": "level of tankage" + }, + "load": { + "type": "double", + "description": "engine load" + } + } + }] +} -- cgit 1.2.3-korg