From a0148012377f63d875dcc7c4ac9a83b2ff8f98ef Mon Sep 17 00:00:00 2001 From: Romain Forlot Date: Fri, 20 Jan 2017 15:46:33 +0000 Subject: [PATCH] Added needed files to easy compilation and output widget file to install on target. TODO: Add unit service file to start using systemd Change-Id: I347255fd54f48d01bf762db8b5a207fa5fa5cf7a Signed-off-by: Romain Forlot --- CMakeLists.txt | 87 +++++++++ canLL-binding.c | 569 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ config.xml.in | 11 ++ icon.png | Bin 0 -> 4035 bytes 4 files changed, 667 insertions(+) create mode 100644 CMakeLists.txt create mode 100644 canLL-binding.c create mode 100644 config.xml.in create mode 100644 icon.png diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 00000000..fc233f56 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,87 @@ +########################################################################### +# Copyright 2016 IoT.bzh +# +# author: José Bollo +# author: Stéphane Desneux +# +# 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(canLL) + +cmake_minimum_required(VERSION 3.3) + +include(GNUInstallDirs) + +set(PROJECT_VERSION "0.1") +set(PROJECT_ICON "icon.png") +set(PROJECT_LIBDIR "libs") + +set(CMAKE_BUILD_TYPE Debug) + +########################################################################### + +link_libraries(-Wl,--as-needed -Wl,--gc-sections) + +add_compile_options(-Wall -Wextra -Wconversion) +add_compile_options(-Wno-unused-parameter) # frankly not using a parameter does it care? +add_compile_options(-Werror=maybe-uninitialized) +add_compile_options(-Werror=implicit-function-declaration) +add_compile_options(-ffunction-sections -fdata-sections) +add_compile_options(-Wl,--as-needed -Wl,--gc-sections) +add_compile_options(-fPIC) + +set(CMAKE_C_FLAGS_PROFILING "-g -O0 -pg -Wp,-U_FORTIFY_SOURCE") +set(CMAKE_C_FLAGS_DEBUG "-g -O0 -ggdb -Wp,-U_FORTIFY_SOURCE") +set(CMAKE_C_FLAGS_RELEASE "-g -O2") +set(CMAKE_C_FLAGS_CCOV "-g -O2 --coverage") + +########################################################################### + +include(FindPkgConfig) + +pkg_check_modules(EXTRAS REQUIRED json-c afb-daemon) +add_compile_options(${EXTRAS_CFLAGS}) +include_directories(${EXTRAS_INCLUDE_DIRS} ${PROJECT_LIBDIR}/openxc-message-format/gen/cpp ${PROJECT_LIBDIR}/nanopb/) +link_libraries(${EXTRAS_LIBRARIES}) + +########################################################################### +# the binding for afb + +message(STATUS "Creation of ${PROJECT_NAME} binding for AFB-DAEMON") +########################################################################### +add_library(${PROJECT_NAME}-binding MODULE ${PROJECT_NAME}-binding.c) + +set_target_properties(${PROJECT_NAME}-binding PROPERTIES + PREFIX "" + LINK_FLAGS "-Wl,--version-script=${CMAKE_CURRENT_SOURCE_DIR}/export.map" +) + +########################################################################### +# WGT packaging + +message(STATUS "Creation of ${PROJECT_NAME}.wgt package for AppFW") +############################################################### +configure_file(config.xml.in config.xml) + +add_custom_command( + OUTPUT ${PROJECT_NAME}.wgt + DEPENDS ${PROJECT_NAME}-binding + COMMAND rm -rf package + COMMAND mkdir -p package/${PROJECT_LIBDIR} package/htdocs + COMMAND cp config.xml package/ + COMMAND cp ${CMAKE_CURRENT_SOURCE_DIR}/${PROJECT_ICON} package/icon.png + COMMAND cp ${PROJECT_NAME}-binding.so package + COMMAND wgtpkg-pack -f -o ${PROJECT_NAME}.wgt package +) +add_custom_target(widget ALL DEPENDS ${PROJECT_NAME}.wgt) diff --git a/canLL-binding.c b/canLL-binding.c new file mode 100644 index 00000000..342eb4f4 --- /dev/null +++ b/canLL-binding.c @@ -0,0 +1,569 @@ +/* + * Copyright (C) 2015, 2016 "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. + */ +#define _GNU_SOURCE + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +/*****************************************************************************************/ +/*****************************************************************************************/ +/** **/ +/** **/ +/** SECTION: GLOBAL VARIABLES **/ +/** **/ +/** **/ +/*****************************************************************************************/ +/*****************************************************************************************/ +/* max. number of CAN interfaces given on the cmdline */ +#define MAXSOCK 16 + +/* buffer sizes for CAN frame string representations */ +#define CL_ID (sizeof("12345678##1")) +#define CL_DATA sizeof(".AA") +#define CL_BINDATA sizeof(".10101010") + + /* CAN FD ASCII hex short representation with DATA_SEPERATORs */ +#define CL_CFSZ (2*CL_ID + 64*CL_DATA) + +#define CANID_DELIM '#' + +/* + * Interface between the daemon and the binding + */ +static const struct afb_binding_interface *interface; + +/* + * the type of position expected + * + * here, this type is the selection of protocol + */ +enum type { + type_OBDII, + type_CAN, + type_DEFAULT = type_CAN, + type_INVALID = -1 +}; + +#define type_size sizeof(enum type)-2 + +/* CAN variable initialization */ +struct canfd_frame canfd_frame; + +struct can_handler { + int socket; + char *device; + openxc_CanMessage *msg; + struct sockaddr_can txAddress; +}; + +/* + * each generated event + */ +struct event { + struct event *next; /* link for the same period */ + const char *name; /* name of the event */ + struct afb_event event; /* the event for the binder */ + enum type type; /* the type of data expected */ + int id; /* id of the event for unsubscribe */ +}; + +/*****************************************************************************************/ +/*****************************************************************************************/ +/** **/ +/** **/ +/** SECTION: UTILITY FUNCTIONS **/ +/** **/ +/** **/ +/*****************************************************************************************/ +/*****************************************************************************************/ + +/* + * @brief Retry a function 3 times + * + * @param int function(): function that return an int wihtout any parameter + * + * @ return : 0 if ok, -1 if failed + * + */ +static int retry( int(*func)()); +static int retry( int(*func)()) +{ + int i; + + for (i=0;i<4;i++) + { + if ( (*func)() >= 0) + { + return 0; + } + usleep(100000); + } + return -1; +} + +/*****************************************************************************************/ +/*****************************************************************************************/ +/** **/ +/** **/ +/** SECTION: HANDLE CAN DEVICE **/ +/** **/ +/** **/ +/*****************************************************************************************/ +/*****************************************************************************************/ +const char hex_asc_upper[] = "0123456789ABCDEF"; + +#define hex_asc_upper_lo(x) hex_asc_upper[((x) & 0x0F)] +#define hex_asc_upper_hi(x) hex_asc_upper[((x) & 0xF0) >> 4] + +static inline void put_hex_byte(char *buf, __u8 byte) +{ + buf[0] = hex_asc_upper_hi(byte); + buf[1] = hex_asc_upper_lo(byte); +} + +static inline void _put_id(char *buf, int end_offset, canid_t id) +{ + /* build 3 (SFF) or 8 (EFF) digit CAN identifier */ + while (end_offset >= 0) { + buf[end_offset--] = hex_asc_upper[id & 0xF]; + id >>= 4; + } +} + +#define put_sff_id(buf, id) _put_id(buf, 2, id) +#define put_eff_id(buf, id) _put_id(buf, 7, id) + +static void canread_frame_parse(struct canfd_frame *canfd_frame, int maxdlen); + +/* + * names of the types + */ +static const char * const type_NAMES[type_size] = { + "OBDII", + "CAN" +}; + + +// Initialize default can_handler values +static struct can_handler can_handler = { + .socket = -1, + .device = "vcan0", +}; + + +/* + * open the can socket + */ +static int open_can_dev() +{ + const int canfd_on = 1; + struct ifreq ifr; + struct timeval timeout = {1,0}; + openxc_CanMessage can_msg = { + .has_bus = false, + .has_id = false, + .has_data = false, + .has_frame_format = false, + }; + + DEBUG(interface, "open_can_dev: CAN Handler socket : %d", can_handler.socket); + close(can_handler.socket); + + can_handler.socket = socket(PF_CAN, SOCK_RAW, CAN_RAW); + if (can_handler.socket < 0) + { + ERROR(interface, "open_can_dev: socket could not be created"); + } + else + { + /* Set timeout for read */ + setsockopt(can_handler.socket, SOL_SOCKET, SO_RCVTIMEO, (char *)&timeout, sizeof(timeout)); + /* try to switch the socket into CAN_FD mode */ + can_msg.has_frame_format = true; + if (setsockopt(can_handler.socket, SOL_CAN_RAW, CAN_RAW_FD_FRAMES, &canfd_on, sizeof(canfd_on)) < 0) + { + NOTICE(interface, "open_can_dev: Can not switch into CAN Extended frame format."); + can_msg.frame_format = openxc_CanMessage_FrameFormat_STANDARD; + } else { + can_msg.frame_format = openxc_CanMessage_FrameFormat_EXTENDED; + } + + /* Attempts to open a socket to CAN bus */ + strcpy(ifr.ifr_name, can_handler.device); + if(ioctl(can_handler.socket, SIOCGIFINDEX, &ifr) < 0) + { + ERROR(interface, "open_can_dev: ioctl failed"); + } + else + { + can_handler.txAddress.can_family = AF_CAN; + can_handler.txAddress.can_ifindex = ifr.ifr_ifindex; + + /* And bind it to txAddress */ + if (bind(can_handler.socket, (struct sockaddr *)&can_handler.txAddress, sizeof(can_handler.txAddress)) < 0) + { + ERROR(interface, "open_can_dev: bind failed"); + } + else + { + fcntl(can_handler.socket, F_SETFL, O_NONBLOCK); + can_handler.msg = &can_msg; + return 0; + } + } + close(can_handler.socket); + can_handler.socket = -1; + } + return -1; +} + +static int write_can() +{ + ssize_t nbytes; + int rc; + + rc = can_handler.socket; + if (rc >= 0) + { +/* + * TODO change old hvac write can frame to generic on_event + */ + nbytes = sendto(can_handler.socket, &canfd_frame, sizeof(struct canfd_frame), 0, + (struct sockaddr*)&can_handler.txAddress, sizeof(can_handler.txAddress)); + if (nbytes < 0) + { + ERROR(interface, "write_can: Sending CAN frame failed."); + } + } + else + { + ERROR(interface, "write_can: socket not initialized. Attempt to reopen can device socket."); + retry(open_can_dev); + } + return rc; +} + +/* + * Read on CAN bus and return how much bytes has been read. + */ +static int read_can() +{ + ssize_t nbytes; + int maxdlen; + + nbytes = read(can_handler.socket, &canfd_frame, CANFD_MTU); + + if (nbytes == CANFD_MTU) + { + DEBUG(interface, "read_can: Got an CAN FD frame with length %d", canfd_frame.len); + } + else if (nbytes == CAN_MTU) + { + DEBUG(interface, "read_can: Got a legacy CAN frame with length %d", canfd_frame.len); + } + else + { + if (errno == ENETDOWN) { + ERROR(interface, "read_can: %s interface down", can_handler.device); + } + ERROR(interface, "read_can: Error reading CAN bus"); + return -1; + } + + /* CAN frame integrity check */ + if ((size_t)nbytes == CAN_MTU) + maxdlen = CAN_MAX_DLEN; + else if ((size_t)nbytes == CANFD_MTU) + maxdlen = CANFD_MAX_DLEN; + else + { + ERROR(interface, "read_can: CAN frame incomplete"); + return -2; + } + + canread_frame_parse(&canfd_frame, maxdlen); +} + +/* + * Parse the CAN frame data payload as a CAN packet + * TODO: parse as an OpenXC Can Message + */ +static void canread_frame_parse(struct canfd_frame *canfd_frame, int maxdlen) +{ + int i,offset; + int len = (canfd_frame->len > maxdlen) ? maxdlen : canfd_frame->len; + char buf[CL_CFSZ]; + + if (canfd_frame->can_id & CAN_ERR_FLAG) { + put_eff_id(buf, canfd_frame->can_id & (CAN_ERR_MASK|CAN_ERR_FLAG)); + buf[8] = '#'; + offset = 9; + } else if (canfd_frame->can_id & CAN_EFF_FLAG) { + put_eff_id(buf, canfd_frame->can_id & CAN_EFF_MASK); + buf[8] = '#'; + offset = 9; + } else { + put_sff_id(buf, canfd_frame->can_id & CAN_SFF_MASK); + buf[3] = '#'; + offset = 4; + } + + /* standard CAN frames may have RTR enabled. There are no ERR frames with RTR */ + if (maxdlen == CAN_MAX_DLEN && canfd_frame->can_id & CAN_RTR_FLAG) { + buf[offset++] = 'R'; + /* print a given CAN 2.0B DLC if it's not zero */ + if (canfd_frame->len && canfd_frame->len <= CAN_MAX_DLC) + buf[offset++] = hex_asc_upper[canfd_frame->len & 0xF]; + + buf[offset] = 0; + } + + if (maxdlen == CANFD_MAX_DLEN) { + /* add CAN FD specific escape char and flags */ + buf[offset++] = '#'; + buf[offset++] = hex_asc_upper[canfd_frame->flags & 0xF]; + } + + for (i = 0; i < len; i++) { + put_hex_byte(buf + offset, canfd_frame->data[i]); + offset += 2; + } + +buf[offset] = 0; +} + +/***************************************************************************************/ +/***************************************************************************************/ +/** **/ +/** **/ +/** SECTION: MANAGING EVENTS **/ +/** **/ +/** **/ +/***************************************************************************************/ +/***************************************************************************************/ +static int connect_to_event_loop(); + +/* + * called on an event on the CAN bus + */ +static int on_event(sd_event_source *s, int fd, uint32_t revents, void *userdata) +{ + openxc_CanMessage can_message; + + /* read available data */ + if ((revents & EPOLLIN) != 0) + { + read_can(&can_message); +// event_send(); + } + + /* check if error or hangup */ + if ((revents & (EPOLLERR|EPOLLRDHUP|EPOLLHUP)) != 0) + { + sd_event_source_unref(s); + close(fd); + connect_to_event_loop(); + } + + return 0; +} + +/* + * get or create an event handler for the type + * TODO: implement function and handle retrieve or create an event as needed + * +static struct event *event_get(enum type type) +{ + +} +*/ + +static struct event *event_of_id(int id) +{ + +} + +/*****************************************************************************************/ +/*****************************************************************************************/ +/** **/ +/** **/ +/** SECTION: BINDING VERBS IMPLEMENTATION **/ +/** **/ +/** **/ +/*****************************************************************************************/ +/*****************************************************************************************/ +/* + * Returns the type corresponding to the given name + */ +static enum type type_of_name(const char *name) +{ + enum type result; + if (name == NULL) + return type_DEFAULT; + for (result = 0 ; result < type_size; result++) + if (strcmp(type_NAMES[result], name) == 0) + return result; + return type_INVALID; +} + +/* + * extract a valid type from the request + */ +static int get_type_for_req(struct afb_req req, enum type *type) +{ + if ((*type = type_of_name(afb_req_value(req, "type"))) != type_INVALID) + return 1; + afb_req_fail(req, "unknown-type", NULL); + return 0; +} + +/* + * subscribe to notification of new CAN messages + * + * parameters of the subscription are: + * + * TODO type: string: choose between CAN and OBDII messages formats. + * + * returns an object with 2 fields: + * + * name: string: the name of the event without its prefix + * id: integer: a numeric identifier of the event to be used for unsubscribing + */ +static void subscribe(struct afb_req req) +{ + enum type type; + const char *period; + struct event *event; + struct json_object *json; + + if (get_type_for_req(req, &type)) + { + event = afb_daemon_make_event(interface->daemon, type_NAMES[type]); + if (event == NULL) + afb_req_fail(req, "out-of-memory", NULL); + else if (afb_req_subscribe(req, event->event) != 0) + afb_req_fail_f(req, "failed", "afb_req_subscribe returned an error: %m"); + else + { + json = json_object_new_object(); + json_object_object_add(json, "name", json_object_new_string(event->name)); + json_object_object_add(json, "id", json_object_new_int(event->id)); + afb_req_success(req, json, NULL); + } + } +} + +/* + * unsubscribe a previous subscription + * + * parameters of the unsubscription are: + * + * id: integer: the numeric identifier of the event as returned when subscribing + */ +static void unsubscribe(struct afb_req req) +{ + const char *id; + struct event *event; + + id = afb_req_value(req, "id"); + if (id == NULL) + afb_req_fail(req, "missing-id", NULL); + else + { + event = event_of_id(atoi(id)); + if (event == NULL) + afb_req_fail(req, "bad-id", NULL); + else + { + afb_req_unsubscribe(req, event->event); + afb_req_success(req, NULL, NULL); + } + } +} + +static int connect_to_event_loop() +{ + sd_event_source *source; + int rc; + + retry(open_can_dev); + + if (can_handler.socket < 0) + { + return can_handler.socket; + } + + rc = sd_event_add_io(afb_daemon_get_event_loop(interface->daemon), &source, can_handler.socket, EPOLLIN, on_event, NULL); + if (rc < 0) + { + close(can_handler.socket); + ERROR(interface, "Can't connect CAN bus %s to the event loop", can_handler.device); + } else + { + NOTICE(interface, "Connected CAN bus %s to the event loop", can_handler.device); + } + + return rc; +} + + +// TODO: Have to change session management flag to AFB_SESSION_CHECK to use token auth +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." }, + {NULL} +}; + +static const struct afb_binding binding_desc = { + .type = AFB_BINDING_VERSION_1, + .v1 = { + .info = "CAN bus service", + .prefix = "can", + .verbs = verbs + } +}; + +const struct afb_binding *afbBindingV1Register (const struct afb_binding_interface *itf) +{ + interface = itf; + + return &binding_desc; +} + +int afbBindingV1ServiceInit(struct afb_service service) +{ + return connect_to_event_loop(); +} diff --git a/config.xml.in b/config.xml.in new file mode 100644 index 00000000..33a8ff85 --- /dev/null +++ b/config.xml.in @@ -0,0 +1,11 @@ + + + @PROJECT_NAME@ + + + This is a demo application used to control and dialog with HVAC system + Romain Forlot <romain.forlot@iot.bzh> + APL 2.0 + + + diff --git a/icon.png b/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..9bd6a6e44f14c6dfeb5b25fe4e19555c5620bd12 GIT binary patch literal 4035 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