diff --git a/MAINTAINERS.yml b/MAINTAINERS.yml index ed368a990e9b2..854d00ebd09cf 100644 --- a/MAINTAINERS.yml +++ b/MAINTAINERS.yml @@ -2573,6 +2573,7 @@ Networking: files-exclude: - doc/connectivity/networking/api/gptp.rst - doc/connectivity/networking/api/ieee802154.rst + - doc/connectivity/networking/api/ocpp.rst - doc/connectivity/networking/api/wifi.rst - include/zephyr/net/gptp.h - include/zephyr/net/ieee802154*.h @@ -2582,6 +2583,7 @@ Networking: - samples/net/gptp/ - samples/net/sockets/coap_*/ - samples/net/lwm2m_client/ + - samples/net/ocpp/ - samples/net/wifi/ - samples/net/dhcpv4_client/ - subsys/net/buf*.c @@ -2591,10 +2593,12 @@ Networking: - subsys/net/lib/coap/ - subsys/net/lib/config/ieee802154* - subsys/net/lib/lwm2m/ + - subsys/net/lib/ocpp/ - subsys/net/lib/tls_credentials/ - subsys/net/lib/dhcpv4/ - tests/net/dhcpv4/ - tests/net/ieee802154/ + - tests/net/lib/ocpp/ - tests/net/wifi/ labels: - "area: Networking" @@ -2764,6 +2768,17 @@ Networking: tests: - net.ieee802154 +"Networking: OCPP": + status: maintained + maintainers: + - ssekar15 + files: + - samples/net/ocpp/ + - subsys/net/lib/ocpp/ + - tests/net/lib/ocpp/ + labels: + - "area: OCPP" + "Networking: OpenThread": status: maintained maintainers: @@ -4928,3 +4943,12 @@ zbus: - "area: Linkable Loadable Extensions" tests: - llext + +"West project: json-c": + status: maintained + maintainers: + - parthitce + files: + - modules/json-c/ + labels: + - "area: JSON" diff --git a/doc/connectivity/networking/api/ocpp.rst b/doc/connectivity/networking/api/ocpp.rst new file mode 100644 index 0000000000000..d4f403997fbe3 --- /dev/null +++ b/doc/connectivity/networking/api/ocpp.rst @@ -0,0 +1,110 @@ +.. _ocpp_interface: + +Open Charge Point Protocol (OCPP) +################################# + +.. contents:: + :local: + :depth: 2 + +Overview +******** + +Open Charge Point Protocol (OCPP) is an application protocol for communication +between Charge Points (Electric vehicle (EV) charging stations) and a central +management system, also known as a charging station network. OCPP is a +`standard `_ +defined by The Open Charge Alliance and goal is to offer a uniform solution for +the method of communication between charge point and central system. With this +protocol it is possible to connect any central system with any charge point, +regardless of the vendor + +Zephyr provides an OCPP Charge Point (CP) library built on top of websocket API +with payload in json format. The library can be enabled with +:kconfig:option:`CONFIG_OCPP` Kconfig option. Currently OCPP 1.6 with basic +core profile is supported. + +OCPP charge point (CP) require a Central System (CS) server to connect, an open +source SteVe server shall be setup locally for devlopment purpose, See +`SteVe server `_ +for more information about the setup. + +The Zephyr OCPP CP library implements the following items: + +* engine to process socket connectivity and events +* OCPP core functions to frame/parse payload, user notification for OCPP events, + heartbeat notification + +Sample usage +************ + +Init ocpp library with overall CP and CS information. Prior to init an OCPP +library, a network interface should be ready using ethernet or wifi or modem. +A filled CP, CS structure and user callback needs to be passed in ocpp_init. + +.. code-block:: c + + static int user_notify_cb(ocpp_notify_reason_t reason, + ocpp_io_value_t *io, + void *user_data) + { + + switch (reason) { + case OCPP_USR_GET_METER_VALUE: + ... + break; + + case OCPP_USR_START_CHARGING: + ... + break; + + ... + ... + } + + /* OCPP configuration */ + ocpp_cp_info_t cpi = { "basic", "zephyr", .num_of_con = 1}; + ocpp_cs_info_t csi = {"192.168.1.3", /* ip address */ + "/steve/websocket/CentralSystemService/zephyr", + 8180, + AF_INET}; + + ret = ocpp_init(&cpi, &csi, user_notify_cb, NULL); + +A unique session must open for each physical connector before any ocpp +transcation API call. + +.. code-block:: c + + ocpp_session_handle_t sh = NULL; + ret = ocpp_session_open(&sh); + +idtag is EV user's authentication token which should match with list on CS. +Authorize request must call to ensure validity of idtag (if charging request +originate from local CP) before start energy transfer to EV. + +.. code-block:: c + + ocpp_auth_status_t status; + ret = ocpp_authorize(sh, idtag, &status, 500); + +On successful, authorization status is available in status. + +Apart from local CP, charging request may originate from CS is notified to user +in callback with OCPP_USR_START_CHARGING, here authorize request call is +optional. When the CS is ready to provide power to EV, a start transaction +is notified to CS with meter reading and connector id using ocpp_start_transaction. + +.. code-block:: c + + const int idcon = 1; + const int mval = 25; //meter reading in wh + ret = ocpp_start_transaction(sh, mval, idcon, 200); + +Once the start transaction is success, user callback is invoked to get meter +readings from the library. callback should be not be hold for longer time. + +API Reference +************* + +.. doxygengroup:: ocpp_api diff --git a/doc/connectivity/networking/api/protocols.rst b/doc/connectivity/networking/api/protocols.rst index 57aeaae8d021a..3d28869cb4096 100644 --- a/doc/connectivity/networking/api/protocols.rst +++ b/doc/connectivity/networking/api/protocols.rst @@ -15,4 +15,5 @@ Protocols lwm2m mqtt mqtt_sn + ocpp tftp diff --git a/include/zephyr/net/ocpp.h b/include/zephyr/net/ocpp.h new file mode 100644 index 0000000000000..b34ef5fb6eeea --- /dev/null +++ b/include/zephyr/net/ocpp.h @@ -0,0 +1,255 @@ +/* + * Copyright (c) 2024 Linumiz + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * @file ocpp.h + * + * @defgroup ocpp_api OCPP library + * @ingroup networking + * @{ + * @brief OCPP Charge Point Implementation + * + * @note The implementation assumes Websocket module is enabled. + * + * @note By default the implementation uses OCPP version 1.6. + */ + +#ifndef ZEPHYR_INCLUDE_NET_OCPP_H_ +#define ZEPHYR_INCLUDE_NET_OCPP_H_ + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/** Max length of string literals e.g idtag */ +#define CISTR50 50 + +/** + * @brief OCPP IdTag authorization status in result to ocpp request + * authorization + */ +enum ocpp_auth_status { + OCPP_AUTH_INVALID, /*< IdTag not valid */ + OCPP_AUTH_ACCEPTED, /*< accepted, allowed to charge */ + OCPP_AUTH_BLOCKED, /*< blocked to charge */ + OCPP_AUTH_EXPIRED, /*< IdTag expired, not allowed to charge */ + OCPP_AUTH_CONCURRENT_TX /*< Parallel access of same IdTag */ +}; + +enum ocpp_notify_reason { + /** User must fill the current reading */ + OCPP_USR_GET_METER_VALUE, + + /** Process the start charging request as like idtag received from local + * e.g authorize etc + */ + OCPP_USR_START_CHARGING, + + /** Process the stop charging sequence */ + OCPP_USR_STOP_CHARGING, + + /** Unlock mechanical connector of CP */ + OCPP_USR_UNLOCK_CONNECTOR, +}; + +/** @brief OCPP meter readings to be filled on user callback request from + * library + */ +enum ocpp_meter_measurand { + OCPP_OMM_CURRENT_FROM_EV, /*< current from EV, in A */ + OCPP_OMM_CURRENT_TO_EV, /*< current to EV, in A */ + OCPP_OMM_CURRENT_MAX_OFFERED_TO_EV, /*< maximum current offered to EV, in A */ + OCPP_OMM_ACTIVE_ENERGY_FROM_EV, /*< active energy from EV, in Wh */ + OCPP_OMM_ACTIVE_ENERGY_TO_EV, /*< active energy to EV, in Wh */ + OCPP_OMM_REACTIVE_ENERGY_FROM_EV, /*< reactive energy from EV, in varh */ + OCPP_OMM_REACTIVE_ENERGY_TO_EV, /*< reactive energy to EV, in varh */ + OCPP_OMM_ACTIVE_POWER_FROM_EV, /*< active power from EV, in W */ + OCPP_OMM_ACTIVE_POWER_TO_EV, /*< active power to EV, in W */ + OCPP_OMM_REACTIVE_POWER_FROM_EV, /*< reactive power from EV, in var */ + OCPP_OMM_REACTIVE_POWER_TO_EV, /*< reactive power to EV, in var */ + OCPP_OMM_POWERLINE_FREQ, /*< powerline frequency, in Hz */ + OCPP_OMM_POWER_FACTOR, /*< power factor of supply */ + OCPP_OMM_POWER_MAX_OFFERED_TO_EV, /*< maximum power offered to EV, in W */ + OCPP_OMM_FAN_SPEED, /*< fan speed, in rpm */ + OCPP_OMM_CHARGING_PERCENT, /*< charging percentage */ + OCPP_OMM_TEMPERATURE, /*< temperature inside charge point, in Celsius */ + OCPP_OMM_VOLTAGE_AC_RMS, /*< AC RMS supply voltage, in V */ + + OCPP_OMM_END +}; + +/** @brief OCPP user callback notification/request of input/output values + * union member should be accessed with enum value ocpp_notify_reason + * correspondingly. + * e.g. callback reason is OCPP_USR_GET_METER_VALUE, struct meter_val + * is valid + */ +union ocpp_io_value { + + struct { + /** Input to user, requested connector_id or 0 - main meter */ + int id_con; + + /** Input to user, measurand */ + enum ocpp_meter_measurand mes; + + /** To be filled by user, value as string */ + char val[CISTR50]; + } meter_val; + + struct { + char idtag[CISTR50]; /**< Input to user */ + + /** Input to user(optional). connector id -1 means invalid */ + int id_con; + } start_charge; + + struct { + int id_con; /**< Input to user, to stop charging connector */ + } stop_charge; + + struct { + int id_con; /**< Input to user, to unlock connector id. */ + } unlock_con; +}; + +/** @brief Parameters for ocpp_init information about Charge Point (CP) + * all are string literal except num_of_con + */ +struct ocpp_cp_info { + char *model; /**< Charge Point (CP) model */ + char *vendor; /**< CP vendor */ + + int num_of_con; /**< Max. number of connector supports */ + + /** optional fields */ + char *sl_no; /**< CP serial number */ + char *box_sl_no; /**< Box serial number */ + char *fw_ver; /**< Firmware version */ + char *iccid; /**< ICC ID */ + char *imsi; /**< IMSI */ + char *meter_sl_no; /**< Main power meter serial number */ + char *meter_type; /**< Main power meter type */ +}; + +/** @brief Parameters for ocpp_init information about central system (CS) */ +struct ocpp_cs_info { + char *cs_ip; /**< Central system IP address */ + char *ws_url; /**< Websocket url exclude ipaddr & port */ + int port; /**< Central system port number */ + sa_family_t sa_family; /** IP protocol family type 4/6 */ +}; + +/** @brief Parameters opaque session handle for ocpp_* API */ +typedef void *ocpp_session_handle_t; + +/** + * @brief Asynchronous event notification callback registered by the + * application. advised callback should not be hold for longer time + * to unblock the ocpp protocol stack/lib. + * + * @param[in] reason for callback invoked. + * @param[in] io reffered corresponding to reason. + * @param[in] user_data passed on ocpp_init. + * + * @return 0 or a negative error code (errno.h) + */ +typedef int (*ocpp_user_notify_callback_t)(enum ocpp_notify_reason reason, + union ocpp_io_value *io, + void *user_data); + +/** + * @brief OCPP library init. + * + * @param[in] cpi Charge Point information + * @param[in] csi Central System information + * @param[in] cb user register callback + * @param[in] user_data same reference will be passed on callback + * + * @return 0 on success or a negative error code (errno.h) indicating reason of failure + * + * @note Must be called before any other ocpp API + */ +int ocpp_init(struct ocpp_cp_info *cpi, + struct ocpp_cs_info *csi, + ocpp_user_notify_callback_t cb, + void *user_data); + +/** + * @brief API to request a new Session + * + * @param[out] hndl a valid opaque handle + * + * @return 0 on success or a negative error code (errno.h) indicating reason of failure + * + * @note Each connector should open unique session after ocpp_init and + * prior to anyother ocpp_* request message api + */ +int ocpp_session_open(ocpp_session_handle_t *hndl); + +/** + * @brief API to close a Session + * + * @param[in] hndl a handle received from session open + */ +void ocpp_session_close(ocpp_session_handle_t hndl); + +/** + * @brief Authorize request call to CS to get validity of idtag + * + * @param[in] hndl session handle + * @param[in] idtag (string literal) to get authorize validity + * @param[out] status authorization status + * @param[in] timeout_ms in msec + * + * @return 0 on success or a negative error code (errno.h) indicating reason of failure + */ +int ocpp_authorize(ocpp_session_handle_t hndl, + char *idtag, + enum ocpp_auth_status *status, + uint32_t timeout_ms); + +/** + * @brief Notify transaction start to CS + * + * @param[in] hndl session handle + * @param[in] Wh energy meter reading of this connector + * @param[in] conn_id connector id should be > 0 and sequential number + * @param[in] timeout_ms timeout in msec + * + * @return: 0 on success + * EACCES - not authorized, should follow the stop charging process + * a negative error code (errno.h) indicating reason of failure + */ +int ocpp_start_transaction(ocpp_session_handle_t hndl, + int Wh, + uint8_t conn_id, + uint32_t timeout_ms); + +/** + * @brief Notify transaction stopped to CS + * + * @param[in] hndl session handle + * @param[in] Wh energy meter reading of this connector + * @param[in] timeout_ms timeout in msec + * + * @return 0 on success or a negative error code (errno.h) indicating reason of failure + */ +int ocpp_stop_transaction(ocpp_session_handle_t hndl, + int Wh, + uint32_t timeout_ms); + +#ifdef __cplusplus +} +#endif + +#endif /* ZEPHYR_INCLUDE_NET_OCPP_H_ */ + +/**@} */ diff --git a/modules/json-c/CMakeLists.txt b/modules/json-c/CMakeLists.txt new file mode 100644 index 0000000000000..f036e3b096793 --- /dev/null +++ b/modules/json-c/CMakeLists.txt @@ -0,0 +1,34 @@ +# Copyright (c) 2023 Linumiz +# SPDX-License-Identifier: Apache-2.0 + +if(CONFIG_JSONC) + +set(JSONC_UPSTREAM ${ZEPHYR_JSON_C_MODULE_DIR}) + +zephyr_interface_library_named(JSONC) + +zephyr_include_directories(include) +zephyr_include_directories(${JSONC_UPSTREAM}) + +zephyr_library() +zephyr_library_sources( + ${JSONC_UPSTREAM}/arraylist.c + ${JSONC_UPSTREAM}/debug.c + ${JSONC_UPSTREAM}/json_c_version.c + ${JSONC_UPSTREAM}/json_object.c + ${JSONC_UPSTREAM}/json_object_iterator.c + ${JSONC_UPSTREAM}/json_pointer.c + ${JSONC_UPSTREAM}/json_tokener.c + ${JSONC_UPSTREAM}/json_util.c + ${JSONC_UPSTREAM}/json_visit.c + ${JSONC_UPSTREAM}/libjson.c + ${JSONC_UPSTREAM}/linkhash.c + ${JSONC_UPSTREAM}/printbuf.c + ${JSONC_UPSTREAM}/random_seed.c + ${JSONC_UPSTREAM}/strerror_override.c +) + +zephyr_library_link_libraries(JSONC) +target_link_libraries(JSONC INTERFACE zephyr_interface) + +endif() diff --git a/modules/json-c/Kconfig b/modules/json-c/Kconfig new file mode 100644 index 0000000000000..69ab879d24d2d --- /dev/null +++ b/modules/json-c/Kconfig @@ -0,0 +1,10 @@ +# Copyright (c) 2023 Linumiz +# SPDX-License-Identifier: Apache-2.0 + +config ZEPHYR_JSONC_MODULE + bool + +config JSONC + bool "JSON implementation in C" + help + This option enables json-c support. diff --git a/modules/json-c/include/apps_config.h b/modules/json-c/include/apps_config.h new file mode 100644 index 0000000000000..79d3497efd711 --- /dev/null +++ b/modules/json-c/include/apps_config.h @@ -0,0 +1,13 @@ +/* + * Copyright (c) 2023 Linumiz + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/* Define to 1 if you have the header file. */ +#define HAVE_SYS_RESOURCE_H + +/* Define if you have the `getrusage' function. */ +#define HAVE_GETRUSAGE + +/* #undef HAVE_JSON_TOKENER_GET_PARSE_END */ diff --git a/modules/json-c/include/config.h b/modules/json-c/include/config.h new file mode 100644 index 0000000000000..693a83018f6a4 --- /dev/null +++ b/modules/json-c/include/config.h @@ -0,0 +1,228 @@ +/* + * Copyright (c) 2023 Linumiz + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/* Enable RDRAND Hardware RNG Hash Seed */ +/* #undef ENABLE_RDRAND */ + +/* Override json_c_get_random_seed() with custom code */ +/* #undef OVERRIDE_GET_RANDOM_SEED */ + +/* Enable partial threading support */ +/* #undef ENABLE_THREADING */ + +/* Define if .gnu.warning accepts long strings. */ +/* #undef HAS_GNU_WARNING_LONG */ + +/* Define to 1 if you have the header file. */ +#define HAVE_DLFCN_H + +/* Define to 1 if you have the header file. */ +/* #define HAVE_ENDIAN_H */ + +/* Define to 1 if you have the header file. */ +#define HAVE_FCNTL_H + +/* Define to 1 if you have the header file. */ +#define HAVE_INTTYPES_H + +/* Define to 1 if you have the header file. */ +#define HAVE_LIMITS_H + +/* Define to 1 if you have the header file. */ +/* #define HAVE_LOCALE_H */ + +/* Define to 1 if you have the header file. */ +#define HAVE_MEMORY_H + +/* Define to 1 if you have the header file. */ +#define HAVE_STDARG_H + +/* Define to 1 if you have the header file. */ +#define HAVE_STDINT_H + +/* Define to 1 if you have the header file. */ +#define HAVE_STDLIB_H + +/* Define to 1 if you have the header file. */ +#define HAVE_STRINGS_H + +/* Define to 1 if you have the header file. */ +#define HAVE_STRING_H + +/* Define to 1 if you have the header file. */ +/* #define HAVE_SYSLOG_H 1 */ + +/* Define to 1 if you have the header file. */ +#define HAVE_SYS_CDEFS_H + +/* Define to 1 if you have the header file. */ +#define HAVE_SYS_PARAM_H 1 + +/* Define to 1 if you have the header file. */ +/* #define HAVE_SYS_RANDOM_H */ + +/* Define to 1 if you have the header file. */ +#define HAVE_SYS_RESOURCE_H + +/* Define to 1 if you have the header file. */ +#define HAVE_SYS_STAT_H + +/* Define to 1 if you have the header file. */ +#define HAVE_SYS_TYPES_H 1 + +/* Define to 1 if you have the header file. */ +#define HAVE_UNISTD_H 1 + +/* Define to 1 if you have the header file. */ +/* #undef HAVE_XLOCALE_H */ + +/* Define to 1 if you have the header file. */ +/* #undef HAVE_BSD_STDLIB_H */ + +/* Define to 1 if you have `arc4random' */ +/* #define HAVE_ARC4RANDOM */ + +/* Define to 1 if you don't have `vprintf' but do have `_doprnt.' */ +/* #undef HAVE_DOPRNT */ + +/* Has atomic builtins */ +#define HAVE_ATOMIC_BUILTINS + +/* Define to 1 if you have the declaration of `INFINITY', and to 0 if you don't. */ +#define HAVE_DECL_INFINITY + +/* Define to 1 if you have the declaration of `isinf', and to 0 if you don't. */ +#define HAVE_DECL_ISINF + +/* Define to 1 if you have the declaration of `isnan', and to 0 if you don't. */ +#define HAVE_DECL_ISNAN + +/* Define to 1 if you have the declaration of `nan', and to 0 if you don't. */ +#define HAVE_DECL_NAN + +/* Define to 1 if you have the declaration of `_finite', and to 0 if you don't. */ +/* #undef HAVE_DECL__FINITE */ + +/* Define to 1 if you have the declaration of `_isnan', and to 0 if you don't. */ +/* #undef HAVE_DECL__ISNAN */ + +/* Define to 1 if you have the `open' function. */ +#define HAVE_OPEN + +/* Define to 1 if you have the `realloc' function. */ +#define HAVE_REALLOC + +/* Define to 1 if you have the `setlocale' function. */ +/* #define HAVE_SETLOCALE */ + +/* Define to 1 if you have the `snprintf' function. */ +#define HAVE_SNPRINTF + + +/* Define to 1 if you have the `strcasecmp' function. */ +#define HAVE_STRCASECMP 1 + +/* Define to 1 if you have the `strdup' function. */ +#define HAVE_STRDUP + +/* Define to 1 if you have the `strerror' function. */ +#define HAVE_STRERROR + +/* Define to 1 if you have the `strncasecmp' function. */ +#define HAVE_STRNCASECMP 1 + +/* Define to 1 if you have the `uselocale' function. */ +/* #define HAVE_USELOCALE */ + +/* Define to 1 if you have the `vasprintf' function. */ +#define HAVE_VASPRINTF + +/* Define to 1 if you have the `vprintf' function. */ +#define HAVE_VPRINTF + +/* Define to 1 if you have the `vsnprintf' function. */ +#define HAVE_VSNPRINTF + +/* Define to 1 if you have the `vsyslog' function. */ +/* #define HAVE_VSYSLOG 1 */ + +/* Define if you have the `getrandom' function. */ +/* #define HAVE_GETRANDOM */ + +/* Define if you have the `getrusage' function. */ +#define HAVE_GETRUSAGE + +#define HAVE_STRTOLL +#if !defined(HAVE_STRTOLL) +#define strtoll strtoll +/* #define json_c_strtoll strtoll*/ +#endif + +#define HAVE_STRTOULL +#if !defined(HAVE_STRTOULL) +#define strtoull strtoull +/* #define json_c_strtoull strtoull */ +#endif + +/* Have __thread */ +/* #define HAVE___THREAD */ + +/* Public define for json_inttypes.h */ +#define JSON_C_HAVE_INTTYPES_H 1 + +/* Name of package */ +#define PACKAGE "json-c" + +/* Define to the address where bug reports for this package should be sent. */ +#define PACKAGE_BUGREPORT "json-c@googlegroups.com" + +/* Define to the full name of this package. */ +#define PACKAGE_NAME "json-c" + +/* Define to the full name and version of this package. */ +#define PACKAGE_STRING "json-c 0.16.99" + +/* Define to the one symbol short name of this package. */ +#define PACKAGE_TARNAME "json-c" + +/* Define to the home page for this package. */ +#define PACKAGE_URL "https://github.com/json-c/json-c" + +/* Define to the version of this package. */ +#define PACKAGE_VERSION "0.16.99" + +/* The number of bytes in type int */ +#define SIZEOF_INT 4 + +/* The number of bytes in type int64_t */ +#define SIZEOF_INT64_T 8 + +/* The number of bytes in type long */ +#define SIZEOF_LONG 8 + +/* The number of bytes in type long long */ +#define SIZEOF_LONG_LONG 8 + +/* The number of bytes in type size_t */ +#define SIZEOF_SIZE_T 8 + +/* The number of bytes in type ssize_t */ +#define SIZEOF_SSIZE_T 8 + +/* Specifier for __thread */ +#define SPEC___THREAD __thread + +/* Define to 1 if you have the ANSI C header files. */ +#define STDC_HEADERS + +/* Version number of package */ +#define VERSION "0.16.99" + +/* Define to empty if `const' does not conform to ANSI C. */ +/* #undef const */ + +/* Define to `unsigned int' if does not define. */ +/* #undef size_t */ diff --git a/modules/json-c/include/json.h b/modules/json-c/include/json.h new file mode 100644 index 0000000000000..6e66f9fc91a79 --- /dev/null +++ b/modules/json-c/include/json.h @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2023 Linumiz + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef _json_h_ +#define _json_h_ + +#ifdef __cplusplus +extern "C" { +#endif + +#include "arraylist.h" +#include "debug.h" +#include "json_c_version.h" +#include "json_object.h" +#include "json_object_iterator.h" +#include "json_pointer.h" +#include "json_tokener.h" +#include "json_util.h" +#include "linkhash.h" + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/modules/json-c/include/json_config.h b/modules/json-c/include/json_config.h new file mode 100644 index 0000000000000..c7f08c63b6f94 --- /dev/null +++ b/modules/json-c/include/json_config.h @@ -0,0 +1,8 @@ +/* + * Copyright (c) 2023 Linumiz + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/* Define to 1 if you have the header file. */ +#define JSON_C_HAVE_INTTYPES_H 1 diff --git a/samples/net/ocpp/CMakeLists.txt b/samples/net/ocpp/CMakeLists.txt new file mode 100644 index 0000000000000..f431c91d1c8b3 --- /dev/null +++ b/samples/net/ocpp/CMakeLists.txt @@ -0,0 +1,23 @@ +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.20.0) + +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(ocpp) + +target_sources(app PRIVATE src/main.c) +set(gen_dir ${ZEPHYR_BINARY_DIR}/include/generated/) + +function(pem_to_mbedtls target input_file_fullpath) + get_filename_component(input_file ${input_file_fullpath} NAME) + file(READ ${input_file_fullpath} input_file_content) + string(REGEX REPLACE "\n" "\\\\n" input_file_content "${input_file_content}") + set(GENERATED_FILE ${gen_dir}/${input_file}.inc) + file(WRITE ${GENERATED_FILE} "\"${input_file_content}\"\n") + generate_unique_target_name_from_filename(${input_file} generated_target_name) + add_custom_target(${generated_target_name} DEPENDS ${GENERATED_FILE}) + add_dependencies(${target} ${generated_target_name}) +endfunction() + +pem_to_mbedtls(app ${ZEPHYR_BASE}/subsys/net/lib/ocpp/credentials/ec.crt) + diff --git a/samples/net/ocpp/Kconfig b/samples/net/ocpp/Kconfig new file mode 100644 index 0000000000000..8edaa266d1e16 --- /dev/null +++ b/samples/net/ocpp/Kconfig @@ -0,0 +1,22 @@ +# Config options for OCPP sample application + +# Copyright (c) 2024 Linumiz +# SPDX-License-Identifier: Apache-2.0 + +mainmenu "OCPP sample application" +config NET_SAMPLE_OCPP_SERVER + string "OCPP server ip" + help + OCPP central system server ip address + +config NET_SAMPLE_OCPP_PORT + int "OCPP server port" + help + OCPP central system server port + +config NET_SAMPLE_SNTP_SERVER + string "SNTP server ip" + help + SNTP server ip to get the time from network + +source "Kconfig.zephyr" diff --git a/samples/net/ocpp/README.rst b/samples/net/ocpp/README.rst new file mode 100644 index 0000000000000..0b9bb7707463f --- /dev/null +++ b/samples/net/ocpp/README.rst @@ -0,0 +1,69 @@ +.. zephyr:code-sample:: ocpp + :name: OCPP charge point + :relevant-api: ocpp_api + + Implement an OCPP charge point that connects to a Central System server and + simulates the meter readings. + +Overview +******** + +Open Charge Point Protocol (OCPP) is an application protocol for communication +between Charge Points (Electric vehicle (EV) charging stations) and a central +management system, also known as a charging station network. + +This ocpp sample application for Zephyr implements the ocpp library +and establishes a connection to an Central System server using the web socket + +The source code for this sample application can be found at: +:zephyr_file:`samples/net/ocpp`. + +Requirements +************ + +- Linux machine +- STEM32 Discovery kit (32F769IDISCOVERY) or any network interface device +- SteVe Demo Server () +- LAN for testing purposes (Ethernet) + +Building and Running +******************** + +Build the ocpp sample application like this: + +.. zephyr-app-commands:: + :zephyr-app: samples/net/ocpp + :board: + :goals: build + :compact: + +The sample application is to built and tested on + +.. code-block:: console + + west build -b stm32f769i_disco + west flash + +The output of sample is: + +.. code-block:: console + + *** Booting Zephyr OS build v3.6.0-rc1-37-g8c035d8f24cf *** + OCPP sample stm32f769i_disco + [00:00:02.642,000] net_dhcpv4: Received: 192.168.1.101 + [00:00:02.642,000] main: net mgr cb + [00:00:02.642,000] main: Your address: 192.168.1.101 + [00:00:02.642,000] main: Lease time: 86400 seconds + [00:00:02.642,000] main: Subnet: 255.255.255.0 + [00:00:02.643,000] main: Router: 192.168.1.1 + [00:00:07.011,000] main: cs server 122.165.245.213 8180 + [00:00:07.011,000] main: IPv4 Address 122.165.245.213 + [00:00:07.024,000] main: sntp succ since Epoch: 1707890823 + [00:00:07.024,000] ocpp: upstream init + [00:00:07.025,000] ocpp: ocpp init success + [00:00:17.066,000] main: ocpp auth 0> idcon 1 status 1 + [00:00:17.101,000] main: ocpp auth 0> idcon 2 status 1 + [00:00:17.197,000] main: ocpp start charging connector id 1 + [00:00:17.255,000] main: ocpp start charging connector id 2 + [00:01:07.064,000] main: ocpp stop charging connector id 1 + [00:01:08.063,000] main: ocpp stop charging connector id 2 diff --git a/samples/net/ocpp/overlay-ec.conf b/samples/net/ocpp/overlay-ec.conf new file mode 100644 index 0000000000000..c109147eb8e50 --- /dev/null +++ b/samples/net/ocpp/overlay-ec.conf @@ -0,0 +1,7 @@ +CONFIG_PSA_WANT_ALG_ECDH=y +CONFIG_PSA_WANT_ALG_ECDSA=y +CONFIG_PSA_WANT_KEY_TYPE_ECC_KEY_PAIR_GENERATE=y +CONFIG_PSA_WANT_KEY_TYPE_ECC_KEY_PAIR_IMPORT=y +CONFIG_PSA_WANT_KEY_TYPE_ECC_KEY_PAIR_EXPORT=y +CONFIG_PSA_WANT_KEY_TYPE_ECC_KEY_PAIR_DERIVE=y +CONFIG_PSA_WANT_ECC_SECP_R1_256=y diff --git a/samples/net/ocpp/overlay-tls13.conf b/samples/net/ocpp/overlay-tls13.conf new file mode 100644 index 0000000000000..f5d6315ce6d95 --- /dev/null +++ b/samples/net/ocpp/overlay-tls13.conf @@ -0,0 +1,40 @@ +# Enable Secure Sockets +CONFIG_NET_SOCKETS_SOCKOPT_TLS=y + +# Mbed TLS configuration +CONFIG_MBEDTLS=y +CONFIG_MBEDTLS_BUILTIN=y +CONFIG_MBEDTLS_ENABLE_HEAP=y +CONFIG_MBEDTLS_HEAP_SIZE=20000 +CONFIG_MBEDTLS_SSL_MAX_CONTENT_LEN=2048 +CONFIG_MBEDTLS_PEM_CERTIFICATE_FORMAT=y +CONFIG_MBEDTLS_SERVER_NAME_INDICATION=y +# Build the PSA Crypto core so that the TLS stack uses the PSA crypto API. +CONFIG_MBEDTLS_PSA_CRYPTO_C=y +CONFIG_ENTROPY_GENERATOR=y + +# Disable some Kconfigs that are implied by CONFIG_NET_SOCKETS_SOCKOPT_TLS. +CONFIG_MBEDTLS_TLS_VERSION_1_2=n +CONFIG_MBEDTLS_KEY_EXCHANGE_RSA_ENABLED=n +CONFIG_MBEDTLS_MD=n +CONFIG_MBEDTLS_RSA_C=n +CONFIG_MBEDTLS_CIPHER_AES_ENABLED=n +CONFIG_PSA_WANT_KEY_TYPE_AES=n +CONFIG_PSA_WANT_ALG_CBC_NO_PADDING=n + +# Debug log options (optional) +# CONFIG_MBEDTLS_LOG_LEVEL_DBG=y +# CONFIG_MBEDTLS_DEBUG=y + +# TLS1.3 Configuration +CONFIG_MBEDTLS_TLS_VERSION_1_3=y +CONFIG_PSA_WANT_ALG_HKDF_EXTRACT=y +CONFIG_PSA_WANT_ALG_HKDF_EXPAND=y +CONFIG_PSA_WANT_ALG_GCM=y +CONFIG_PSA_WANT_KEY_TYPE_AES=y +CONFIG_PSA_WANT_ALG_CBC_NO_PADDING=y +CONFIG_PSA_WANT_ALG_SHA_256=y +CONFIG_PSA_WANT_ALG_SHA_384=y +CONFIG_MBEDTLS_SSL_TLS1_3_KEY_EXCHANGE_MODE_EPHEMERAL_ENABLED=y + + diff --git a/samples/net/ocpp/prj.conf b/samples/net/ocpp/prj.conf new file mode 100644 index 0000000000000..fc78757a4054f --- /dev/null +++ b/samples/net/ocpp/prj.conf @@ -0,0 +1,60 @@ +CONFIG_HTTP_CLIENT=y +CONFIG_WEBSOCKET_CLIENT=y + +# Networking config +CONFIG_NETWORKING=y +CONFIG_NET_IPV4=y +CONFIG_NET_IPV6=n +CONFIG_NET_TCP=y +CONFIG_NET_SOCKETS=y +CONFIG_NET_SOCKETS_POLL_MAX=4 +CONFIG_NET_L2_ETHERNET=y +CONFIG_ETH_DRIVER=y +CONFIG_LOG=y + +CONFIG_LOG_MODE_IMMEDIATE=n + +# Networking debug (Optional) +CONFIG_NET_LOG=n +CONFIG_NET_SOCKETS_LOG_LEVEL_DBG=n + +# OCPP +CONFIG_OCPP=y + +# Please set the server addresses when compiling the sample +CONFIG_NET_SAMPLE_SNTP_SERVER="" +CONFIG_NET_SAMPLE_OCPP_SERVER="" +CONFIG_NET_SAMPLE_OCPP_PORT=8443 + +CONFIG_MAIN_STACK_SIZE=4096 +CONFIG_HEAP_MEM_POOL_SIZE=15000 + +CONFIG_JSONC=y +CONFIG_PICOLIBC=y +CONFIG_NET_ARP=y +CONFIG_NET_UDP=y +CONFIG_NET_DHCPV4=y +CONFIG_NET_DHCPV4_OPTION_CALLBACKS=y +CONFIG_TEST_RANDOM_GENERATOR=y + +CONFIG_NET_TX_STACK_SIZE=2048 +CONFIG_NET_RX_STACK_SIZE=2048 +CONFIG_NET_PKT_RX_COUNT=28 +CONFIG_NET_BUF_RX_COUNT=60 +CONFIG_NET_MGMT=y +CONFIG_NET_MGMT_EVENT=y + +CONFIG_SNTP=y + +CONFIG_ZBUS=y +CONFIG_ZBUS_RUNTIME_OBSERVERS=y + +CONFIG_POSIX_API=y +CONFIG_POSIX_CLOCK_SELECTION=y + +# Logging +CONFIG_LOG=y +CONFIG_PRINTK=y + + + diff --git a/samples/net/ocpp/sample.yaml b/samples/net/ocpp/sample.yaml new file mode 100644 index 0000000000000..53607dfc68b55 --- /dev/null +++ b/samples/net/ocpp/sample.yaml @@ -0,0 +1,18 @@ +sample: + description: OCPP sample application + name: OCPP charge point +common: + harness: net + tags: + - net + - ocpp + +tests: + sample.net.ocpp: + platform_allow: + - stm32f769i_disco + integration_platforms: + - qemu_x86 + tags: + - net + - ocpp diff --git a/samples/net/ocpp/src/main.c b/samples/net/ocpp/src/main.c new file mode 100644 index 0000000000000..0c77c8eaef055 --- /dev/null +++ b/samples/net/ocpp/src/main.c @@ -0,0 +1,395 @@ +/* + * Copyright (c) 2024 Linumiz + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +LOG_MODULE_REGISTER(main, LOG_LEVEL_INF); + +#define NO_OF_CONN 2 +K_KERNEL_STACK_ARRAY_DEFINE(cp_stk, NO_OF_CONN, 2 * 1024); +K_SEM_DEFINE(run_app, 0, 1); + +static struct k_thread tinfo[NO_OF_CONN]; +static k_tid_t tid[NO_OF_CONN]; +static char idtag[NO_OF_CONN][25]; + +#if defined(CONFIG_NET_DHCPV4) +static struct net_mgmt_event_callback ocpp_dhcp_cb; + +static void ocpp_handler_cb(struct net_mgmt_event_callback *cb, + uint32_t mgmt_event, struct net_if *iface) +{ + char buf[NET_IPV4_ADDR_LEN]; + + LOG_INF("net mgr cb"); + if (mgmt_event != NET_EVENT_IPV4_DHCP_BOUND) { + return; + } + + LOG_INF("Your address: %s", + net_addr_ntop(AF_INET, + &iface->config.dhcpv4.requested_ip, + buf, sizeof(buf))); + LOG_INF("Lease time: %u seconds", + iface->config.dhcpv4.lease_time); + LOG_INF("Router: %s", + net_addr_ntop(AF_INET, + &iface->config.ip.ipv4->gw, + buf, sizeof(buf))); + + k_sem_give(&run_app); +} + +static int ocpp_test_dhcp_init(void) +{ + struct net_if *iface; + + net_mgmt_init_event_callback(&ocpp_dhcp_cb, ocpp_handler_cb, + NET_EVENT_IPV4_DHCP_BOUND); + + net_mgmt_add_event_callback(&ocpp_dhcp_cb); + + iface = net_if_get_default(); + if (!iface) { + LOG_ERR("wifi/eth interface not available"); + return -1; + } + + net_dhcpv4_start(iface); + + return 0; +} +#endif + +static int ocpp_get_time_from_sntp(void) +{ + struct sntp_ctx ctx; + struct sntp_time stime; + struct sockaddr_in addr; + struct timespec tv; + int ret; + + /* ipv4 */ + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_port = htons(123); + inet_pton(AF_INET, CONFIG_NET_SAMPLE_SNTP_SERVER, &addr.sin_addr); + + ret = sntp_init(&ctx, (struct sockaddr *) &addr, + sizeof(struct sockaddr_in)); + if (ret < 0) { + LOG_ERR("Failed to init SNTP IPv4 ctx: %d", ret); + return ret; + } + + ret = sntp_query(&ctx, 60, &stime); + if (ret < 0) { + LOG_ERR("SNTP IPv4 request failed: %d", ret); + return ret; + } + + LOG_INF("sntp succ since Epoch: %llu\n", stime.seconds); + tv.tv_sec = stime.seconds; + clock_settime(CLOCK_REALTIME, &tv); + sntp_close(&ctx); + return 0; +} + +ZBUS_CHAN_DEFINE(ch_event, /* Name */ + union ocpp_io_value, + NULL, /* Validator */ + NULL, /* User data */ + ZBUS_OBSERVERS_EMPTY, /* observers */ + ZBUS_MSG_INIT(0) /* Initial value {0} */ +); + +ZBUS_SUBSCRIBER_DEFINE(cp_thread0, 5); +ZBUS_SUBSCRIBER_DEFINE(cp_thread1, 5); +struct zbus_observer *obs[NO_OF_CONN] = {&cp_thread0, &cp_thread1}; + +static void ocpp_cp_entry(void *p1, void *p2, void *p3); +static int user_notify_cb(enum ocpp_notify_reason reason, + union ocpp_io_value *io, + void *user_data) +{ + static int wh = 6 + NO_OF_CONN; + int idx; + int i; + + switch (reason) { + case OCPP_USR_GET_METER_VALUE: + if (OCPP_OMM_ACTIVE_ENERGY_TO_EV == io->meter_val.mes) { + snprintf(io->meter_val.val, CISTR50, "%u", + wh + io->meter_val.id_con); + + wh++; + LOG_DBG("mtr reading val %s con %d", io->meter_val.val, + io->meter_val.id_con); + + return 0; + } + break; + + case OCPP_USR_START_CHARGING: + if (io->start_charge.id_con < 0) { + for (i = 0; i < NO_OF_CONN; i++) { + if (!tid[i]) { + break; + } + } + + if (i >= NO_OF_CONN) { + return -EBUSY; + } + idx = i; + } else { + idx = io->start_charge.id_con - 1; + } + + if (!tid[idx]) { + LOG_INF("Remote start charging idtag %s connector %d\n", + idtag[idx], idx + 1); + + strncpy(idtag[idx], io->start_charge.idtag, + sizeof(idtag[0])); + + tid[idx] = k_thread_create(&tinfo[idx], cp_stk[idx], + sizeof(cp_stk[idx]), ocpp_cp_entry, + (void *)(idx + 1), idtag[idx], + obs[idx], 7, 0, K_NO_WAIT); + + return 0; + } + break; + + case OCPP_USR_STOP_CHARGING: + zbus_chan_pub(&ch_event, io, K_MSEC(100)); + return 0; + + case OCPP_USR_UNLOCK_CONNECTOR: + LOG_INF("unlock connector %d\n", io->unlock_con.id_con); + return 0; + } + + return -ENOTSUP; +} + +static void ocpp_cp_entry(void *p1, void *p2, void *p3) +{ + int ret; + int idcon = (uint32_t)p1; + char *idtag = (char *)p2; + struct zbus_observer *obs = (struct zbus_observer *)p3; + ocpp_session_handle_t sh = NULL; + enum ocpp_auth_status status; + const uint32_t timeout_ms = 500; + + ret = ocpp_session_open(&sh); + if (ret) { + LOG_ERR("ocpp open ses idcon %d> res %d\n", idcon, ret); + return; + } + + while (1) { + /* Avoid quick retry since authorization request is possible only + * after Bootnotification process (handled in lib) completed. + */ + + k_sleep(K_SECONDS(5)); + ret = ocpp_authorize(sh, + idtag, + &status, + timeout_ms); + if (ret) { + LOG_ERR("ocpp auth %d> idcon %d status %d\n", + ret, idcon, status); + } else { + LOG_INF("ocpp auth %d> idcon %d status %d\n", + ret, idcon, status); + break; + } + + } + + if (status != OCPP_AUTH_ACCEPTED) { + LOG_ERR("ocpp start idcon %d> not authorized status %d\n", + idcon, status); + return; + } + + ret = ocpp_start_transaction(sh, sys_rand32_get(), idcon, timeout_ms); + if (!ret) { + const struct zbus_channel *chan; + union ocpp_io_value io; + + LOG_INF("ocpp start charging connector id %d\n", idcon); + memset(&io, 0xff, sizeof(io)); + + /* wait for stop charging event from main or remote CS */ + zbus_chan_add_obs(&ch_event, obs, K_SECONDS(1)); + do { + zbus_sub_wait(obs, &chan, K_FOREVER); + zbus_chan_read(chan, &io, K_SECONDS(1)); + + if (io.stop_charge.id_con == idcon) { + break; + } + + } while (1); + } + + ret = ocpp_stop_transaction(sh, sys_rand32_get(), timeout_ms); + if (ret) { + LOG_ERR("ocpp stop txn idcon %d> %d\n", idcon, ret); + return; + } + + LOG_INF("ocpp stop charging connector id %d\n", idcon); + k_sleep(K_SECONDS(1)); + ocpp_session_close(sh); + tid[idcon - 1] = NULL; + k_sleep(K_SECONDS(1)); + k_thread_abort(k_current_get()); +} + +static int ocpp_getaddrinfo(char *server, int port, char **ip) +{ + int ret; + uint8_t retry = 5; + char addr_str[INET_ADDRSTRLEN]; + struct sockaddr_storage b; + struct addrinfo *result = NULL; + struct addrinfo *addr; + struct addrinfo hints = { + .ai_family = AF_INET, + .ai_socktype = SOCK_STREAM + }; + + LOG_INF("cs server %s %d", server, port); + do { + ret = getaddrinfo(server, NULL, &hints, &result); + if (ret == -EAGAIN) { + LOG_ERR("ERROR: getaddrinfo %d, rebind", ret); + k_sleep(K_SECONDS(1)); + } else if (ret) { + LOG_ERR("ERROR: getaddrinfo failed %d", ret); + return ret; + } + } while (--retry && ret); + + addr = result; + while (addr != NULL) { + /* IPv4 Address. */ + if (addr->ai_addrlen == sizeof(struct sockaddr_in)) { + struct sockaddr_in *broker = + ((struct sockaddr_in *)&b); + + broker->sin_addr.s_addr = + ((struct sockaddr_in *)addr->ai_addr) + ->sin_addr.s_addr; + broker->sin_family = AF_INET; + broker->sin_port = htons(port); + + inet_ntop(AF_INET, &broker->sin_addr, addr_str, + sizeof(addr_str)); + + *ip = strdup(addr_str); + LOG_INF("IPv4 Address %s", addr_str); + break; + } + + LOG_ERR("error: ai_addrlen = %u should be %u or %u", + (unsigned int)addr->ai_addrlen, + (unsigned int)sizeof(struct sockaddr_in), + (unsigned int)sizeof(struct sockaddr_in6)); + + addr = addr->ai_next; + } + + /* Free the address. */ + freeaddrinfo(result); + + return 0; +} + +int main(void) +{ + int ret; + int i; + char *ip = NULL; + + struct ocpp_cp_info cpi = { "basic", "zephyr", .num_of_con = NO_OF_CONN }; + struct ocpp_cs_info csi = {NULL, + "/steve/websocket/CentralSystemService/zephyr", + CONFIG_NET_SAMPLE_OCPP_PORT, + AF_INET}; + + printk("OCPP sample %s\n", CONFIG_BOARD); + +#if defined(CONFIG_NET_DHCPV4) + ocpp_test_dhcp_init(); +#endif + + /* Wait for device dhcp ip recive */ + k_sem_take(&run_app, K_FOREVER); + + ret = ocpp_getaddrinfo(CONFIG_NET_SAMPLE_OCPP_SERVER, CONFIG_NET_SAMPLE_OCPP_PORT, &ip); + if (ret < 0) { + return ret; + } + + csi.cs_ip = ip; + + ocpp_get_time_from_sntp(); + + ret = ocpp_init(&cpi, + &csi, + user_notify_cb, + NULL); + if (ret) { + LOG_ERR("ocpp init failed %d\n", ret); + return ret; + } + + + /* Spawn threads for each connector */ + for (i = 0; i < NO_OF_CONN; i++) { + snprintf(idtag[i], sizeof(idtag[0]), "ZepId%02d", i); + + tid[i] = k_thread_create(&tinfo[i], cp_stk[i], + sizeof(cp_stk[i]), + ocpp_cp_entry, (void *)(i + 1), + idtag[i], obs[i], 7, 0, K_NO_WAIT); + } + + /* Active charging session */ + k_sleep(K_SECONDS(30)); + + /* Send stop charging to thread */ + for (i = 0; i < NO_OF_CONN; i++) { + union ocpp_io_value io = {0}; + + io.stop_charge.id_con = i + 1; + + zbus_chan_pub(&ch_event, &io, K_MSEC(100)); + k_sleep(K_SECONDS(1)); + } + + /* User could trigger remote start/stop transcation from CS server */ + k_sleep(K_SECONDS(1200)); + + return 0; +} diff --git a/subsys/net/lib/CMakeLists.txt b/subsys/net/lib/CMakeLists.txt index 02d43e2190af5..2ab4b4d5f3025 100644 --- a/subsys/net/lib/CMakeLists.txt +++ b/subsys/net/lib/CMakeLists.txt @@ -16,6 +16,7 @@ add_subdirectory_ifdef(CONFIG_NET_ZPERF zperf) add_subdirectory_ifdef(CONFIG_NET_SHELL shell) add_subdirectory_ifdef(CONFIG_NET_TRICKLE trickle) add_subdirectory_ifdef(CONFIG_NET_DHCPV6 dhcpv6) +add_subdirectory_ifdef(CONFIG_OCPP ocpp) if (CONFIG_NET_DHCPV4 OR CONFIG_NET_DHCPV4_SERVER) add_subdirectory(dhcpv4) diff --git a/subsys/net/lib/Kconfig b/subsys/net/lib/Kconfig index b8c92030c4373..ac3b7f614f48f 100644 --- a/subsys/net/lib/Kconfig +++ b/subsys/net/lib/Kconfig @@ -23,6 +23,8 @@ source "subsys/net/lib/socks/Kconfig" source "subsys/net/lib/sntp/Kconfig" +source "subsys/net/lib/ocpp/Kconfig" + endmenu menu "Network Libraries" diff --git a/subsys/net/lib/ocpp/CMakeLists.txt b/subsys/net/lib/ocpp/CMakeLists.txt new file mode 100644 index 0000000000000..5d7a237bd3bf6 --- /dev/null +++ b/subsys/net/lib/ocpp/CMakeLists.txt @@ -0,0 +1,17 @@ +# SPDX-License-Identifier: Apache-2.0 + +zephyr_include_directories(.) + +zephyr_interface_library_named(ocpp) + +zephyr_library() + +zephyr_library_sources( + ocpp.c + core.c + key_mgmt.c + ocpp_j.c + ocpp_wamp_rpc.c +) + +zephyr_library_link_libraries(ocpp) diff --git a/subsys/net/lib/ocpp/Kconfig b/subsys/net/lib/ocpp/Kconfig new file mode 100644 index 0000000000000..0fae04bf05095 --- /dev/null +++ b/subsys/net/lib/ocpp/Kconfig @@ -0,0 +1,83 @@ +# Copyright (c) 2024 Linumiz +# SPDX-License-Identifier: Apache-2.0 + +config OCPP + bool "Open Charge Point Protocol [EXPERIMENTAL]" + depends on JSONC + depends on WEBSOCKET_CLIENT + select EXPERIMENTAL + help + This option enables the open charge point protocol library + +if OCPP + +module=OCPP +module-dep=NET_LOG +module-str=Log level for OCPP +module-help=Enables ocpp debug messages. +source "subsys/net/Kconfig.template.log_config.net" + +config OCPP_INT_THREAD_STACKSIZE + int "OCPP internal thread stacksize" + default 4096 + help + OCPP internal thread stacksize + +config OCPP_WSREADER_THREAD_STACKSIZE + int "OCPP websocket reader thread stacksize" + default 4096 + help + OCPP websocket reader thread stacksize + +config OCPP_RECV_BUFFER_SIZE + int "OCPP websocket recive buffer size" + default 2048 + help + OCPP websocket recive buffer size + +config OCPP_INTERNAL_MSGQ_CNT + int "OCPP internal message queue count" + default 10 + help + Messages communication between websocket reader and internal + process thread + +config OCPP_MSG_JSON + bool "PDU message format as JSON" + default y + help + If enabled y, then the Charge Point and Central System PDU message + format as JSON + +config OCPP_PROFILE_SMART_CHARGE + bool "OCPP profile smart charging" + help + Enables the OCPP library to support the Smart Charging profile + functionality. Charge Point may support this profile as optional. + +config OCPP_PROFILE_REMOTE_TRIG + bool "OCPP profile remote trigger" + help + Enables the OCPP library to support the Remote Trigger + functionality. Charge Point may support this profile as optional. + +config OCPP_PROFILE_RESERVATION + bool "OCPP profile reservation" + help + Enables the OCPP library to support the Profile Reservation + functionality. Charge Point may support this profile as optional. + +config OCPP_PROFILE_LOCAL_AUTH_LIST + bool "OCPP profile local authorization list" + help + Enables the OCPP library to support the Local Authorization List + functionality. Stores IdTag and its validity information in the + persistent stroage. Charge Point may support this as optional. + +config OCPP_PROFILE_FIRMWARE_MGNT + bool "OCPP profile firmware management" + help + Enables the OCPP library to support the Firmware Management of + Charge Point as optional profile. + +endif # OCPP diff --git a/subsys/net/lib/ocpp/core.c b/subsys/net/lib/ocpp/core.c new file mode 100644 index 0000000000000..44d7c3579bd55 --- /dev/null +++ b/subsys/net/lib/ocpp/core.c @@ -0,0 +1,500 @@ +/* + * Copyright (c) 2024 Linumiz + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "ocpp_i.h" + +#define OCPP_PDU_TIMEOUT 2 +#define OCPP_USER_REQ_PDU_BUF 350 +#define FILL_METER_TABLE(_mes, _smes, _unit)[_mes] = {.mes = _mes, \ + .smes = _smes, .unit = _unit} + +static struct { + enum ocpp_meter_measurand mes; + const char * const smes; + const char * const unit; +} mtr_ref_table[] = { + FILL_METER_TABLE(OCPP_OMM_CURRENT_FROM_EV, + "Current.Export", "A"), + FILL_METER_TABLE(OCPP_OMM_CURRENT_TO_EV, + "Current.Import", "A"), + FILL_METER_TABLE(OCPP_OMM_CURRENT_MAX_OFFERED_TO_EV, + "Current.OfferedMaximum", "A"), + FILL_METER_TABLE(OCPP_OMM_ACTIVE_ENERGY_FROM_EV, + "Energy.Active.Export.Register", "Wh"), + FILL_METER_TABLE(OCPP_OMM_ACTIVE_ENERGY_TO_EV, + "Energy.Active.Import.Register", "Wh"), + FILL_METER_TABLE(OCPP_OMM_REACTIVE_ENERGY_FROM_EV, + "Energy.Reactive.Export.Register", "varh"), + FILL_METER_TABLE(OCPP_OMM_REACTIVE_ENERGY_TO_EV, + "Energy.Reactive.Import.Register", "varh"), + FILL_METER_TABLE(OCPP_OMM_ACTIVE_POWER_FROM_EV, + "Power.Active.Export", "W"), + FILL_METER_TABLE(OCPP_OMM_ACTIVE_POWER_TO_EV, + "Power.Active.Import", "W"), + FILL_METER_TABLE(OCPP_OMM_REACTIVE_POWER_FROM_EV, + "Power.Reactive.Export", "var"), + FILL_METER_TABLE(OCPP_OMM_REACTIVE_POWER_TO_EV, + "Power.Reactive.Import", "var"), + FILL_METER_TABLE(OCPP_OMM_POWERLINE_FREQ, + "Frequency", NULL), + FILL_METER_TABLE(OCPP_OMM_POWER_FACTOR, + "Power.Factor", NULL), + FILL_METER_TABLE(OCPP_OMM_POWER_MAX_OFFERED_TO_EV, + "Power.Offered", NULL), + FILL_METER_TABLE(OCPP_OMM_FAN_SPEED, + "RPM", "rpm"), + FILL_METER_TABLE(OCPP_OMM_CHARGING_PERCENT, + "SoCState", "Percent"), + FILL_METER_TABLE(OCPP_OMM_TEMPERATURE, + "Temperature", "Celsius"), + FILL_METER_TABLE(OCPP_OMM_VOLTAGE_AC_RMS, + "Voltage", "V") +}; + +int ocpp_boot_notification(ocpp_session_handle_t hndl, + struct ocpp_cp_info *cpi) +{ + int ret; + struct ocpp_session *sh = (struct ocpp_session *)hndl; + struct ocpp_info *ctx = sh->ctx; + struct ocpp_upstream_info *ui = &ctx->ui; + char *buf = ctx->pdu_buf; + ocpp_msg_fp_t fn; + struct ocpp_wamp_rpc_msg rmsg = {0}; + + fn = ctx->cfn[PDU_BOOTNOTIFICATION]; + sh->uid = fn(buf, sizeof(ctx->pdu_buf), sh, cpi); + + rmsg.ctx = ctx; + rmsg.msg = buf; + rmsg.msg_len = strlen(buf) + 1; + rmsg.sndlock = &ui->ws_sndlock; + rmsg.rspsig = &ui->ws_rspsig; + + ret = ocpp_send_to_server(&rmsg, K_SECONDS(OCPP_PDU_TIMEOUT)); + if (!ret && sh->resp_status != BOOT_ACCEPTED) { + ret = -EAGAIN; + } + + return ret; +} + +int ocpp_get_configuration(enum ocpp_key key, struct ocpp_info *ctx, char *uid) +{ + int ret; + char tmp[32]; + char *sval = NULL; + char *buf = ctx->pdu_buf; + struct ocpp_wamp_rpc_msg rmsg = {0}; + union ocpp_keyval *kval; + ocpp_msg_fp_t fn; + bool is_rw; + enum ocpp_key_type ktype; + + if (key >= OCPP_CFG_END) { + return -EINVAL; + } + + ktype = ocpp_get_keyval_type(key); + is_rw = ocpp_is_key_rw(key); + kval = ocpp_get_key_val(key); + + if (ktype < KEY_TYPE_STR) { + sval = tmp; + snprintk(tmp, sizeof(tmp), "%d", kval->ival); + } else { + sval = kval->str; + } + + fn = ctx->cfn[PDU_GET_CONFIGURATION]; + fn(buf, sizeof(ctx->pdu_buf), ocpp_get_key_literal(key), sval, is_rw, uid); + + rmsg.ctx = ctx; + rmsg.msg = buf; + rmsg.msg_len = strlen(buf) + 1; + + ret = ocpp_send_to_server(&rmsg, K_SECONDS(OCPP_PDU_TIMEOUT)); + return ret; +} + +int ocpp_change_configuration(char *skey, struct ocpp_info *ctx, + char *sval, char *uid) +{ + int ret = -EINVAL; + char *buf = ctx->pdu_buf; + struct ocpp_wamp_rpc_msg rmsg = {0}; + union ocpp_keyval kval; + ocpp_msg_fp_t fn; + enum ocpp_key key; + const char *res = "Accepted"; + + key = ocpp_key_to_cfg(skey); + if (key < OCPP_CFG_END) { + enum ocpp_key_type ktype; + + ktype = ocpp_get_keyval_type(key); + + if (ktype < KEY_TYPE_STR) { + kval.ival = atoi(sval); + } else { + kval.str = sval; + } + + ret = ocpp_update_cfg_val(key, &kval); + if (ret) { + res = "Rejected"; + } + } else { + res = "NotSupported"; + } + + if (ret == 0) { + switch (key) { + case CFG_MTR_VAL_SAMPLE_INTERVAL: + if (atomic_get(&ctx->mtr_timer_ref_cnt) <= 0) { + break; + } + + k_timer_start(&ctx->mtr_timer, + K_SECONDS(kval.ival), + K_SECONDS(kval.ival)); + break; + + default: + break; + } + } + + fn = ctx->cfn[PDU_CHANGE_CONFIGURATION]; + fn(buf, sizeof(ctx->pdu_buf), res, uid); + + rmsg.ctx = ctx; + rmsg.msg = buf; + rmsg.msg_len = strlen(buf) + 1; + + ret = ocpp_send_to_server(&rmsg, K_SECONDS(OCPP_PDU_TIMEOUT)); + + return ret; +} + +int ocpp_authorize(ocpp_session_handle_t hndl, char *idtag, + enum ocpp_auth_status *status, + uint32_t timeout_ms) +{ + int ret; + struct ocpp_session *sh = (struct ocpp_session *)hndl; + struct ocpp_info *ctx; + struct ocpp_upstream_info *ui; + char buf[OCPP_USER_REQ_PDU_BUF]; + union ocpp_keyval *val; + struct ocpp_wamp_rpc_msg rmsg = {0}; + ocpp_msg_fp_t fn; + + if (!idtag || !status || !ocpp_session_is_valid(sh)) { + return -EINVAL; + } + + ctx = sh->ctx; + if (ctx->state < CP_STATE_READY) { + return -EAGAIN; + } + + if (ctx->is_cs_offline) { + val = ocpp_get_key_val(CFG_LOCAL_AUTH_OFFLINE); + if (val && !val->ival) { + return -EAGAIN; + } + } + + strncpy(sh->idtag, idtag, sizeof(sh->idtag)); + ui = &ctx->ui; + fn = ctx->cfn[PDU_AUTHORIZE]; + sh->uid = fn(buf, sizeof(buf), sh); + + rmsg.ctx = ctx; + rmsg.msg = buf; + rmsg.msg_len = strlen(buf) + 1; + rmsg.sndlock = &ui->ws_sndlock; + rmsg.rspsig = &ui->ws_rspsig; + ret = ocpp_send_to_server(&rmsg, K_MSEC(timeout_ms)); + if (ret < 0) { + return ret; + } + + *status = sh->resp_status; + if (sh->resp_status == OCPP_AUTH_ACCEPTED) { + sh->is_active = true; + } + + return 0; +} + +int ocpp_heartbeat(ocpp_session_handle_t hndl) +{ + int ret; + struct ocpp_session *sh = (struct ocpp_session *)hndl; + struct ocpp_info *ctx = sh->ctx; + struct ocpp_upstream_info *ui = &ctx->ui; + ocpp_msg_fp_t fn; + struct ocpp_wamp_rpc_msg rmsg = {0}; + char *buf = ctx->pdu_buf; + + fn = ctx->cfn[PDU_HEARTBEAT]; + sh->uid = fn(buf, sizeof(ctx->pdu_buf), sh); + + rmsg.ctx = ctx; + rmsg.msg = buf; + rmsg.msg_len = strlen(buf) + 1; + rmsg.sndlock = &ui->ws_sndlock; + rmsg.rspsig = &ui->ws_rspsig; + + ret = ocpp_send_to_server(&rmsg, K_SECONDS(OCPP_PDU_TIMEOUT)); + if (!ret && sh->resp_status != BOOT_ACCEPTED) { + ret = -EAGAIN; + } + + return ret; +} + +int ocpp_start_transaction(ocpp_session_handle_t hndl, + int Wh, + uint8_t conn_id, + uint32_t timeout_ms) +{ + struct ocpp_session *sh = (struct ocpp_session *)hndl; + struct ocpp_info *ctx; + struct ocpp_upstream_info *ui; + char buf[OCPP_USER_REQ_PDU_BUF]; + struct ocpp_wamp_rpc_msg rmsg = {0}; + ocpp_msg_fp_t fn; + char utc[CISTR25] = {0}; + int ret; + + if (!ocpp_session_is_valid(sh) || + conn_id <= 0) { + return -EINVAL; + } + ctx = sh->ctx; + sh->idcon = conn_id; + if (ctx->state < CP_STATE_READY) { + return -EAGAIN; + } + + if (ctx->is_cs_offline) { + /* todo: fill meter reading + * server offline, accept start txn and save it in Q. + */ + return 0; + } + + fn = ctx->cfn[PDU_START_TRANSACTION]; + ocpp_get_utc_now(utc); + sh->uid = fn(buf, sizeof(buf), sh, Wh, -1, utc); + + ui = &ctx->ui; + rmsg.ctx = ctx; + rmsg.msg = buf; + rmsg.msg_len = strlen(buf) + 1; + rmsg.sndlock = &ui->ws_sndlock; + rmsg.rspsig = &ui->ws_rspsig; + ret = ocpp_send_to_server(&rmsg, K_MSEC(timeout_ms)); + if (ret < 0) { + return ret; + } + + if (sh->resp_status != OCPP_AUTH_ACCEPTED) { + sh->is_active = false; + ret = -EACCES; + } else { + union ocpp_keyval *keyval; + + keyval = ocpp_get_key_val(CFG_MTR_VAL_SAMPLE_INTERVAL); + if (!atomic_inc(&ctx->mtr_timer_ref_cnt)) { + k_timer_start(&ctx->mtr_timer, + K_SECONDS(keyval->ival), + K_SECONDS(keyval->ival)); + } + } + + return ret; +} + +int ocpp_stop_transaction(ocpp_session_handle_t hndl, + int Wh, + uint32_t timeout_ms) +{ + struct ocpp_session *sh = (struct ocpp_session *)hndl; + struct ocpp_info *ctx; + struct ocpp_upstream_info *ui; + char buf[OCPP_USER_REQ_PDU_BUF]; + char utc[CISTR25] = {0}; + struct ocpp_wamp_rpc_msg rmsg = {0}; + ocpp_msg_fp_t fn; + int ret; + + if (!ocpp_session_is_valid(sh)) { + return -EINVAL; + } + + ctx = sh->ctx; + if (ctx->state < CP_STATE_READY) { + return -EAGAIN; + } + + sh->is_active = false; + if (atomic_dec(&ctx->mtr_timer_ref_cnt) <= 1) { + k_timer_stop(&ctx->mtr_timer); + } + + if (ctx->is_cs_offline) { + /* todo: fill meter reading + * server offline, accept start txn and save it in Q. + */ + return 0; + } + + fn = ctx->cfn[PDU_STOP_TRANSACTION]; + ocpp_get_utc_now(utc); + sh->uid = fn(buf, sizeof(buf), sh, Wh, NULL, utc); + + ui = &ctx->ui; + rmsg.ctx = ctx; + rmsg.msg = buf; + rmsg.msg_len = strlen(buf) + 1; + rmsg.sndlock = &ui->ws_sndlock; + rmsg.rspsig = &ui->ws_rspsig; + ret = ocpp_send_to_server(&rmsg, K_MSEC(timeout_ms)); + + return ret; +} + +int ocpp_remote_start_transaction(struct ocpp_info *ctx, + struct internal_msg *msg, + char *uid) +{ + char *buf = ctx->pdu_buf; + const char *resp = "Rejected"; + struct ocpp_wamp_rpc_msg rmsg = {0}; + ocpp_msg_fp_t fn; + int ret; + + ret = k_msgq_put(ctx->msgq, msg, K_MSEC(100)); + if (ret == 0) { + resp = "Accepted"; + } + + fn = ctx->cfn[PDU_REMOTE_START_TRANSACTION]; + fn(buf, sizeof(ctx->pdu_buf), resp, uid); + + rmsg.ctx = ctx; + rmsg.msg = buf; + rmsg.msg_len = strlen(buf) + 1; + ret = ocpp_send_to_server(&rmsg, K_SECONDS(OCPP_PDU_TIMEOUT)); + + return ret; +} + +int ocpp_remote_stop_transaction(struct ocpp_info *ctx, + struct internal_msg *msg, + int idtxn, char *uid) +{ + char *buf = ctx->pdu_buf; + const char *resp = "Rejected"; + struct ocpp_wamp_rpc_msg rmsg = {0}; + ocpp_msg_fp_t fn; + sys_snode_t *node; + struct ocpp_session *sh; + bool is_found = false; + int ret; + + k_mutex_lock(&ctx->ilock, K_FOREVER); + SYS_SLIST_FOR_EACH_NODE(&ctx->slist, node) { + sh = to_session(node); + if (sh->is_active && + sh->idtxn == idtxn) { + is_found = true; + break; + } + } + k_mutex_unlock(&ctx->ilock); + + if (is_found) { + msg->usr.stop_charge.id_con = sh->idcon; + ret = k_msgq_put(ctx->msgq, msg, K_MSEC(100)); + if (ret == 0) { + resp = "Accepted"; + } + } + + fn = ctx->cfn[PDU_REMOTE_STOP_TRANSACTION]; + fn(buf, sizeof(ctx->pdu_buf), resp, uid); + + rmsg.ctx = ctx; + rmsg.msg = buf; + rmsg.msg_len = strlen(buf) + 1; + ret = ocpp_send_to_server(&rmsg, K_SECONDS(OCPP_PDU_TIMEOUT)); + + return ret; +} + +int ocpp_unlock_connector(struct ocpp_info *ctx, + struct internal_msg *msg, + char *uid) +{ + char *buf = ctx->pdu_buf; + char *resp = "UnlockFailed"; + struct ocpp_wamp_rpc_msg rmsg = {0}; + ocpp_msg_fp_t fn; + int ret; + + ret = k_msgq_put(ctx->msgq, msg, K_MSEC(100)); + if (ret == 0) { + resp = "Unlocked"; + } + + fn = ctx->cfn[PDU_UNLOCK_CONNECTOR]; + fn(buf, sizeof(ctx->pdu_buf), resp, uid); + + rmsg.ctx = ctx; + rmsg.msg = buf; + rmsg.msg_len = strlen(buf) + 1; + ret = ocpp_send_to_server(&rmsg, K_SECONDS(OCPP_PDU_TIMEOUT)); + + return ret; +} + +int ocpp_meter_values(ocpp_session_handle_t hndl, + enum ocpp_meter_measurand mes, + char *sval) +{ + struct ocpp_session *sh = (struct ocpp_session *)hndl; + struct ocpp_info *ctx = sh->ctx; + struct ocpp_upstream_info *ui = &ctx->ui; + char *buf = ctx->pdu_buf; + char utc[CISTR25] = {0}; + struct ocpp_wamp_rpc_msg rmsg = {0}; + ocpp_msg_fp_t fn; + int ret; + + if (ctx->is_cs_offline) { + return -EAGAIN; + } + + fn = ctx->cfn[PDU_METER_VALUES]; + ocpp_get_utc_now(utc); + sh->uid = fn(buf, sizeof(ctx->pdu_buf), sh, utc, sval, + mtr_ref_table[mes].smes, + mtr_ref_table[mes].unit); + + rmsg.ctx = ctx; + rmsg.msg = buf; + rmsg.msg_len = strlen(buf) + 1; + rmsg.sndlock = &ui->ws_sndlock; + rmsg.rspsig = &ui->ws_rspsig; + ret = ocpp_send_to_server(&rmsg, K_SECONDS(OCPP_PDU_TIMEOUT)); + + return ret; +} diff --git a/subsys/net/lib/ocpp/credentials/certificate.h b/subsys/net/lib/ocpp/credentials/certificate.h new file mode 100644 index 0000000000000..5a32b4f5d469d --- /dev/null +++ b/subsys/net/lib/ocpp/credentials/certificate.h @@ -0,0 +1,6 @@ +#define CA_CERTIFICATE_TAG 1 +#define TLS_PEER_HOSTNAME "localhost" + +static const unsigned char ca_certificate[] = { +#include "ec.crt.inc" +}; diff --git a/subsys/net/lib/ocpp/credentials/ec-priv.key b/subsys/net/lib/ocpp/credentials/ec-priv.key new file mode 100644 index 0000000000000..782c19d3fa0f8 --- /dev/null +++ b/subsys/net/lib/ocpp/credentials/ec-priv.key @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgwecn6Plv8ONZs2cF +IIlr4B4xaoPYEMHm9mSG4esgQ2uhRANCAARVR4/COFJhHmmGdERod/1DhM5hBcq/ +xQHuUtxC1a977tMFCzINWTy155+/E8uj35FUhsLeFMoyGtgvKHKIBpgt +-----END PRIVATE KEY----- diff --git a/subsys/net/lib/ocpp/credentials/ec.crt b/subsys/net/lib/ocpp/credentials/ec.crt new file mode 100644 index 0000000000000..8ee9a3598fe06 --- /dev/null +++ b/subsys/net/lib/ocpp/credentials/ec.crt @@ -0,0 +1,10 @@ +-----BEGIN CERTIFICATE----- +MIIBfDCCASOgAwIBAgIUW0crZnSm9CwlYmnYdDSohFSG5UwwCgYIKoZIzj0EAwIw +FDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTI0MDgyNzA5NDcxN1oXDTM0MDgyNTA5 +NDcxN1owFDESMBAGA1UEAwwJbG9jYWxob3N0MFkwEwYHKoZIzj0CAQYIKoZIzj0D +AQcDQgAEVUePwjhSYR5phnREaHf9Q4TOYQXKv8UB7lLcQtWve+7TBQsyDVk8teef +vxPLo9+RVIbC3hTKMhrYLyhyiAaYLaNTMFEwHQYDVR0OBBYEFDi6b5XH5Z5d4cSe +S5OVBHaWjB8SMB8GA1UdIwQYMBaAFDi6b5XH5Z5d4cSeS5OVBHaWjB8SMA8GA1Ud +EwEB/wQFMAMBAf8wCgYIKoZIzj0EAwIDRwAwRAIgMEnVFWqIHRphQtWn5CbXomkH +H/mDhf4ux5k55dmRRH8CIFwL0gYBrp26n0AsRSpVN1RroAt7M1MpCgEycVr3QNMQ +-----END CERTIFICATE----- diff --git a/subsys/net/lib/ocpp/key_mgmt.c b/subsys/net/lib/ocpp/key_mgmt.c new file mode 100644 index 0000000000000..2031c42b48a6a --- /dev/null +++ b/subsys/net/lib/ocpp/key_mgmt.c @@ -0,0 +1,191 @@ +/* + * Copyright (c) 2024 Linumiz + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "ocpp_i.h" + +#if defined(CONFIG_OCPP_PROFILE_SMART_CHARGE) +#define CP_SC ",SmartCharging" +#else +#define CP_SC "" +#endif + +#if defined(CONFIG_OCPP_PROFILE_REMOTE_TRIG) +#define CP_RT ",RemoteTrigger" +#else +#define CP_RT "" +#endif + +#if defined(CONFIG_OCPP_PROFILE_RESERVATION) +#define CP_RE ",Reservation" +#else +#define CP_RE "" +#endif + +#if defined(CONFIG_OCPP_PROFILE_LOCAL_AUTH_LIST) +#define CP_LAL ",LocalAuthListManagement" +#else +#define CP_LAL "" +#endif + +#if defined(CONFIG_OCPP_PROFILE_FIRMWARE_MGNT) +#define CP_FM ",FirmwareManagement" +#else +#define CP_FM "" +#endif + +#define CP_CORE "Core" + +struct ocpp_cfg_info { + enum ocpp_key_type type; + bool is_rw; + enum ocpp_key key; + char *skey; + union ocpp_keyval val; +}; + +#define FILL_KEY_TABLE(_type, _rw, _key, _skey, _val)[_key] = \ + {.type = _type, .is_rw = _rw, .key = _key, .skey = _skey, .val = {_val}} + +static struct ocpp_cfg_info cached_key_cfg[OCPP_CFG_END] = { + FILL_KEY_TABLE(KEY_TYPE_BOOL, 1, CFG_ALLOW_OFFLINE_TX_FOR_UNKN_ID, + "AllowOfflineTxForUnknownId", false), + FILL_KEY_TABLE(KEY_TYPE_BOOL, 1, CFG_AUTH_CACHE_ENABLED, + "AuthorizationCacheEnabled", false), + FILL_KEY_TABLE(KEY_TYPE_BOOL, 0, CFG_AUTH_REMOTE_TX_REQ, + "AuthorizeRemoteTxRequests", true), + FILL_KEY_TABLE(KEY_TYPE_INT, 1, CFG_BLINK_REPEAT, "BlinkRepeat", 0), + FILL_KEY_TABLE(KEY_TYPE_INT, 1, CFG_CLK_ALIGN_DATA_INTERVAL, + "ClockAlignedDataInterval", 0), + FILL_KEY_TABLE(KEY_TYPE_INT, 1, CFG_CONN_TIMEOUT, + "ConnectionTimeOut", 0), + FILL_KEY_TABLE(KEY_TYPE_INT, 0, CFG_GETCFG_MAX_KEY, + "GetConfigurationMaxKeys", 1), + FILL_KEY_TABLE(KEY_TYPE_INT, 1, CFG_HEARTBEAT_INTERVAL, + "HeartbeatInterval", 0), + FILL_KEY_TABLE(KEY_TYPE_INT, 1, CFG_LIGHT_INTENSITY, + "LightIntensity", 0), + FILL_KEY_TABLE(KEY_TYPE_BOOL, 1, CFG_LOCAL_AUTH_OFFLINE, + "LocalAuthorizeOffline", 0), + FILL_KEY_TABLE(KEY_TYPE_BOOL, 1, CFG_LOCAL_PREAUTH, + "LocalPreAuthorize", 0), + FILL_KEY_TABLE(KEY_TYPE_INT, 1, CFG_MAX_ENERGYON_INVL_ID, + "MaxEnergyOnInvalidId", 0), + FILL_KEY_TABLE(KEY_TYPE_CSL, 1, CFG_MTR_VAL_ALGIN_DATA, + "MeterValuesAlignedData", 0), + FILL_KEY_TABLE(KEY_TYPE_INT, 0, CFG_MTR_VAL_ALGIN_DATA_MAXLEN, + "MeterValuesAlignedDataMaxLength", 1), + FILL_KEY_TABLE(KEY_TYPE_CSL, 1, CFG_MTR_VAL_SAMPLED_DATA, + "MeterValuesSampledData", 0), + FILL_KEY_TABLE(KEY_TYPE_INT, 0, CFG_MTR_VAL_SAMPLED_DATA_MAXLEN, + "MeterValuesSampledDataMaxLength", 1), + FILL_KEY_TABLE(KEY_TYPE_INT, 1, CFG_MTR_VAL_SAMPLE_INTERVAL, + "MeterValueSampleInterval", 5), + FILL_KEY_TABLE(KEY_TYPE_INT, 1, CFG_MIN_STATUS_DURATION, + "MinimumStatusDuration", 0), + FILL_KEY_TABLE(KEY_TYPE_INT, 0, CFG_NO_OF_CONNECTORS, + "NumberOfConnectors", 0), + FILL_KEY_TABLE(KEY_TYPE_INT, 1, CFG_REST_RETRIES, + "ResetRetries", 0), + FILL_KEY_TABLE(KEY_TYPE_CSL, 1, CFG_CONN_PHASE_ROT, + "ConnectorPhaseRotation", 0), + FILL_KEY_TABLE(KEY_TYPE_INT, 0, CFG_CONN_PHASE_ROT_MAXLEN, + "ConnectorPhaseRotationMaxLength", 1), + FILL_KEY_TABLE(KEY_TYPE_BOOL, 1, CFG_STOP_TXN_ON_EVSIDE_DISCON, + "StopTransactionOnEVSideDisconnect", 0), + FILL_KEY_TABLE(KEY_TYPE_BOOL, 1, CFG_STOP_TXN_ON_INVL_ID, + "StopTransactionOnInvalidId", 0), + FILL_KEY_TABLE(KEY_TYPE_CSL, 1, CFG_STOP_TXN_ALIGNED_DATA, + "StopTxnAlignedData", 0), + FILL_KEY_TABLE(KEY_TYPE_INT, 0, CFG_STOP_TXN_ALIGNED_DATA_MAXLEN, + "StopTxnAlignedDataMaxLength", 1), + FILL_KEY_TABLE(KEY_TYPE_CSL, 0, CFG_SUPPORTED_FEATURE_PROFILE, + "SupportedFeatureProfiles", + (int) CP_CORE CP_SC CP_RT CP_RE CP_LAL CP_FM), + FILL_KEY_TABLE(KEY_TYPE_CSL, 0, CFG_SUPPORTED_FEATURE_PROFILE_MAXLEN, + "SupportedFeatureProfilesMaxLength", 6), + FILL_KEY_TABLE(KEY_TYPE_INT, 1, CFG_TXN_MSG_ATTEMPTS, + "TransactionMessageAttempts", 0), + FILL_KEY_TABLE(KEY_TYPE_INT, 1, CFG_TXN_MSG_RETRY_INTERVAL, + "TransactionMessageRetryInterval", 0), + FILL_KEY_TABLE(KEY_TYPE_BOOL, 1, CFG_UNLOCK_CONN_ON_EVSIDE_DISCON, + "UnlockConnectorOnEVSideDisconnect", 0), + FILL_KEY_TABLE(KEY_TYPE_INT, 1, CFG_WEBSOCK_PING_INTERVAL, + "WebSocketPingInterval", 0), + + /* optional */ +}; + +enum ocpp_key_type ocpp_get_keyval_type(enum ocpp_key key) +{ + return cached_key_cfg[key].type; +} + +enum ocpp_key ocpp_key_to_cfg(const char *skey) +{ + int i; + struct ocpp_cfg_info *cfg = cached_key_cfg; + + for (i = 0; i < OCPP_CFG_END; i++) { + if (!strncmp(cfg[i].skey, skey, strlen(cfg[i].skey))) { + break; + } + } + + return i; +} + +bool ocpp_is_key_rw(enum ocpp_key key) +{ + return cached_key_cfg[key].is_rw; +} + +union ocpp_keyval *ocpp_get_key_val(enum ocpp_key key) +{ + if (key >= OCPP_CFG_END) { + return NULL; + } + + return &cached_key_cfg[key].val; +} + +char *ocpp_get_key_literal(enum ocpp_key key) +{ + if (key >= OCPP_CFG_END) { + return NULL; + } + + return cached_key_cfg[key].skey; +} + +int ocpp_set_cfg_val(enum ocpp_key key, union ocpp_keyval *val) +{ + enum ocpp_key_type type; + struct ocpp_cfg_info *key_cfg; + + key_cfg = &cached_key_cfg[key]; + type = ocpp_get_keyval_type(key); + if (type < KEY_TYPE_STR) { + key_cfg->val.ival = val->ival; + } else { + if (key_cfg->val.str) { + free(key_cfg->val.str); + } + key_cfg->val.str = strdup(val->str); + } + + return 0; +} + +int ocpp_update_cfg_val(enum ocpp_key key, union ocpp_keyval *val) +{ + + if (key >= OCPP_CFG_END || + !cached_key_cfg[key].is_rw) { + return -EINVAL; + } + + return ocpp_set_cfg_val(key, val); +} diff --git a/subsys/net/lib/ocpp/ocpp.c b/subsys/net/lib/ocpp/ocpp.c new file mode 100644 index 0000000000000..f6113cf2f93d2 --- /dev/null +++ b/subsys/net/lib/ocpp/ocpp.c @@ -0,0 +1,772 @@ +/* + * Copyright (c) 2024 Linumiz + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "ocpp_i.h" +#include +#include + +#if defined(CONFIG_NET_SOCKETS_SOCKOPT_TLS) +#include "credentials/certificate.h" +#endif + +LOG_MODULE_REGISTER(ocpp, CONFIG_OCPP_LOG_LEVEL); + +#define OCPP_UPSTREAM_PRIORITY 7 +#define OCPP_WS_TIMEOUT 5000 +#define OCPP_INTERNAL_MSG_SIZE (sizeof(struct internal_msg)) +#define FILL_PDU_TABLE(_pdu, _spdu)[_pdu] = {_pdu, _spdu, } + +struct ocpp_msg_table { + enum ocpp_pdu_msg pdu; + char *spdu; +}; + +static K_THREAD_STACK_DEFINE(ocpp_int_handler_stack, CONFIG_OCPP_INT_THREAD_STACKSIZE); +static K_THREAD_STACK_DEFINE(ocpp_wsreader_stack, CONFIG_OCPP_WSREADER_THREAD_STACKSIZE); + + +K_MSGQ_DEFINE(ocpp_iq, OCPP_INTERNAL_MSG_SIZE, CONFIG_OCPP_INTERNAL_MSGQ_CNT, sizeof(uint32_t)); + +struct ocpp_info *gctx; + +static struct ocpp_msg_table pdu_msg_table[] = { + FILL_PDU_TABLE(PDU_BOOTNOTIFICATION, "BootNotification"), + FILL_PDU_TABLE(PDU_AUTHORIZE, "Authorize"), + FILL_PDU_TABLE(PDU_START_TRANSACTION, "StartTransaction"), + FILL_PDU_TABLE(PDU_STOP_TRANSACTION, "StopTransaction"), + FILL_PDU_TABLE(PDU_HEARTBEAT, "Heartbeat"), + FILL_PDU_TABLE(PDU_METER_VALUES, "MeterValues"), + FILL_PDU_TABLE(PDU_CLEAR_CACHE, "ClearCache"), + FILL_PDU_TABLE(PDU_REMOTE_START_TRANSACTION, "RemoteStartTransaction"), + FILL_PDU_TABLE(PDU_REMOTE_STOP_TRANSACTION, "RemoteStopTransaction"), + FILL_PDU_TABLE(PDU_GET_CONFIGURATION, "GetConfiguration"), + FILL_PDU_TABLE(PDU_CHANGE_CONFIGURATION, "ChangeConfiguration"), + FILL_PDU_TABLE(PDU_CHANGE_AVAILABILITY, "ChangeAvailability"), + FILL_PDU_TABLE(PDU_UNLOCK_CONNECTOR, "UnlockConnector"), + FILL_PDU_TABLE(PDU_RESET, "Reset"), +}; + +const char *ocpp_get_pdu_literal(enum ocpp_pdu_msg pdu) +{ + if (pdu >= PDU_MSG_END) { + return ""; + } + + return pdu_msg_table[pdu].spdu; +} + +int ocpp_find_pdu_from_literal(const char *msg) +{ + struct ocpp_msg_table *mt = pdu_msg_table; + int i; + + for (i = 0; i < PDU_MSG_END; i++) { + if (!strncmp(mt[i].spdu, msg, strlen(mt[i].spdu))) { + break; + } + } + + return i; +} + +void ocpp_get_utc_now(char utc[CISTR25]) +{ + struct timespec tv; + struct tm htime = {0}; + + gettimeofday(&tv, NULL); + gmtime_r(&tv.tv_sec, &htime); + + snprintk(utc, CISTR25, "%04d-%02d-%02dT%02d:%02d:%02dZ", + htime.tm_year + 1900, + htime.tm_mon + 1, + htime.tm_mday, + htime.tm_hour, + htime.tm_min, + htime.tm_sec); +} + +bool ocpp_session_is_valid(struct ocpp_session *sh) +{ + sys_snode_t *node; + struct ocpp_session *lsh; + bool is_found = false; + + if (!sh) { + return is_found; + } + + k_mutex_lock(&gctx->ilock, K_FOREVER); + SYS_SLIST_FOR_EACH_NODE(&gctx->slist, node) { + lsh = to_session(node); + if (lsh == sh) { + is_found = true; + break; + } + } + k_mutex_unlock(&gctx->ilock); + + return is_found; +} + +static int ocpp_ws_connect_cb(int ws_sock, struct http_request *req, + void *user_data) +{ + struct ocpp_info *ctx = user_data; + + ctx->is_cs_offline = false; + return 0; +} + +static int ocpp_connect_to_cs(struct ocpp_info *ctx) +{ + int ret; + struct websocket_request config = {0}; + struct ocpp_upstream_info *ui = &ctx->ui; + struct sockaddr addr4; + struct sockaddr_in6 addr6; + struct sockaddr *addr; + int addr_size; + + if (ui->csi.sa_family == AF_INET) { + addr = &addr4; + addr_size = sizeof(addr4); + addr->sa_family = ui->csi.sa_family; + net_sin(addr)->sin_port = htons(ui->csi.port); + zsock_inet_pton(addr->sa_family, ui->csi.cs_ip, + &net_sin(addr)->sin_addr); + } else { + addr = (struct sockaddr *)&addr6; + addr_size = sizeof(addr6); + addr->sa_family = ui->csi.sa_family; + net_sin6(addr)->sin6_port = htons(ui->csi.port); + zsock_inet_pton(addr->sa_family, ui->csi.cs_ip, + &net_sin6(addr)->sin6_addr); + } + + ret = zsock_connect(ui->tcpsock, addr, addr_size); + if (ret < 0 && errno != EALREADY && errno != EISCONN) { + LOG_ERR("tcp socket connect fail %d %d", ret, errno); + return ret; + } + + if (ui->wssock >= 0) { + websocket_disconnect(ui->wssock); + } + + if (ui->wssock < 0) { + char buf[128]; + char const *optional_hdr[] = { + "Sec-WebSocket-Protocol: ocpp1.6\r\n", + NULL }; + + snprintk(buf, sizeof(buf), "%s:%u", ui->csi.cs_ip, ui->csi.port); + config.url = ui->csi.ws_url; + config.host = buf; + config.tmp_buf = ui->wsrecv_buf; + config.tmp_buf_len = sizeof(ui->wsrecv_buf); + config.cb = ocpp_ws_connect_cb; + config.optional_headers = optional_hdr; + + ret = websocket_connect(ui->tcpsock, &config, + OCPP_WS_TIMEOUT, ctx); + if (ret < 0) { + LOG_ERR("Websocket connect fail %d", ret); + return ret; + } + ui->wssock = ret; + } + LOG_DBG("WS connect success %d", ui->wssock); + return 0; +} + +static inline void bootnotification_free_resource(struct ocpp_cp_info *cpi) +{ + free(cpi->model); + free(cpi->vendor); + free(cpi->sl_no); + free(cpi->box_sl_no); + free(cpi->fw_ver); + free(cpi->iccid); + free(cpi->imsi); + free(cpi->meter_sl_no); + free(cpi->meter_type); + k_free(cpi); +} + +static void ocpp_internal_handler(void *p1, void *p2, void *p3) +{ + struct ocpp_info *ctx = p1; + struct ocpp_cp_info *cpi = p2; + ocpp_session_handle_t sh; + struct internal_msg msg = {0}; + int ret; + int i; + + /* open internal session */ + ocpp_session_open(&sh); + + while (!k_msgq_get(ctx->msgq, &msg, K_FOREVER)) { + switch (msg.msgtype) { + case PDU_BOOTNOTIFICATION: + + if (!ctx->is_cs_offline && cpi) { + ret = ocpp_boot_notification(sh, cpi); + if (!ret) { + bootnotification_free_resource(cpi); + cpi = NULL; + ctx->state = CP_STATE_READY; + } + } + + k_timer_start(&ctx->hb_timer, + K_SECONDS(ctx->hb_sec), + K_NO_WAIT); + break; + + case PDU_METER_VALUES: + union ocpp_io_value io; + sys_snode_t *node; + struct ocpp_session *lsh; + + /* list of all active session */ + k_mutex_lock(&ctx->ilock, K_FOREVER); + SYS_SLIST_FOR_EACH_NODE(&ctx->slist, node) { + lsh = to_session(node); + if (lsh == sh || + !lsh->is_active) { + continue; + } + + k_mutex_lock(&lsh->slock, K_FOREVER); + k_mutex_unlock(&ctx->ilock); + io.meter_val.id_con = lsh->idcon; + for (i = 0; i < OCPP_OMM_END; i++) { + io.meter_val.mes = i; + ret = ctx->cb(OCPP_USR_GET_METER_VALUE, + &io, ctx->user_data); + if (ret) { + continue; + } + ocpp_meter_values(lsh, i, + io.meter_val.val); + } + k_mutex_lock(&ctx->ilock, K_FOREVER); + k_mutex_unlock(&lsh->slock); + } + k_mutex_unlock(&ctx->ilock); + break; + + case PDU_HEARTBEAT: + ocpp_heartbeat(sh); + /* adjust local time with cs time ! */ + k_timer_start(&ctx->hb_timer, + K_SECONDS(ctx->hb_sec), + K_NO_WAIT); + break; + + case PDU_CS_ONLINE: + /* check offline msg and do nothing on empty + * else read msg and send to server + */ + break; + + case PDU_REMOTE_START_TRANSACTION: + msg.msgtype = OCPP_USR_START_CHARGING; + ctx->cb(msg.msgtype, &msg.usr, ctx->user_data); + break; + + case PDU_REMOTE_STOP_TRANSACTION: + msg.msgtype = OCPP_USR_STOP_CHARGING; + ctx->cb(msg.msgtype, &msg.usr, ctx->user_data); + break; + + case PDU_UNLOCK_CONNECTOR: + msg.msgtype = OCPP_USR_UNLOCK_CONNECTOR; + ctx->cb(msg.msgtype, &msg.usr, ctx->user_data); + break; + + default: + break; + } + } +} + +static int ocpp_process_server_msg(struct ocpp_info *ctx) +{ + ocpp_msg_fp_t fn; + struct ocpp_session *sh = NULL; + struct ocpp_upstream_info *ui = &ctx->ui; + char *buf, *tmp; + char uid[128]; + int ret, idtxn, pdu, i; + bool is_rsp; + struct internal_msg msg; + + ret = parse_rpc_msg(ui->recv_buf, sizeof(ui->recv_buf), + uid, sizeof(uid), &pdu, &is_rsp); + if (ret) { + return ret; + } + + if (is_rsp) { + buf = strtok_r(uid, "-", &tmp); + sh = (struct ocpp_session *) atoi(buf); + + buf = strtok_r(NULL, "-", &tmp); + pdu = atoi(buf); + + if (!ocpp_session_is_valid(sh)) { + sh = NULL; + } + } + + if (pdu >= PDU_MSG_END) { + return -EINVAL; + } + + fn = ctx->pfn[pdu]; + + switch (pdu) { + char skey[CISTR50]; + + case PDU_BOOTNOTIFICATION: + struct boot_notif binfo; + + ret = fn(ui->recv_buf, &binfo); + if (!ret && sh) { + sh->resp_status = binfo.status; + ctx->hb_sec = binfo.interval; + } + break; + + case PDU_AUTHORIZE: + case PDU_STOP_TRANSACTION: + struct ocpp_idtag_info idinfo = {0}; + + if (!sh) { + break; + } + + ret = fn(ui->recv_buf, &idinfo); + if (!ret) { + sh->resp_status = idinfo.auth_status; + } + break; + + case PDU_START_TRANSACTION: + if (!sh) { + break; + } + + ret = fn(ui->recv_buf, &idtxn, &idinfo); + if (!ret) { + sh->idtxn = idtxn; + sh->resp_status = idinfo.auth_status; + } + break; + + case PDU_GET_CONFIGURATION: + memset(skey, 0, sizeof(skey)); + + ret = fn(ui->recv_buf, skey); + if (ret) { + break; + } + + if (*skey) { + enum ocpp_key key; + + key = ocpp_key_to_cfg(skey); + ocpp_get_configuration(key, ctx, uid); + } else { + for (i = 0; i < OCPP_CFG_END; i++) { + ocpp_get_configuration(i, ctx, uid); + } + } + + break; + + case PDU_CHANGE_CONFIGURATION: + char sval[CISTR500] = {0}; + + memset(skey, 0, sizeof(skey)); + ret = fn(ui->recv_buf, skey, sval); + if (ret) { + break; + } + + ocpp_change_configuration(skey, ctx, sval, uid); + break; + + case PDU_HEARTBEAT: + /* todo : sync time */ + break; + + case PDU_REMOTE_START_TRANSACTION: + memset(&msg, 0, OCPP_INTERNAL_MSG_SIZE); + msg.msgtype = PDU_REMOTE_START_TRANSACTION; + + ret = fn(ui->recv_buf, &msg.usr.start_charge.id_con, + msg.usr.start_charge.idtag); + if (ret) { + break; + } + + ocpp_remote_start_transaction(ctx, &msg, uid); + break; + + case PDU_REMOTE_STOP_TRANSACTION: + memset(&msg, 0, OCPP_INTERNAL_MSG_SIZE); + msg.msgtype = PDU_REMOTE_STOP_TRANSACTION; + + ret = fn(ui->recv_buf, &idtxn); + if (ret) { + break; + } + + ocpp_remote_stop_transaction(ctx, &msg, idtxn, uid); + break; + + case PDU_UNLOCK_CONNECTOR: + memset(&msg, 0, OCPP_INTERNAL_MSG_SIZE); + msg.msgtype = PDU_UNLOCK_CONNECTOR; + + ret = fn(ui->recv_buf, &msg.usr.unlock_con.id_con); + if (ret) { + break; + } + + ocpp_unlock_connector(ctx, &msg, uid); + break; + + case PDU_METER_VALUES: + default: + break; + } + + if (is_rsp) { + k_poll_signal_raise(&ui->ws_rspsig, 0); + } + return 0; +} + +#define TCP_CONNECT_AFTER 20 /* fixme arbitrary */ +static void ocpp_wsreader(void *p1, void *p2, void *p3) +{ + struct ocpp_info *ctx = p1; + struct ocpp_upstream_info *ui = &ctx->ui; + struct zsock_pollfd tcpfd; + uint8_t ret; + uint8_t retry_cnt = 0; + + ctx->is_cs_offline = true; + tcpfd.fd = ui->tcpsock; + + tcpfd.events = ZSOCK_POLLIN | ZSOCK_POLLERR + | ZSOCK_POLLHUP | ZSOCK_POLLNVAL; + + while (1) { + + if (ctx->is_cs_offline && + !(retry_cnt++ % TCP_CONNECT_AFTER)) { + k_mutex_lock(&ctx->ilock, K_FOREVER); + ocpp_connect_to_cs(ctx); + k_mutex_unlock(&ctx->ilock); + } + + ret = zsock_poll(&tcpfd, 1, 200); + if (ret <= 0) { + continue; + } + + if ((tcpfd.revents & ZSOCK_POLLERR) || + (tcpfd.revents & ZSOCK_POLLHUP) || + (tcpfd.revents & ZSOCK_POLLNVAL)) { + LOG_ERR("poll err %d", tcpfd.revents); + ctx->is_cs_offline = true; + continue; + } + + if (tcpfd.revents & ZSOCK_POLLIN) { + struct ocpp_wamp_rpc_msg rcv = {0}; + + rcv.ctx = ctx; + rcv.msg = ui->recv_buf; + rcv.sndlock = &ui->ws_sndlock; + rcv.msg_len = sizeof(ui->recv_buf); + + memset(ui->recv_buf, 0, rcv.msg_len); + ret = ocpp_receive_from_server(&rcv, 200); + if (ret < 0) { + continue; + } + + ocpp_process_server_msg(ctx); + } + } +} + + +int ocpp_upstream_init(struct ocpp_info *ctx, struct ocpp_cs_info *csi) +{ + struct ocpp_upstream_info *ui = &ctx->ui; + ui->csi.ws_url = strdup(csi->ws_url); + ui->csi.cs_ip = strdup(csi->cs_ip); + ui->csi.port = csi->port; + ui->csi.sa_family = csi->sa_family; + + LOG_INF("upstream init"); + +#if defined(CONFIG_NET_SOCKETS_SOCKOPT_TLS) + int ret = tls_credential_add(CA_CERTIFICATE_TAG, TLS_CREDENTIAL_CA_CERTIFICATE, ca_certificate, sizeof(ca_certificate)); + + if (ret < 0) + { + LOG_ERR("Failed to register CA certificate: %d", ret); + return ret; + } + + ui->tcpsock = zsock_socket(csi->sa_family, SOCK_STREAM, IPPROTO_TLS_1_3); + if (ui->tcpsock < 0) + { + LOG_ERR("Failed to create TLS socket: %d", errno); + return -errno; + } + + sec_tag_t sec_tag_list[] = {CA_CERTIFICATE_TAG}; + + ret = zsock_setsockopt(ui->tcpsock, SOL_TLS, TLS_SEC_TAG_LIST, sec_tag_list, sizeof(sec_tag_list)); + + if (ret < 0) + { + LOG_ERR("Failed to set TLS_SEC_TAG_LIST: %d", -errno); + goto fail; + } + + ret = zsock_setsockopt(ui->tcpsock, SOL_TLS, TLS_HOSTNAME, TLS_PEER_HOSTNAME, sizeof(TLS_PEER_HOSTNAME)); + if (ret < 0) + { + LOG_ERR("Failed to set TLS_HOSTNAME: %d", -errno); + goto fail; + } +#else + ui->tcpsock = zsock_socket(csi->sa_family, SOCK_STREAM, IPPROTO_TCP); + + if (ui->tcpsock < 0) + { + LOG_ERR("Failed to create TCP socket: %d", -errno); + return -errno; + } +#endif + + k_mutex_init(&ui->ws_sndlock); + k_poll_signal_init(&ui->ws_rspsig); + ui->wssock = -1; + + websocket_init(); + + k_thread_create(&ui->tinfo, ocpp_wsreader_stack, + CONFIG_OCPP_WSREADER_THREAD_STACKSIZE, + ocpp_wsreader, ctx, NULL, NULL, + OCPP_UPSTREAM_PRIORITY, 0, K_MSEC(100)); + + return 0; + +fail: + if (ui->tcpsock >= 0) + { + zsock_close(ui->tcpsock); + ui->tcpsock = -1; + } + return -errno; +} + + +static void timer_heartbeat_cb(struct k_timer *t) +{ + struct ocpp_info *ctx; + struct internal_msg msg = {PDU_HEARTBEAT}; + + ctx = k_timer_user_data_get(t); + if (ctx->state == CP_STATE_BOOTNOTIF) { + msg.msgtype = PDU_BOOTNOTIFICATION; + } + + k_msgq_put(ctx->msgq, &msg, K_NO_WAIT); +} + +static void timer_meter_cb(struct k_timer *t) +{ + struct ocpp_info *ctx; + struct internal_msg msg = {PDU_METER_VALUES}; + + ctx = k_timer_user_data_get(t); + + k_msgq_put(ctx->msgq, &msg, K_NO_WAIT); +} + +static inline void bootnotification_fill_resource(struct ocpp_cp_info *cp, + struct ocpp_cp_info *cpi, + struct ocpp_info *ctx) +{ + struct k_timer *hbt = &ctx->hb_timer; + + cp->model = strdup(cpi->model); + cp->vendor = strdup(cpi->vendor); + if (cp->sl_no) { + cp->sl_no = strdup(cpi->sl_no); + } + + if (cp->box_sl_no) { + cp->box_sl_no = strdup(cpi->box_sl_no); + } + + if (cp->fw_ver) { + cp->fw_ver = strdup(cpi->fw_ver); + } + + if (cp->iccid) { + cp->iccid = strdup(cpi->iccid); + } + + if (cp->imsi) { + cp->imsi = strdup(cpi->imsi); + } + + if (cp->meter_sl_no) { + cp->meter_sl_no = strdup(cpi->meter_sl_no); + } + + if (cp->meter_type) { + cp->meter_type = strdup(cpi->meter_type); + } + + ctx->state = CP_STATE_BOOTNOTIF; + ctx->hb_sec = 10; + k_timer_start(hbt, K_SECONDS(1), K_NO_WAIT); +} + +int ocpp_session_open(ocpp_session_handle_t *hndl) +{ + struct ocpp_session *sh; + + if (!hndl) { + return -EINVAL; + } + + sh = (struct ocpp_session *)k_calloc(1, sizeof(struct ocpp_session)); + if (!sh) { + return -ENOMEM; + } + + sh->is_active = false; + sh->idcon = INVALID_CONN_ID; + sh->idtxn = INVALID_TXN_ID; + sh->ctx = gctx; + k_mutex_init(&sh->slock); + + k_mutex_lock(&gctx->ilock, K_FOREVER); + sys_slist_append(&gctx->slist, &sh->node); + k_mutex_unlock(&gctx->ilock); + + *hndl = (void *)sh; + + return 0; +} + +void ocpp_session_close(ocpp_session_handle_t hndl) +{ + bool is_removed; + struct ocpp_session *sh = (struct ocpp_session *)hndl; + + if (!sh) { + return; + } + + k_mutex_lock(&gctx->ilock, K_FOREVER); + is_removed = sys_slist_find_and_remove(&gctx->slist, &sh->node); + k_mutex_unlock(&gctx->ilock); + + if (is_removed) { + k_mutex_lock(&sh->slock, K_FOREVER); + k_free(sh); + } +} + +int ocpp_init(struct ocpp_cp_info *cpi, + struct ocpp_cs_info *csi, + ocpp_user_notify_callback_t cb, + void *user_data) +{ + struct ocpp_info *ctx; + struct ocpp_cp_info *cp; + union ocpp_keyval val; + int ret; + + if (!cpi || + !cpi->model || + !cpi->vendor || + !csi || + !csi->cs_ip || + !csi->ws_url || + !cb || + cpi->num_of_con < 1) { + return -EINVAL; + } + + ctx = (struct ocpp_info *)k_calloc(1, sizeof(struct ocpp_info)); + if (!ctx) { + return -ENOMEM; + } + + gctx = ctx; + k_mutex_init(&ctx->ilock); + sys_slist_init(&ctx->slist); + ocpp_parser_init(&ctx->cfn, &ctx->pfn); + + ctx->state = CP_STATE_INIT; + ctx->msgq = &ocpp_iq; + + k_timer_init(&ctx->hb_timer, timer_heartbeat_cb, NULL); + k_timer_user_data_set(&ctx->hb_timer, ctx); + k_timer_init(&ctx->mtr_timer, timer_meter_cb, NULL); + k_timer_user_data_set(&ctx->mtr_timer, ctx); + atomic_set(&ctx->mtr_timer_ref_cnt, 0); + + ctx->user_data = user_data; + ctx->cb = cb; + val.ival = cpi->num_of_con; + ocpp_set_cfg_val(CFG_NO_OF_CONNECTORS, &val); + + ret = ocpp_upstream_init(ctx, csi); + if (ret) { + LOG_ERR("ocpp upstream init fail %d", ret); + goto out; + } + + /* free after success of bootnotification to CS */ + cp = (struct ocpp_cp_info *)k_calloc(1, sizeof(struct ocpp_cp_info)); + if (!cp) { + ret = -ENOMEM; + goto out; + } + + bootnotification_fill_resource(cp, cpi, ctx); + + k_thread_create(&ctx->tinfo, ocpp_int_handler_stack, + CONFIG_OCPP_INT_THREAD_STACKSIZE, + ocpp_internal_handler, ctx, + cp, NULL, OCPP_UPSTREAM_PRIORITY, + 0, K_NO_WAIT); + + LOG_INF("ocpp init success"); + return 0; + +out: + k_free(ctx); + return ret; +} diff --git a/subsys/net/lib/ocpp/ocpp_i.h b/subsys/net/lib/ocpp/ocpp_i.h new file mode 100644 index 0000000000000..5f56168be6d9c --- /dev/null +++ b/subsys/net/lib/ocpp/ocpp_i.h @@ -0,0 +1,228 @@ +/* + * Copyright (c) 2023 Linumiz + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef __OCPP_I_ +#define __OCPP_I_ + +#include "string.h" +#include +#include +#include +#include +#include + +/* case-insensitive */ +#define CISTR20 20 +#define CISTR25 25 +#define CISTR500 500 + +#define INVALID_CONN_ID ((uint8_t) -1) +#define INVALID_TXN_ID ((int) -1) + +#define to_session(ptr) CONTAINER_OF(ptr, struct ocpp_session, node) + +enum ocpp_pdu_msg { + PDU_BOOTNOTIFICATION, + PDU_AUTHORIZE, + PDU_START_TRANSACTION, + PDU_STOP_TRANSACTION, + PDU_HEARTBEAT, + PDU_METER_VALUES, + PDU_CLEAR_CACHE, + PDU_REMOTE_START_TRANSACTION, + PDU_REMOTE_STOP_TRANSACTION, + PDU_GET_CONFIGURATION, + PDU_CHANGE_CONFIGURATION, + PDU_CHANGE_AVAILABILITY, + PDU_UNLOCK_CONNECTOR, + PDU_RESET, + + PDU_MSG_END, + + /* internal msg */ + PDU_CS_ONLINE, +}; + +enum ocpp_key_type { + KEY_TYPE_BOOL = 1, + KEY_TYPE_INT = sizeof(int), + KEY_TYPE_STR, + KEY_TYPE_CSL, +}; + +enum boot_status { + BOOT_ACCEPTED, + BOOT_PENDING, + BOOT_REJECTED +}; + +enum ocpp_key { + /* core mandatory */ + CFG_ALLOW_OFFLINE_TX_FOR_UNKN_ID, + CFG_AUTH_CACHE_ENABLED, + CFG_AUTH_REMOTE_TX_REQ, + CFG_BLINK_REPEAT, + CFG_CLK_ALIGN_DATA_INTERVAL, + CFG_CONN_TIMEOUT, + CFG_GETCFG_MAX_KEY, + CFG_HEARTBEAT_INTERVAL, + CFG_LIGHT_INTENSITY, + CFG_LOCAL_AUTH_OFFLINE, + CFG_LOCAL_PREAUTH, + CFG_MAX_ENERGYON_INVL_ID, + CFG_MTR_VAL_ALGIN_DATA, + CFG_MTR_VAL_ALGIN_DATA_MAXLEN, + CFG_MTR_VAL_SAMPLED_DATA, + CFG_MTR_VAL_SAMPLED_DATA_MAXLEN, + CFG_MTR_VAL_SAMPLE_INTERVAL, + CFG_MIN_STATUS_DURATION, + CFG_NO_OF_CONNECTORS, + CFG_REST_RETRIES, + CFG_CONN_PHASE_ROT, + CFG_CONN_PHASE_ROT_MAXLEN, + CFG_STOP_TXN_ON_EVSIDE_DISCON, + CFG_STOP_TXN_ON_INVL_ID, + CFG_STOP_TXN_ALIGNED_DATA, + CFG_STOP_TXN_ALIGNED_DATA_MAXLEN, + CFG_SUPPORTED_FEATURE_PROFILE, + CFG_SUPPORTED_FEATURE_PROFILE_MAXLEN, + CFG_TXN_MSG_ATTEMPTS, + CFG_TXN_MSG_RETRY_INTERVAL, + CFG_UNLOCK_CONN_ON_EVSIDE_DISCON, + CFG_WEBSOCK_PING_INTERVAL, + + /* optional */ + + OCPP_CFG_END +}; + +enum ocpp_cp_state { + CP_STATE_INIT, + CP_STATE_BOOTNOTIF, + CP_STATE_READY, +}; + +enum ocpp_wamp_rpc { + OCPP_WAMP_RPC_REQ = '2', + OCPP_WAMP_RPC_RESP = '3', + OCPP_WAMP_RPC_ERR = '4' +}; + +typedef int (*ocpp_msg_fp_t)(char *buf, ...); + +struct boot_notif { + enum boot_status status; + int interval; + struct timeval date; +}; + +struct ocpp_idtag_info { + char idtag[CISTR20]; + char p_idtag[CISTR20]; + enum ocpp_auth_status auth_status; + char exptime[CISTR25]; +}; + +struct ocpp_upstream_info { + struct k_mutex ws_sndlock; /* to server */ + struct k_poll_signal ws_rspsig; /* wait for resp parsed */ + int tcpsock; + int wssock; + struct k_thread tinfo; + char recv_buf[CONFIG_OCPP_RECV_BUFFER_SIZE]; + char wsrecv_buf[CONFIG_OCPP_RECV_BUFFER_SIZE * 2]; + + struct ocpp_cs_info csi; +}; + +struct ocpp_info { + struct k_mutex ilock; /* internal to lib */ + sys_slist_t slist; /* session list */ + ocpp_msg_fp_t *cfn; + ocpp_msg_fp_t *pfn; + bool is_cs_offline; + struct k_timer hb_timer; + struct k_timer mtr_timer; + atomic_t mtr_timer_ref_cnt; + int hb_sec; /* heartbeat interval */ + struct k_msgq *msgq; + struct k_thread tinfo; + struct ocpp_upstream_info ui; + bool is_cs_connected; /* central system */ + + ocpp_user_notify_callback_t cb; + void *user_data; + enum ocpp_cp_state state; + /* only for pdu message from internal thread (ocpp_internal_handler) */ + char pdu_buf[512]; +}; + +struct ocpp_session { + struct k_mutex slock; /* session lock */ + char idtag[CISTR20]; + bool is_active; + uint8_t idcon; + int idtxn; + int resp_status; + int uid; + sys_snode_t node; + struct ocpp_info *ctx; +}; + +union ocpp_keyval { + int ival; + char *str; +}; + +struct ocpp_wamp_rpc_msg { + char *msg; + size_t msg_len; + struct ocpp_info *ctx; + struct k_mutex *sndlock; + struct k_poll_signal *rspsig; +}; + +struct internal_msg { + enum ocpp_pdu_msg msgtype; + union ocpp_io_value usr; +}; + +void ocpp_parser_init(ocpp_msg_fp_t **cfn, ocpp_msg_fp_t **pfn); +int parse_rpc_msg(char *msg, int msglen, char *uid, int uidlen, + int *pdu, bool *is_rsp); +int ocpp_send_to_server(struct ocpp_wamp_rpc_msg *snd, k_timeout_t timeout); +int ocpp_receive_from_server(struct ocpp_wamp_rpc_msg *rcv, uint32_t timeout); + +int ocpp_unlock_connector(struct ocpp_info *ctx, + struct internal_msg *msg, char *uid); +int ocpp_get_configuration(enum ocpp_key key, struct ocpp_info *ctx, char *uid); +int ocpp_boot_notification(ocpp_session_handle_t hndl, + struct ocpp_cp_info *cpi); +int ocpp_heartbeat(ocpp_session_handle_t hndl); +void ocpp_get_utc_now(char utc[CISTR25]); +bool ocpp_session_is_valid(struct ocpp_session *sh); +int ocpp_remote_start_transaction(struct ocpp_info *ctx, + struct internal_msg *msg, + char *uid); +int ocpp_remote_stop_transaction(struct ocpp_info *ctx, + struct internal_msg *msg, + int idtxn, char *uid); +int ocpp_change_configuration(char *skey, struct ocpp_info *ctx, + char *sval, char *uid); +int ocpp_meter_values(ocpp_session_handle_t hndl, + enum ocpp_meter_measurand mes, + char *sval); + +union ocpp_keyval *ocpp_get_key_val(enum ocpp_key key); +enum ocpp_key ocpp_key_to_cfg(const char *skey); +enum ocpp_key_type ocpp_get_keyval_type(enum ocpp_key key); +char *ocpp_get_key_literal(enum ocpp_key key); +bool ocpp_is_key_rw(enum ocpp_key key); +int ocpp_set_cfg_val(enum ocpp_key key, union ocpp_keyval *val); +int ocpp_update_cfg_val(enum ocpp_key key, union ocpp_keyval *val); +int ocpp_find_pdu_from_literal(const char *msg); +const char *ocpp_get_pdu_literal(enum ocpp_pdu_msg pdu); +#endif /* __OCPP_I_ */ diff --git a/subsys/net/lib/ocpp/ocpp_j.c b/subsys/net/lib/ocpp/ocpp_j.c new file mode 100644 index 0000000000000..7f87332bbcf30 --- /dev/null +++ b/subsys/net/lib/ocpp/ocpp_j.c @@ -0,0 +1,936 @@ +/* + * Copyright (c) 2024 Linumiz + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "ocpp_i.h" +#include +#include + +/* construct msg to server */ +static int frame_rpc_call_req(char *rpcbuf, int len, int pdu, uint32_t ses, + struct json_object *pdumsg) +{ + const char *to_send; + char uid[32] = {0}; + uint32_t rnd; + struct json_object *rpc; + + rpc = json_object_new_array(); + if (!rpc) { + return -ENOMEM; + } + + json_object_array_put_idx(rpc, 0, json_object_new_int(2)); + + rnd = sys_rand32_get(); + snprintk(uid, sizeof(uid), "%u-%u-%u", ses, pdu, rnd); + json_object_array_put_idx(rpc, 1, json_object_new_string(uid)); + + json_object_array_put_idx(rpc, 2, + json_object_new_string(ocpp_get_pdu_literal(pdu))); + + json_object_array_put_idx(rpc, 3, pdumsg); + to_send = json_object_to_json_string_ext(rpc, JSON_C_TO_STRING_PLAIN); + + strncpy(rpcbuf, to_send, len); + + json_object_put(rpc); + + return 0; +} + +static int frame_rpc_call_res(char *rpcbuf, int len, char *uid, + struct json_object *pdumsg) +{ + const char *to_send; + struct json_object *rpc; + + rpc = json_object_new_array(); + if (!rpc) { + return -ENOMEM; + } + + json_object_array_put_idx(rpc, 0, json_object_new_int(3)); + json_object_array_put_idx(rpc, 1, json_object_new_string(uid)); + + json_object_array_put_idx(rpc, 2, pdumsg); + to_send = json_object_to_json_string_ext(rpc, JSON_C_TO_STRING_PLAIN); + + strncpy(rpcbuf, to_send, len); + + json_object_put(rpc); + + return 0; +} + +static int frame_authorize_msg(char *buf, int len, + struct ocpp_session *ses) +{ + int ret; + struct json_object *auth; + struct json_object *tmp; + + auth = json_object_new_object(); + if (!auth) { + return -ENOMEM; + } + + tmp = json_object_new_string(ses->idtag); + if (!tmp) { + ret = -ENOMEM; + goto out; + } + + json_object_object_add(auth, "idTag", tmp); + ret = frame_rpc_call_req(buf, len, PDU_AUTHORIZE, (uint32_t)ses, + auth); + if (ret) { + goto out; + } + + return 0; + +out: + json_object_put(auth); + return ret; +} + +static int frame_heartbeat_msg(char *buf, int len, + struct ocpp_session *ses) +{ + int ret; + struct json_object *hb; + + hb = json_object_new_object(); + if (!hb) { + return -ENOMEM; + } + + ret = frame_rpc_call_req(buf, len, PDU_HEARTBEAT, + (uint32_t)ses, hb); + if (ret) { + json_object_put(hb); + } + + return ret; +} + +static int frame_bootnotif_msg(char *buf, int len, + struct ocpp_session *ses, + struct ocpp_cp_info *cpi) +{ + int ret = -ENOMEM; + struct json_object *txn; + struct json_object *tmp; + + txn = json_object_new_object(); + if (!txn) { + return -ENOMEM; + } + + tmp = json_object_new_string(cpi->model); + if (!tmp) { + goto out; + } + + json_object_object_add(txn, "chargePointModel", tmp); + tmp = json_object_new_string(cpi->vendor); + if (!tmp) { + goto out; + } + + json_object_object_add(txn, "chargePointVendor", tmp); + if (cpi->box_sl_no) { + tmp = json_object_new_string(cpi->box_sl_no); + if (!tmp) { + goto out; + } + + json_object_object_add(txn, "chargeBoxSerialNumber", tmp); + } + + if (cpi->sl_no) { + tmp = json_object_new_string(cpi->sl_no); + if (!tmp) { + goto out; + } + + json_object_object_add(txn, "chargePointSerialNumber", tmp); + } + + if (cpi->fw_ver) { + tmp = json_object_new_string(cpi->fw_ver); + if (!tmp) { + goto out; + } + + json_object_object_add(txn, "firmwareVersion", tmp); + } + + if (cpi->iccid) { + tmp = json_object_new_string(cpi->iccid); + if (!tmp) { + goto out; + } + + json_object_object_add(txn, "iccid", tmp); + } + + if (cpi->imsi) { + tmp = json_object_new_string(cpi->imsi); + if (!tmp) { + goto out; + } + + json_object_object_add(txn, "imsi", tmp); + } + + if (cpi->meter_sl_no) { + tmp = json_object_new_string(cpi->meter_sl_no); + if (!tmp) { + goto out; + } + + json_object_object_add(txn, "meterSerialNumber", tmp); + } + + if (cpi->meter_type) { + tmp = json_object_new_string(cpi->meter_type); + if (!tmp) { + goto out; + } + + json_object_object_add(txn, "meterType", tmp); + } + + ret = frame_rpc_call_req(buf, len, PDU_BOOTNOTIFICATION, + (uint32_t)ses, txn); + if (ret) { + goto out; + } + + return 0; + +out: + json_object_put(txn); + return ret; +} + +static int frame_meter_val_msg(char *buf, int len, struct ocpp_session *ses, + char *timestamp, char *val, char *measurand, + char *unit) +{ + int ret = -ENOMEM; + struct json_object *mtr; + struct json_object *tmp; + struct json_object *persample; + struct json_object *sampleval; + struct json_object *smplarr; + struct json_object *mtrarr; + + + mtr = json_object_new_object(); + if (!mtr) { + return -ENOMEM; + } + + tmp = json_object_new_int(ses ? ses->idcon : 0); + if (!tmp) { + goto out; + } + + json_object_object_add(mtr, "connectorId", tmp); + if (ses) { + tmp = json_object_new_int(ses->idtxn); + if (!tmp) { + goto out; + } + + json_object_object_add(mtr, "transactionId", tmp); + } + + persample = json_object_new_object(); + if (!persample) { + goto out; + } + + tmp = json_object_new_string(timestamp); + if (!tmp) { + goto out_persample; + } + + json_object_object_add(persample, "timestamp", tmp); + + sampleval = json_object_new_object(); + if (!sampleval) { + goto out_persample; + } + + tmp = json_object_new_string(measurand); + if (!tmp) { + goto out_sampleval; + } + + json_object_object_add(sampleval, "measurand", tmp); + tmp = json_object_new_string(val); + if (!tmp) { + goto out_sampleval; + } + + json_object_object_add(sampleval, "value", tmp); + if (unit) { + tmp = json_object_new_string(unit); + if (!tmp) { + goto out_sampleval; + } + + json_object_object_add(sampleval, "unit", tmp); + } + smplarr = json_object_new_array(); + if (!smplarr) { + goto out_sampleval; + } + + mtrarr = json_object_new_array(); + if (!mtrarr) { + goto out_smplarr; + } + + json_object_array_put_idx(smplarr, 0, sampleval); + json_object_object_add(persample, "sampledValue", smplarr); + json_object_array_put_idx(mtrarr, 0, persample); + json_object_object_add(mtr, "meterValue", mtrarr); + + ret = frame_rpc_call_req(buf, len, PDU_METER_VALUES, + (uint32_t)ses, mtr); + if (ret) { + goto out; + } + + return 0; + +out_smplarr: + json_object_put(smplarr); + +out_sampleval: + json_object_put(sampleval); + +out_persample: + json_object_put(persample); + +out: + json_object_put(mtr); + return ret; +} + +static int frame_stop_txn_msg(char *buf, int len, struct ocpp_session *ses, + int Wh, char *reason, char *timestamp) +{ + int ret = -ENOMEM; + struct json_object *txn; + struct json_object *tmp; + + txn = json_object_new_object(); + if (!txn) { + return -ENOMEM; + } + + tmp = json_object_new_int(ses->idtxn); + if (!tmp) { + goto out; + } + + json_object_object_add(txn, "transactionId", tmp); + tmp = json_object_new_int(Wh); + if (!tmp) { + goto out; + } + + json_object_object_add(txn, "meterStop", tmp); + tmp = json_object_new_string(timestamp); + if (!tmp) { + goto out; + } + + json_object_object_add(txn, "timestamp", tmp); + if (reason) { + tmp = json_object_new_string(reason); + if (!tmp) { + goto out; + } + + json_object_object_add(txn, "reason", tmp); + } + + if (!ses->idtag[0]) { + tmp = json_object_new_string(ses->idtag); + if (!tmp) { + goto out; + } + + json_object_object_add(txn, "idTag", tmp); + } + + + ret = frame_rpc_call_req(buf, len, PDU_STOP_TRANSACTION, + (uint32_t)ses, txn); + if (ret) { + goto out; + } + + return 0; + +out: + json_object_put(txn); + return ret; +} + +static int frame_start_txn_msg(char *buf, int len, struct ocpp_session *ses, + int Wh, int reserv_id, char *timestamp) +{ + int ret = -ENOMEM; + struct json_object *txn; + struct json_object *tmp; + + txn = json_object_new_object(); + if (!txn) { + return -ENOMEM; + } + + tmp = json_object_new_int(ses->idcon); + if (!tmp) { + goto out; + } + + json_object_object_add(txn, "connectorId", tmp); + tmp = json_object_new_string(ses->idtag); + if (!tmp) { + goto out; + } + + json_object_object_add(txn, "idTag", tmp); + tmp = json_object_new_int(Wh); + if (!tmp) { + goto out; + } + + json_object_object_add(txn, "meterStart", tmp); + tmp = json_object_new_string(timestamp); + if (!tmp) { + goto out; + } + + json_object_object_add(txn, "timestamp", tmp); + if (reserv_id >= 0) { + tmp = json_object_new_int(reserv_id); + if (!tmp) { + goto out; + } + + json_object_object_add(txn, "reservationId", tmp); + } + + ret = frame_rpc_call_req(buf, len, PDU_START_TRANSACTION, (uint32_t)ses, + txn); + if (ret) { + goto out; + } + + return 0; + +out: + json_object_put(txn); + return ret; +} + +static int frame_getconfig_msg(char *buf, int len, char *key, + char *val, bool is_rw, char *uid) +{ + int ret = -ENOMEM; + struct json_object *cfg; + struct json_object *keyval; + struct json_object *arr; + struct json_object *tmp; + + cfg = json_object_new_object(); + if (!cfg) { + return -ENOMEM; + } + + tmp = json_object_new_string(key); + if (!tmp) { + goto out; + } + + if (val) { + keyval = json_object_new_object(); + if (!keyval) { + goto out; + } + + json_object_object_add(keyval, "key", tmp); + tmp = json_object_new_int(!is_rw); + if (!tmp) { + goto out_keyval; + } + + json_object_object_add(keyval, "readonly", tmp); + tmp = json_object_new_string(val); + if (!tmp) { + goto out_keyval; + } + + json_object_object_add(keyval, "value", tmp); + + arr = json_object_new_array(); + if (!arr) { + goto out_keyval; + } + + json_object_array_put_idx(arr, 0, keyval); + json_object_object_add(cfg, "configurationKey", arr); + } else { + json_object_object_add(cfg, "unknownKey", tmp); + } + + ret = frame_rpc_call_res(buf, len, uid, cfg); + if (ret) { + goto out; + } + + return 0; + +out_keyval: + json_object_put(keyval); + +out: + json_object_put(cfg); + return ret; +} + +static int frame_status_resp_msg(char *buf, int len, char *res, char *uid) +{ + int ret; + struct json_object *stat; + struct json_object *tmp; + + stat = json_object_new_object(); + if (!stat) { + return -ENOMEM; + } + + tmp = json_object_new_string(res); + if (!tmp) { + ret = -ENOMEM; + goto out; + } + + json_object_object_add(stat, "status", tmp); + ret = frame_rpc_call_res(buf, len, uid, stat); + if (ret) { + goto out; + } + + return 0; + +out: + json_object_put(stat); + return ret; +} + +/* parse msg from server */ +int parse_rpc_msg(char *msg, int msglen, char *uid, int uidlen, + int *pdu, bool *is_rsp) +{ + int ret = 0; + int idx = 0, rpc_id; + const char *str, *spdu, *payload; + struct json_object *rpc; + struct json_object *tmp; + + rpc = json_tokener_parse(msg); + if (!rpc) { + return -EINVAL; + } + + tmp = json_object_array_get_idx(rpc, idx++); + rpc_id = json_object_get_int(tmp); + tmp = json_object_array_get_idx(rpc, idx++); + str = json_object_get_string(tmp); + strncpy(uid, str, uidlen); + + switch (rpc_id + '0') { + case OCPP_WAMP_RPC_REQ: + tmp = json_object_array_get_idx(rpc, idx++); + spdu = json_object_get_string(tmp); + *pdu = ocpp_find_pdu_from_literal(spdu); + /* fall through */ + + case OCPP_WAMP_RPC_RESP: + *is_rsp = rpc_id - 2; + + tmp = json_object_array_get_idx(rpc, idx); + payload = json_object_get_string(tmp); + strncpy(msg, payload, msglen); + break; + + case OCPP_WAMP_RPC_ERR: + /* fall through */ + + default: + ret = -EINVAL; + } + + json_object_put(rpc); + + return ret; +} + +static int parse_idtag_info(struct json_object *root, + struct ocpp_idtag_info *idtag_info) +{ + struct json_object *idinfo; + struct json_object *tmp; + const char *str; + + if (!json_object_object_get_ex(root, "idTagInfo", &idinfo)) { + return -EINVAL; + } + + if (!json_object_object_get_ex(idinfo, "status", &tmp)) { + return -EINVAL; + } + + str = json_object_get_string(tmp); + switch (*str) { + case 'A': + idtag_info->auth_status = OCPP_AUTH_ACCEPTED; + break; + + case 'B': + idtag_info->auth_status = OCPP_AUTH_BLOCKED; + break; + + case 'E': + idtag_info->auth_status = OCPP_AUTH_EXPIRED; + break; + + case 'I': + idtag_info->auth_status = OCPP_AUTH_INVALID; + break; + + case 'C': + idtag_info->auth_status = OCPP_AUTH_CONCURRENT_TX; + break; + + default: + return -EINVAL; + } + + if (json_object_object_get_ex(idinfo, "parentIdTag", &tmp)) { + strncpy(idtag_info->p_idtag, json_object_get_string(tmp), + sizeof(idtag_info->p_idtag)); + } + + if (json_object_object_get_ex(idinfo, "expiryDate", &tmp)) { + strncpy(idtag_info->exptime, json_object_get_string(tmp), + sizeof(idtag_info->exptime)); + } + + return 0; +} + +static int parse_heartbeat_msg(char *buf, struct timeval *date) +{ + struct json_object *root; + struct json_object *tmp; + int ret = 0; + + root = json_tokener_parse(buf); + if (!root) { + return -EINVAL; + } + + ret = json_object_object_get_ex(root, "currentTime", &tmp); + if (!ret) { + /* todo: convert civil time to epoch and update local time */ + *date; + } + + json_object_put(root); + return ret; +} + +static int parse_authorize_msg(char *buf, struct ocpp_idtag_info *idtag_info) +{ + struct json_object *root; + int ret = 0; + + root = json_tokener_parse(buf); + if (!root) { + return -EINVAL; + } + + ret = parse_idtag_info(root, idtag_info); + json_object_put(root); + + return ret; +} + +static int parse_bootnotification_msg(char *buf, struct boot_notif *binfo) +{ + struct json_object *root; + struct json_object *tmp; + const char *str; + int ret = 0; + + root = json_tokener_parse(buf); + if (!root) { + return -EINVAL; + } + + if (!json_object_object_get_ex(root, "status", &tmp)) { + ret = -EINVAL; + goto out; + } + + str = json_object_get_string(tmp); + switch (*str) { + case 'A': /* accepted */ + binfo->status = BOOT_ACCEPTED; + break; + + case 'P': /* pending */ + binfo->status = BOOT_PENDING; + break; + + case 'R': /* rejected */ + binfo->status = BOOT_REJECTED; + break; + + default: + ret = -EINVAL; + goto out; + } + + if (!json_object_object_get_ex(root, "interval", &tmp)) { + ret = -EINVAL; + goto out; + } + + binfo->interval = json_object_get_int(tmp); + if (!json_object_object_get_ex(root, "currentTime", &tmp)) { + ret = -EINVAL; + goto out; + } + + /* todo: convert civil time to epoch and update local time */ + binfo->date; + +out: + json_object_put(root); + return ret; +} + +static int parse_start_txn_msg(char *buf, + int *idtxn, + struct ocpp_idtag_info *idtag_info) +{ + struct json_object *root; + struct json_object *tmp; + int ret = 0; + + root = json_tokener_parse(buf); + if (!root) { + return -EINVAL; + } + + if (!json_object_object_get_ex(root, "transactionId", &tmp)) { + ret = -EINVAL; + goto out; + } + + *idtxn = json_object_get_int(tmp); + ret = parse_idtag_info(root, idtag_info); + +out: + json_object_put(root); + + return ret; +} + +static int parse_getconfig_msg(char *buf, char *key) +{ + struct json_object *root; + struct json_object *keys; + struct json_object *cfg; + const char *str; + int ret = 0; + + root = json_tokener_parse(buf); + if (!root) { + return -EINVAL; + } + + if (!json_object_object_get_ex(root, "key", &keys)) { + return -EINVAL; + } + + cfg = json_object_array_get_idx(keys, 0); + if (!cfg) { + goto out; + } + + str = json_object_get_string(cfg); + if (str) { + strcpy(key, str); + } + +out: + json_object_put(root); + return ret; +} + +static int parse_changeconfig_msg(char *buf, char *key, char *val) +{ + struct json_object *root; + struct json_object *tmp; + const char *str; + int ret = 0; + + root = json_tokener_parse(buf); + if (!root) { + return -EINVAL; + } + + if (!json_object_object_get_ex(root, "key", &tmp)) { + return -EINVAL; + } + + str = json_object_get_string(tmp); + strncpy(key, str, CISTR50); + + if (!json_object_object_get_ex(root, "value", &tmp)) { + return -EINVAL; + } + + str = json_object_get_string(tmp); + strncpy(val, str, CISTR500); + + json_object_put(root); + return ret; +} + +static int parse_remote_start_txn_msg(char *buf, + int *idcon, + char *idtag) +{ + struct json_object *root; + struct json_object *tmp; + int ret = 0; + + root = json_tokener_parse(buf); + if (!root) { + return -EINVAL; + } + + if (!json_object_object_get_ex(root, "idTag", &tmp)) { + ret = -EINVAL; + goto out; + } + + strncpy(idtag, json_object_get_string(tmp), CISTR50); + if (json_object_object_get_ex(root, "connectorId", &tmp)) { + *idcon = json_object_get_int(tmp); + } + +out: + json_object_put(root); + + return ret; +} + +static int parse_remote_stop_txn_msg(char *buf, int *idtxn) +{ + struct json_object *root; + struct json_object *tmp; + int ret = 0; + + root = json_tokener_parse(buf); + if (!root) { + return -EINVAL; + } + + if (!json_object_object_get_ex(root, "transactionId", &tmp)) { + ret = -EINVAL; + goto out; + } + + *idtxn = json_object_get_int(tmp); + +out: + json_object_put(root); + + return ret; +} + +static int parse_unlock_connectormsg(char *buf, + int *idcon) +{ + struct json_object *root; + struct json_object *tmp; + int ret = 0; + + root = json_tokener_parse(buf); + if (!root) { + return -EINVAL; + } + + if (!json_object_object_get_ex(root, "connectorId", &tmp)) { + ret = -EINVAL; + goto out; + } + + *idcon = json_object_get_int(tmp); + +out: + json_object_put(root); + + return ret; +} + +static ocpp_msg_fp_t ocpp_json_parser[PDU_MSG_END] = { + [PDU_BOOTNOTIFICATION] = (ocpp_msg_fp_t)parse_bootnotification_msg, + [PDU_AUTHORIZE] = (ocpp_msg_fp_t)parse_authorize_msg, + [PDU_START_TRANSACTION] = (ocpp_msg_fp_t)parse_start_txn_msg, + [PDU_STOP_TRANSACTION] = (ocpp_msg_fp_t)parse_authorize_msg, + [PDU_METER_VALUES] = NULL, + [PDU_HEARTBEAT] = (ocpp_msg_fp_t)parse_heartbeat_msg, + [PDU_GET_CONFIGURATION] = (ocpp_msg_fp_t)parse_getconfig_msg, + [PDU_CHANGE_CONFIGURATION] = (ocpp_msg_fp_t)parse_changeconfig_msg, + [PDU_REMOTE_START_TRANSACTION] = (ocpp_msg_fp_t)parse_remote_start_txn_msg, + [PDU_REMOTE_STOP_TRANSACTION] = (ocpp_msg_fp_t)parse_remote_stop_txn_msg, + [PDU_UNLOCK_CONNECTOR] = (ocpp_msg_fp_t)parse_unlock_connectormsg, +}; + +static ocpp_msg_fp_t ocpp_json_frame[PDU_MSG_END] = { + [PDU_BOOTNOTIFICATION] = (ocpp_msg_fp_t)frame_bootnotif_msg, + [PDU_AUTHORIZE] = (ocpp_msg_fp_t)frame_authorize_msg, + [PDU_START_TRANSACTION] = (ocpp_msg_fp_t)frame_start_txn_msg, + [PDU_STOP_TRANSACTION] = (ocpp_msg_fp_t)frame_stop_txn_msg, + [PDU_METER_VALUES] = (ocpp_msg_fp_t)frame_meter_val_msg, + [PDU_HEARTBEAT] = (ocpp_msg_fp_t)frame_heartbeat_msg, + [PDU_GET_CONFIGURATION] = (ocpp_msg_fp_t)frame_getconfig_msg, + [PDU_CHANGE_CONFIGURATION] (ocpp_msg_fp_t)frame_status_resp_msg, + [PDU_REMOTE_START_TRANSACTION] = (ocpp_msg_fp_t)frame_status_resp_msg, + [PDU_REMOTE_STOP_TRANSACTION] = (ocpp_msg_fp_t)frame_status_resp_msg, + [PDU_UNLOCK_CONNECTOR] = (ocpp_msg_fp_t)frame_status_resp_msg, +}; + +void ocpp_parser_init(ocpp_msg_fp_t **cfn, ocpp_msg_fp_t **pfn) +{ + *pfn = ocpp_json_parser; + *cfn = ocpp_json_frame; +} diff --git a/subsys/net/lib/ocpp/ocpp_wamp_rpc.c b/subsys/net/lib/ocpp/ocpp_wamp_rpc.c new file mode 100644 index 0000000000000..b41484a98bb6b --- /dev/null +++ b/subsys/net/lib/ocpp/ocpp_wamp_rpc.c @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2024 Linumiz + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "ocpp_i.h" + +LOG_MODULE_REGISTER(ocpp_rpc, CONFIG_OCPP_LOG_LEVEL); + +#define OCPP_WAMP_RPC_TYPE_IDX 1 + +int ocpp_send_to_server(struct ocpp_wamp_rpc_msg *snd, k_timeout_t timeout) +{ + int ret; + int sock; + struct ocpp_info *ctx = snd->ctx; + + switch (snd->msg[OCPP_WAMP_RPC_TYPE_IDX]) { + case OCPP_WAMP_RPC_REQ: + /* ocpp spec - allow only one active call at a time + * release lock on response received from CS or timeout + */ + ret = k_mutex_lock(snd->sndlock, timeout); + if (ret) { + return ret; + } + k_poll_signal_reset(snd->rspsig); + break; + + case OCPP_WAMP_RPC_RESP: + case OCPP_WAMP_RPC_ERR: + break; + + default: + return -EINVAL; + } + + k_mutex_lock(&ctx->ilock, K_FOREVER); + sock = ctx->ui.wssock; + k_mutex_unlock(&ctx->ilock); + if (sock < 0) { + ret = -EAGAIN; + goto unlock; + } + + ret = websocket_send_msg(sock, snd->msg, snd->msg_len, + WEBSOCKET_OPCODE_DATA_TEXT, true, + true, 5000); /* fixme timeout */ + if (ret < 0) { + goto unlock; + } + + if (snd->rspsig) { + struct k_poll_event events[1] = { + K_POLL_EVENT_INITIALIZER(K_POLL_TYPE_SIGNAL, + K_POLL_MODE_NOTIFY_ONLY, + snd->rspsig), + }; + + ret = k_poll(events, 1, timeout); + goto unlock; + } + + return 0; + +unlock: + if (snd->sndlock) { + k_mutex_unlock(snd->sndlock); + } + + return ret; +} + +int ocpp_receive_from_server(struct ocpp_wamp_rpc_msg *rcv, uint32_t timeout) +{ + int ret; + int sock; + int msg_type; + uint64_t remaining = 0; + struct ocpp_info *ctx = rcv->ctx; + + k_mutex_lock(&ctx->ilock, K_MSEC(timeout)); + sock = ctx->ui.wssock; + k_mutex_unlock(&ctx->ilock); + if (sock < 0) { + return -EAGAIN; + } + + ret = websocket_recv_msg(sock, rcv->msg, + rcv->msg_len, + &msg_type, + &remaining, + timeout); + + return ret; +} diff --git a/tests/net/lib/ocpp/CMakeLists.txt b/tests/net/lib/ocpp/CMakeLists.txt new file mode 100644 index 0000000000000..50a0f7dd9670e --- /dev/null +++ b/tests/net/lib/ocpp/CMakeLists.txt @@ -0,0 +1,12 @@ +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.20.0) + +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(ocpp_test) + +target_include_directories(app PRIVATE + ${ZEPHYR_BASE}/subsys/net/ip + ) +FILE(GLOB app_sources src/*.c) +target_sources(app PRIVATE ${app_sources}) diff --git a/tests/net/lib/ocpp/prj.conf b/tests/net/lib/ocpp/prj.conf new file mode 100644 index 0000000000000..b2537b400278b --- /dev/null +++ b/tests/net/lib/ocpp/prj.conf @@ -0,0 +1,43 @@ +# HTTP & Websocket +CONFIG_HTTP_CLIENT=y +CONFIG_WEBSOCKET_CLIENT=y + +# Networking config +CONFIG_NETWORKING=y +CONFIG_NET_IPV4=y +CONFIG_NET_IPV6=n +CONFIG_NET_TCP=y +CONFIG_NET_ARP=y +CONFIG_NET_UDP=y +CONFIG_NET_SOCKETS=y +CONFIG_NET_SOCKETS_POLL_MAX=4 +CONFIG_NET_L2_ETHERNET=y +CONFIG_ETH_DRIVER=y +CONFIG_LOG=y +CONFIG_NET_LOG=y +CONFIG_LOG_DEFAULT_LEVEL=3 +CONFIG_NET_DHCPV4=y + +# OCPP +CONFIG_OCPP=y +CONFIG_TEST_EXTRA_STACK_SIZE=2048 +CONFIG_MAIN_STACK_SIZE=2048 +CONFIG_HEAP_MEM_POOL_SIZE=15000 + +CONFIG_JSONC=y +CONFIG_PICOLIBC=y + +CONFIG_NET_TX_STACK_SIZE=2048 +CONFIG_NET_RX_STACK_SIZE=2048 +CONFIG_NET_PKT_RX_COUNT=28 +CONFIG_NET_BUF_RX_COUNT=60 +CONFIG_NET_MGMT=y +CONFIG_NET_MGMT_EVENT=y + +CONFIG_ENTROPY_GENERATOR=y +CONFIG_TEST_RANDOM_GENERATOR=y + +CONFIG_POSIX_CLOCK=y +CONFIG_POSIX_API=y + +CONFIG_ZTEST=y diff --git a/tests/net/lib/ocpp/src/main.c b/tests/net/lib/ocpp/src/main.c new file mode 100644 index 0000000000000..65df05cb45a23 --- /dev/null +++ b/tests/net/lib/ocpp/src/main.c @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2024 Linumiz + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include + +static void test_ocpp_charge_cycle(ocpp_session_handle_t hndl) +{ + int ret; + int retry = 3; + enum ocpp_auth_status status; + const uint32_t timeout_ms = 500; + + while (retry--) { + ret = ocpp_authorize(hndl, "ZepId00", &status, timeout_ms); + + TC_PRINT("auth req ret %d status %d", ret, status); + k_sleep(K_SECONDS(1)); + + if (!ret) { + break; + } + } + zassert_equal(ret, 0, "CP authorize fail %d"); + zassert_equal(status, OCPP_AUTH_ACCEPTED, "idtag not authorized"); + + ret = ocpp_start_transaction(hndl, sys_rand32_get(), 1, timeout_ms); + zassert_equal(ret, 0, "start transaction fail"); + + /* Active charging session */ + k_sleep(K_SECONDS(20)); + ret = ocpp_stop_transaction(hndl, sys_rand32_get(), timeout_ms); + zassert_equal(ret, 0, "stop transaction fail"); +} + +static int test_ocpp_user_notify_cb(enum ocpp_notify_reason reason, + union ocpp_io_value *io, + void *user_data) +{ + switch (reason) { + case OCPP_USR_GET_METER_VALUE: + if (OCPP_OMM_ACTIVE_ENERGY_TO_EV == io->meter_val.mes) { + snprintf(io->meter_val.val, CISTR50, "%u", + sys_rand32_get()); + + TC_PRINT("mtr reading val %s con %d", + io->meter_val.val, + io->meter_val.id_con); + return 0; + } + break; + + case OCPP_USR_START_CHARGING: + TC_PRINT("start charging idtag %s connector %d\n", + io->start_charge.idtag, + io->stop_charge.id_con); + return 0; + + case OCPP_USR_STOP_CHARGING: + TC_PRINT("stop charging connector %d\n", io->stop_charge.id_con); + return 0; + + case OCPP_USR_UNLOCK_CONNECTOR: + TC_PRINT("unlock connector %d\n", io->unlock_con.id_con); + return 0; + } + + return -ENOTSUP; +} + +int test_ocpp_init(void) +{ + int ret; + + struct ocpp_cp_info cpi = { "basic", "zephyr", .num_of_con = 1 }; + struct ocpp_cs_info csi = { "122.165.245.213", /* ssh.linumiz.com */ + "/steve/websocket/CentralSystemService/zephyr", + 8180, + AF_INET }; + + net_dhcpv4_start(net_if_get_default()); + + /* wait for device dhcp ip recive */ + k_sleep(K_SECONDS(3)); + + ret = ocpp_init(&cpi, + &csi, + test_ocpp_user_notify_cb, + NULL); + if (ret) { + TC_PRINT("ocpp init failed %d\n", ret); + return ret; + } + + return 0; +} + +ZTEST(net_ocpp, test_ocpp_chargepoint) +{ + int ret; + ocpp_session_handle_t hndl = NULL; + + ret = test_ocpp_init(); + zassert_equal(ret, 0, "ocpp init failed %d", ret); + + ret = ocpp_session_open(&hndl); + zassert_equal(ret, 0, "session open failed %d", ret); + + k_sleep(K_SECONDS(2)); + test_ocpp_charge_cycle(hndl); + + ocpp_session_close(hndl); +} + +ZTEST_SUITE(net_ocpp, NULL, NULL, NULL, NULL, NULL); diff --git a/tests/net/lib/ocpp/testcase.yaml b/tests/net/lib/ocpp/testcase.yaml new file mode 100644 index 0000000000000..5447b2cd75d41 --- /dev/null +++ b/tests/net/lib/ocpp/testcase.yaml @@ -0,0 +1,7 @@ +tests: + net.ocpp: + depends_on: netif + min_ram: 16 + tags: + - net + - ocpp diff --git a/west.yml b/west.yml index 11cc8064bb0b7..d7746868d4bec 100644 --- a/west.yml +++ b/west.yml @@ -23,6 +23,8 @@ manifest: url-base: https://github.com/zephyrproject-rtos - name: babblesim url-base: https://github.com/BabbleSim + - name: linumiz + url-base: https://github.com/linumiz group-filter: [-babblesim, -optional] @@ -337,6 +339,10 @@ manifest: - name: zcbor revision: 75d088037eb237b18e7ec1f47c9ce494b9b95aab path: modules/lib/zcbor + - name: json-c + remote: linumiz + path: modules/lib/json-c + revision: dev/zephyr self: path: zephyr