From 2923af7b7289d175a87f8ba954e80cfe376ab5c2 Mon Sep 17 00:00:00 2001 From: Matteo Zella Date: Tue, 2 Jun 2026 02:41:00 +0200 Subject: [PATCH] Add Renesas platform integration - Add the Renesas-specific OSAL platform implementation, configuration headers and sample TLVs - Move firmware management config to platform types - Add Renesas IANA Private Enterprise Number - Fix Renesas CC-RX compiler warnings --- Vendors/Renesas/Readme.md | 43 + Vendors/Renesas/resources/renesas-logo.jpg | Bin 0 -> 81649 bytes include/iana_pen.h | 4 +- osal/efr32_wisun/osal_platform_types.h | 5 + osal/freertos/osal_platform_types.h | 5 + osal/linux/osal_platform_types.h | 5 +- osal/osal.h | 9 - osal/renesas_wisun/osal_platform_types.h | 128 ++ osal/renesas_wisun/osal_renesas_config.h | 57 + osal/renesas_wisun/osal_renesas_wisun.c | 540 +++++++ osal/renesas_wisun/osal_renesas_wrapper.h | 49 + sample/tlvs/renesas_tlvs.c | 1708 ++++++++++++++++++++ src/csmpagent/csmp_firmwareMgmt.c | 2 +- src/csmpapi/csmpservice.c | 2 +- src/csmpservice/cgmsagent.c | 4 +- src/lib/protobuf-c/protobuf-c.c | 2 +- 16 files changed, 2547 insertions(+), 16 deletions(-) create mode 100644 Vendors/Renesas/Readme.md create mode 100644 Vendors/Renesas/resources/renesas-logo.jpg create mode 100644 osal/renesas_wisun/osal_platform_types.h create mode 100644 osal/renesas_wisun/osal_renesas_config.h create mode 100644 osal/renesas_wisun/osal_renesas_wisun.c create mode 100644 osal/renesas_wisun/osal_renesas_wrapper.h create mode 100644 sample/tlvs/renesas_tlvs.c diff --git a/Vendors/Renesas/Readme.md b/Vendors/Renesas/Readme.md new file mode 100644 index 0000000..6ffc5a3 --- /dev/null +++ b/Vendors/Renesas/Readme.md @@ -0,0 +1,43 @@ +# Renesas Wi-SUN FAN CSMP Agent Integration + +

+ Renesas logo +

