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 f4f3b1f88c1af..86e312d5c5eef 100644 --- a/doc/connectivity/networking/api/protocols.rst +++ b/doc/connectivity/networking/api/protocols.rst @@ -15,5 +15,6 @@ Protocols lwm2m mqtt mqtt_sn + ocpp ptp tftp diff --git a/include/zephyr/net/ocpp.h b/include/zephyr/net/ocpp.h new file mode 100644 index 0000000000000..bb6221778e065 --- /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/samples/net/ocpp/CMakeLists.txt b/samples/net/ocpp/CMakeLists.txt new file mode 100644 index 0000000000000..b7e93067df31f --- /dev/null +++ b/samples/net/ocpp/CMakeLists.txt @@ -0,0 +1,9 @@ +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.20.0) + +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(ocpp) + +include(${ZEPHYR_BASE}/samples/net/common/common.cmake) +target_sources(app PRIVATE src/main.c) diff --git a/samples/net/ocpp/Kconfig b/samples/net/ocpp/Kconfig new file mode 100644 index 0000000000000..3117b73e76791 --- /dev/null +++ b/samples/net/ocpp/Kconfig @@ -0,0 +1,22 @@ +# Config options for OCPP sample application + +# Copyright (c) 2025 Linumiz GmbH +# 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/prj.conf b/samples/net/ocpp/prj.conf new file mode 100644 index 0000000000000..3897041e6c96a --- /dev/null +++ b/samples/net/ocpp/prj.conf @@ -0,0 +1,50 @@ +# 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_CONNECTION_MANAGER=y +CONFIG_NET_CONFIG_SETTINGS=y +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_NET_LOG=y + +# 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=8180 + +CONFIG_MAIN_STACK_SIZE=4096 +CONFIG_HEAP_MEM_POOL_SIZE=15000 + +CONFIG_JSON_LIBRARY=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 diff --git a/samples/net/ocpp/sample.yaml b/samples/net/ocpp/sample.yaml new file mode 100644 index 0000000000000..591ba7d6d24dc --- /dev/null +++ b/samples/net/ocpp/sample.yaml @@ -0,0 +1,16 @@ +sample: + description: OCPP sample application + name: OCPP charge point +common: + harness: net + tags: + - net + - ocpp + +tests: + sample.net.ocpp: + platform_allow: + - stm32f769i_disco + 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..be784f20800a2 --- /dev/null +++ b/samples/net/ocpp/src/main.c @@ -0,0 +1,346 @@ +/* + * Copyright (c) 2025 Linumiz GmbH + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "net_sample_common.h" + +#if __POSIX_VISIBLE < 200809 +char *strdup(const char *); +#endif + +LOG_MODULE_REGISTER(main, LOG_LEVEL_INF); + +#define NO_OF_CONN 2 +K_KERNEL_STACK_ARRAY_DEFINE(cp_stk, NO_OF_CONN, 2 * 1024); + +static struct k_thread tinfo[NO_OF_CONN]; +static k_tid_t tid[NO_OF_CONN]; +static char idtag[NO_OF_CONN][25]; + +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] = {(struct zbus_observer *)&cp_thread0, + (struct zbus_observer *)&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); + + wait_for_network(); + + 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 ee23de2ab027d..3f1c4b31e05d8 100644 --- a/subsys/net/lib/CMakeLists.txt +++ b/subsys/net/lib/CMakeLists.txt @@ -19,6 +19,7 @@ add_subdirectory_ifdef(CONFIG_NET_TRICKLE trickle) add_subdirectory_ifdef(CONFIG_NET_DHCPV6 dhcpv6) add_subdirectory_ifdef(CONFIG_PROMETHEUS prometheus) add_subdirectory_ifdef(CONFIG_WIFI_CREDENTIALS wifi_credentials) +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 e530877125afa..ca960102e7b63 100644 --- a/subsys/net/lib/Kconfig +++ b/subsys/net/lib/Kconfig @@ -25,6 +25,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..ffa835b876790 --- /dev/null +++ b/subsys/net/lib/ocpp/Kconfig @@ -0,0 +1,83 @@ +# Copyright (c) 2025 Linumiz GmbH +# SPDX-License-Identifier: Apache-2.0 + +config OCPP + bool "Open Charge Point Protocol [EXPERIMENTAL]" + depends on JSON_LIBRARY + 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..54f40bcd811f3 --- /dev/null +++ b/subsys/net/lib/ocpp/core.c @@ -0,0 +1,500 @@ +/* + * Copyright (c) 2025 Linumiz GmbH + * + * 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); + 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); + + 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); + + 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); + 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); + 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); + 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); + 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); + 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); + 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); + 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); + 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/key_mgmt.c b/subsys/net/lib/ocpp/key_mgmt.c new file mode 100644 index 0000000000000..c9b3a2b71c705 --- /dev/null +++ b/subsys/net/lib/ocpp/key_mgmt.c @@ -0,0 +1,191 @@ +/* + * Copyright (c) 2025 Linumiz GmbH + * + * 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", (uint32_t)"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", (uint32_t)"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", (uint32_t)"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", (uint32_t)"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", + (uint32_t) CP_CORE CP_SC CP_RT CP_RE CP_LAL CP_FM), + FILL_KEY_TABLE(KEY_TYPE_INT, 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..885721beb8e5c --- /dev/null +++ b/subsys/net/lib/ocpp/ocpp.c @@ -0,0 +1,737 @@ +/* + * Copyright (c) 2025 Linumiz GmbH + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "ocpp_i.h" +#include + +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 timeval tv; + struct tm htime = {0}; + + gettimeofday(&tv, NULL); + gmtime_r(&tv.tv_sec, &htime); + + snprintk(utc, CISTR25, "%04hu-%02hu-%02huT%02hu:%02hu:%02huZ", + 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 addr_buf; + struct sockaddr *addr = &addr_buf; + int addr_size; + + if (ui->csi.sa_family == AF_INET) { +#if defined(CONFIG_NET_IPV4) + addr_size = sizeof(addr_buf); + 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); +#endif + } else { +#if defined(CONFIG_NET_IPV6) + addr_size = sizeof(struct sockaddr_in6); + 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); +#endif + } + + 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); + ui->wssock = -1; + } + + 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}; + uint32_t msg_type; + + 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, &msg_type, 200); + + if (ret < 0) { + if (ret == -ENOTCONN) { + ctx->is_cs_offline = true; + } + continue; + } + + if (msg_type & WEBSOCKET_FLAG_PING) { + websocket_send_msg(ctx->ui.wssock, NULL, 0, + WEBSOCKET_OPCODE_PONG, true, + true, 100); + } else if (msg_type & WEBSOCKET_FLAG_CLOSE) { + ctx->is_cs_offline = true; + } else { + 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; + + LOG_INF("upstream init"); + + 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; + ui->tcpsock = zsock_socket(csi->sa_family, SOCK_STREAM, + IPPROTO_TCP); + if (ui->tcpsock < 0) { + return -errno; + } + + 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; +} + +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..8c74407b9c0cf --- /dev/null +++ b/subsys/net/lib/ocpp/ocpp_i.h @@ -0,0 +1,233 @@ +/* + * Copyright (c) 2025 Linumiz GmbH + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef __OCPP_I_ +#define __OCPP_I_ + +#include "string.h" +#include +#include +#include +#include +#include + +#if __POSIX_VISIBLE < 200809 +char *strdup(const char *); +#endif + +/* 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 *msg_type, + 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..4b075e23a5bc6 --- /dev/null +++ b/subsys/net/lib/ocpp/ocpp_j.c @@ -0,0 +1,883 @@ +/* + * Copyright (c) 2025 Linumiz GmbH + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "ocpp_i.h" +#include "ocpp_j.h" +#include +#include + +static int extract_string_field(char *out_buf, int outlen, char *token) +{ + char *end; + + if (!out_buf || !token) { + return -EINVAL; + } + + strncpy(out_buf, token + 1, outlen - 1); + end = strchr(out_buf, '"'); + if (end) { + *end = '\0'; + } + + return 0; +} + +static int extract_payload(char *msg, int msglen) +{ + size_t len; + char *start = strchr(msg, '{'); + char *end = strrchr(msg, '}'); + + if (!start || !end || end < start) { + return -EINVAL; + } + + len = end - start + 1; + if (len >= msglen) { + return -ENOMEM; + } + + memmove(msg, start, len); + msg[len] = '\0'; + + return 0; +} + +static int frame_rpc_call_req(char *rpcbuf, int len, int pdu, + uint32_t ses, char *pdumsg) +{ + int ret; + char uid[JSON_MSG_BUF_128]; + const char *action; + uint32_t rnd = sys_rand32_get(); + + snprintk(uid, sizeof(uid), "%u-%d-%u", ses, pdu, rnd); + + action = ocpp_get_pdu_literal(pdu); + if (!action) { + return -EINVAL; + } + + /* Encode OCPP Call Request msg: [2,"","",] */ + ret = snprintk(rpcbuf, len, + "[2,\"%s\",\"%s\",%s]", + uid, action, pdumsg); + + if (ret < 0 || ret >= len) { + return -ENOMEM; + } + + return 0; +} + +static int frame_rpc_call_res(char *rpcbuf, int len, + char *uid, char *pdumsg) +{ + int ret; + + /* Encode OCPP Call Result msg: [3,"",] */ + ret = snprintk(rpcbuf, len, "[3,\"%s\",%s]", uid, pdumsg); + + if (ret < 0 || ret >= len) { + return -ENOMEM; + } + + return 0; +} + +static int frame_authorize_msg(char *buf, int len, + struct ocpp_session *ses) +{ + int ret; + char auth_obj[JSON_MSG_BUF_128]; + + struct json_obj_descr authorize_descr[] = { + JSON_OBJ_DESCR_PRIM_NAMED(struct json_common_payload_field_str, "idTag", + val1, JSON_TOK_STRING), + }; + + struct json_common_payload_field_str payload = { + .val1 = ses->idtag, + }; + + ret = json_obj_encode_buf(authorize_descr, + ARRAY_SIZE(authorize_descr), + &payload, + auth_obj, + sizeof(auth_obj)); + if (ret < 0) { + return ret; + } + + ret = frame_rpc_call_req(buf, len, PDU_AUTHORIZE, + (uint32_t)ses, auth_obj); + if (ret < 0) { + return ret; + } + + return 0; +} + +static int frame_heartbeat_msg(char *buf, int len, struct ocpp_session *ses) +{ + int ret; + char tmp_buf[8] = "{}"; + + ret = frame_rpc_call_req(buf, len, PDU_HEARTBEAT, + (uint32_t)ses, tmp_buf); + if (ret < 0) { + return ret; + } + + return 0; +} + +static int frame_bootnotif_msg(char *buf, int len, + struct ocpp_session *ses, + struct ocpp_cp_info *cpi) +{ + int ret; + uint8_t descr_count = BOOTNOTIF_MIN_FIELDS; + char tmp_buf[JSON_MSG_BUF_512]; + + struct json_ocpp_bootnotif_msg msg = { + .charge_point_model = cpi->model, + .charge_point_vendor = cpi->vendor, + .charge_box_serial_number = cpi->box_sl_no ? cpi->box_sl_no : NULL, + .charge_point_serial_number = cpi->sl_no ? cpi->sl_no : NULL, + .firmware_version = cpi->fw_ver ? cpi->fw_ver : NULL, + .iccid = cpi->iccid ? cpi->iccid : NULL, + .imsi = cpi->imsi ? cpi->imsi : NULL, + .meter_serial_number = cpi->meter_sl_no ? cpi->meter_sl_no : NULL, + .meter_type = cpi->meter_type ? cpi->meter_type : NULL, + }; + + struct json_obj_descr bootnotif_descr[BOOTNOTIF_MAX_FIELDS] = { + JSON_OBJ_DESCR_PRIM_NAMED(struct json_ocpp_bootnotif_msg, "chargePointModel", + charge_point_model, JSON_TOK_STRING), + JSON_OBJ_DESCR_PRIM_NAMED(struct json_ocpp_bootnotif_msg, "chargePointVendor", + charge_point_vendor, JSON_TOK_STRING), + }; + + if (msg.charge_box_serial_number != NULL) { + bootnotif_descr[descr_count++] = (struct json_obj_descr) + JSON_OBJ_DESCR_PRIM_NAMED(struct json_ocpp_bootnotif_msg, "chargeBoxSerialNumber", + charge_box_serial_number, JSON_TOK_STRING); + } + + if (msg.charge_point_serial_number != NULL) { + bootnotif_descr[descr_count++] = (struct json_obj_descr) + JSON_OBJ_DESCR_PRIM_NAMED(struct json_ocpp_bootnotif_msg, "chargePointSerialNumber", + charge_point_serial_number, JSON_TOK_STRING); + } + + if (msg.firmware_version != NULL) { + bootnotif_descr[descr_count++] = (struct json_obj_descr) + JSON_OBJ_DESCR_PRIM_NAMED(struct json_ocpp_bootnotif_msg, "firmwareVersion", + firmware_version, JSON_TOK_STRING); + } + + if (msg.iccid != NULL) { + bootnotif_descr[descr_count++] = (struct json_obj_descr) + JSON_OBJ_DESCR_PRIM_NAMED(struct json_ocpp_bootnotif_msg, "iccid", + iccid, JSON_TOK_STRING); + } + + if (msg.imsi != NULL) { + bootnotif_descr[descr_count++] = (struct json_obj_descr) + JSON_OBJ_DESCR_PRIM_NAMED(struct json_ocpp_bootnotif_msg, "imsi", + imsi, JSON_TOK_STRING); + } + + if (msg.meter_serial_number != NULL) { + bootnotif_descr[descr_count++] = (struct json_obj_descr) + JSON_OBJ_DESCR_PRIM_NAMED(struct json_ocpp_bootnotif_msg, "meterSerialNumber", + meter_serial_number, JSON_TOK_STRING); + } + + if (msg.meter_type != NULL) { + bootnotif_descr[descr_count++] = (struct json_obj_descr) + JSON_OBJ_DESCR_PRIM_NAMED(struct json_ocpp_bootnotif_msg, "meterType", + meter_type, JSON_TOK_STRING); + } + + ret = json_obj_encode_buf(bootnotif_descr, descr_count, + &msg, tmp_buf, sizeof(tmp_buf)); + if (ret < 0) { + return ret; + } + + ret = frame_rpc_call_req(buf, len, PDU_BOOTNOTIFICATION, + (uint32_t)ses, tmp_buf); + if (ret < 0) { + return ret; + } + + return 0; +} + +static int frame_meter_val_msg(char *buf, int len, struct ocpp_session *ses, char *timestamp, + char *val, char *measurand, char *unit) +{ + int ret = 0; + char tmp_buf[JSON_MSG_BUF_512]; + uint8_t descr_count = SAMPLED_VALUE_MIN_FIELDS; + + struct json_ocpp_meter_val_msg msg = { + .connector_id = ses ? ses->idcon : 0, + .transaction_id = ses ? ses->idtxn : 0, + .meter_value = {{ + .timestamp = timestamp, + .sampled_value = {{ + .measurand = measurand, + .value = val, + .unit = unit ? unit : NULL, + }}, + .sampled_value_len = 1, + }}, + .meter_value_len = 1, + }; + + struct json_obj_descr sampled_value_descr[SAMPLED_VALUE_MAX_FIELDS] = { + JSON_OBJ_DESCR_PRIM_NAMED(struct json_ocpp_sample_val, "measurand", + measurand, JSON_TOK_STRING), + JSON_OBJ_DESCR_PRIM_NAMED(struct json_ocpp_sample_val, "value", + value, JSON_TOK_STRING), + }; + + if (msg.meter_value[0].sampled_value[0].unit != NULL) { + sampled_value_descr[descr_count++] = (struct json_obj_descr) + JSON_OBJ_DESCR_PRIM_NAMED(struct json_ocpp_sample_val, "unit", + unit, JSON_TOK_STRING); + } + + struct json_obj_descr meter_value_descr[] = { + JSON_OBJ_DESCR_PRIM_NAMED(struct json_ocpp_meter_val, "timestamp", + timestamp, JSON_TOK_STRING), + JSON_OBJ_DESCR_OBJ_ARRAY_NAMED(struct json_ocpp_meter_val, "sampledValue", + sampled_value, 1, sampled_value_len, + sampled_value_descr, + descr_count), + }; + + struct json_obj_descr meter_val_msg_descr[] = { + JSON_OBJ_DESCR_PRIM_NAMED(struct json_ocpp_meter_val_msg, "connectorId", + connector_id, JSON_TOK_NUMBER), + JSON_OBJ_DESCR_PRIM_NAMED(struct json_ocpp_meter_val_msg, "transactionId", + transaction_id, JSON_TOK_NUMBER), + JSON_OBJ_DESCR_OBJ_ARRAY_NAMED(struct json_ocpp_meter_val_msg, "meterValue", + meter_value, 1, meter_value_len, + meter_value_descr, + ARRAY_SIZE(meter_value_descr)), + }; + + ret = json_obj_encode_buf(meter_val_msg_descr, + ARRAY_SIZE(meter_val_msg_descr), + &msg, tmp_buf, sizeof(tmp_buf)); + if (ret < 0) { + return ret; + } + + ret = frame_rpc_call_req(buf, len, PDU_METER_VALUES, + (uint32_t)ses, tmp_buf); + if (ret < 0) { + return ret; + } + + return 0; +} + +static int frame_stop_txn_msg(char *buf, int len, struct ocpp_session *ses, + int Wh, char *reason, char *timestamp) +{ + int ret = 0; + char tmp_buf[JSON_MSG_BUF_256]; + uint8_t descr_count = STOP_TXN_MIN_FIELDS; + + struct json_ocpp_stop_txn_msg msg = { + .transaction_id = ses->idtxn, + .meter_stop = Wh, + .timestamp = timestamp, + .reason = reason ? reason : NULL, + .id_tag = ses->idtag[0] ? ses->idtag : NULL, + }; + + struct json_obj_descr descr[STOP_TXN_MAX_FIELDS] = { + JSON_OBJ_DESCR_PRIM_NAMED(struct json_ocpp_stop_txn_msg, "transactionId", + transaction_id, JSON_TOK_NUMBER), + JSON_OBJ_DESCR_PRIM_NAMED(struct json_ocpp_stop_txn_msg, "meterStop", + meter_stop, JSON_TOK_NUMBER), + JSON_OBJ_DESCR_PRIM_NAMED(struct json_ocpp_stop_txn_msg, "timestamp", + timestamp, JSON_TOK_STRING), + }; + + if (msg.reason != NULL) { + descr[descr_count++] = (struct json_obj_descr) + JSON_OBJ_DESCR_PRIM_NAMED(struct json_ocpp_stop_txn_msg, "reason", + reason, JSON_TOK_STRING); + } + + if (msg.id_tag != NULL) { + descr[descr_count++] = (struct json_obj_descr) + JSON_OBJ_DESCR_PRIM_NAMED(struct json_ocpp_stop_txn_msg, "idTag", + id_tag, JSON_TOK_STRING); + } + + ret = json_obj_encode_buf(descr, descr_count, &msg, + tmp_buf, sizeof(tmp_buf)); + if (ret < 0) { + return ret; + } + + ret = frame_rpc_call_req(buf, len, PDU_STOP_TRANSACTION, + (uint32_t)ses, tmp_buf); + if (ret < 0) { + return ret; + } + + return 0; +} + +static int frame_start_txn_msg(char *buf, int len, struct ocpp_session *ses, + int Wh, int reserv_id, char *timestamp) +{ + int ret = 0; + char tmp_buf[JSON_MSG_BUF_256]; + uint8_t descr_count = START_TXN_MIN_FIELDS; + + struct json_ocpp_start_txn_msg msg = { + .connector_id = ses->idcon, + .id_tag = ses->idtag, + .meter_start = Wh, + .timestamp = timestamp, + .reservation_id = (reserv_id >= 0) ? reserv_id : -1, + }; + + struct json_obj_descr descr[START_TXN_MAX_FIELDS] = { + JSON_OBJ_DESCR_PRIM_NAMED(struct json_ocpp_start_txn_msg, "connectorId", + connector_id, JSON_TOK_NUMBER), + JSON_OBJ_DESCR_PRIM_NAMED(struct json_ocpp_start_txn_msg, "idTag", + id_tag, JSON_TOK_STRING), + JSON_OBJ_DESCR_PRIM_NAMED(struct json_ocpp_start_txn_msg, "meterStart", + meter_start, JSON_TOK_NUMBER), + JSON_OBJ_DESCR_PRIM_NAMED(struct json_ocpp_start_txn_msg, "timestamp", + timestamp, JSON_TOK_STRING), + }; + + if (msg.reservation_id != -1) { + descr[descr_count++] = (struct json_obj_descr) + JSON_OBJ_DESCR_PRIM_NAMED(struct json_ocpp_start_txn_msg, "reservationId", + reservation_id, JSON_TOK_NUMBER); + } + + ret = json_obj_encode_buf(descr, descr_count, &msg, + tmp_buf, sizeof(tmp_buf)); + if (ret < 0) { + return ret; + } + + ret = frame_rpc_call_req(buf, len, PDU_START_TRANSACTION, + (uint32_t)ses, tmp_buf); + if (ret < 0) { + return ret; + } + + return 0; +} + +static int frame_getconfig_msg(char *buf, int len, char *key, char *val, + bool is_rw, char *uid) +{ + int ret = 0; + char tmp_buf[JSON_MSG_BUF_128]; + + struct json_ocpp_getconfig_msg msg = { 0 }; + + if (val) { + msg.configuration_key[0].key = key; + msg.configuration_key[0].readonly = !is_rw; + msg.configuration_key[0].value = val; + msg.configuration_key_len = 1; + } else { + msg.unknown_key = key; + } + + struct json_obj_descr keyval_descr[] = { + JSON_OBJ_DESCR_PRIM_NAMED(struct json_ocpp_key_val, "key", + key, JSON_TOK_STRING), + JSON_OBJ_DESCR_PRIM_NAMED(struct json_ocpp_key_val, "readonly", + readonly, JSON_TOK_NUMBER), + JSON_OBJ_DESCR_PRIM_NAMED(struct json_ocpp_key_val, "value", + value, JSON_TOK_STRING), + }; + + struct json_obj_descr config_descr[GET_CFG_MAX_FIELDS] = { + JSON_OBJ_DESCR_OBJ_ARRAY_NAMED(struct json_ocpp_getconfig_msg, "configurationKey", + configuration_key, 1, configuration_key_len, + keyval_descr, ARRAY_SIZE(keyval_descr)) + }; + + if (!val) { + config_descr[0] = (struct json_obj_descr) + JSON_OBJ_DESCR_PRIM_NAMED(struct json_ocpp_getconfig_msg, "unknownKey", + unknown_key, JSON_TOK_STRING); + } + + ret = json_obj_encode_buf(config_descr, GET_CFG_MAX_FIELDS, &msg, + tmp_buf, sizeof(tmp_buf)); + if (ret < 0) { + return ret; + } + + ret = frame_rpc_call_res(buf, len, uid, tmp_buf); + if (ret < 0) { + return ret; + } + + return 0; +} + +static int frame_status_resp_msg(char *buf, int len, char *res, char *uid) +{ + int ret = 0; + char tmp_buf[JSON_MSG_BUF_128]; + + struct json_obj_descr descr[] = { + JSON_OBJ_DESCR_PRIM_NAMED(struct json_common_payload_field_str, "status", + val1, JSON_TOK_STRING), + }; + + struct json_common_payload_field_str msg = { + .val1 = res, + }; + + ret = json_obj_encode_buf(descr, ARRAY_SIZE(descr), &msg, + tmp_buf, sizeof(tmp_buf)); + if (ret < 0) { + return ret; + } + + ret = frame_rpc_call_res(buf, len, uid, tmp_buf); + if (ret < 0) { + return ret; + } + + return 0; +} + +/* parse msg from server */ +int parse_rpc_msg(char *msg, int msglen, char *uid, int uidlen, + int *pdu, bool *is_rsp) +{ + int ret = 0; + char local_buf[JSON_MSG_BUF_512]; + char action[JSON_MSG_BUF_128]; + char *token; + int rpc_id = -1; + + if (!msg || !uid || !pdu || !is_rsp) { + return -EINVAL; + } + + memcpy(local_buf, msg + 1, sizeof(local_buf) - 1); + local_buf[sizeof(local_buf) - 1] = '\0'; + + token = strtok(local_buf, ","); + if (!token) { + return -EINVAL; + } + + rpc_id = *token - '0'; + + token = strtok(NULL, ","); + if (!token) { + return -EINVAL; + } + + ret = extract_string_field(uid, uidlen, token); + if (ret < 0) { + return ret; + } + + switch (rpc_id + '0') { + case OCPP_WAMP_RPC_REQ: + token = strtok(NULL, ","); + if (!token) { + return -EINVAL; + } + + ret = extract_string_field(action, sizeof(action), token); + if (ret < 0) { + return ret; + } + *pdu = ocpp_find_pdu_from_literal(action); + /* fall through */ + __fallthrough; + + case OCPP_WAMP_RPC_RESP: + *is_rsp = rpc_id - 2; + ret = extract_payload(msg, msglen); + if (ret < 0) { + return ret; + } + break; + + case OCPP_WAMP_RPC_ERR: + /* fall through */ + __fallthrough; + + default: + return -EINVAL; + } + + return 0; +} + +static int parse_idtag_info(char *json, struct ocpp_idtag_info *idtag_info) +{ + int ret = 0; + char *status; + + struct json_obj_descr inner_descr[] = { + JSON_OBJ_DESCR_PRIM_NAMED(struct json_idtag_info_root, "status", + json_id_tag_info.status, JSON_TOK_STRING), + JSON_OBJ_DESCR_PRIM_NAMED(struct json_idtag_info_root, "parentIdTag", + json_id_tag_info.parent_id_tag, JSON_TOK_STRING), + JSON_OBJ_DESCR_PRIM_NAMED(struct json_idtag_info_root, "expiryDate", + json_id_tag_info.expiry_date, JSON_TOK_STRING), + }; + + struct json_obj_descr root_descr[] = { + JSON_OBJ_DESCR_OBJECT_NAMED(struct json_idtag_info_root, "idTagInfo", + json_id_tag_info, inner_descr), + }; + + struct json_idtag_info_root parsed = { 0 }; + + ret = json_obj_parse(json, strlen(json), root_descr, + ARRAY_SIZE(root_descr), &parsed); + if (ret < 0) { + return ret; + } + + status = parsed.json_id_tag_info.status; + if (!status) { + return -EINVAL; + } + + switch (*status) { + 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 (parsed.json_id_tag_info.parent_id_tag) { + strncpy(idtag_info->p_idtag, parsed.json_id_tag_info.parent_id_tag, + sizeof(idtag_info->p_idtag)); + } + + if (parsed.json_id_tag_info.expiry_date) { + strncpy(idtag_info->exptime, parsed.json_id_tag_info.expiry_date, + sizeof(idtag_info->exptime)); + } + + return 0; +} + +static int parse_heartbeat_msg(char *json, struct timeval *date) +{ + int ret = 0; + + struct json_obj_descr descr[] = { + JSON_OBJ_DESCR_PRIM_NAMED(struct json_common_payload_field_str, "currentTime", + val1, JSON_TOK_STRING), + }; + + struct json_common_payload_field_str heartbeat = {0}; + + ret = json_obj_parse(json, strlen(json), descr, + ARRAY_SIZE(descr), &heartbeat); + + /* todo: convert civil time to epoch and update local time */ + + if (ret < 0) { + return ret; + } + + return 0; +} + +static int parse_authorize_msg(char *json, struct ocpp_idtag_info *idtag_info) +{ + int ret = 0; + + ret = parse_idtag_info(json, idtag_info); + if (ret < 0) { + return ret; + } + + return 0; +} + +static int parse_bootnotification_msg(char *json, struct boot_notif *binfo) +{ + int ret = 0; + + struct json_obj_descr descr[] = { + JSON_OBJ_DESCR_PRIM_NAMED(struct json_bootnotif_payload, "status", + status, JSON_TOK_STRING), + JSON_OBJ_DESCR_PRIM_NAMED(struct json_bootnotif_payload, "interval", + interval, JSON_TOK_NUMBER), + JSON_OBJ_DESCR_PRIM_NAMED(struct json_bootnotif_payload, "currentTime", + current_time, JSON_TOK_STRING), + }; + + struct json_bootnotif_payload msg = { 0 }; + + ret = json_obj_parse(json, strlen(json), descr, ARRAY_SIZE(descr), &msg); + if (ret < 0) { + return ret; + } + + if (!msg.status) { + return -EINVAL; + } + + switch (*msg.status) { + 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: + return -EINVAL; + } + + if (!msg.interval) { + return -EINVAL; + } + + binfo->interval = msg.interval; + + if (!msg.current_time) { + return -EINVAL; + } + + /* todo: convert civil time to epoch and update local time */ + (void)binfo->date; + + return 0; +} + +static int parse_start_txn_msg(char *json, + int *idtxn, + struct ocpp_idtag_info *idtag_info) +{ + int ret = 0; + + struct json_obj_descr descr[] = { + JSON_OBJ_DESCR_PRIM_NAMED(struct json_common_payload_field, "transactionId", + val1, JSON_TOK_NUMBER), + }; + + struct json_common_payload_field payload = { 0 }; + + ret = json_obj_parse(json, strlen(json), descr, ARRAY_SIZE(descr), &payload); + if (ret < 0) { + return ret; + } + + *idtxn = payload.val1; + + ret = parse_idtag_info(json, idtag_info); + if (ret < 0) { + return ret; + } + + return 0; +} + +static int parse_getconfig_msg(char *json, char *key) +{ + int ret = 0; + + struct json_obj_descr descr[] = { + JSON_OBJ_DESCR_ARRAY_NAMED(struct json_getconfig_payload, "key", + key, 1, key_len, JSON_TOK_STRING), + }; + + struct json_getconfig_payload payload = { 0 }; + + ret = json_obj_parse(json, strlen(json), descr, + ARRAY_SIZE(descr), &payload); + if (ret < 0) { + return ret; + } + + /* key is optional so return success*/ + if (payload.key[0] != NULL) { + strcpy(key, payload.key[0]); + } + + return 0; +} + +static int parse_changeconfig_msg(char *json, char *key, char *val) +{ + int ret = 0; + + struct json_obj_descr descr[] = { + JSON_OBJ_DESCR_PRIM_NAMED(struct json_common_payload_field_str, "key", + val1, JSON_TOK_STRING), + JSON_OBJ_DESCR_PRIM_NAMED(struct json_common_payload_field_str, "value", + val2, JSON_TOK_STRING), + }; + + struct json_common_payload_field_str payload = { 0 }; + + ret = json_obj_parse(json, strlen(json), descr, ARRAY_SIZE(descr), &payload); + if (ret < 0) { + return ret; + } + + if (!payload.val1 || !payload.val2) { + return -EINVAL; + } + + strncpy(key, payload.val1, CISTR50); + strncpy(val, payload.val2, CISTR500); + + return 0; +} + +static int parse_remote_start_txn_msg(char *json, + int *idcon, + char *idtag) +{ + int ret = 0; + + struct json_obj_descr descr[] = { + JSON_OBJ_DESCR_PRIM_NAMED(struct json_common_payload_field, "connectorId", + val1, JSON_TOK_NUMBER), + JSON_OBJ_DESCR_PRIM_NAMED(struct json_common_payload_field, "idTag", + val2, JSON_TOK_STRING), + }; + + struct json_common_payload_field payload = { 0 }; + + ret = json_obj_parse(json, strlen(json), descr, ARRAY_SIZE(descr), &payload); + if (ret < 0) { + return ret; + } + + if (!payload.val2) { + return -EINVAL; + } + + strncpy(idtag, payload.val2, CISTR50); + *idcon = payload.val1; + + return 0; +} + +static int parse_remote_stop_txn_msg(char *json, int *idtxn) +{ + int ret = 0; + + struct json_obj_descr descr[] = { + JSON_OBJ_DESCR_PRIM_NAMED(struct json_common_payload_field, "transactionId", + val1, JSON_TOK_NUMBER), + }; + + struct json_common_payload_field payload = { 0 }; + + ret = json_obj_parse(json, strlen(json), descr, ARRAY_SIZE(descr), &payload); + if (ret < 0) { + return ret; + } + + *idtxn = payload.val1; + + return 0; +} + +static int parse_unlock_connectormsg(char *json, int *idcon) +{ + int ret = 0; + + struct json_obj_descr descr[] = { + JSON_OBJ_DESCR_PRIM_NAMED(struct json_common_payload_field, "connectorId", + val1, JSON_TOK_NUMBER), + }; + + struct json_common_payload_field payload = { 0 }; + + ret = json_obj_parse(json, strlen(json), descr, ARRAY_SIZE(descr), &payload); + if (ret < 0) { + return ret; + } + + if (payload.val1 == 0) { + return -EINVAL; + } + + *idcon = payload.val1; + + return 0; +} + +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_j.h b/subsys/net/lib/ocpp/ocpp_j.h new file mode 100644 index 0000000000000..71306d915052c --- /dev/null +++ b/subsys/net/lib/ocpp/ocpp_j.h @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2025 Linumiz GmbH + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef __OCPP_J_ +#define __OCPP_J_ + +#define JSON_MSG_BUF_128 128 +#define JSON_MSG_BUF_256 256 +#define JSON_MSG_BUF_512 512 + +#define BOOTNOTIF_MIN_FIELDS 2 +#define BOOTNOTIF_MAX_FIELDS 9 + +#define STOP_TXN_MIN_FIELDS 3 +#define STOP_TXN_MAX_FIELDS 5 + +#define START_TXN_MIN_FIELDS 4 +#define START_TXN_MAX_FIELDS 5 + +#define GET_CFG_MAX_FIELDS 1 + +#define SAMPLED_VALUE_MIN_FIELDS 2 +#define SAMPLED_VALUE_MAX_FIELDS 3 + +struct json_common_payload_field { + int val1; + char *val2; +}; + +struct json_common_payload_field_str { + char *val1; + char *val2; +}; + +struct json_ocpp_bootnotif_msg { + char *charge_point_model; + char *charge_point_vendor; + char *charge_box_serial_number; + char *charge_point_serial_number; + char *firmware_version; + char *iccid; + char *imsi; + char *meter_serial_number; + char *meter_type; +}; + +struct json_ocpp_meter_val_msg { + int connector_id; + int transaction_id; + + struct json_ocpp_meter_val { + char *timestamp; + + struct json_ocpp_sample_val { + char *measurand; + char *value; + char *unit; + } sampled_value[1]; + size_t sampled_value_len; + } meter_value[1]; + + size_t meter_value_len; +}; + +struct json_ocpp_stop_txn_msg { + int transaction_id; + int meter_stop; + char *timestamp; + char *reason; + char *id_tag; +}; + +struct json_ocpp_start_txn_msg { + int connector_id; + char *id_tag; + int meter_start; + char *timestamp; + int reservation_id; +}; + +struct json_ocpp_getconfig_msg { + struct json_ocpp_key_val { + char *key; + int readonly; + char *value; + } configuration_key[1]; + + size_t configuration_key_len; + char *unknown_key; +}; + +struct json_idtag_info_root { + struct { + char *status; + char *parent_id_tag; + char *expiry_date; + } json_id_tag_info; +}; + +struct json_bootnotif_payload { + char *status; + int interval; + char *current_time; +}; + +struct json_getconfig_payload { + char *key[1]; + size_t key_len; +}; + +#endif /* __OCPP_J_ */ 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..713364229f27d --- /dev/null +++ b/subsys/net/lib/ocpp/ocpp_wamp_rpc.c @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2025 Linumiz GmbH + * + * 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 *msg_type, + uint32_t timeout) +{ + int ret; + int sock; + 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..b949422a4c020 --- /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_JSON_LIBRARY=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_API=y +CONFIG_POSIX_CLOCK_SELECTION=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..29d7bb0b56841 --- /dev/null +++ b/tests/net/lib/ocpp/src/main.c @@ -0,0 +1,121 @@ +/* + * 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_EXPECT_FAIL(net_ocpp, test_ocpp_chargepoint); +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..fabee74523ee8 --- /dev/null +++ b/tests/net/lib/ocpp/testcase.yaml @@ -0,0 +1,9 @@ +tests: + net.ocpp: + depends_on: netif + platform_allow: + - native_sim/native + min_ram: 16 + tags: + - net + - ocpp