+ +## Summary + +The integration targets Renesas Wi-SUN FAN products based on the Renesas RX MCU family. +Renesas provides Wi-SUN FAN-compliant Sub-GHz solutions for Wi-SUN FAN applications. +For the list of officially supported devices, stack versions, configuration options, and available features, please refer to the Renesas Wi-SUN FAN product and software documentation. + +The purpose of this integration is to enable a Renesas Wi-SUN FAN node to communicate with Cisco Field Network Director (FND) using CSMP. +It provides platform-specific functionality required by the CSMP Agent Library, including: + +- UDP/IPv6 communication over the Renesas Wi-SUN FAN protocol stack, +- access to platform, stack, and firmware information, +- mapping of Wi-SUN FAN network and firmware information to CSMP TLV objects, +- firmware update integration. + +## Requirements + +The following components are required to build and run the Renesas Wi-SUN FAN CSMP Agent integration: + +- A running instance of Cisco Field Network Director (FND). For a development setup, please refer to the [CSMP Developer Tutorial](../../docs/CSMP%20Developer%20Tutorial%20-%200v11.pdf). + +- At least two [Renesas Wi-SUN FAN Development Kits](https://www.renesas.com/en/design-resources/boards-kits/rtk0ee0013d10002bj). + +- The latest Renesas [Wi-SUN FAN Protocol Stack](https://www.renesas.com/en/software-tool/sub-ghzwi-sun-protocol-stack). + +## Renesas Wi-SUN FAN Stack + +This Renesas CSMP integration layer is designed to be used in conjunction with the latest version of the [Renesas Wi-SUN FAN Protocol Stack](https://www.renesas.com/en/software-tool/sub-ghzwi-sun-protocol-stack). +Different versions may not work, so please make sure that this CSMP integration layer is used with the correct version of the Renesas Wi-SUN FAN Protocol Stack! + +Further information about the CSMP usage with the Renesas Wi-SUN FAN protocol stack, APIs, configuration, sample applications, and supported platforms is provided in the user manual of the Renesas Wi-SUN FAN Protocol Stack and accompanying documentation, which are located in the release package of the Renesas Wi-SUN FAN Protocol Stack. +It also includes instructions for configuring and running a sample setup for CSMP with Renesas Wi-SUN FAN nodes. + +Relevant Renesas resources: + +- [Renesas Sub-GHz/Wi-SUN FAN Transceivers](https://www.renesas.com/en/products/wireless-connectivity/sub-ghz-Wi-SUN-transceivers) + +- [Renesas Sub-GHz/Wi-SUN FAN Protocol Stack](https://www.renesas.com/en/software-tool/sub-ghzWi-SUN-protocol-stack) diff --git a/Vendors/Renesas/resources/renesas-logo.jpg b/Vendors/Renesas/resources/renesas-logo.jpg new file mode 100644 index 0000000000000000000000000000000000000000..42706f59d22078d6300dd7797b3be5c752053a94 GIT binary patch literal 81649 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 +#include +#include "FreeRTOS.h" +#include "task.h" +#include "semphr.h" +#include "r_byte_swap.h" +#include "r_header_utils.h" +#if defined(__i386) || defined(__x86_64) + +/* The Wi-SUN FAN simulator requires Unix sockets for basic functionality -> avoid redefinition */ +#include +#include +#include +#else + +/* Supported address families. */ +#define AF_INET6 10 /* IP version 6 */ + +/* Type to represent a port. */ +typedef uint16_t in_port_t; + +typedef uint16_t sa_family_t; + +typedef uint32_t socklen_t; + +struct in6_addr +{ + unsigned char s6_addr[16]; /* IPv6 address */ +}; + +/* Structure describing a generic socket address. */ +struct sockaddr +{ + sa_family_t sa_family; /* Common data: address family and length. */ + char sa_data[14]; /* Address data. */ +}; + +struct sockaddr_in6 +{ + sa_family_t sin6_family; /* AF_INET6 */ + in_port_t sin6_port; /* port number */ + uint32_t sin6_flowinfo; /* IPv6 flow information */ + struct in6_addr sin6_addr; /* IPv6 address */ + uint32_t sin6_scope_id; /* Scope ID (new in 2.4) */ +}; + +/* Structure for scatter/gather I/O. */ +struct iovec +{ + void* iov_base; /* Pointer to data. */ + size_t iov_len; /* Length of data. */ +}; + +/* Structure describing messages sent by + `sendmsg' and received by `recvmsg'. */ +struct msghdr +{ + void* msg_name; /* Address to send to/receive from. */ + socklen_t msg_namelen; /* Length of address data. */ + + struct iovec* msg_iov; /* Vector of data to send/receive into. */ + size_t msg_iovlen; /* Number of elements in the vector. */ + + void* msg_control; /* Ancillary data (eg BSD filedesc passing). */ + size_t msg_controllen; /* Ancillary data buffer length. + !! The type should be socklen_t but the + definition of the kernel is incompatible + with this. */ + + int msg_flags; /* Flags on received message. */ +}; + +#endif + +typedef void (*osal_sighandler_t)(int); + +typedef struct sockaddr_in6 osal_sockaddr_t; +typedef SemaphoreHandle_t osal_sem_t; +typedef int osal_sigset_t; +typedef socklen_t osal_socklen_t; +typedef TaskHandle_t osal_task_t; +typedef TaskFunction_t osal_task_fnc_t; +typedef int32_t osal_ssize_t; +typedef uint64_t osal_time_t; +typedef BaseType_t osal_basetype_t; +typedef int osal_socket_handle_t; +typedef int osal_sd_set_t; + +#define OSAL_AF_INET6 AF_INET6 +#define OSAL_SOCK_DGRAM 0 + +#define OSAL_ATTR_PACKED R_HEADER_UTILS_ATTR_PACKED + +#define CSMP_MAX_SOCKETS 2 + +#if !defined(__i386) && !defined(__x86_64) +struct timeval +{ + uint32_t tv_sec; /* Seconds. */ + uint32_t tv_usec; /* Microseconds. */ +}; +#endif + +struct timezone { + int tz_minuteswest; /* minutes west of Greenwich */ + int tz_dsttime; /* type of DST correction */ +}; + +#endif \ No newline at end of file diff --git a/osal/renesas_wisun/osal_renesas_config.h b/osal/renesas_wisun/osal_renesas_config.h new file mode 100644 index 0000000..659abe9 --- /dev/null +++ b/osal/renesas_wisun/osal_renesas_config.h @@ -0,0 +1,57 @@ +/****************************************************************************** + * DISCLAIMER + * This software is supplied by Renesas Electronics Corporation and is only + * intended for use with Renesas products. No other uses are authorized. This + * software is owned by Renesas Electronics Corporation and is protected under + * all applicable laws, including copyright laws. + * THIS SOFTWARE IS PROVIDED "AS IS" AND RENESAS MAKES NO WARRANTIES REGARDING + * THIS SOFTWARE, WHETHER EXPRESS, IMPLIED OR STATUTORY, INCLUDING BUT NOT + * LIMITED TO WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE + * AND NON-INFRINGEMENT. ALL SUCH WARRANTIES ARE EXPRESSLY DISCLAIMED. + * TO THE MAXIMUM EXTENT PERMITTED NOT PROHIBITED BY LAW, NEITHER RENESAS + * ELECTRONICS CORPORATION NOR ANY OF ITS AFFILIATED COMPANIES SHALL BE LIABLE + * FOR ANY DIRECT, INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES FOR + * ANY REASON RELATED TO THIS SOFTWARE, EVEN IF RENESAS OR ITS AFFILIATES HAVE + * BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + * Renesas reserves the right, without notice, to make changes to this + * software and to discontinue the availability of this software. By using this + * software, you agree to the additional terms and conditions found by + * accessing the following link: + * http://www.renesas.com/disclaimer + * + * Copyright (C) 2025 Renesas Electronics Corporation. All rights reserved. + *****************************************************************************/ + +/** + * @file osal_renesas_config.h + * @brief Configuration of the CSMP module running within the Renesas Wi-SUN FAN sample application. + */ + +#ifndef OSAL_RENESAS_CONFIG_H +#define OSAL_RENESAS_CONFIG_H + +/** the major version of the CSMP module */ +#define R_CSMP_MAJOR_VERSION "0" + +/** the major version of the CSMP module */ +#define R_CSMP_MINOR_VERSION "1" + +/** the build version of the CSMP module */ +#define R_CSMP_BUILD_VERSION "0" + +/** string to identify this version of the CSMP module (in format "major.minor.build") */ +#define R_CSMP_VERSION_STRING R_CSMP_MAJOR_VERSION "." R_CSMP_MINOR_VERSION "." R_CSMP_BUILD_VERSION + +/** string to identify the running firmware */ +#define R_CSMP_FILE_NAME "Wi-SUN-FAN-SampleApp" + +/** string to identify this hardware platform as used by FND */ +#define R_CSMP_HARDWARE_ID "renesastest" + +/** max number of ip addresses */ +#define R_TLV_IPADDRESS_MAX_NUM 3 + +/** max number of routes */ +#define R_TLV_ROUTES_MAX_NUM 2 + +#endif /* OSAL_RENESAS_CONFIG_H */ diff --git a/osal/renesas_wisun/osal_renesas_wisun.c b/osal/renesas_wisun/osal_renesas_wisun.c new file mode 100644 index 0000000..e9b9555 --- /dev/null +++ b/osal/renesas_wisun/osal_renesas_wisun.c @@ -0,0 +1,540 @@ +/****************************************************************************** +* DISCLAIMER + * This software is supplied by Renesas Electronics Corporation and is only + * intended for use with Renesas products. No other uses are authorized. This + * software is owned by Renesas Electronics Corporation and is protected under + * all applicable laws, including copyright laws. + * THIS SOFTWARE IS PROVIDED "AS IS" AND RENESAS MAKES NO WARRANTIES REGARDING + * THIS SOFTWARE, WHETHER EXPRESS, IMPLIED OR STATUTORY, INCLUDING BUT NOT + * LIMITED TO WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE + * AND NON-INFRINGEMENT. ALL SUCH WARRANTIES ARE EXPRESSLY DISCLAIMED. + * TO THE MAXIMUM EXTENT PERMITTED NOT PROHIBITED BY LAW, NEITHER RENESAS + * ELECTRONICS CORPORATION NOR ANY OF ITS AFFILIATED COMPANIES SHALL BE LIABLE + * FOR ANY DIRECT, INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES FOR + * ANY REASON RELATED TO THIS SOFTWARE, EVEN IF RENESAS OR ITS AFFILIATES HAVE + * BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + * Renesas reserves the right, without notice, to make changes to this + * software and to discontinue the availability of this software. By using this + * software, you agree to the additional terms and conditions found by + * accessing the following link: + * http://www.renesas.com/disclaimer + * + * Copyright (C) 2025 Renesas Electronics Corporation. All rights reserved. + *****************************************************************************/ + +/** + * @file osal_renesas_wisun.c + * @brief Implementation of the OSAL CSMP interface to be used within the Renesas Wi-SUN FAN sample application. + */ + +#include "osal.h" +#include "csmp.h" +#include "csmp_service.h" +#include "CsmpAgentLib_sample_tlvs.h" +#include "signature_verify.h" +#include "r_nwk_api.h" +#include "r_mem_tools.h" +#include "r_impl_utils.h" +#include "r_ipv6_helper.h" +#if R_FWUP_SUPPORT +#include "r_fwup_wrapper.h" +#endif +#include "lib/random.h" +#include "osal_renesas_wrapper.h" + +#define MY_HEAP_ID R_HEAP_ID_CSMP +#include "r_heap.h" + +// R_LOG_PREFIX is processed by RLog code generator +#define R_LOG_PREFIX CSMP +#include "r_log_internal.h" +#if R_LOG_THRESHOLD > R_LOG_SEVERITY_OFF +#include "r_loggen_osal_renesas_wisun.h" +#endif + +typedef struct r_trickle_timer_s +{ + r_timer_id_t id; + uint32_t t0; + uint32_t tfire; + uint32_t icur; + uint32_t imin; + uint32_t imax; + uint8_t is_running : 1; + r_timer_t cb_timer; + trickle_timer_fired_t csmp_cb; //!< The CSMP callback function that should be executed once this timer expires +} r_trickle_timer_t; + +static r_trickle_timer_t timers[timer_num]; + +uint16_t socket_ports[CSMP_MAX_SOCKETS]; + +csmp_handle_t r_csmp_handle; + +extern uint32_t m_GroupIds[CSMP_GROUP_NUM_TYPES]; +STATIC_ASSERT(R_FLASH_CSMP_GROUPS_SIZE == sizeof(m_GroupIds), Size_of_csmp_group_array_must_equal_R_FLASH_CSMP_GROUPS_SIZE); + +/** The csmp module is uninitialized by default (R_FALSE) */ +static r_boolean_t csmpIsInitialized = R_FALSE; + +osal_ssize_t osal_sendmsg(osal_socket_handle_t sockd, const struct msghdr msg, osal_basetype_t flags) +{ + if (msg.msg_iov == NULL) + { + R_LOG_ERR("r_sendmsg failed: data vector is null"); + return R_RESULT_INVALID_PARAMETER; + } + + if (msg.msg_name == NULL) + { + R_LOG_ERR("r_sendmsg failed: address is null"); + return R_RESULT_INVALID_PARAMETER; + } + + const osal_sockaddr_t *addr = msg.msg_name; + osal_socklen_t addrlen = msg.msg_namelen; + + osal_ssize_t total_sent = 0; + + for (size_t i = 0; i < msg.msg_iovlen; ++i) + { + const struct iovec *iov = &msg.msg_iov[i]; + const uint8_t *buf = (const uint8_t *)iov->iov_base; + size_t len = iov->iov_len; + + while (len > 0) + { + osal_ssize_t n = osal_sendto(sockd, buf, len, flags, addr, addrlen); + + if (n <= 0) + { + return OSAL_FAILURE; + } + + buf += (size_t)n; + len -= (size_t)n; + total_sent += n; + } + } + + return total_sent; +} + +osal_ssize_t osal_sendto(osal_socket_handle_t sockd, const void *buf, size_t len, osal_basetype_t flags, + const osal_sockaddr_t *dest_addr, osal_socklen_t addrlen) +{ + if (sockd < 0 || sockd >= CSMP_MAX_SOCKETS || addrlen != sizeof(struct sockaddr_in6)) + { + return R_RESULT_INVALID_PARAMETER; + } + + struct sockaddr_in6* dest = (struct sockaddr_in6*)dest_addr; + if (dest->sin6_family != AF_INET6) + { + return R_RESULT_INVALID_PARAMETER; + } + + uint16_t dstPort = ntohs(dest->sin6_port); // port in socket struct is network byte order but R_UDP_DataRequest expects host byte order + uint16_t srcPort = ntohs(socket_ports[sockd]); // port in socket struct is network byte order but R_UDP_DataRequest expects host byte order + r_result_t res = R_UDP_DataRequest(dest->sin6_addr.s6_addr, dstPort, srcPort, buf, len, 0); + if (res == R_RESULT_SUCCESS) + { + R_LOG_DBG("Transmission of %{u16} bytes to %{ipv6addr}:%{u16} from port %{u16}", len, dest->sin6_addr.s6_addr, dstPort, srcPort); + return len; + } + R_LOG_ERR("Transmission of %{u16} bytes to %{ipv6addr} failed: 0x%{hex8}", len, dest->sin6_addr.s6_addr, res); + return OSAL_FAILURE; +} + +void osal_print_formatted_ip(const osal_sockaddr_t *sockadd) +{ + R_LOG_DBG("[%{ipv6addr}]:%{u16}\n", sockadd->sin6_addr.s6_addr, sockadd->sin6_port); +} + +static void R_TrickleTimer_HandleExpired(void* timer) +{ + r_trickle_timer_t* trickleTimer = timer; + if (trickleTimer->id == reg_timer) + { + R_LOG_DBG("Register trickle timer fired"); + } + else if (trickleTimer->id == rpt_timer) + { + R_LOG_DBG("Periodic metrics report trickle timer fired"); + } + else if (trickleTimer->id == lrq_timer) + { + R_LOG_DBG("Firmware load request timer fired"); + } + else if (trickleTimer->id == async_timer) + { + R_LOG_DBG("Async CSMP Reponse timer fired"); + } + else + { + R_LOG_ERR("Invalid trickle timer id"); + } + + /* Set timer again */ + trickleTimer->t0 += trickleTimer->icur; // update t0 to next interval + + /* Double interval size */ + trickleTimer->icur <<= 1; + if (trickleTimer->icur > trickleTimer->imax) + { + trickleTimer->icur = trickleTimer->imax; + } + + uint32_t min = trickleTimer->icur >> 1; + trickleTimer->tfire = trickleTimer->t0 + min + (random32() % (trickleTimer->icur - min)); + r_result_t res = R_Timer_StartSecs(&trickleTimer->cb_timer, trickleTimer->icur, R_TrickleTimer_HandleExpired, trickleTimer); + if (res != R_RESULT_SUCCESS) + { + R_LOG_ERR("Fatal error: Trickle timer could not be restarted"); + } + + /* Execute CSMP callback */ + trickleTimer->csmp_cb(); +} + +void osal_trickle_timer_start(osal_timerid_t timerid, uint32_t imin, uint32_t imax, trickle_timer_fired_t trickle_timer_fired) +{ + if (timerid >= timer_num) + { + R_LOG_ERR("Invalid trickle timer id"); + return; // Invalid timer ID + } + + if (timerid == reg_timer) + { + R_LOG_DBG("Register trickle timer start"); + } + else if (timerid == rpt_timer) + { + R_LOG_DBG("Periodic metrics report trickle timer start"); + } + else if (timerid == lrq_timer) + { + R_LOG_DBG("Firmware load request timer start"); + } + else if (timerid == async_timer) + { + R_LOG_DBG("Async CSMP Reponse timer start"); + } + + timers[timerid].t0 = clock_seconds() + (random32() % imin); + timers[timerid].icur = imin; + timers[timerid].imin = imin; + timers[timerid].imax = imax; + uint32_t min = timers[timerid].icur >> 1; + timers[timerid].tfire = timers[timerid].t0 + min + (random32() % (timers[timerid].icur - min)); + timers[timerid].id = timerid; + timers[timerid].csmp_cb = trickle_timer_fired; + + r_result_t timerRes = R_Timer_StartSecs(&timers[timerid].cb_timer, timers[timerid].icur, R_TrickleTimer_HandleExpired, &timers[timerid]); + if (timerRes != R_RESULT_SUCCESS) + { + R_LOG_ERR("Fatal error: Timer %{u8} could not be started: 0x%{hex8}", (uint8_t)timerid, timerRes); + return; + } + + R_LOG_DBG("Start timer %{u8} (expires in %{u32}s)", timerid, timers[timerid].icur); + timers[timerid].is_running = R_TRUE; +} + +void osal_trickle_timer_stop(osal_timerid_t timerid) +{ + if (timerid >= timer_num) + { + R_LOG_ERR("Invalid trickle timer id"); + return; // Invalid timer ID + } + + r_result_t res = R_Timer_Stop(&timers[timerid].cb_timer); + + if (timerid == reg_timer) + { + R_LOG_DBG("Register trickle timer stop"); + } + else if (timerid == rpt_timer) + { + R_LOG_DBG("Periodic metrics report trickle timer stop"); + } + else if (timerid == lrq_timer) + { + R_LOG_DBG("Firmware load request timer stop"); + } + else if (timerid == async_timer) + { + R_LOG_DBG("Async CSMP Reponse timer stop"); + } + + switch (res) + { + case R_RESULT_SUCCESS: + R_LOG_DBG("Timer %{u8} stopped", timerid); + timers[timerid].is_running = R_FALSE; + break; + + case R_RESULT_NOTHING_TO_DO: + break; // Do nothing (timer was not started) + + default: + R_LOG_ERR("Trickle timer could not be stopped: 0x%{hex8}", res); + break; + } +} + +void *osal_malloc(size_t size) +{ + return my_alloc(size); +} + +void osal_free(void *ptr) +{ + my_free(ptr); +} + +osal_basetype_t osal_gettime(struct timeval *tv, struct timezone *tz) +{ + if (tv == NULL) + { + return OSAL_FAILURE; + } + + tv->tv_sec = R_CLOCK_GetUnixTimestamp();; + tv->tv_usec = 0UL; + + if (tz != NULL) + { + tz->tz_minuteswest = 0; + tz->tz_dsttime = 0; + } + + return OSAL_SUCCESS; +} + +osal_basetype_t osal_settime(struct timeval *tv, struct timezone *tz) +{ + if (tv == NULL) + { + return OSAL_FAILURE; + } + uint64_t currentUnixTime = tv->tv_sec + tv->tv_usec / 1000000; + + if (tz != NULL) + { + currentUnixTime += tz->tz_minuteswest * 60; + if (tz->tz_dsttime) + { + currentUnixTime -= 3600; + } + } + + R_CLOCK_SetUnixTimestampReference(currentUnixTime); + + return OSAL_SUCCESS; +} + +/* + * The following functions are not used in any CSMP platform independent code. + * For the Renesas platform, the functionalities are already provided by the dedicated + * FW update module. The corresponding methods are then directly called when processing + * the relevant TLVs. + */ +osal_basetype_t osal_system_reboot(struct in6_addr *NMSaddr) +{ + return OSAL_FAILURE; +} + +osal_basetype_t osal_read_firmware(uint8_t slotid, uint8_t *data, uint32_t size) +{ + return OSAL_FAILURE; +} + +osal_basetype_t osal_write_firmware(uint8_t slotid, uint8_t *data, uint32_t size) +{ + return OSAL_FAILURE; +} + +osal_basetype_t osal_write_slothdr(uint8_t slotid, Csmp_Slothdr *slot) +{ + return OSAL_FAILURE; +} + +osal_basetype_t osal_read_slothdr(uint8_t slotid, Csmp_Slothdr *slot) +{ + return OSAL_FAILURE; +} + +osal_basetype_t osal_copy_firmware(uint8_t source_slotid, uint8_t dest_slotid, Csmp_Slothdr *slot) +{ + return OSAL_FAILURE; +} + +osal_basetype_t osal_task_create(osal_task_t * thread, + const char * name, + uint32_t priority, + size_t stacksize, + osal_task_fnc_t entry, + void * arg) +{ + return OSAL_SUCCESS; +} + +osal_basetype_t osal_task_cancel(osal_task_t thread) +{ + return OSAL_SUCCESS; +} + +osal_socket_handle_t osal_socket(osal_basetype_t domain, + osal_basetype_t type, + osal_basetype_t protocol) +{ + osal_socket_handle_t sockd = 0; + uint16_t port = CSMP_DEFAULT_PORT + 1; + while (sockd < CSMP_MAX_SOCKETS) + { + if (socket_ports[sockd] == 0) + { + socket_ports[sockd] = htons(port); + return sockd; + } + port = ntohs(socket_ports[sockd]) + 1; + sockd++; + } + return OSAL_FAILURE; +} + +osal_basetype_t osal_socket_close(osal_socket_handle_t sockd) +{ + if (sockd < 0 || sockd >= CSMP_MAX_SOCKETS) + { + return OSAL_FAILURE; + } + socket_ports[sockd] = 0; + return OSAL_SUCCESS; +} + +osal_basetype_t osal_bind(osal_socket_handle_t sockd, osal_sockaddr_t *addr, osal_socklen_t addrlen) +{ + if (sockd >= CSMP_MAX_SOCKETS || sockd < 0) + { + return OSAL_FAILURE; + } + socket_ports[sockd] = addr->sin6_port; + return OSAL_SUCCESS; +} + +osal_basetype_t osal_task_setcanceltype() +{ + return OSAL_FAILURE; +} + +void osal_update_sockaddr(osal_sockaddr_t *listen_addr, uint16_t sport) +{ + listen_addr->sin6_port = htons(sport); +} + +osal_basetype_t osal_read_groups(uint32_t *groups, uint8_t num_groups) +{ + if (num_groups < CSMP_GROUP_NUM_TYPES) + { + return R_RESULT_INSUFFICIENT_OUTPUT_BUFFER; + } + + uint32_t csmpGroups[CSMP_GROUP_NUM_TYPES] = {0}; + r_result_t flashRes = R_Flash_Read(R_FLASH_CSMP_GROUPS, &csmpGroups, R_FLASH_CSMP_GROUPS_SIZE); + if (flashRes == R_RESULT_SUCCESS) + { + R_LOG_DBG("Restored CSMP group information from NVM"); + R_memcpy(groups, csmpGroups, R_FLASH_CSMP_GROUPS_SIZE); + return OSAL_SUCCESS; + } + return OSAL_FAILURE; +} + +osal_basetype_t osal_write_groups(uint32_t *groups, uint8_t num_groups) +{ + if (num_groups > CSMP_GROUP_NUM_TYPES) + { + return R_RESULT_INSUFFICIENT_OUTPUT_BUFFER; + } + + R_Flash_Write(R_FLASH_CSMP_GROUPS, groups, num_groups * sizeof(groups[0])); + return OSAL_SUCCESS; +} + +int R_CSMP_Start(const r_ipv6addr_t* serverAddr, const uint8_t* localEui64, uint32_t regImin, uint32_t regImax) +{ + dev_config_t devConfig = { 0 }; + if (!serverAddr || R_IPv6_IsUnspecified(serverAddr) || R_IPv6_IsMulticast(serverAddr)) + { + R_LOG_WARN("CSMP could not be started due to invalid NMS address: %{ipv6addr}", serverAddr->bytes); + return R_RESULT_INVALID_PARAMETER; + } + R_memcpy(devConfig.NMSaddr.s6_addr, serverAddr->bytes, R_IPV6_ADDRESS_LENGTH); + R_memcpy(devConfig.ieee_eui64.data, localEui64, R_EUI64_LEN); + devConfig.reginterval_min = regImin; + devConfig.reginterval_max = regImax; + + if (devConfig.reginterval_max < devConfig.reginterval_min || devConfig.reginterval_min == 0 + || devConfig.reginterval_max > 36000) + { + R_LOG_ERR("reg interval error (min=%{u32}; max=%{u32})", devConfig.reginterval_min, devConfig.reginterval_max); + return R_RESULT_INVALID_PARAMETER; + } + + /* Enable the CSMP signature parameters */ + devConfig.csmp_sig_settings.reqsignedpost = true; + devConfig.csmp_sig_settings.reqvalidcheckpost = true; + devConfig.csmp_sig_settings.reqtimesyncpost = true; + devConfig.csmp_sig_settings.reqseclocalpost = true; + devConfig.csmp_sig_settings.reqsignedresp = true; + devConfig.csmp_sig_settings.reqvalidcheckresp = true; + devConfig.csmp_sig_settings.reqtimesyncresp = true; + devConfig.csmp_sig_settings.reqseclocalresp = true; + + /************************************************************* + init the csmp_handle parameter of csmp_service_start func: + * callback function for the GET TLV request + * callback function for the POST TLV request + * callback function for the signature verification + **************************************************************/ + r_csmp_handle.csmptlvs_get = (csmptlvs_get_t)csmptlvs_get; + r_csmp_handle.csmptlvs_post = (csmptlvs_post_t)csmptlvs_post; + r_csmp_handle.signature_verify = (signature_verify_t)signature_verify; + + if (csmp_service_start(&devConfig, &r_csmp_handle) < 0) + { + R_LOG_ERR("start csmp agent service: fail!"); + return 1; + } + R_LOG_DBG("start csmp agent service: success!"); + + /* Initialize csmpIsInitialized for the subsequent stop */ + csmpIsInitialized = R_TRUE; + return 0; +} + +int R_CSMP_Stop() +{ + if (csmpIsInitialized) + { + /* Set csmpIsInitialized to FALSE to avoid a double reset. */ + csmpIsInitialized = R_FALSE; + + if (csmp_service_stop()) + { + R_LOG_DBG("stop csmp agent service: success!"); + return 0; + } + else + { + R_LOG_ERR("stop csmp agent service: fail!"); + return 1; + } + } + /* return immediately */ + return 0; +} diff --git a/osal/renesas_wisun/osal_renesas_wrapper.h b/osal/renesas_wisun/osal_renesas_wrapper.h new file mode 100644 index 0000000..717efe8 --- /dev/null +++ b/osal/renesas_wisun/osal_renesas_wrapper.h @@ -0,0 +1,49 @@ +/****************************************************************************** + * DISCLAIMER + * This software is supplied by Renesas Electronics Corporation and is only + * intended for use with Renesas products. No other uses are authorized. This + * software is owned by Renesas Electronics Corporation and is protected under + * all applicable laws, including copyright laws. + * THIS SOFTWARE IS PROVIDED "AS IS" AND RENESAS MAKES NO WARRANTIES REGARDING + * THIS SOFTWARE, WHETHER EXPRESS, IMPLIED OR STATUTORY, INCLUDING BUT NOT + * LIMITED TO WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE + * AND NON-INFRINGEMENT. ALL SUCH WARRANTIES ARE EXPRESSLY DISCLAIMED. + * TO THE MAXIMUM EXTENT PERMITTED NOT PROHIBITED BY LAW, NEITHER RENESAS + * ELECTRONICS CORPORATION NOR ANY OF ITS AFFILIATED COMPANIES SHALL BE LIABLE + * FOR ANY DIRECT, INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES FOR + * ANY REASON RELATED TO THIS SOFTWARE, EVEN IF RENESAS OR ITS AFFILIATES HAVE + * BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + * Renesas reserves the right, without notice, to make changes to this + * software and to discontinue the availability of this software. By using this + * software, you agree to the additional terms and conditions found by + * accessing the following link: + * http://www.renesas.com/disclaimer + * + * Copyright (C) 2025 Renesas Electronics Corporation. All rights reserved. + *****************************************************************************/ + +/** + * @file osal_renesas_wrapper.h + * @brief API of the Renesas wrapper around the CsmpAgentLib implementation by Cisco so that it may be used within the + * Renesas Wi-SUN FAN sample application. + */ + +#ifndef OSAL_RENESAS_WRAPPER_H +#define OSAL_RENESAS_WRAPPER_H + +/** + * Start the CSMP module. Several periodic timers will be started. + * @param serverAddr The IPv6 address of the server (Cisco IoT Field Network Director). + * @param localEui64 The EUI-64 of this device. + * @param regImin The minimum registration interval of this device. + * @param regImax The maximum registration interval of this device. + * @return 0 if the CSMP module was successfully started. + */ +int R_CSMP_Start(const r_ipv6addr_t* serverAddr, const uint8_t* localEui64, uint32_t regImin, uint32_t regImax); + +/** + * Stop the CSMP module and related timers. + * @return 0 if the CSMP module was successfully stopped. + */ +int R_CSMP_Stop(); +#endif /* OSAL_RENESAS_WRAPPER_H */ diff --git a/sample/tlvs/renesas_tlvs.c b/sample/tlvs/renesas_tlvs.c new file mode 100644 index 0000000..6c6476c --- /dev/null +++ b/sample/tlvs/renesas_tlvs.c @@ -0,0 +1,1708 @@ +/****************************************************************************** + * DISCLAIMER + * This software is supplied by Renesas Electronics Corporation and is only + * intended for use with Renesas products. No other uses are authorized. This + * software is owned by Renesas Electronics Corporation and is protected under + * all applicable laws, including copyright laws. + * THIS SOFTWARE IS PROVIDED "AS IS" AND RENESAS MAKES NO WARRANTIES REGARDING + * THIS SOFTWARE, WHETHER EXPRESS, IMPLIED OR STATUTORY, INCLUDING BUT NOT + * LIMITED TO WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE + * AND NON-INFRINGEMENT. ALL SUCH WARRANTIES ARE EXPRESSLY DISCLAIMED. + * TO THE MAXIMUM EXTENT PERMITTED NOT PROHIBITED BY LAW, NEITHER RENESAS + * ELECTRONICS CORPORATION NOR ANY OF ITS AFFILIATED COMPANIES SHALL BE LIABLE + * FOR ANY DIRECT, INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES FOR + * ANY REASON RELATED TO THIS SOFTWARE, EVEN IF RENESAS OR ITS AFFILIATES HAVE + * BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + * Renesas reserves the right, without notice, to make changes to this + * software and to discontinue the availability of this software. By using this + * software, you agree to the additional terms and conditions found by + * accessing the following link: + * http://www.renesas.com/disclaimer + * + * Copyright (C) 2026 Renesas Electronics Corporation. All rights reserved. + *****************************************************************************/ + +/** + * @file renesas_wisun_tlvs.c + * @brief Renesas implementation of the individual CSMP get and post TLV requests. + */ + + +#include +#include + +#include "r_nwk_api.h" +#include "r_impl_utils.h" +#include "r_mem_tools.h" +#include "r_app_config.h" + +#include "osal_renesas_config.h" + +#include "csmp_service.h" +#include "csmp_info.h" + +#include "signature_verify.h" +#if R_FWUP_SUPPORT +#include "r_flash_wrapper.h" +#include "r_fwup_wrapper.h" +#if !defined(__i386) && !defined(__x86_64) +#include "r_fwup_private.h" +#include "r_fwup_wrap_flash.h" +#include "r_fwup_config.h" +#endif +#endif + +// R_LOG_PREFIX is processed by RLog code generator +#define R_LOG_PREFIX CSMP +#include "r_log_internal.h" +#if R_LOG_THRESHOLD > R_LOG_SEVERITY_OFF +#include "r_loggen_renesas_wisun_tlvs.h" +#endif + +/** The hardware information */ +Hardware_Desc g_tlv_hardwareDesc = HARDWARE_DESC_INIT; + +/** The interface information */ +Interface_Desc g_tlv_interfaceDesc = INTERFACE_DESC_INIT; + +/** The ipaddress information */ +IP_Address g_tlv_ipAddress[R_TLV_IPADDRESS_MAX_NUM] = {IPADDRESS_INIT}; + +/** The ip route information */ +IP_Route g_tlv_ipRoute[R_TLV_ROUTES_MAX_NUM] = {IPROUTE_INIT}; + +/** The current time information */ +Current_Time g_tlv_currentTime = CURRENT_TIME_INIT; + +/** The up time information */ +Up_Time g_tlv_upTime = UPTIME_INIT; + +/** The interface metrics data */ +Interface_Metrics g_tlv_interfaceMetrics = INTERFACE_METRICS_INIT; + +/** The ip route rpl metrics data */ +IPRoute_RPLMetrics g_tlv_iprouteRplmetrics[R_TLV_ROUTES_MAX_NUM] = {IPROUTE_RPLMETRICS_INIT}; + +/** The wpan status data */ +WPAN_Status g_tlv_wpanStatus = WPANSTATUS_INIT; + +/** The rpl data */ +RPL_Instance g_tlv_rplInstance = RPLINSTANCE_INIT; + +/** The signature settings data */ +Signature_Settings g_SignatureSettings = SIGNATURE_SETTINGS_INIT; + +/** The transfer request data */ +Transfer_Request g_transferRequest = TRANSFER_REQUEST_INIT; + +/** The image block data */ +Image_Block g_imageBlock = IMAGE_BLOCK_INIT; + +/** The load request data */ +Load_Request g_loadRequest = LOAD_REQUEST_INIT; + +/** The cancel load request data */ +Cancel_Load_Request g_cancelLoadRequest = CANCEL_LOAD_REQUEST_INIT; + +/** The firmware info data */ +Firmware_Image_Info g_firmwareImageInfo[CSMP_FWMGMT_ACTIVE_SLOTS] = {FIRMWARE_IMAGE_INFO_INIT}; + +bool g_downloadbusy = false; // Track ongoing download + +#if R_FWUP_SUPPORT +static const char baseHwId[] = R_CSMP_HARDWARE_ID; +static const char baseFileName[] = R_CSMP_FILE_NAME; +static const char baseCsmpVersion[] = R_CSMP_VERSION_STRING; +static const char baseLibraryVersion[] = R_NWK_BASE_VERSION_STRING; +static bool initxfer = false; // Track transfer request +static bool initload = false; // Track load request +static uint32_t curloadtime = 0; // Track current loadtime + +static r_boolean_t uploadSlotValid = R_FALSE; +extern r_fwup_state_t fwupState; +/** The information about the firmware image being uploaded */ +static Csmp_Slothdr uploadSlotHdr = CSMP_SLOTHDR_INIT; + +/** The file hash is either read from non-volatile memory or from the running firmware header */ +static uint8_t runImageFileHash[SHA256_HASH_SIZE] = { 0 }; +/** The file name is either read from non-volatile memory or computed from the app name and the file hash */ +static char runImageFileName[FILE_NAME_SIZE] = ""; +/** + * The file size is read from non-volatile memory. It is valid only after switching to an image transferred by FND; + * otherwise it should be 0. + */ +static uint32_t runImageFileSize = 0; +#endif + +extern const uint8_t* AppIpGetMacAddr(void); + +typedef union +{ + r_nwk_nd_cache_t nd_cache; + uint8_t placeholder[sizeof(r_nwk_nd_cache_t) + R_NWK_MAX_NEIGHBORS_PER_CONFIRM * sizeof(r_nwk_nd_cache_entry_t)]; +} nd_cache_buffer_t; + +/** public key */ +static const char pubkey[PUBLIC_KEY_LEN] = { + 0x04, 0x23, 0xD2, 0x83, 0x45, 0xE8, 0xD5, 0xDF, 0x86, 0x9D, + 0x6E, 0xE7, 0x58, 0x0D, 0xC1, 0x8F, 0x35, 0x9D, 0x57, 0xB1, + 0x3D, 0x50, 0x4A, 0x16, 0x01, 0x15, 0xC4, 0x81, 0x19, 0xB0, + 0xE6, 0x60, 0xB8, 0x64, 0x14, 0x01, 0x5D, 0x56, 0x83, 0xBE, + 0xE1, 0x85, 0x98, 0xCB, 0x90, 0xE1, 0xF7, 0x9B, 0xF4, 0x33, + 0x5A, 0x4B, 0x29, 0xAD, 0x35, 0x69, 0x9B, 0x4F, 0xDC, 0x42, + 0x7F, 0xEB, 0xC2, 0x99, 0xA5 +}; + +void pubkey_get(const char** key, size_t len) +{ + if (key && (len == sizeof(pubkey))) + { + *key = pubkey; + } +} + +void* hardware_desc_get(uint32_t* num) +{ + *num = 1; + MEMZERO_S(&g_tlv_hardwareDesc); + + g_tlv_hardwareDesc.has_entphysicalindex = true; + g_tlv_hardwareDesc.has_entphysicaldescr = true; + g_tlv_hardwareDesc.has_entphysicalclass = true; + g_tlv_hardwareDesc.has_entphysicalname = true; + g_tlv_hardwareDesc.has_entphysicalhardwarerev = true; + g_tlv_hardwareDesc.has_entphysicalfirmwarerev = true; + g_tlv_hardwareDesc.has_entphysicalsoftwarerev = true; + g_tlv_hardwareDesc.has_entphysicalserialnum = true; + g_tlv_hardwareDesc.has_entphysicalmfgname = true; + g_tlv_hardwareDesc.has_entphysicalmodelname = true; + g_tlv_hardwareDesc.has_entphysicalmfgdate = true; + g_tlv_hardwareDesc.has_entphysicalfunction = true; + g_tlv_hardwareDesc.has_entphysicaloui = true; + + g_tlv_hardwareDesc.entphysicalindex = 1; + sprintf(g_tlv_hardwareDesc.entphysicaldescr, "Renesas Wi-SUN FAN CSMP Agent"); + g_tlv_hardwareDesc.entphysicalclass = CLASS_MODULE; + sprintf(g_tlv_hardwareDesc.entphysicalname, "lowpan"); // Other values ("Wi-SUN FAN Node") seem to conflict with the server + sprintf(g_tlv_hardwareDesc.entphysicalhardwarerev, "1.0"); + sprintf(g_tlv_hardwareDesc.entphysicalfirmwarerev, R_CSMP_VERSION_STRING); + const char* appName = R_APP_GetAppName(); + strncpy(g_tlv_hardwareDesc.entphysicalsoftwarerev, appName, sizeof(g_tlv_hardwareDesc.entphysicalsoftwarerev)); + g_tlv_hardwareDesc.entphysicalsoftwarerev[sizeof(g_tlv_hardwareDesc.entphysicalsoftwarerev) - 1] = '\0'; + const uint8_t* p_macAddr = AppIpGetMacAddr(); + snprintf(g_tlv_hardwareDesc.entphysicalserialnum, sizeof(g_tlv_hardwareDesc.entphysicalserialnum), + "%02X%02X%02X%02X%02X%02X%02X%02X", + p_macAddr[0], p_macAddr[1], p_macAddr[2], p_macAddr[3], + p_macAddr[4], p_macAddr[5], p_macAddr[6], p_macAddr[7]); + snprintf(g_tlv_hardwareDesc.entphysicaloui, sizeof(g_tlv_hardwareDesc.entphysicaloui), + "%02X%02X%02X", + p_macAddr[0], p_macAddr[1], p_macAddr[2]); + sprintf(g_tlv_hardwareDesc.entphysicalmfgname, "Renesas Electronics"); + const char* hardwareName = R_APP_GetHardwareName(); + strncpy(g_tlv_hardwareDesc.entphysicalmodelname, hardwareName, sizeof(g_tlv_hardwareDesc.entphysicalmodelname)); + g_tlv_hardwareDesc.entphysicalmodelname[sizeof(g_tlv_hardwareDesc.entphysicalmodelname) - 1] = '\0'; + g_tlv_hardwareDesc.entphysicalmfgdate = R_APP_RELEASE_DATE; + g_tlv_hardwareDesc.entphysicalfunction = 1; + + return &g_tlv_hardwareDesc; +} + +void* interface_desc_get(uint32_t* num) +{ + *num = 1; + MEMZERO_S(&g_tlv_interfaceDesc); + + g_tlv_interfaceDesc.has_ifindex = true; + g_tlv_interfaceDesc.has_ifname = true; + g_tlv_interfaceDesc.has_ifdescr = true; + g_tlv_interfaceDesc.has_iftype = true; + g_tlv_interfaceDesc.has_ifmtu = true; + g_tlv_interfaceDesc.has_ifphysaddress = true; + + g_tlv_interfaceDesc.ifindex = 1; + sprintf(g_tlv_interfaceDesc.ifname, "lowpan"); + sprintf(g_tlv_interfaceDesc.ifdescr, "ieee802154"); + g_tlv_interfaceDesc.iftype = 259; + g_tlv_interfaceDesc.ifmtu = R_MAX_MTU_SIZE; + const uint8_t* p_macAddr = AppIpGetMacAddr(); + g_tlv_interfaceDesc.ifphysaddress.len = 8; + MEMCPY_A(g_tlv_interfaceDesc.ifphysaddress.data, p_macAddr); + + return &g_tlv_interfaceDesc; +} + +void* ipaddress_get(uint32_t* num) +{ + uint32_t i = 0; + uint8_t ipv6Addr[R_IPV6_ADDRESS_LENGTH]; + const uint8_t* p_macAddr = AppIpGetMacAddr(); + MEMZERO_A(g_tlv_ipAddress); + + g_tlv_ipAddress[i].ipaddressaddrtype = IPV6; + g_tlv_ipAddress[i].ipaddressaddr.len = R_IPV6_ADDRESS_LENGTH; + g_tlv_ipAddress[i].ipaddressaddr.data[0] = 0xfe; + g_tlv_ipAddress[i].ipaddressaddr.data[1] = 0x80; + R_memcpy(g_tlv_ipAddress[i].ipaddressaddr.data + 8, p_macAddr, 8); + g_tlv_ipAddress[i].ipaddressaddr.data[8] ^= 0x02; + g_tlv_ipAddress[i].ipaddressifindex = 1; + g_tlv_ipAddress[i].ipaddresstype = UNICAST; + g_tlv_ipAddress[i].ipaddressorigin = LINKLAYER; + g_tlv_ipAddress[i].ipaddresspfxlen = R_IPV6_PREFIX_LEN * 8u; + g_tlv_ipAddress[i].ipaddressindex = (int32_t)i + 1; + i++; + + if (R_NWK_GetRequest(R_NWK_nwkIpv6Address, ipv6Addr, sizeof(ipv6Addr)) == R_RESULT_SUCCESS + && MEMISNOTZERO_S(&ipv6Addr)) + { + g_tlv_ipAddress[i].ipaddressaddrtype = IPV6; + g_tlv_ipAddress[i].ipaddressaddr.len = R_IPV6_ADDRESS_LENGTH; + MEMCPY_A(g_tlv_ipAddress[i].ipaddressaddr.data, ipv6Addr); + g_tlv_ipAddress[i].ipaddressifindex = 1; + g_tlv_ipAddress[i].ipaddresstype = UNICAST; + g_tlv_ipAddress[i].ipaddressorigin = DHCP; + g_tlv_ipAddress[i].ipaddresspfxlen = R_IPV6_PREFIX_LEN * 8u; + g_tlv_ipAddress[i].ipaddressindex = (int32_t)i + 1; + i++; + } + *num = i; + for (i = 0; i < *num; i++) + { + g_tlv_ipAddress[i].has_ipaddressindex = true; + g_tlv_ipAddress[i].has_ipaddressaddrtype = true; + g_tlv_ipAddress[i].has_ipaddressaddr = true; + g_tlv_ipAddress[i].has_ipaddressifindex = true; + g_tlv_ipAddress[i].has_ipaddresstype = true; + g_tlv_ipAddress[i].has_ipaddressorigin = true; + g_tlv_ipAddress[i].has_ipaddresspfxlen = true; + } + + if (*num == 0) + { + return NULL; + } + + return &g_tlv_ipAddress; +} + +void* iproute_get(uint32_t* num) +{ + uint32_t i = 0; + r_ipv6addr_t apAddr; + MEMZERO_A(g_tlv_ipRoute); + +#if R_BORDER_ROUTER_ENABLED + uint8_t deviceType; + if (R_NWK_GetRequest(R_NWK_deviceType, &deviceType, sizeof(deviceType)) == R_RESULT_SUCCESS && + deviceType != R_BORDERROUTER) +#endif + { + if (R_NWK_GetRequest(R_NWK_preferredParentAddress, &apAddr, sizeof(apAddr)) == R_RESULT_SUCCESS + && MEMISNOTZERO_S(&apAddr)) + { + g_tlv_ipRoute[i].inetcidrroutedesttype = IPV6; + g_tlv_ipRoute[i].inetcidrroutedest.len = R_IPV6_ADDRESS_LENGTH; + MEMCPY_A(g_tlv_ipRoute[i].inetcidrroutedest.data, apAddr.bytes); + g_tlv_ipRoute[i].inetcidrroutepfxlen = R_IPV6_PREFIX_LEN * 8u; + g_tlv_ipRoute[i].inetcidrroutenexthoptype = IPV6; + g_tlv_ipRoute[i].inetcidrroutenexthop.len = R_IPV6_ADDRESS_LENGTH; + MEMCPY_A(g_tlv_ipRoute[i].inetcidrroutenexthop.data, apAddr.bytes); + g_tlv_ipRoute[i].inetcidrrouteifindex = 1; + g_tlv_ipRoute[i].inetcidrrouteindex = (int32_t)i + 1; + i++; + } + if (R_NWK_GetRequest(R_NWK_alternateParentAddress, &apAddr, sizeof(apAddr)) == R_RESULT_SUCCESS + && MEMISNOTZERO_S(&apAddr)) + { + g_tlv_ipRoute[i].inetcidrroutedesttype = IPV6; + g_tlv_ipRoute[i].inetcidrroutedest.len = R_IPV6_ADDRESS_LENGTH; + MEMCPY_A(g_tlv_ipRoute[i].inetcidrroutedest.data, apAddr.bytes); + g_tlv_ipRoute[i].inetcidrroutepfxlen = R_IPV6_PREFIX_LEN * 8u; + g_tlv_ipRoute[i].inetcidrroutenexthoptype = IPV6; + g_tlv_ipRoute[i].inetcidrroutenexthop.len = R_IPV6_ADDRESS_LENGTH; + MEMCPY_A(g_tlv_ipRoute[i].inetcidrroutenexthop.data, apAddr.bytes); + g_tlv_ipRoute[i].inetcidrrouteifindex = 1; + g_tlv_ipRoute[i].inetcidrrouteindex = (int32_t)i + 1; + i++; + } + } + + *num = i; + for (i = 0; i < *num; i++) + { + g_tlv_ipRoute[i].has_inetcidrrouteindex = true; + g_tlv_ipRoute[i].has_inetcidrroutedesttype = true; + g_tlv_ipRoute[i].has_inetcidrroutedest = true; + g_tlv_ipRoute[i].has_inetcidrroutepfxlen = true; + g_tlv_ipRoute[i].has_inetcidrroutenexthoptype = true; + g_tlv_ipRoute[i].has_inetcidrroutenexthop = true; + g_tlv_ipRoute[i].has_inetcidrrouteifindex = true; + } + + if (*num == 0) + { + return NULL; + } + + return &g_tlv_ipRoute; +} + +void* currenttime_get(uint32_t* num) +{ + struct timeval tv = {0}; + + *num = 1; + MEMZERO_S(&g_tlv_currentTime); + + g_tlv_currentTime.has_posix = true; + g_tlv_currentTime.has_source = true; + + osal_gettime(&tv, NULL); + g_tlv_currentTime.posix = tv.tv_sec; + g_tlv_currentTime.source = 1; // Local + + return &g_tlv_currentTime; +} + +/** + * @brief Parse exactly @p n decimal digits from a character buffer. + * + * @param s Pointer to the input character buffer containing at least @p n characters. + * @param n Number of digits to parse. + * @param parsed The parsed numeric value. + * + * @return r_result_t R_RESULT_SUCCESS on success, R_RESULT_FAILED on parse error. + */ +static r_result_t parse_ndigits(const char *s, uint8_t n, uint32_t *parsed) +{ + *parsed = 0; + for (int i = 0; i < n; ++i) + { + unsigned char c = (unsigned char)s[i]; + if (c < '0' || c > '9') + { + return R_RESULT_FAILED; + } + *parsed = *parsed * 10 + (c - '0'); + } + return R_RESULT_SUCCESS; +} + +/** + * @brief Determine whether a given year is a leap year. + * + * @param year Year to evaluate. + * + * @return r_boolean_t TRUE if @p year is a leap year, FALSE otherwise. + */ +static r_boolean_t is_leap(uint16_t year) +{ + return (year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)); +} + +/** + * @brief Get the number of days in a given month of a specific year. + * + * @param year Year used to determine leap-year behavior. + * @param month Month index (1–12) for which the day count is requested. + * + * @return uint8_t Number of days in the specified month. + */ +static uint8_t days_in_month(const uint16_t year, const uint8_t month) +{ + static const uint8_t base_days[12] = { + 31, 28, 31, 30, 31, 30, + 31, 31, 30, 31, 30, 31 + }; + uint8_t d = base_days[month - 1]; + if (month == 2 && is_leap(year)) + { + d = 29; + } + return d; +} + +/** + * @brief Compute the number of days since the Unix epoch (1970-01-01). + * + * @param year Year of the input date. + * @param month Month (1–12) of the input date. + * @param day Day (1–31) of the input date. + * @param days_since_epoch Pointer receiving the computed day count. + * + * @return r_result_t R_RESULT_SUCCESS on success, R_RESULT_FAILED on validation error. + */ +static r_result_t days_since_epoch(const uint16_t year, const uint8_t month, const uint8_t day, uint32_t *days_since_epoch) +{ + *days_since_epoch = 0; + + if (year >= 1970) + { + uint16_t y; + for (y = 1970; y < year; ++y) + { + *days_since_epoch += 365 + is_leap(y); + } + } + else + { + return R_RESULT_FAILED; + } + + for (uint8_t m = 1; m < month; ++m) + { + *days_since_epoch += days_in_month(year, m); + } + + *days_since_epoch += (day - 1); // 1970-01-01 -> day index 0 + + return R_RESULT_SUCCESS; +} + +/** + * @brief Parse an ISO 8601 timestamp string and convert it to a Unix timestamp. + * + * Supported formats: + * - "YYYY-MM-DDTHH:MM:SSZ" + * - "YYYY-MM-DDTHH:MM:SS+HH:MM" + * - "YYYY-MM-DDTHH:MM:SS-HH:MM" + * - "YYYY-MM-DD HH:MM:SSZ" (space instead of 'T') + * + * @param from Pointer to a zero-terminated ISO 8601 input string. + * @param to Pointer receiving the computed Unix timestamp in seconds. + * + * @return r_result_t R_RESULT_SUCCESS on success, R_RESULT_FAILED on parse or validation error. + */ +r_result_t iso8601_to_unix(const char *from, uint32_t *to) +{ + if (from == NULL) + { + return R_RESULT_FAILED; + } + + const size_t len = strlen(from); + if (len < 19) + { + // Minimal length: "YYYY-MM-DDTHH:MM:SS" + return R_RESULT_FAILED; + } + + // Parse date/time components + uint32_t year, month, day, hour, min, sec; + r_result_t result = parse_ndigits(from + 0, 4, &year); + result |= parse_ndigits(from + 5, 2, &month); + result |= parse_ndigits(from + 8, 2, &day); + const char sep1 = from[4]; // '-' + const char sep2 = from[7]; // '-' + const char sep3 = from[10]; // 'T' or ' ' + result |= parse_ndigits(from + 11, 2, &hour); + const char sep4 = from[13]; // ':' + result |= parse_ndigits(from + 14, 2, &min); + const char sep5 = from[16]; // ':' + result |= parse_ndigits(from + 17, 2, &sec); + + if (result != R_RESULT_SUCCESS) + { + return result; + } + + if (month < 1 || month > 12 || day < 1 || day > 31 || + hour > 23 || min > 59 || sec > 60) + { + return R_RESULT_FAILED; + } + + if (sep1 != '-' || sep2 != '-' || + (sep3 != 'T' && sep3 != ' ') || + sep4 != ':' || sep5 != ':') + { + return R_RESULT_FAILED; + } + + // Validate day against month/year + if (day > days_in_month(year, month)) + { + return R_RESULT_FAILED; + } + + // Timezone offset in seconds (relative to UTC) + int32_t offset_seconds = 0; + + // Position after "YYYY-MM-DDTHH:MM:SS" (19 chars) + const uint8_t pos = 19; + if (pos < len) + { + const char c = from[pos]; + if (c == 'Z' || c == 'z') + { + // UTC, offset 0 + offset_seconds = 0; + } + else if (c == '+' || c == '-') + { + // +HH:MM or -HH:MM + if ((uint16_t) len < pos + 6) + { + return R_RESULT_FAILED; // not enough room for "+HH:MM" + } + const int32_t sign = (c == '+') ? 1 : -1; + + uint32_t off_hour, off_min; + result = parse_ndigits(from + pos + 1, 2, &off_hour); + const char colon = from[pos + 3]; + result |= parse_ndigits(from + pos + 4, 2, &off_min); + + if (result != R_RESULT_SUCCESS || + off_hour > 23 || off_min > 59 || + colon != ':') + { + return R_RESULT_FAILED; + } + + offset_seconds = sign * (int32_t) (off_hour * 3600 + off_min * 60); + } + else + { + // Unexpected trailing character + return R_RESULT_FAILED; + } + } + + // Compute days since epoch and then total seconds + uint32_t days; + result = days_since_epoch(year, month, day, &days); + + if (result != R_RESULT_SUCCESS) + { + return result; + } + + *to = days * 86400 + hour * 3600 + min * 60 + sec; + + /* + * Interpretation of timezone: "2025-11-15T10:30:00+02:00" + * means local_time = UTC + 2h => UTC = local_time - 2h + * + * We have parsed the *local* time (10:30). + * So we must subtract the offset to get UTC. + */ + *to -= offset_seconds; + + return R_RESULT_SUCCESS; +} + +void currenttime_post(Current_Time* tlv) +{ + struct timeval tv = {0}; + + if (tlv->has_posix) + { + tv.tv_sec = tlv->posix; + osal_settime(&tv, NULL); + } + else if (tlv->has_iso8601) + { + uint32_t posix_seconds = 0; + iso8601_to_unix(tlv->iso8601, &posix_seconds); + tv.tv_sec = posix_seconds; + osal_settime(&tv, NULL); + } +} + +void* uptime_get(uint32_t* num) +{ + *num = 1; + MEMZERO_S(&g_tlv_upTime); + + g_tlv_upTime.has_sysuptime = true; + + g_tlv_upTime.sysuptime = clock_seconds(); + + return &g_tlv_upTime; +} + +void* interface_metrics_get(uint32_t* num) +{ + uint16_t phy_DataRate; + *num = 1; + MEMZERO_S(&g_tlv_interfaceMetrics); + + g_tlv_interfaceMetrics.has_ifindex = true; + g_tlv_interfaceMetrics.has_ifinspeed = true; + g_tlv_interfaceMetrics.has_ifoutspeed = true; + g_tlv_interfaceMetrics.has_ifadminstatus = true; + g_tlv_interfaceMetrics.has_ifoperstatus = true; + g_tlv_interfaceMetrics.has_iflastchange = true; + g_tlv_interfaceMetrics.has_ifinoctets = true; + g_tlv_interfaceMetrics.has_ifoutoctets = true; + g_tlv_interfaceMetrics.has_ifindiscards = true; + g_tlv_interfaceMetrics.has_ifinerrors = true; + g_tlv_interfaceMetrics.has_ifoutdiscards = true; + g_tlv_interfaceMetrics.has_ifouterrors = true; + + g_tlv_interfaceMetrics.ifindex = 1; + if (R_NWK_GetRequest(R_NWK_phyDataRate, &phy_DataRate, sizeof(phy_DataRate)) == R_RESULT_SUCCESS) + { + g_tlv_interfaceMetrics.ifinspeed = phy_DataRate; + g_tlv_interfaceMetrics.ifoutspeed = phy_DataRate; + } + else + { + g_tlv_interfaceMetrics.has_ifinspeed = false; + g_tlv_interfaceMetrics.has_ifoutspeed = false; + } + g_tlv_interfaceMetrics.ifadminstatus = IF_ADMIN_STATUS_UP; + g_tlv_interfaceMetrics.ifoperstatus = IF_OPER_STATUS_UP; + g_tlv_interfaceMetrics.iflastchange = clock_seconds(); + r_nwk_if_mib_t mgmt; + if (R_NWK_GetRequest(R_NWK_fanInterfaceMgmtInfo, &mgmt, sizeof(mgmt)) == R_RESULT_SUCCESS) + { + g_tlv_interfaceMetrics.ifinoctets = mgmt.stats.ifinoctets; + g_tlv_interfaceMetrics.ifoutoctets = mgmt.stats.ifoutoctets; + g_tlv_interfaceMetrics.ifindiscards = mgmt.stats.ifindiscards + mgmt.stats.ifinunknownprotos; + g_tlv_interfaceMetrics.ifinerrors = mgmt.stats.ifinerrors; + g_tlv_interfaceMetrics.ifoutdiscards = mgmt.stats.ifoutdiscards; + g_tlv_interfaceMetrics.ifouterrors = mgmt.stats.ifouterrors; + } + else + { + g_tlv_interfaceMetrics.has_ifinoctets = false; + g_tlv_interfaceMetrics.has_ifoutoctets = false; + g_tlv_interfaceMetrics.has_ifindiscards = false; + g_tlv_interfaceMetrics.has_ifinerrors = false; + g_tlv_interfaceMetrics.has_ifoutdiscards = false; + g_tlv_interfaceMetrics.has_ifouterrors = false; + } + + return &g_tlv_interfaceMetrics; +} + +/** + * Search in the neighbor cache for a given address and fills the RPL metric information + * @param index The index of the metric information in the reported TLV + * @param apAddr The address for which the metrics needs to be retrieved + * @param metric The metric structure to be filled + * @return R_RESULT_SUCCESS if the address was found, R_RESULT_FAILED otherwise + */ +r_result_t fillIpRouteRplMetric(uint32_t index, r_ipv6addr_t* apAddr, IPRoute_RPLMetrics* metric) +{ + r_result_t res; + r_boolean_t isContinuation = R_FALSE; + do + { + nd_cache_buffer_t cfm; + res = R_NWK_GetRequestMultipart(R_NWK_ndCache, &cfm, sizeof(cfm), isContinuation); + if (res == R_RESULT_SUCCESS || res == R_RESULT_SUCCESS_ADDITIONAL_DATA) + { + isContinuation = R_TRUE; + + for (uint16_t i = 0; i < cfm.nd_cache.count; i++) + { + r_boolean_t check = R_TRUE; + for (uint16_t j = 0; j < sizeof(cfm.nd_cache.entries[i].ipAddress.bytes); j++) + { + if (cfm.nd_cache.entries[i].ipAddress.bytes[j] != apAddr->bytes[j]) + { + check = R_FALSE; + break; + } + } + if (check == R_TRUE) + { + metric->inetcidrrouteindex = (int32_t)index + 1; + metric->instanceindex = 1; + metric->rank = cfm.nd_cache.entries[i].rank; + metric->pathetx = cfm.nd_cache.entries[i].routingCost; + metric->linketx = cfm.nd_cache.entries[i].linkMetric; + metric->lqiforward = cfm.nd_cache.entries[i].nodeToNeighRsl; + metric->lqireverse = cfm.nd_cache.entries[i].neighToNodeRsl; + metric->rssiforward = -174 + cfm.nd_cache.entries[i].nodeToNeighRsl; + metric->rssireverse = -174 + cfm.nd_cache.entries[i].neighToNodeRsl; + metric->dagsize = cfm.nd_cache.entries[i].panSize; + return R_RESULT_SUCCESS; + } + } + } + else + { + return R_RESULT_FAILED; + } + } + while (res == R_RESULT_SUCCESS_ADDITIONAL_DATA); + return R_RESULT_FAILED; +} + +void* iproute_rplmetrics_get(uint32_t* num) +{ + uint32_t index = 0; + r_ipv6addr_t apAddr; + MEMZERO_A(g_tlv_iprouteRplmetrics); + +#if R_BORDER_ROUTER_ENABLED + uint8_t deviceType; + if (R_NWK_GetRequest(R_NWK_deviceType, &deviceType, sizeof(deviceType)) == R_RESULT_SUCCESS && + deviceType != R_BORDERROUTER) +#endif + { + if (R_NWK_GetRequest(R_NWK_preferredParentAddress, &apAddr, sizeof(apAddr)) == R_RESULT_SUCCESS + && MEMISNOTZERO_S(&apAddr)) + { + if (fillIpRouteRplMetric(index, &apAddr, &g_tlv_iprouteRplmetrics[index]) == R_RESULT_SUCCESS) + { + index++; + } + } + + if (R_NWK_GetRequest(R_NWK_alternateParentAddress, &apAddr, sizeof(apAddr)) == R_RESULT_SUCCESS + && MEMISNOTZERO_S(&apAddr)) + { + if (fillIpRouteRplMetric(index, &apAddr, &g_tlv_iprouteRplmetrics[index]) == R_RESULT_SUCCESS) + { + index++; + } + } + } + + *num = index; + for (uint32_t i = 0; i < *num; i++) + { + g_tlv_iprouteRplmetrics[i].has_inetcidrrouteindex = true; + g_tlv_iprouteRplmetrics[i].has_instanceindex = true; + g_tlv_iprouteRplmetrics[i].has_rank = true; + g_tlv_iprouteRplmetrics[i].has_pathetx = true; + g_tlv_iprouteRplmetrics[i].has_linketx = true; + g_tlv_iprouteRplmetrics[i].has_rssiforward = true; + g_tlv_iprouteRplmetrics[i].has_rssireverse = true; + g_tlv_iprouteRplmetrics[i].has_lqiforward = true; + g_tlv_iprouteRplmetrics[i].has_lqireverse = true; + g_tlv_iprouteRplmetrics[i].has_dagsize = true; + } + + if (*num == 0) + { + return NULL; + } + + return &g_tlv_iprouteRplmetrics; +} + +void* wpanstatus_get(uint32_t* num) +{ + *num = 1; + MEMZERO_S(&g_tlv_wpanStatus); + + g_tlv_wpanStatus.has_ifindex = true; + g_tlv_wpanStatus.has_ssid = true; + g_tlv_wpanStatus.has_panid = true; + g_tlv_wpanStatus.has_dot1xenabled = true; + g_tlv_wpanStatus.has_securitylevel = true; + g_tlv_wpanStatus.has_rank = true; + g_tlv_wpanStatus.has_beaconvalid = true; + g_tlv_wpanStatus.has_beaconversion = true; + g_tlv_wpanStatus.has_beaconage = true; + g_tlv_wpanStatus.has_txpower = true; + g_tlv_wpanStatus.has_dagsize = true; + g_tlv_wpanStatus.has_metric = true; + g_tlv_wpanStatus.has_lastchanged = true; + g_tlv_wpanStatus.has_lastchangedreason = true; + g_tlv_wpanStatus.has_demomodeenabled = false; + + g_tlv_wpanStatus.ifindex = 1; + const char* netName = R_APP_GetNetworkName(); + strncpy((char *) g_tlv_wpanStatus.ssid.data, netName, sizeof(g_tlv_wpanStatus.ssid.data)); + g_tlv_wpanStatus.ssid.data[sizeof(g_tlv_wpanStatus.ssid.data) - 1] = '\0'; + g_tlv_wpanStatus.ssid.len = strlen(netName) + 1; + uint16_t panId; + if (R_NWK_GetRequest(R_NWK_macPANId, &panId, sizeof(panId)) == R_RESULT_SUCCESS) + { + g_tlv_wpanStatus.panid = panId; + } + else + { + g_tlv_wpanStatus.has_panid = false; + } + g_tlv_wpanStatus.dot1xenabled = true; + g_tlv_wpanStatus.securitylevel = IEEE154_SEC_MIC_32; +#if !R_LEAF_NODE_ENABLED + r_nwk_rpl_info_t rplInfo; + if (R_NWK_GetRequest(R_NWK_rplInfo, &rplInfo, sizeof(rplInfo)) == R_RESULT_SUCCESS) + { + g_tlv_wpanStatus.rank = rplInfo.rank; + } + else +#endif + { + g_tlv_wpanStatus.has_rank = false; + } +#if !R_LEAF_NODE_ENABLED + uint16_t panVersion; + if (R_NWK_GetRequest(R_NWK_panVersion, &panVersion, sizeof(panVersion)) == R_RESULT_SUCCESS) + { + g_tlv_wpanStatus.beaconversion = panVersion; + } + else +#endif + { + g_tlv_wpanStatus.has_beaconversion = false; + } + g_tlv_wpanStatus.beaconvalid = true; + clock_time_t beaconAge; + if (R_NWK_GetRequest(R_NWK_lastPanConfigRecvTime, &beaconAge, sizeof(beaconAge)) == R_RESULT_SUCCESS) + { + g_tlv_wpanStatus.beaconage = clock_seconds() - beaconAge; + } + else + { + g_tlv_wpanStatus.has_beaconage = false; + } + + int32_t csmpTxPower = INT32_MAX; +#if R_PHY_TYPE_CWX_M + int8_t cwxTxPower; + if (R_NWK_GetRequest(R_NWK_phyFskTransmitPower, &cwxTxPower, sizeof(cwxTxPower)) == R_RESULT_SUCCESS) + { + /* Convert CWX value to actual TX power output at antenna connector in dBm */ + csmpTxPower = cwxTxPower / 2; // CWX value is in units of 0.5 dBm + csmpTxPower = csmpTxPower + 22; // 20-24 dB FEM gain depending on CWX board type -> Add avg FEM gain + } +#else + uint8_t trgTxPower; + if (R_NWK_GetRequest(R_NWK_phyTransmitPower, &trgTxPower, sizeof(trgTxPower)) == R_RESULT_SUCCESS) + { + csmpTxPower = trgTxPower; // TODO convert TRG value to dBm (requires mapping table depending on region) + } +#endif + if (csmpTxPower != INT32_MAX) + { + g_tlv_wpanStatus.txpower = csmpTxPower; + } + else + { + g_tlv_wpanStatus.has_txpower = false; + } + uint16_t panSize = 0; + if (R_NWK_GetRequest(R_NWK_panSize, &panSize, sizeof(panSize))) + { + g_tlv_wpanStatus.dagsize = panSize; + } + else + { + g_tlv_wpanStatus.has_dagsize = false; + } + +#if R_BORDER_ROUTER_ENABLED + uint8_t deviceType; + if (R_NWK_GetRequest(R_NWK_deviceType, &deviceType, sizeof(deviceType)) == R_RESULT_SUCCESS && + deviceType == R_BORDERROUTER) + { + g_tlv_wpanStatus.has_metric = false; + } + else +#endif + { + r_result_t res; + r_ipv6addr_t addrPreferred; + r_boolean_t foundPreferred = R_FALSE; + if (R_NWK_GetRequest(R_NWK_preferredParentAddress, &addrPreferred, sizeof(addrPreferred)) == R_RESULT_SUCCESS + && MEMISNOTZERO_S(&addrPreferred)) + { + r_boolean_t isContinuation = R_FALSE; + do + { + nd_cache_buffer_t cfm; + res = R_NWK_GetRequestMultipart(R_NWK_ndCache, &cfm, sizeof(cfm), isContinuation); + if (res == R_RESULT_SUCCESS || res == R_RESULT_SUCCESS_ADDITIONAL_DATA) + { + if (!isContinuation) + { + isContinuation = R_TRUE; + } + for (uint16_t i = 0; i < cfm.nd_cache.count; i++) + { + r_boolean_t check = R_TRUE; + for (uint16_t j = 0; j < sizeof(cfm.nd_cache.entries[i].ipAddress.bytes); j++) + { + if (cfm.nd_cache.entries[i].ipAddress.bytes[j] != addrPreferred.bytes[j]) + { + check = R_FALSE; + break; + } + } + if (check == R_TRUE) + { + g_tlv_wpanStatus.metric = cfm.nd_cache.entries[i].routingCost + cfm.nd_cache.entries[i].linkMetric; + foundPreferred = R_TRUE; + break; + } + } + } + else + { + break; + } + } + while (res == R_RESULT_SUCCESS_ADDITIONAL_DATA); + if (!foundPreferred) + { + g_tlv_wpanStatus.has_metric = false; + } + } + else + { + g_tlv_wpanStatus.has_metric = false; + } + } + + g_tlv_wpanStatus.lastchanged = 0; + g_tlv_wpanStatus.lastchangedreason = 0; + + return &g_tlv_wpanStatus; +} + +void* rplinstance_get(uint32_t* num) +{ + *num = 1; + MEMZERO_S(&g_tlv_rplInstance); + + g_tlv_rplInstance.has_instanceindex = true; + g_tlv_rplInstance.has_instanceid = true; + g_tlv_rplInstance.has_dodagid = true; + g_tlv_rplInstance.has_dodagversionnumber = true; + g_tlv_rplInstance.has_rank = true; + g_tlv_rplInstance.has_parentcount = true; + g_tlv_rplInstance.has_dagsize = true; + + g_tlv_rplInstance.instanceindex = 1; +#if !R_LEAF_NODE_ENABLED + r_nwk_rpl_info_t rplInfo; + if (R_NWK_GetRequest(R_NWK_rplInfo, &rplInfo, sizeof(rplInfo)) == R_RESULT_SUCCESS) + { + MEMCPY_A(g_tlv_rplInstance.dodagid.data, rplInfo.dodag_id.bytes); + g_tlv_rplInstance.dodagid.len = 16; + g_tlv_rplInstance.dodagversionnumber = rplInfo.dodag_version; + g_tlv_rplInstance.rank = rplInfo.rank; + g_tlv_rplInstance.instanceid = rplInfo.instance_id; + } + else +#endif + { + g_tlv_rplInstance.has_dodagid = false; + g_tlv_rplInstance.has_dodagversionnumber = false; + g_tlv_rplInstance.has_rank = false; + g_tlv_rplInstance.has_instanceid = false; + } + +#if R_BORDER_ROUTER_ENABLED + uint8_t deviceType; + if (R_NWK_GetRequest(R_NWK_deviceType, &deviceType, sizeof(deviceType)) == R_RESULT_SUCCESS && + deviceType == R_BORDERROUTER) + { + g_tlv_rplInstance.parentcount = 0; + } + else +#endif + { + g_tlv_rplInstance.parentcount = 1; // We must have a Preferred Parent (otherwise we could not communicate with NMS) + r_ipv6addr_t apAddr; + if (R_NWK_GetRequest(R_NWK_alternateParentAddress, &apAddr, sizeof(apAddr)) == R_RESULT_SUCCESS + && MEMISNOTZERO_S(&apAddr)) + { + g_tlv_rplInstance.parentcount++; // We also have an Alternate Parent -> Increment counter + } + } + + uint16_t nwkPanSize = 0; + if (R_NWK_GetRequest(R_NWK_panSize, &nwkPanSize, sizeof(nwkPanSize)) == R_RESULT_SUCCESS) + { + g_tlv_rplInstance.dagsize = nwkPanSize; + } + else + { + g_tlv_rplInstance.has_dagsize = false; + } + + return &g_tlv_rplInstance; +} + +void* signature_settings_get(uint32_t* num) +{ + *num = 1; + + return &g_SignatureSettings; +} + +void signature_settings_post(Signature_Settings* tlv) +{ + g_SignatureSettings.has_reqsignedpost = true; + g_SignatureSettings.reqsignedpost = tlv->reqsignedpost; + + g_SignatureSettings.has_reqvalidcheckpost = true; + g_SignatureSettings.reqvalidcheckpost = tlv->reqvalidcheckpost; + + g_SignatureSettings.has_reqtimesyncpost = true; + g_SignatureSettings.reqtimesyncpost = tlv->reqtimesyncpost; + + g_SignatureSettings.has_reqseclocalpost = true; + g_SignatureSettings.reqseclocalpost = tlv->reqseclocalpost; + + g_SignatureSettings.has_reqsignedresp = true; + g_SignatureSettings.reqsignedresp = tlv->reqsignedresp; + + g_SignatureSettings.has_reqvalidcheckresp = true; + g_SignatureSettings.reqvalidcheckresp = tlv->reqvalidcheckresp; + + g_SignatureSettings.has_reqtimesyncresp = true; + g_SignatureSettings.reqtimesyncresp = tlv->reqtimesyncresp; + + g_SignatureSettings.has_reqseclocalresp = true; + g_SignatureSettings.reqseclocalresp = tlv->reqseclocalresp; + + g_SignatureSettings.has_cert = true; + g_SignatureSettings.cert.len = tlv->cert.len; + MEMCPY_A(g_SignatureSettings.cert.data, tlv->cert.data); +} + +void* vendorTlv_get(tlvid_t tlvid, uint32_t *num) +{ + return NULL; +} + +void vendorTlv_post(tlvid_t tlvid, Vendor_Tlv *tlv) +{ + return; +} + +void* transferRequest_get(tlvid_t tlvid, uint32_t* num) +{ +#if R_FWUP_SUPPORT + (void)tlvid; + *num = 1; + R_LOG_DBG("## transferRequest_get: GET for TLV %{u32}", tlvid.type); + + // Check upload slot download status + if (uploadSlotValid && uploadSlotHdr.status != (uint32_t) FWHDR_STATUS_DOWNLOAD) + { + g_transferRequest.status = uploadSlotHdr.status; + R_LOG_DBG("transferRequest_get: Transfer request download status = 0x%{hex32}", g_transferRequest.status); + return &g_transferRequest; + } + // Update g_transferRequest fields + MEMCPY_A(g_transferRequest.filehash.data, uploadSlotHdr.filehash); + strncpy(g_transferRequest.filename, uploadSlotHdr.filename, sizeof(g_transferRequest.filename)); + g_transferRequest.filename[sizeof(g_transferRequest.filename) - 1] = '\0'; + strncpy(g_transferRequest.version, uploadSlotHdr.version, sizeof(g_transferRequest.version)); + g_transferRequest.version[sizeof(g_transferRequest.version) -1] = '\0'; + strncpy(g_transferRequest.hwinfo.hwid, uploadSlotHdr.hwid, sizeof(g_transferRequest.hwinfo.hwid)); + g_transferRequest.hwinfo.hwid[sizeof(g_transferRequest.hwinfo.hwid) -1] = '\0'; + g_transferRequest.filesize = uploadSlotHdr.filesize; + g_transferRequest.blocksize = uploadSlotHdr.blocksize; + g_transferRequest.report_int_min = uploadSlotHdr.reportintervalmin; + g_transferRequest.report_int_max = uploadSlotHdr.reportintervalmax; + g_transferRequest.status = uploadSlotHdr.status; + + R_LOG_DBG("## transferRequest_get: GET for TLV %{u32} done.", tlvid.type); + return &g_transferRequest; +#else + *num = 0; + return NULL; +#endif +} + +void transferRequest_post(tlvid_t tlvid, Transfer_Request* tlv) +{ +#if R_FWUP_SUPPORT + (void)tlvid; + R_LOG_DBG("## transferRequest_post: POST for TLV %{u32}.", tlvid.type); + + if (!tlv) + { + R_LOG_DBG("## transferRequest_post: Transfer request tlv context is NULL"); + return; + } + + uint32_t tmin = tlv->report_int_min * 1000; + uint32_t tmax = tlv->report_int_max * 1000; + + // Check hardware id + if (!(tlv->hwinfo.has_hwid)) + { + tlv->response = RESPONSE_INCOMPATIBLE_HW; + R_LOG_DBG("## transferRequest_post: Invalid hardware id: %s", tlv->hwinfo.hwid); + return; + } + // Check filehash len + if (tlv->filehash.len != SHA256_HASH_SIZE) + { + tlv->response = RESPONSE_INVALID_REQ; + R_LOG_DBG("## transferRequest_post: Invalid filehash size: %{u16}", tlv->filehash.len); + return; + } + // Check filesize + if (tlv->filesize == 0 || + tlv->filesize > CSMP_FWMGMT_SLOTIMG_SIZE) + { + tlv->response = RESPONSE_FILE_SIZE_TOO_BIG; + R_LOG_DBG("## transferRequest_post: Invalid file size: %{u32}", tlv->filesize); + return; + } + // blocksize should be smaller than csmp's MTU (1024) + // blocksize should be larger than filesize/1024 since there is only 1024 bitmaps + if (tlv->blocksize == 0 || + tlv->blocksize > BLOCK_SIZE || + tlv->blocksize < tlv->filesize / (CSMP_FWMGMT_BLKMAP_CNT * 32)) + { + tlv->response = RESPONSE_INVALID_BLOCK_SIZE; + R_LOG_DBG("## transferRequest_post: Invalid block size: %{u32}", tlv->blocksize); + return; + } + // Check pending reboot + if (initload) + { + tlv->response = RESPONSE_PENDING_REBOOT; + R_LOG_DBG("## transferRequest_post: Pending reboot for upload image"); + return; + } + // Check duplicate request on Upload slot + if ((memcmp(tlv->filehash.data, uploadSlotHdr.filehash, tlv->filehash.len)) == 0) + { + tlv->response = RESPONSE_DUP_XFER; + R_LOG_DBG("## transferRequest_post: Duplicate transfer request"); + return; + } + // Check duplicate request on Run slot + if ((memcmp(tlv->filehash.data, runImageFileHash, tlv->filehash.len)) == 0) + { + tlv->response = RESPONSE_MATCH_RUN_XFER; + R_LOG_DBG("## transferRequest_post: Transfer request matches Run image"); + return; + } + + // Initiliase new transfer - start + tlv->has_response = true; + tlv->response = RESPONSE_OK; + MEMCPY_S(&g_transferRequest, tlv); + if (!initxfer) + { + initxfer = true; + } + + // Init report intervals + tmin = (tmin >= MIN_REPORT_MIN) ? tmin : MIN_REPORT_MIN; + tmax = (tmax >= MAX_REPORT_MIN) ? tmax : MAX_REPORT_MIN; + tmax = (tmax < tmin) ? tmin : tmax; + + // Erase upload slot + memset(&uploadSlotHdr, 0xFF, sizeof(uploadSlotHdr)); + + // Init upload slot from tlv context + MEMCPY_A(uploadSlotHdr.filehash, tlv->filehash.data); + strncpy(uploadSlotHdr.filename, tlv->filename, sizeof(uploadSlotHdr.filename)); + uploadSlotHdr.filename[sizeof(uploadSlotHdr.filename) - 1] = '\0'; + strncpy(uploadSlotHdr.version, tlv->version, sizeof(uploadSlotHdr.version)); + uploadSlotHdr.version[sizeof(uploadSlotHdr.version) - 1] = '\0'; + strncpy(uploadSlotHdr.hwid, tlv->hwinfo.hwid, sizeof(uploadSlotHdr.hwid)); + uploadSlotHdr.hwid[sizeof(uploadSlotHdr.hwid) - 1] = '\0'; + uploadSlotHdr.filesize = tlv->filesize; + uploadSlotHdr.blocksize = tlv->blocksize; + uploadSlotHdr.filesizelastblk = uploadSlotHdr.filesize % + uploadSlotHdr.blocksize; + if (uploadSlotHdr.filesizelastblk == 0) + { + uploadSlotHdr.filesizelastblk = uploadSlotHdr.blocksize; + } + uploadSlotHdr.blockcnt = (uploadSlotHdr.filesize + (uploadSlotHdr.blocksize - 1)) / + uploadSlotHdr.blocksize; + uploadSlotHdr.reportintervalmin = tmin; + uploadSlotHdr.reportintervalmax = tmax; + uploadSlotHdr.status = (uint32_t) FWHDR_STATUS_DOWNLOAD; + + r_fwup_result_t result = R_APP_FWUP_TransferRequest(uploadSlotHdr.filehash, uploadSlotHdr.filename, uploadSlotHdr.version, + uploadSlotHdr.filesize, uploadSlotHdr.blocksize); + + if (result == R_FWUP_RESULT_OK) + { + // Initiliase new transfer - done + initxfer = false; + uploadSlotValid = R_TRUE; + R_LOG_DBG("## transferRequest_post: POST for TLV %{u32} done.", tlvid.type); + } + else + { + tlv->response = RESPONSE_INVALID_REQ; + R_LOG_ERR("## transferRequest_post: R_APP_FWUP_TransferRequest failed: 0x%{hex8}", result); + } +#endif +} + +void imageBlock_post(tlvid_t tlvid, Image_Block* tlv) +{ +#if R_FWUP_SUPPORT + (void)tlvid; + R_LOG_DBG("## imageBlock_post: POST for TLV %{u32}.", tlvid.type); + + if (!tlv) + { + R_LOG_ERR("## imageBlock_post: Image block tlv context is NULL"); + return; + } + + /* Update g_imageBlock structure from tlv context */ + MEMCPY_S(&g_imageBlock, tlv); + + /* Check file hash length, file hash data, block data length with upload slot (as from transfer request) */ + if ((g_imageBlock.filehash.len >= MIN_HASH_COMPARE_LEN) && (g_imageBlock.filehash.len <= sizeof(uploadSlotHdr.filehash)) && + (memcmp(g_imageBlock.filehash.data, uploadSlotHdr.filehash, g_imageBlock.filehash.len) == 0) && + (g_imageBlock.blockdata.len <= uploadSlotHdr.blocksize)) + { + tlv->retval = true; + R_LOG_DBG("## imageBlock_post: Writing image block %{u32} to upload slot", g_imageBlock.blocknum); + /* Check the initialization of the transfer request */ + if (initxfer) + { + tlv->retval = false; + R_LOG_DBG("## imageBlock_post: Transfer still initializing "); + g_downloadbusy = false; + return; + } + + /* Note: FND sends only a small Hash while the FWUP library expects the full filehash */ + r_fwup_result_t result = R_APP_FWUP_BlockRequest(uploadSlotHdr.filehash, g_imageBlock.blocknum, + g_imageBlock.blockdata.len, g_imageBlock.blockdata.data); + if (result != R_FWUP_RESULT_OK) + { + R_LOG_ERR("## imageBlock_post: R_APP_FWUP_BlockRequest failed: 0x%{hex8}", result); + tlv->retval = false; + g_downloadbusy = false; + return; + } + R_LOG_DBG("## imageBlock_post: Successful write of image block %{u32} (len=%{u32})", g_imageBlock.blocknum, g_imageBlock.blockdata.len); + if (g_imageBlock.blocknum == uploadSlotHdr.filesize / uploadSlotHdr.blocksize) + { + R_LOG_DBG("## imageBlock_post: Image transfer completed!"); + uploadSlotHdr.status = (uint32_t) FWHDR_STATUS_COMPLETE; + } + } + else + { + tlv->retval = false; + R_LOG_DBG("## imageBlock_post: Image block POST failed!"); + } + + g_downloadbusy = false; + R_LOG_DBG("## imageBlock_post: POST for TLV %{u32} done.", tlvid.type); +#endif +} + +void* loadRequest_get(tlvid_t tlvid, uint32_t* num) +{ +#if R_FWUP_SUPPORT + (void)tlvid; + *num = 1; + R_LOG_DBG("## loadRequest_get: GET for TLV %{u32}.", tlvid.type); + + // Check for pending active load requests + if (!initload) + { + R_LOG_DBG("## loadRequest_get: No active load requests pending"); + return NULL; + } + + MEMCPY_A(g_loadRequest.filehash.data, uploadSlotHdr.filehash); + g_loadRequest.filehash.len = SHA256_HASH_SIZE; + g_loadRequest.loadtime = curloadtime; + + R_LOG_DBG("## loadRequest_get: GET for TLV %{u32} done.", tlvid.type); + return &g_loadRequest; +#else + *num = 0; + return NULL; +#endif +} + +void loadRequest_post(tlvid_t tlvid, Load_Request* tlv) +{ +#if R_FWUP_SUPPORT + (void)tlvid; + R_LOG_DBG("## loadRequest_post: POST for TLV %{u32}.", tlvid.type); + + if (!tlv) + { + R_LOG_ERR("## loadRequest_post: Load request tlv context is NULL"); + return; + } + + // Reference g_loadRequest via tlv context + g_loadRequest = *tlv; + + if (memcmp(g_loadRequest.filehash.data, uploadSlotHdr.filehash, + g_loadRequest.filehash.len) == 0) + { + // Filehash matches UPLOAD image + if (uploadSlotHdr.status == (uint32_t) FWHDR_STATUS_COMPLETE) + { + R_LOG_DBG("## loadRequest_post: Load request ok for upload slot image"); + tlv->response = RESPONSE_OK; + } + else if (uploadSlotHdr.status == (uint32_t) FWHDR_STATUS_BADIMAGE || + uploadSlotHdr.status == (uint32_t) FWHDR_STATUS_BADHASH) + { + R_LOG_DBG("## loadRequest_post: Load request on bad upload slot image"); + tlv->response = RESPONSE_SIGNATURE_FAILED; + } + else + { + R_LOG_DBG("## loadRequest_post: Load request incomplete"); + tlv->response = RESPONSE_INCOMPLETE; + } + if (tlv->response != RESPONSE_OK) + { + R_LOG_ERR("## loadRequest_post: Load request failed"); + return; + } + } + else if (memcmp(g_loadRequest.filehash.data, runImageFileHash, + g_loadRequest.filehash.len) == 0) + { + // Filehash matches RUN image + R_LOG_DBG("## loadRequest_post: Load request on running image"); + tlv->response = RESPONSE_IMAGE_RUNNING; + return; + } + else + { + // Load request for unknown filehash + R_LOG_ERR("## loadRequest_post: Load request with unknown filehash"); + tlv->response = RESPONSE_UNKNOWN_HASH; + return; + } + // Check for redundant load request + if (initload && (g_loadRequest.loadtime == curloadtime)) + { + R_LOG_DBG("## loadRequest_post: Redundant load request"); + return; + } + + curloadtime = 0; + initload = true; + + r_fwup_result_t result = R_APP_FWUP_LoadRequest(g_loadRequest.filehash.data, g_loadRequest.loadtime); + if (result != R_FWUP_RESULT_OK) + { + initload = false; + R_LOG_ERR("## loadRequest_post: R_APP_FWUP_LoadRequest failed: 0x%{hex8}", result); + return; + } + + // Upon success update g_loadRequest structure from tlv context + MEMCPY_S(&g_loadRequest, tlv); + // Save new load time + curloadtime = g_loadRequest.loadtime; + + R_LOG_DBG("## loadRequest_post: POST for TLV 0x%{hex32} done.", tlvid.type); +#endif +} + +void cancelLoadRequest_post(tlvid_t tlvid, Cancel_Load_Request* tlv) +{ +#if R_FWUP_SUPPORT + (void)tlvid; + R_LOG_DBG("## cancelLoadRequest_post: POST for TLV 0x%{hex32}.", tlvid.type); + if (!tlv) + { + R_LOG_DBG("## cancelLoadRequest_post: Cancel load request tlv context is NULL"); + return; + } + + if (memcmp(tlv->filehash.data, uploadSlotHdr.filehash, SHA256_HASH_SIZE) == 0) + { + R_LOG_DBG("## cancelLoadRequest_post: Cancel load request valid, cancelling current load request"); + + // Cancel current load request + r_fwup_result_t result = R_APP_FWUP_CancelRequest(g_loadRequest.filehash.data); + if (result != R_FWUP_RESULT_OK) + { + R_LOG_ERR("## cancelLoadRequest_post: R_APP_FWUP_CancelRequest failed: 0x%{hex8}", result); + } + else + { + initload = false; + curloadtime = 0; + } + } + + R_LOG_DBG("## cancelLoadRequest_post: POST for TLV 0x%{hex32} done.", tlvid.type); +#endif +} + +void setBackupRequest_post(tlvid_t tlvid, Set_Backup_Request *tlv) +{ +#if R_FWUP_SUPPORT + R_LOG_ERR("## setBackupRequest_post: Backup image slot not supported"); + return; +#endif +} + +#if R_FWUP_SUPPORT +static uint8_t reverse_bits8(uint8_t x) +{ + x = (uint8_t)((x >> 4) | (x << 4)); // swap nibbles + x = (uint8_t)(((x & 0xCCu) >> 2) | ((x & 0x33u) << 2)); // swap bit pairs + x = (uint8_t)(((x & 0xAAu) >> 1) | ((x & 0x55u) << 1)); // swap individual bits + return x; +} + +static void initialize_file_hash() +{ + // Initialize the file hash through the firmware hash attribute stored in flash + r_result_t flashRes = R_Flash_Read(R_FLASH_FIRMWARE_HASH, runImageFileHash, SHA256_HASH_SIZE); + if (flashRes != R_RESULT_SUCCESS) + { + R_LOG_ERR("Unable to read the firmware hash from flash"); + } + + if (MEMISZERO_A(runImageFileHash)) + { +#if !defined(__i386) && !defined(__x86_64) + uint8_t buf[128u]; + st_fw_header_t *p_hdr = (st_fw_header_t *)buf; + r_fwup_wrap_flash_read((uint32_t) buf, FWUP_CFG_MAIN_AREA_ADDR_L, 128u); + R_memcpy(runImageFileHash, p_hdr->sig, SHA256_HASH_SIZE); +#else + memset(runImageFileHash, 0x12, SHA256_HASH_SIZE); +#endif + } +} + +static char hex_digit(uint8_t v) +{ + v &= 0x0F; + return (v < 10) ? (char)('0' + v) : (char)('a' + (v - 10)); +} + +static void initialize_file_name() +{ + // Initialize the file hash through the firmware hash attribute stored in flash + r_result_t flashRes = R_Flash_Read(R_FLASH_FIRMWARE_NAME, runImageFileName, FILE_NAME_SIZE); + if (flashRes != R_RESULT_SUCCESS) + { + R_LOG_ERR("Unable to read the firmware name from flash"); + } + + /** + * If no firmware file name is stored in data flash (meaning that the current firmware image was not uploaded by + * FND), create a file name using the same syntax as FND so that images that are uploaded via a programmer will + * report a similar file name as firmware images uploaded by FND + */ + if (runImageFileName[0] == 0) + { + if (MEMISZERO_A(runImageFileHash)) + { + initialize_file_hash(); + } + if (sizeof(baseFileName) + sizeof(baseLibraryVersion) + sizeof(baseCsmpVersion) + 16 >= sizeof(runImageFileName)) + { + R_LOG_ERR("File name initialization failed: File name too long"); + strncpy(runImageFileName, baseFileName, sizeof(runImageFileName)); + runImageFileName[sizeof(runImageFileName) - 1] = '\0'; + } + R_memcpy(runImageFileName, baseFileName, sizeof(baseFileName)); + size_t j = sizeof(baseFileName) - 1; + runImageFileName[j++] = '-'; + R_memcpy(&runImageFileName[j], baseLibraryVersion, sizeof(baseLibraryVersion)); + j += sizeof(baseLibraryVersion) - 1; + runImageFileName[j++] = '-'; + R_memcpy(&runImageFileName[j], baseCsmpVersion, sizeof(baseCsmpVersion)); + j += sizeof(baseCsmpVersion) - 1; + runImageFileName[j++] = '-'; + for (size_t i = 0; i < 8; i++) { + uint8_t b = runImageFileHash[i]; + runImageFileName[j++] = hex_digit(b >> 4); + runImageFileName[j++] = hex_digit(b); + } + runImageFileName[j] = '\0'; + } +} + +static void initialize_file_size() +{ + // Initialize the file size through the attribute stored in flash + r_result_t flashRes = R_Flash_Read(R_FLASH_FIRMWARE_SIZE, &runImageFileSize, sizeof(runImageFileSize)); + if (flashRes != R_RESULT_SUCCESS) + { + R_LOG_ERR("Unable to read the firmware size from flash"); + } +} +#endif + +void* firmwareImageInfo_get(tlvid_t tlvid, uint32_t* num) +{ +#if R_FWUP_SUPPORT + (void)tlvid; + *num = 0; + R_LOG_DBG("## firmwareImageInfo_get: GET for TLV %{u32}.", tlvid.type); + + R_LOG_DBG("## firmwareImageInfo_get: Reading firmware image info for running image"); + Firmware_Image_Info* imageInfo = &g_firmwareImageInfo[RUN_IMAGE]; + MEMZERO_S(imageInfo); + (*num)++; + + // Index + imageInfo->has_index = true; + imageInfo->index = RUN_IMAGE + 1; + // Filehash + imageInfo->has_filehash = true; + if (MEMISZERO_A(runImageFileHash)) + { + initialize_file_hash(); + initialize_file_name(); + initialize_file_size(); + } + MEMCPY_A(imageInfo->filehash.data, runImageFileHash); + imageInfo->filehash.len = SHA256_HASH_SIZE; + // Filename + imageInfo->has_filename = true; + strncpy(imageInfo->filename, runImageFileName, sizeof(imageInfo->filename)); + imageInfo->filename[sizeof(imageInfo->filename) - 1] = '\0'; // Ensure null-termination of filename string + // Version + imageInfo->has_version = true; + strncpy(imageInfo->version, baseCsmpVersion, sizeof(imageInfo->version)); + imageInfo->version[sizeof(imageInfo->version) - 1] = '\0'; // Ensure null-termination of version string + // Filesize + if (runImageFileSize != 0) + { + imageInfo->has_filesize = true; + imageInfo->filesize = runImageFileSize; + } + else + { + imageInfo->has_filesize = false; + } + // Blocksize + imageInfo->has_blocksize = false; + // Blockcount + imageInfo->blockcnt = 0xFFFFFFFF; + // Bitmap + imageInfo->has_bitmap = false; + // Default image? + imageInfo->has_isdefault = true; + imageInfo->isdefault = false; + // Running image? + imageInfo->has_isrunning = true; + imageInfo->isrunning = true; + // Loadtime + imageInfo->has_loadtime = false; + // Hardware Id + imageInfo->has_hwinfo = true; + imageInfo->hwinfo.has_hwid = true; + strncpy(imageInfo->hwinfo.hwid, baseHwId, sizeof(imageInfo->hwinfo.hwid)); + imageInfo->hwinfo.hwid[sizeof(imageInfo->hwinfo.hwid) - 1] = '\0'; // Ensure null-termination of hardware string + // Vendor Hardware Id + imageInfo->hwinfo.has_vendorhwid = false; + // Kernel version + imageInfo->has_kernelversion = false; + // Subkernel version + imageInfo->has_subkernelversion = false; + // Loader error + imageInfo->has_loaderrorcode = false; + // Subloader error + imageInfo->has_subloaderrorcode = false; + // Download status + imageInfo->status = FWHDR_STATUS_COMPLETE; + + R_LOG_DBG("## firmwareImageInfo_get: Reading firmware image info for upload image"); + imageInfo = &g_firmwareImageInfo[UPLOAD_IMAGE]; + memset(imageInfo, 0, sizeof(Firmware_Image_Info)); + (*num)++; + + // Index + imageInfo->has_index = true; + imageInfo->index = UPLOAD_IMAGE + 1; + // Filehash + imageInfo->has_filehash = true; + imageInfo->filehash.len = sizeof(uploadSlotHdr.filehash); + MEMCPY_A(imageInfo->filehash.data, uploadSlotHdr.filehash); + // Filename + imageInfo->has_filename = true; + strncpy(imageInfo->filename, uploadSlotHdr.filename, sizeof(imageInfo->filename)); + imageInfo->filename[sizeof(imageInfo->filename) - 1] = '\0'; + // Version + imageInfo->has_version = true; + strncpy(imageInfo->version, uploadSlotHdr.version, sizeof(imageInfo->version)); + imageInfo->version[sizeof(imageInfo->version) - 1] = '\0'; + // Filesize + imageInfo->has_filesize = true; + imageInfo->filesize = uploadSlotHdr.filesize; + // Blocksize + imageInfo->has_blocksize = true; + imageInfo->blocksize = uploadSlotHdr.blocksize; + // Blockcount + imageInfo->blockcnt = uploadSlotHdr.blockcnt; + // Bitmap + if (uploadSlotHdr.status == (uint32_t) FWHDR_STATUS_DOWNLOAD) + { + imageInfo->has_bitmap = true; + imageInfo->bitmap.len = + (((uploadSlotHdr.filesize + uploadSlotHdr.blocksize - 1u) / uploadSlotHdr.blocksize) + 7u) / 8u; + for (uint8_t i = 0; i < imageInfo->bitmap.len; i++) + { + imageInfo->bitmap.data[i] = reverse_bits8(fwupState.imageBitmap[sizeof(fwupState.imageBitmap) - i - 1]); + } + } + else + { + imageInfo->has_bitmap = false; + } + // Default image? + imageInfo->has_isdefault = true; + imageInfo->isdefault = false; + // Running image? + imageInfo->has_isrunning = true; + imageInfo->isrunning = false; + // Loadtime + if (curloadtime != 0) + { + imageInfo->has_loadtime = true; + imageInfo->loadtime = curloadtime; + } + // Hardware Id + imageInfo->has_hwinfo = true; + imageInfo->hwinfo.has_hwid = true; + strncpy(imageInfo->hwinfo.hwid, uploadSlotHdr.hwid, sizeof(imageInfo->hwinfo.hwid)); + imageInfo->hwinfo.hwid[sizeof(imageInfo->hwinfo.hwid) - 1] = '\0'; + // Vendor Hardware Id + imageInfo->hwinfo.has_vendorhwid = false; + // Kernel version + imageInfo->has_kernelversion = false; + // Subkernel version + imageInfo->has_subkernelversion = false; + // Loader error + imageInfo->has_loaderrorcode = false; + // Subloader error + imageInfo->has_subloaderrorcode = false; + // Download status + imageInfo->status = uploadSlotHdr.status; + + DPRINTF("## firmwareImageInfo_get: GET for TLV %{u32} done.", tlvid.type); + return &g_firmwareImageInfo; +#else + *num = 0; + return NULL; +#endif +} + +void rebootRequest_post(tlvid_t tlvid, Reboot_Request* tlv) +{ +#if R_FWUP_SUPPORT + switch (tlv->flag) + { + case REBOOT: + R_LOG_DBG("Rebooting system...\n"); + R_FWUP_SoftwareReset(); + // Should not reach here + break; + default: + R_LOG_ERR("csmp rebootRequest: Reboot flag not supported!"); + } +#else + R_LOG_ERR("csmp rebootRequest: Reboot not supported!"); +#endif +} \ No newline at end of file diff --git a/src/csmpagent/csmp_firmwareMgmt.c b/src/csmpagent/csmp_firmwareMgmt.c index c1e632f..baabb76 100755 --- a/src/csmpagent/csmp_firmwareMgmt.c +++ b/src/csmpagent/csmp_firmwareMgmt.c @@ -49,7 +49,7 @@ int csmp_get_transferRequest(tlvid_t tlvid, uint8_t *buf, size_t len, if(xfer_req) { // Firmware status check - if (!xfer_req->has_status || xfer_req->status != FWHDR_STATUS_DOWNLOAD) { + if (!xfer_req->has_status || xfer_req->status != (uint32_t) FWHDR_STATUS_DOWNLOAD) { DPRINTF("csmpagent_firmwaremgmt: Transfer request status error! (%x)\n", xfer_req->status); return CSMP_OP_TLV_RD_EMPTY; diff --git a/src/csmpapi/csmpservice.c b/src/csmpapi/csmpservice.c index ccd20d2..2fd8956 100644 --- a/src/csmpapi/csmpservice.c +++ b/src/csmpapi/csmpservice.c @@ -22,7 +22,7 @@ #include "cgmsagent.h" #include "csmpserver.h" -uint8_t g_csmplib_status = SERVICE_NOT_START; +csmp_service_status_t g_csmplib_status = SERVICE_NOT_START; uint8_t g_csmplib_eui64[8]; uint32_t g_csmplib_reginterval_min = 0; diff --git a/src/csmpservice/cgmsagent.c b/src/csmpservice/cgmsagent.c index 1bef450..898c785 100644 --- a/src/csmpservice/cgmsagent.c +++ b/src/csmpservice/cgmsagent.c @@ -42,7 +42,7 @@ enum { REASON_OUTAGE_RECOVERY = 8 }; -extern uint8_t g_csmplib_status; +extern csmp_service_status_t g_csmplib_status; extern uint32_t g_csmplib_reginterval_min; extern uint32_t g_csmplib_reginterval_max; extern csmp_subscription_list_t g_csmplib_report_list; @@ -287,6 +287,8 @@ void response_handler(struct sockaddr_in6 *from, uint16_t status, const void *bo g_csmplib_stats.reg_fails_stats.error_process++; } break; + default: + break; } return; } diff --git a/src/lib/protobuf-c/protobuf-c.c b/src/lib/protobuf-c/protobuf-c.c index 945f9ab..e0736fe 100644 --- a/src/lib/protobuf-c/protobuf-c.c +++ b/src/lib/protobuf-c/protobuf-c.c @@ -2892,7 +2892,7 @@ parse_member(ScannedMember *scanned_member, message->unknown_fields + (message->n_unknown_fields++); ufield->tag = scanned_member->tag; - ufield->wire_type = scanned_member->wire_type; + ufield->wire_type = (ProtobufCWireType) scanned_member->wire_type; ufield->len = scanned_member->len; ufield->data = do_alloc(allocator, scanned_member->len); if (ufield->data == NULL)