From 52aeb1156109de0a6a59316b007471c75b68af6e Mon Sep 17 00:00:00 2001 From: Jorge Guzman Date: Mon, 3 Aug 2026 00:53:59 -0300 Subject: [PATCH] wireless/lora_pkt_fwd: add Semtech UDP packet forwarder and lora command Forward the packets received by a LoRa concentrator to a LoRaWAN network server with the Semtech UDP protocol version 2, and turn the downlink requests of the server into transmissions. The server may be given as a name and is resolved with getaddrinfo(). No floating point is used anywhere. The same program provides the "lora" command, which mirrors the AT command set of the vendor gateway firmwares, plus tx, which sends a single packet and so brings a gateway up against any LoRa receiver without a network server. Nothing here names a chip: the types and the commands are the device independent gateway ones of the nuttx repository, nuttx/wireless/lpwan/lora_gw.h. The application depends on LPWAN_LORA_GW, the symbol such a driver selects, so until one is merged nothing here is compiled. Assisted-by: Claude Code 4.8 Signed-off-by: Jorge Guzman --- wireless/lora_pkt_fwd/CMakeLists.txt | 35 + wireless/lora_pkt_fwd/Kconfig | 92 ++ wireless/lora_pkt_fwd/Make.defs | 25 + wireless/lora_pkt_fwd/Makefile | 35 + wireless/lora_pkt_fwd/lora_main.c | 653 ++++++++++++++ wireless/lora_pkt_fwd/lora_pkt_fwd.c | 1242 ++++++++++++++++++++++++++ wireless/lora_pkt_fwd/lora_pkt_fwd.h | 109 +++ 7 files changed, 2191 insertions(+) create mode 100644 wireless/lora_pkt_fwd/CMakeLists.txt create mode 100644 wireless/lora_pkt_fwd/Kconfig create mode 100644 wireless/lora_pkt_fwd/Make.defs create mode 100644 wireless/lora_pkt_fwd/Makefile create mode 100644 wireless/lora_pkt_fwd/lora_main.c create mode 100644 wireless/lora_pkt_fwd/lora_pkt_fwd.c create mode 100644 wireless/lora_pkt_fwd/lora_pkt_fwd.h diff --git a/wireless/lora_pkt_fwd/CMakeLists.txt b/wireless/lora_pkt_fwd/CMakeLists.txt new file mode 100644 index 00000000000..d09b238c592 --- /dev/null +++ b/wireless/lora_pkt_fwd/CMakeLists.txt @@ -0,0 +1,35 @@ +# ############################################################################## +# apps/wireless/lora_pkt_fwd/CMakeLists.txt +# +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed to the Apache Software Foundation (ASF) under one or more contributor +# license agreements. See the NOTICE file distributed with this work for +# additional information regarding copyright ownership. The ASF licenses this +# file to you under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. +# +# ############################################################################## +if(CONFIG_WIRELESS_LORA_PKT_FWD) + nuttx_add_application( + NAME + ${CONFIG_LORA_PKT_FWD_PROGNAME} + PRIORITY + ${CONFIG_LORA_PKT_FWD_PRIORITY} + STACKSIZE + ${CONFIG_LORA_PKT_FWD_STACKSIZE} + MODULE + ${CONFIG_WIRELESS_LORA_PKT_FWD} + SRCS + lora_main.c + lora_pkt_fwd.c) +endif() diff --git a/wireless/lora_pkt_fwd/Kconfig b/wireless/lora_pkt_fwd/Kconfig new file mode 100644 index 00000000000..16a28cfd685 --- /dev/null +++ b/wireless/lora_pkt_fwd/Kconfig @@ -0,0 +1,92 @@ +# +# For a description of the syntax of this configuration file, +# see the file kconfig-language.txt in the NuttX tools repository. +# + +config WIRELESS_LORA_PKT_FWD + tristate "LoRaWAN Semtech UDP packet forwarder" + default n + depends on LPWAN_LORA_GW && NET_UDP && NETUTILS_NETLIB + select NETUTILS_CODECS + select CODECS_BASE64 + select LIBC_STRERROR if DEBUG_FEATURES + ---help--- + Forwards the packets received by a LoRa concentrator to a LoRaWAN + network server using the Semtech UDP protocol version 2, and turns + the downlink requests of the server into transmissions. + + Also provides the "lora" command, the equivalent of the AT command + set of the vendor gateway firmwares. + +if WIRELESS_LORA_PKT_FWD + +config LORA_PKT_FWD_PROGNAME + string "Program name" + default "lora" + +config LORA_PKT_FWD_PRIORITY + int "Daemon task priority" + default 100 + +config LORA_PKT_FWD_STACKSIZE + int "Daemon stack size" + default 8192 + +config LORA_PKT_FWD_VERSION + string "Version reported by \"lora ver\"" + default "1.0.0" + +config LORA_PKT_FWD_DEVPATH + string "Concentrator device" + default "/dev/lora0" + +config LORA_PKT_FWD_SERVER + string "Network server host name or address" + default "192.168.15.100" + ---help--- + Host name or address of the LoRaWAN network server. A name is + resolved with getaddrinfo(), which needs NETDB_DNSCLIENT. + + The Things Network uses the regional cluster names, for example + au1.cloud.thethings.network for AU915. + +config LORA_PKT_FWD_PORT_UP + int "Uplink UDP port" + default 1700 + +config LORA_PKT_FWD_PORT_DOWN + int "Downlink UDP port" + default 1700 + +config LORA_PKT_FWD_EUI + string "Gateway identifier" + default "080027FFFF0C2338" + ---help--- + The 64 bit gateway identifier, as sixteen hexadecimal digits. It has + to match the gateway registered in the network server. + +config LORA_PKT_FWD_EUI_FROM_MAC + bool "Derive the gateway identifier from the Ethernet address" + default n + ---help--- + Build the identifier out of the Ethernet address of eth0 instead of + using the configured one, inserting FFFE in the middle as usual for + an EUI-64. + +config LORA_PKT_FWD_TXPOWER + int "Transmit power of \"lora tx\" in dBm" + default 14 + ---help--- + Antenna power requested for the packet sent by the "lora tx" + command. The concentrator picks the closest entry of its gain + table. + +config LORA_PKT_FWD_KEEPALIVE + int "PULL_DATA keepalive interval in seconds" + default 5 + +config LORA_PKT_FWD_STATINTERVAL + int "Status push interval in seconds" + default 30 + +endif # WIRELESS_LORA_PKT_FWD diff --git a/wireless/lora_pkt_fwd/Make.defs b/wireless/lora_pkt_fwd/Make.defs new file mode 100644 index 00000000000..463cf1435e1 --- /dev/null +++ b/wireless/lora_pkt_fwd/Make.defs @@ -0,0 +1,25 @@ +############################################################################ +# apps/wireless/lora_pkt_fwd/Make.defs +# +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. The +# ASF licenses this file to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance with the +# License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +############################################################################ + +ifneq ($(CONFIG_WIRELESS_LORA_PKT_FWD),) +CONFIGURED_APPS += $(APPDIR)/wireless/lora_pkt_fwd +endif diff --git a/wireless/lora_pkt_fwd/Makefile b/wireless/lora_pkt_fwd/Makefile new file mode 100644 index 00000000000..b62b7898c69 --- /dev/null +++ b/wireless/lora_pkt_fwd/Makefile @@ -0,0 +1,35 @@ +############################################################################ +# apps/wireless/lora_pkt_fwd/Makefile +# +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. The +# ASF licenses this file to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance with the +# License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +############################################################################ + +include $(APPDIR)/Make.defs + +# LoRaWAN packet forwarder and the "lora" command + +PROGNAME = $(CONFIG_LORA_PKT_FWD_PROGNAME) +PRIORITY = $(CONFIG_LORA_PKT_FWD_PRIORITY) +STACKSIZE = $(CONFIG_LORA_PKT_FWD_STACKSIZE) +MODULE = $(CONFIG_WIRELESS_LORA_PKT_FWD) + +CSRCS = lora_pkt_fwd.c +MAINSRC = lora_main.c + +include $(APPDIR)/Application.mk diff --git a/wireless/lora_pkt_fwd/lora_main.c b/wireless/lora_pkt_fwd/lora_main.c new file mode 100644 index 00000000000..800997148ac --- /dev/null +++ b/wireless/lora_pkt_fwd/lora_main.c @@ -0,0 +1,653 @@ +/**************************************************************************** + * apps/wireless/lora_pkt_fwd/lora_main.c + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. The + * ASF licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + ****************************************************************************/ + +/* The "lora" command, the equivalent of the AT command set of the vendor + * gateway firmware: + * + * lora sys full system configuration (AT+SYS) + * lora ver firmware version (AT+VER) + * lora status concentrator and forwarder state + * lora ch show or change the channel plan (AT+CH) + * lora server show or change the server (AT+PKTFWD) + * lora ip network configuration (AT+IP) + * lora mac Ethernet address and gateway id (AT+MAC) + * lora reset restart the concentrator (AT+RESET) + * lora tx transmit one packet, for bring-up + * lora start start the packet forwarder + * lora stop stop the packet forwarder + */ + +/**************************************************************************** + * Included Files + ****************************************************************************/ + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include + +#include "netutils/netlib.h" + +#include "lora_pkt_fwd.h" + +/**************************************************************************** + * Pre-processor Definitions + ****************************************************************************/ + +#define LORA_SEPARATOR \ + "---------------------------------------------------------------" + +/**************************************************************************** + * Private Functions + ****************************************************************************/ + +/**************************************************************************** + * Name: lora_usage + ****************************************************************************/ + +static void lora_usage(void) +{ + printf("lora - LoRa Gateway commands (concentrator + forwarder)\n"); + printf("Subcommands:\n"); + printf(" sys : Show full system configuration (like AT+SYS)\n"); + printf(" ver : Show firmware version (like AT+VER)\n"); + printf(" status : Show gateway status and statistics\n"); + printf(" ch : Show/set channel plan (like AT+CH)\n"); + printf(" Usage: lora ch [REGION]\n"); + printf(" server : Show/set LoRaWAN server (like AT+PKTFWD)\n"); + printf(" Usage: lora server [HOST [PORT_UP [PORT_DOWN]]]\n"); + printf(" ip : Show network configuration (like AT+IP)\n"); + printf(" mac : Show MAC address and Gateway ID (like AT+MAC)\n"); + printf(" reset : Reset concentrator and restart (like AT+RESET)\n"); + printf(" tx : Transmit one packet, for bring-up\n"); + printf(" Usage: lora tx FREQ_HZ [SF [TEXT]]\n"); + printf(" start : Start packet forwarder\n"); + printf(" stop : Stop packet forwarder\n"); +} + +/**************************************************************************** + * Name: lora_opendev + ****************************************************************************/ + +static int lora_opendev(void) +{ + int fd; + + fd = open(CONFIG_LORA_PKT_FWD_DEVPATH, O_RDWR | O_NONBLOCK | O_CLOEXEC); + if (fd < 0) + { + fprintf(stderr, "lora: cannot open %s: %d\n", + CONFIG_LORA_PKT_FWD_DEVPATH, errno); + return -errno; + } + + return fd; +} + +/**************************************************************************** + * Name: lora_getregion + ****************************************************************************/ + +static int lora_getregion(FAR struct lora_gw_regioninfo_s *info, int index) +{ + struct lora_gw_regionreq_s req; + int fd; + int ret; + + fd = lora_opendev(); + if (fd < 0) + { + return fd; + } + + req.index = index; + ret = ioctl(fd, WLIOC_GW_GETREGION, (unsigned long)&req); + close(fd); + + if (ret < 0) + { + return -errno; + } + + memcpy(info, &req.info, sizeof(*info)); + return OK; +} + +/**************************************************************************** + * Name: lora_getstatus + ****************************************************************************/ + +static int lora_getstatus(FAR struct lora_gw_status_s *status) +{ + int fd; + int ret; + + fd = lora_opendev(); + if (fd < 0) + { + return fd; + } + + ret = ioctl(fd, WLIOC_GW_GETSTATUS, (unsigned long)status); + close(fd); + + return ret < 0 ? -errno : OK; +} + +/**************************************************************************** + * Name: lora_printchannels + ****************************************************************************/ + +static void lora_printchannels(FAR const struct lora_gw_regioninfo_s *info, + FAR const char *indent) +{ + FAR const struct lora_gw_chaninfo_s *ch; + FAR const char *type; + char rate[24]; + int i; + + for (i = 0; i < LORA_GW_IF_CHAIN_NB; i++) + { + ch = &info->channels[i]; + + switch (ch->type) + { + case LORA_GW_CHAN_MULTI_SF: + type = "LORA_MULTI_SF"; + strlcpy(rate, "SF7/SF12, BW125KHz", sizeof(rate)); + break; + + case LORA_GW_CHAN_STD: + type = "LORA_STANDARD"; + snprintf(rate, sizeof(rate), "SF%u, BW%uKHz", ch->datarate, + ch->bandwidth == LORA_GW_BW_500K ? 500 : + (ch->bandwidth == LORA_GW_BW_250K ? 250 : 125)); + break; + + case LORA_GW_CHAN_FSK: + type = "FSK"; + rate[0] = '\0'; + break; + + default: + type = "OFF"; + rate[0] = '\0'; + break; + } + + if (!ch->enable) + { + printf("%sCHANNEL%d: OFF (%s)\n", + indent, i, type); + continue; + } + + printf("%sCHANNEL%d: %" PRIu32 ", %c, %s (%s)\n", + indent, i, ch->freq_hz, ch->rf_chain == 0 ? 'A' : 'B', + rate, type); + } +} + +/**************************************************************************** + * Name: lora_cmd_ver + ****************************************************************************/ + +static int lora_cmd_ver(void) +{ + printf("Powered by NuttX & LoRa Gateway HAL\n"); + printf("VERSION: %s (NuttX %s)\n", CONFIG_LORA_PKT_FWD_VERSION, + CONFIG_VERSION_STRING); + return EXIT_SUCCESS; +} + +/**************************************************************************** + * Name: lora_cmd_ip + ****************************************************************************/ + +static int lora_cmd_ip(void) +{ + struct in_addr addr; + uint8_t flags = 0; + + netlib_getifstatus(LORA_FWD_IFNAME, &flags); + + printf(" ETHERNET: %s\n", +#ifdef CONFIG_NETUTILS_DHCPC + "DHCP" +#else + "static" +#endif + ); + + printf(" LINK: %s\n", + (flags & IFF_UP) != 0 ? "UP" : "DOWN"); + + if (netlib_get_ipv4addr(LORA_FWD_IFNAME, &addr) == OK) + { + printf(" IP: %s\n", inet_ntoa(addr)); + } + + if (netlib_get_ipv4netmask(LORA_FWD_IFNAME, &addr) == OK) + { + printf(" NETMASK: %s\n", inet_ntoa(addr)); + } + + if (netlib_get_dripv4addr(LORA_FWD_IFNAME, &addr) == OK) + { + printf(" GATEWAY: %s\n", inet_ntoa(addr)); + } + + return EXIT_SUCCESS; +} + +/**************************************************************************** + * Name: lora_cmd_mac + ****************************************************************************/ + +static int lora_cmd_mac(void) +{ + FAR struct lora_fwd_config_s *config = lora_fwd_config(); + uint8_t mac[IFHWADDRLEN]; + + if (netlib_getmacaddr(LORA_FWD_IFNAME, mac) == OK) + { + printf(" MACADDR: %02X:%02X:%02X:%02X:%02X:%02X\n", + mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + } + + printf(" GATEWAY ID: %02X%02X%02X%02X%02X%02X%02X%02X\n", + config->eui[0], config->eui[1], config->eui[2], config->eui[3], + config->eui[4], config->eui[5], config->eui[6], config->eui[7]); + + return EXIT_SUCCESS; +} + +/**************************************************************************** + * Name: lora_cmd_server + ****************************************************************************/ + +static int lora_cmd_server(int argc, FAR char *argv[]) +{ + FAR struct lora_fwd_config_s *config = lora_fwd_config(); + + if (argc > 2) + { + unsigned long port_up = (argc > 3) ? strtoul(argv[3], NULL, 10) : 0; + unsigned long port_down = (argc > 4) ? strtoul(argv[4], NULL, 10) : 0; + + lora_fwd_setserver(argv[2], port_up, port_down); + + if (lora_fwd_isrunning()) + { + printf("lora: restart the forwarder to use the new server\n"); + } + } + + printf(" LORAWAN SERVER: %s\n", config->server); + printf(" UPLINK UDP PORT: %u\n", config->port_up); + printf("DOWNLINK UDP PORT: %u\n", config->port_down); + + return EXIT_SUCCESS; +} + +/**************************************************************************** + * Name: lora_cmd_ch + ****************************************************************************/ + +static int lora_cmd_ch(int argc, FAR char *argv[]) +{ + struct lora_gw_regioninfo_s info; + int ret; + int i; + + if (argc > 2) + { + int fd = lora_opendev(); + + if (fd < 0) + { + return EXIT_FAILURE; + } + + ret = ioctl(fd, WLIOC_GW_SETREGION, (unsigned long)argv[2]); + if (ret < 0) + { + fprintf(stderr, "lora: cannot select %s: %d\n", argv[2], errno); + close(fd); + return EXIT_FAILURE; + } + + /* Restart the concentrator so that the new plan takes effect */ + + if (lora_fwd_isrunning()) + { + ioctl(fd, WLIOC_GW_RESET, 0); + } + + close(fd); + } + + ret = lora_getregion(&info, -1); + if (ret < 0) + { + fprintf(stderr, "lora: cannot read the channel plan: %d\n", -ret); + return EXIT_FAILURE; + } + + printf("%s Channel Plan (%s):\n", info.name, info.desc); + printf(" Radio A: %" PRIu32 " Hz\n", info.radio_freq[0]); + printf(" Radio B: %" PRIu32 " Hz\n", info.radio_freq[1]); + printf("\n"); + + lora_printchannels(&info, " "); + + printf("\nAvailable regions:"); + for (i = 0; lora_getregion(&info, i) == OK; i++) + { + printf(" %s", info.name); + } + + printf("\n"); + return EXIT_SUCCESS; +} + +/**************************************************************************** + * Name: lora_cmd_status + ****************************************************************************/ + +static int lora_cmd_status(void) +{ + struct lora_gw_status_s status; + struct lora_fwd_stats_s stats; + + if (lora_getstatus(&status) < 0) + { + return EXIT_FAILURE; + } + + lora_fwd_getstats(&stats); + + printf("Concentrator: %s\n", status.started ? "RUNNING" : "STOPPED"); + printf("Pkt Forwarder: %s\n", + lora_fwd_isrunning() ? "RUNNING" : "STOPPED"); + printf("Uptime: %" PRIu32 " s\n", stats.uptime_sec); + printf("RX OK: %" PRIu32 "\n", status.rx_ok); + printf("RX BAD: %" PRIu32 "\n", status.rx_bad); + printf("RX NOCRC:%" PRIu32 "\n", status.rx_nocrc); + printf("RX ERR: %" PRIu32 "\n", status.rx_err); + printf("RX FWD: %" PRIu32 "\n", stats.rx_fwd); + printf("TX OK: %" PRIu32 "\n", status.tx_ok); + printf("TX ERR: %" PRIu32 "\n", status.tx_err); + printf("PUSH: %" PRIu32 " sent, %" PRIu32 " acked\n", + stats.push_sent, stats.push_ack); + printf("PULL: %" PRIu32 " sent, %" PRIu32 " acked\n", + stats.pull_sent, stats.pull_ack); + + return EXIT_SUCCESS; +} + +/**************************************************************************** + * Name: lora_cmd_sys + ****************************************************************************/ + +static int lora_cmd_sys(void) +{ + FAR struct lora_fwd_config_s *config = lora_fwd_config(); + struct lora_gw_regioninfo_s info; + struct lora_gw_status_s status; + + printf("\n"); + printf("Powered by NuttX & LoRa Gateway HAL\n"); + printf("%s\n", LORA_SEPARATOR); + printf(" VERSION: %s (NuttX %s)\n", CONFIG_LORA_PKT_FWD_VERSION, + CONFIG_VERSION_STRING); + printf(" LORAWAN: Public\n"); + + lora_cmd_mac(); + + printf(" LORAWAN SERVER: %s\n", config->server); + printf(" UPLINK UDP PORT: %u\n", config->port_up); + printf("DOWNLINK UDP PORT: %u\n", config->port_down); + + lora_cmd_ip(); + + if (lora_getregion(&info, -1) == OK) + { + printf(" REGION: %s (%s)\n", info.name, info.desc); + lora_printchannels(&info, " "); + } + + printf("%s\n", LORA_SEPARATOR); + + if (lora_getstatus(&status) == OK) + { + printf(" CONCENTRATOR: %s\n", + status.started ? "Running" : "Stopped"); + } + + printf(" PKT FWD: %s\n", + lora_fwd_isrunning() ? "Running" : "Stopped"); + + return EXIT_SUCCESS; +} + +/**************************************************************************** + * Name: lora_cmd_tx + * + * Description: + * Transmit a single packet. This exists to bring a gateway up without a + * network server: any LoRa receiver tuned to the same frequency, spreading + * factor and bandwidth sees it. The polarity is the one of an uplink, not + * the inverted one of a LoRaWAN downlink, so that a plain end device radio + * can pick it up. + * + ****************************************************************************/ + +static int lora_cmd_tx(int argc, FAR char *argv[]) +{ + struct lora_gw_txpkt_s txpkt; + FAR const char *text; + unsigned long datarate; + size_t len; + int fd; + int ret; + + if (argc < 3) + { + fprintf(stderr, "lora: usage: lora tx FREQ_HZ [SF [TEXT]]\n"); + return EXIT_FAILURE; + } + + memset(&txpkt, 0, sizeof(txpkt)); + + datarate = (argc > 3) ? strtoul(argv[3], NULL, 10) : 7; + text = (argc > 4) ? argv[4] : "NuttX LoRa gateway"; + + if (datarate < 6 || datarate > 12) + { + fprintf(stderr, "lora: spreading factor out of range\n"); + return EXIT_FAILURE; + } + + len = strlen(text); + if (len > sizeof(txpkt.payload)) + { + len = sizeof(txpkt.payload); + } + + txpkt.freq_hz = strtoul(argv[2], NULL, 10); + txpkt.datarate = datarate; + + txpkt.tx_mode = LORA_GW_TX_IMMEDIATE; + txpkt.rf_chain = 0; + txpkt.rf_power = CONFIG_LORA_PKT_FWD_TXPOWER; + txpkt.modulation = LORA_GW_MOD_LORA; + txpkt.bandwidth = LORA_GW_BW_125K; + txpkt.coderate = WLIOC_LORA_CR_4_5; + txpkt.preamble = 8; + txpkt.size = len; + + memcpy(txpkt.payload, text, len); + + fd = lora_opendev(); + if (fd < 0) + { + return EXIT_FAILURE; + } + + ret = write(fd, &txpkt, sizeof(txpkt)); + close(fd); + + if (ret != sizeof(txpkt)) + { + fprintf(stderr, "lora: transmit failed: %d\n", errno); + return EXIT_FAILURE; + } + + printf("lora: sent %zu bytes at %" PRIu32 " Hz, SF%lu\n", + len, txpkt.freq_hz, datarate); + return EXIT_SUCCESS; +} + +/**************************************************************************** + * Name: lora_cmd_reset + ****************************************************************************/ + +static int lora_cmd_reset(void) +{ + int fd; + int ret; + + fd = lora_opendev(); + if (fd < 0) + { + return EXIT_FAILURE; + } + + ret = ioctl(fd, WLIOC_GW_RESET, 0); + close(fd); + + if (ret < 0) + { + fprintf(stderr, "lora: reset failed: %d\n", errno); + return EXIT_FAILURE; + } + + printf("lora: concentrator restarted\n"); + return EXIT_SUCCESS; +} + +/**************************************************************************** + * Public Functions + ****************************************************************************/ + +/**************************************************************************** + * Name: main + ****************************************************************************/ + +int main(int argc, FAR char *argv[]) +{ + int ret; + + lora_fwd_init(); + + if (argc < 2) + { + lora_usage(); + return EXIT_SUCCESS; + } + + if (strcmp(argv[1], "sys") == 0) + { + return lora_cmd_sys(); + } + else if (strcmp(argv[1], "ver") == 0) + { + return lora_cmd_ver(); + } + else if (strcmp(argv[1], "status") == 0) + { + return lora_cmd_status(); + } + else if (strcmp(argv[1], "ch") == 0) + { + return lora_cmd_ch(argc, argv); + } + else if (strcmp(argv[1], "server") == 0) + { + return lora_cmd_server(argc, argv); + } + else if (strcmp(argv[1], "ip") == 0) + { + return lora_cmd_ip(); + } + else if (strcmp(argv[1], "mac") == 0) + { + return lora_cmd_mac(); + } + else if (strcmp(argv[1], "reset") == 0) + { + return lora_cmd_reset(); + } + else if (strcmp(argv[1], "tx") == 0) + { + return lora_cmd_tx(argc, argv); + } + else if (strcmp(argv[1], "start") == 0) + { + ret = lora_fwd_start(); + if (ret < 0) + { + fprintf(stderr, "lora: cannot start the forwarder: %d\n", -ret); + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; + } + else if (strcmp(argv[1], "stop") == 0) + { + ret = lora_fwd_stop(); + if (ret < 0) + { + fprintf(stderr, "lora: cannot stop the forwarder: %d\n", -ret); + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; + } + + fprintf(stderr, "lora: unknown subcommand '%s'\n", argv[1]); + lora_usage(); + return EXIT_FAILURE; +} diff --git a/wireless/lora_pkt_fwd/lora_pkt_fwd.c b/wireless/lora_pkt_fwd/lora_pkt_fwd.c new file mode 100644 index 00000000000..2e60d0c9171 --- /dev/null +++ b/wireless/lora_pkt_fwd/lora_pkt_fwd.c @@ -0,0 +1,1242 @@ +/**************************************************************************** + * apps/wireless/lora_pkt_fwd/lora_pkt_fwd.c + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. The + * ASF licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + ****************************************************************************/ + +/* Semtech UDP packet forwarder, protocol version 2. + * + * Two loops share the concentrator character device: + * + * uplink - polls /dev/loraN, serialises the packets into a "rxpk" JSON + * object and sends it as PUSH_DATA. Also pushes the periodic + * "stat" object. + * downlink - sends PULL_DATA keepalives so that the server knows where to + * reach us, and turns each PULL_RESP into a write() on the + * concentrator followed by a TX_ACK. + * + * No floating point is used: frequencies are printed from their integer Hz + * value and the signal levels come from the driver already scaled by ten. + */ + +/**************************************************************************** + * Included Files + ****************************************************************************/ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include + +#include "netutils/base64.h" +#include "netutils/netlib.h" + +#include "lora_pkt_fwd.h" + +/**************************************************************************** + * Pre-processor Definitions + ****************************************************************************/ + +#define LORA_FWD_PROTOCOL 2 + +#define LORA_FWD_JSONSIZE 4096 /* Largest JSON object we build */ +#define LORA_FWD_RXBUFSIZE 1024 +#define LORA_FWD_NPKT 4 /* Packets fetched per read() */ +#define LORA_FWD_POLL_MS 100 /* Receive polling interval */ +#define LORA_FWD_RECV_MS 500 /* Downlink socket timeout */ +#define LORA_FWD_RESOLVE_TRIES 10 /* Attempts to resolve the server */ +#define LORA_FWD_RESOLVE_DELAY 5 /* Seconds between those attempts */ + +/* A socket call can block for a while when the stack is resolving the + * hardware address of the server, so give both loops room to notice a stop + * request before giving up on them. + */ + +#define LORA_FWD_STOP_TIMEOUT 20 + +/**************************************************************************** + * Private Data + ****************************************************************************/ + +static struct lora_fwd_config_s g_config = +{ + .server = CONFIG_LORA_PKT_FWD_SERVER, + .port_up = CONFIG_LORA_PKT_FWD_PORT_UP, + .port_down = CONFIG_LORA_PKT_FWD_PORT_DOWN, + .keepalive_sec = CONFIG_LORA_PKT_FWD_KEEPALIVE, + .stat_sec = CONFIG_LORA_PKT_FWD_STATINTERVAL +}; + +static struct lora_fwd_stats_s g_stats; + +static volatile bool g_running; +static volatile bool g_stopreq; +static time_t g_starttime; + +static int g_devfd = -1; +static int g_sock_up = -1; +static int g_sock_down = -1; + +static struct sockaddr_in g_addr_up; +static struct sockaddr_in g_addr_down; + +static pthread_t g_downthread; + +/* Last error reported by each socket, so that an unreachable server is + * logged once instead of on every datagram. + */ + +static int g_lasterr_up; +static int g_lasterr_down; + +/* Datagram of the uplink path, with the JSON built in place right after the + * twelve byte header. It lives in static storage because two buffers of + * this size do not fit on the stack of the daemon. Only the uplink loop + * touches it. + */ + +static uint8_t g_pushbuf[12 + LORA_FWD_JSONSIZE]; + +#define LORA_FWD_JSON ((FAR char *)&g_pushbuf[12]) + +/**************************************************************************** + * Private Functions + ****************************************************************************/ + +/**************************************************************************** + * Name: lora_fwd_token + * + * Description: + * Two random bytes identifying a request, echoed back in the acknowledge. + * + ****************************************************************************/ + +static void lora_fwd_token(FAR uint8_t *buffer) +{ + uint32_t value = arc4random(); + + buffer[0] = value & 0xff; + buffer[1] = (value >> 8) & 0xff; +} + +/**************************************************************************** + * Name: lora_fwd_resolve + * + * Description: + * Resolve the configured server, which may be a host name or a literal + * address, and fill in the uplink and downlink socket addresses. + * + ****************************************************************************/ + +static int lora_fwd_resolve(void) +{ + FAR struct addrinfo *res = NULL; + struct addrinfo hints; + int ret; + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_DGRAM; + hints.ai_protocol = IPPROTO_UDP; + + ret = getaddrinfo(g_config.server, NULL, &hints, &res); + if (ret != 0 || res == NULL) + { + fprintf(stderr, "lora: cannot resolve %s\n", g_config.server); + return -EHOSTUNREACH; + } + + memcpy(&g_addr_up, res->ai_addr, sizeof(struct sockaddr_in)); + freeaddrinfo(res); + + g_addr_up.sin_family = AF_INET; + g_addr_up.sin_port = htons(g_config.port_up); + + g_addr_down = g_addr_up; + g_addr_down.sin_port = htons(g_config.port_down); + + return OK; +} + +/**************************************************************************** + * Name: lora_fwd_opensockets + ****************************************************************************/ + +static int lora_fwd_opensockets(void) +{ + struct timeval tv; + int ret; + + ret = lora_fwd_resolve(); + if (ret < 0) + { + return ret; + } + + g_sock_up = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + if (g_sock_up < 0) + { + return -errno; + } + + g_sock_down = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + if (g_sock_down < 0) + { + ret = -errno; + close(g_sock_up); + g_sock_up = -1; + return ret; + } + + /* Both sockets time out so that neither loop can wedge */ + + tv.tv_sec = 0; + tv.tv_usec = LORA_FWD_RECV_MS * 1000; + setsockopt(g_sock_down, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); + setsockopt(g_sock_up, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); + + printf("lora: forwarding to %s (up %u, down %u)\n", + g_config.server, g_config.port_up, g_config.port_down); + return OK; +} + +/**************************************************************************** + * Name: lora_fwd_closesockets + ****************************************************************************/ + +static void lora_fwd_closesockets(void) +{ + if (g_sock_up >= 0) + { + close(g_sock_up); + g_sock_up = -1; + } + + if (g_sock_down >= 0) + { + close(g_sock_down); + g_sock_down = -1; + } +} + +/**************************************************************************** + * Name: lora_fwd_push + * + * Description: + * Send one PUSH_DATA carrying a JSON object. + * + ****************************************************************************/ + +static int lora_fwd_push(size_t jsonlen) +{ + int ret; + + if (jsonlen == 0 || jsonlen > LORA_FWD_JSONSIZE) + { + return -E2BIG; + } + + g_pushbuf[0] = LORA_FWD_PROTOCOL; + lora_fwd_token(&g_pushbuf[1]); + g_pushbuf[3] = LORA_PKT_PUSH_DATA; + memcpy(&g_pushbuf[4], g_config.eui, LORA_FWD_EUILEN); + + ret = sendto(g_sock_up, g_pushbuf, 12 + jsonlen, 0, + (FAR struct sockaddr *)&g_addr_up, sizeof(g_addr_up)); + if (ret < 0) + { + ret = -errno; + if (ret != g_lasterr_up) + { + g_lasterr_up = ret; + fprintf(stderr, "lora: cannot reach %s: %d\n", g_config.server, + -ret); + } + + return ret; + } + + g_lasterr_up = 0; + + g_stats.push_sent++; + return OK; +} + +/**************************************************************************** + * Name: lora_fwd_pull + * + * Description: + * Send one PULL_DATA so that the server learns our address and can push + * downlinks back through the same NAT binding. + * + ****************************************************************************/ + +static int lora_fwd_pull(void) +{ + uint8_t buffer[12]; + int ret; + + buffer[0] = LORA_FWD_PROTOCOL; + lora_fwd_token(&buffer[1]); + buffer[3] = LORA_PKT_PULL_DATA; + memcpy(&buffer[4], g_config.eui, LORA_FWD_EUILEN); + + ret = sendto(g_sock_down, buffer, sizeof(buffer), 0, + (FAR struct sockaddr *)&g_addr_down, sizeof(g_addr_down)); + if (ret < 0) + { + ret = -errno; + if (ret != g_lasterr_down) + { + g_lasterr_down = ret; + fprintf(stderr, "lora: keepalive to %s failed: %d\n", + g_config.server, -ret); + } + + return ret; + } + + g_lasterr_down = 0; + + g_stats.pull_sent++; + return OK; +} + +/**************************************************************************** + * Name: lora_fwd_bwstr + ****************************************************************************/ + +static FAR const char *lora_fwd_bwstr(uint8_t bandwidth) +{ + switch (bandwidth) + { + case LORA_GW_BW_250K: + return "BW250"; + + case LORA_GW_BW_500K: + return "BW500"; + + default: + return "BW125"; + } +} + +/**************************************************************************** + * Name: lora_fwd_crstr + ****************************************************************************/ + +static FAR const char *lora_fwd_crstr(uint8_t coderate) +{ + switch (coderate) + { + case WLIOC_LORA_CR_4_6: + return "4/6"; + + case WLIOC_LORA_CR_4_7: + return "4/7"; + + case WLIOC_LORA_CR_4_8: + return "4/8"; + + default: + return "4/5"; + } +} + +/**************************************************************************** + * Name: lora_fwd_buildrxpk + * + * Description: + * Serialise received packets into the "rxpk" object of the Semtech + * protocol. Frequencies are printed straight from their integer Hz value + * so that no floating point support is needed. + * + ****************************************************************************/ + +static int lora_fwd_buildrxpk(FAR const struct lora_gw_rxpkt_s *pkts, + int npkt, FAR char *json, size_t jsonlen) +{ + char payload[512]; + size_t b64len; + size_t pos = 0; + int i; + int ret; + + ret = snprintf(json, jsonlen, "{\"rxpk\":["); + if (ret < 0) + { + return ret; + } + + pos = ret; + + for (i = 0; i < npkt; i++) + { + FAR const struct lora_gw_rxpkt_s *pkt = &pkts[i]; + + b64len = sizeof(payload); + if (base64_encode(pkt->payload, pkt->size, payload, &b64len) == NULL) + { + continue; + } + + payload[b64len] = '\0'; + + ret = snprintf(json + pos, jsonlen - pos, + "%s{\"tmst\":%" PRIu32 "," + "\"chan\":%u," + "\"rfch\":%u," + "\"freq\":%" PRIu32 ".%06" PRIu32 "," + "\"stat\":%d," + "\"modu\":\"LORA\"," + "\"datr\":\"SF%u%s\"," + "\"codr\":\"%s\"," + "\"lsnr\":%d.%d," + "\"rssi\":%d," + "\"size\":%u," + "\"data\":\"%s\"}", + i > 0 ? "," : "", + pkt->count_us, + pkt->if_chain, + pkt->rf_chain, + pkt->freq_hz / 1000000, pkt->freq_hz % 1000000, + pkt->status == LORA_GW_STAT_CRC_OK ? 1 : + (pkt->status == LORA_GW_STAT_NO_CRC ? 0 : -1), + pkt->datarate, lora_fwd_bwstr(pkt->bandwidth), + lora_fwd_crstr(pkt->coderate), + pkt->snr_db10 / 10, abs(pkt->snr_db10 % 10), + pkt->rssi_dbm10 / 10, + pkt->size, payload); + + if (ret < 0 || (size_t)ret >= jsonlen - pos) + { + break; + } + + pos += ret; + } + + ret = snprintf(json + pos, jsonlen - pos, "]}"); + if (ret < 0) + { + return ret; + } + + return pos + ret; +} + +/**************************************************************************** + * Name: lora_fwd_buildstat + ****************************************************************************/ + +static int lora_fwd_buildstat(FAR char *json, size_t jsonlen) +{ + struct lora_gw_status_s status; + char timestr[32]; + struct tm tm; + time_t now; + unsigned int ackr_int = 0; + unsigned int ackr_frac = 0; + + memset(&status, 0, sizeof(status)); + if (g_devfd >= 0) + { + ioctl(g_devfd, WLIOC_GW_GETSTATUS, (unsigned long)&status); + } + + now = time(NULL); + gmtime_r(&now, &tm); + strftime(timestr, sizeof(timestr), "%Y-%m-%d %H:%M:%S", &tm); + + /* Acknowledge ratio in percent, one decimal, computed with integers */ + + if (g_stats.push_sent > 0) + { + uint32_t permille = (uint32_t)((uint64_t)g_stats.push_ack * 1000 / + g_stats.push_sent); + ackr_int = permille / 10; + ackr_frac = permille % 10; + } + + return snprintf(json, jsonlen, + "{\"stat\":{" + "\"time\":\"%s GMT\"," + "\"rxnb\":%" PRIu32 "," + "\"rxok\":%" PRIu32 "," + "\"rxfw\":%" PRIu32 "," + "\"ackr\":%u.%u," + "\"dwnb\":%" PRIu32 "," + "\"txnb\":%" PRIu32 + "}}", + timestr, + status.rx_ok + status.rx_nocrc, + status.rx_ok, + g_stats.rx_fwd, + ackr_int, ackr_frac, + g_stats.tx_req, + g_stats.tx_ok); +} + +/**************************************************************************** + * Name: lora_fwd_jsonfind + * + * Description: + * Locate the value of a key in a flat JSON object. + * + ****************************************************************************/ + +static FAR const char *lora_fwd_jsonfind(FAR const char *json, + FAR const char *key) +{ + char pattern[24]; + FAR const char *p; + + snprintf(pattern, sizeof(pattern), "\"%s\"", key); + + p = strstr(json, pattern); + if (p == NULL) + { + return NULL; + } + + p = strchr(p + strlen(pattern), ':'); + if (p == NULL) + { + return NULL; + } + + p++; + while (*p == ' ') + { + p++; + } + + return p; +} + +/**************************************************************************** + * Name: lora_fwd_jsonbool + ****************************************************************************/ + +static bool lora_fwd_jsonbool(FAR const char *json, FAR const char *key, + bool dflt) +{ + FAR const char *value = lora_fwd_jsonfind(json, key); + + return value != NULL ? (*value == 't') : dflt; +} + +/**************************************************************************** + * Name: lora_fwd_jsonuint + ****************************************************************************/ + +static uint32_t lora_fwd_jsonuint(FAR const char *json, FAR const char *key, + uint32_t dflt) +{ + FAR const char *value = lora_fwd_jsonfind(json, key); + + return value != NULL ? strtoul(value, NULL, 10) : dflt; +} + +/**************************************************************************** + * Name: lora_fwd_jsonint + ****************************************************************************/ + +static int32_t lora_fwd_jsonint(FAR const char *json, FAR const char *key, + int32_t dflt) +{ + FAR const char *value = lora_fwd_jsonfind(json, key); + + return value != NULL ? strtol(value, NULL, 10) : dflt; +} + +/**************************************************************************** + * Name: lora_fwd_jsonstr + ****************************************************************************/ + +static int lora_fwd_jsonstr(FAR const char *json, FAR const char *key, + FAR char *out, size_t outlen) +{ + FAR const char *value = lora_fwd_jsonfind(json, key); + size_t i = 0; + + if (value == NULL || *value != '"') + { + return -ENOENT; + } + + value++; + while (*value != '\0' && *value != '"' && i < outlen - 1) + { + out[i++] = *value++; + } + + out[i] = '\0'; + return i; +} + +/**************************************************************************** + * Name: lora_fwd_jsonfreq + * + * Description: + * Read a frequency given in MHz with a fractional part and return it in + * Hz, without going through a double. "917.2" and "917.200000" both give + * 917200000. + * + ****************************************************************************/ + +static uint32_t lora_fwd_jsonfreq(FAR const char *json, + FAR const char *key) +{ + FAR const char *value = lora_fwd_jsonfind(json, key); + uint32_t mhz = 0; + uint32_t frac = 0; + int digits = 0; + + if (value == NULL) + { + return 0; + } + + while (*value >= '0' && *value <= '9') + { + mhz = mhz * 10 + (*value++ - '0'); + } + + if (*value == '.') + { + value++; + while (*value >= '0' && *value <= '9' && digits < 6) + { + frac = frac * 10 + (*value++ - '0'); + digits++; + } + } + + while (digits++ < 6) + { + frac *= 10; + } + + return mhz * 1000000 + frac; +} + +/**************************************************************************** + * Name: lora_fwd_txack + ****************************************************************************/ + +static void lora_fwd_txack(FAR const uint8_t *token, + FAR const struct sockaddr_in *peer, + FAR const char *error) +{ + uint8_t buffer[128]; + int len; + + buffer[0] = LORA_FWD_PROTOCOL; + buffer[1] = token[0]; + buffer[2] = token[1]; + buffer[3] = LORA_PKT_TX_ACK; + + len = snprintf((FAR char *)&buffer[4], sizeof(buffer) - 4, + "{\"txpk_ack\":{\"error\":\"%s\"}}", error); + + sendto(g_sock_down, buffer, 4 + len, 0, + (FAR const struct sockaddr *)peer, sizeof(*peer)); +} + +/**************************************************************************** + * Name: lora_fwd_pullresp + * + * Description: + * Turn a PULL_RESP into a transmission. + * + ****************************************************************************/ + +static void lora_fwd_pullresp(FAR const uint8_t *raw, size_t rawlen, + FAR const uint8_t *token, + FAR const struct sockaddr_in *peer) +{ + struct lora_gw_txpkt_s txpkt; + char json[LORA_FWD_RXBUFSIZE]; + char datr[16]; + char codr[8]; + char data[512]; + size_t decoded; + size_t len; + int sf; + int bw; + int ret; + + g_stats.tx_req++; + + len = rawlen < sizeof(json) - 1 ? rawlen : sizeof(json) - 1; + memcpy(json, raw, len); + json[len] = '\0'; + + if (strstr(json, "txpk") == NULL) + { + fprintf(stderr, "lora: PULL_RESP without txpk\n"); + lora_fwd_txack(token, peer, "INVALID_JSON"); + return; + } + + memset(&txpkt, 0, sizeof(txpkt)); + + if (lora_fwd_jsonbool(json, "imme", false)) + { + txpkt.tx_mode = LORA_GW_TX_IMMEDIATE; + } + else + { + txpkt.tx_mode = LORA_GW_TX_TIMESTAMPED; + txpkt.count_us = lora_fwd_jsonuint(json, "tmst", 0); + } + + txpkt.freq_hz = lora_fwd_jsonfreq(json, "freq"); + txpkt.rf_chain = lora_fwd_jsonuint(json, "rfch", 0); + txpkt.rf_power = lora_fwd_jsonint(json, "powe", 14); + txpkt.modulation = LORA_GW_MOD_LORA; + txpkt.invert_pol = lora_fwd_jsonbool(json, "ipol", true); + txpkt.preamble = lora_fwd_jsonuint(json, "prea", 8); + txpkt.no_crc = lora_fwd_jsonbool(json, "ncrc", false); + txpkt.size = lora_fwd_jsonuint(json, "size", 0); + + /* Data rate, given as "SF7BW125" */ + + sf = 7; + bw = 125; + memset(datr, 0, sizeof(datr)); + if (lora_fwd_jsonstr(json, "datr", datr, sizeof(datr)) > 0) + { + sscanf(datr, "SF%dBW%d", &sf, &bw); + } + + txpkt.datarate = sf; + if (bw == 500) + { + txpkt.bandwidth = LORA_GW_BW_500K; + } + else if (bw == 250) + { + txpkt.bandwidth = LORA_GW_BW_250K; + } + else + { + txpkt.bandwidth = LORA_GW_BW_125K; + } + + /* Coding rate */ + + memset(codr, 0, sizeof(codr)); + lora_fwd_jsonstr(json, "codr", codr, sizeof(codr)); + if (strcmp(codr, "4/6") == 0 || strcmp(codr, "2/3") == 0) + { + txpkt.coderate = WLIOC_LORA_CR_4_6; + } + else if (strcmp(codr, "4/7") == 0) + { + txpkt.coderate = WLIOC_LORA_CR_4_7; + } + else if (strcmp(codr, "4/8") == 0 || strcmp(codr, "1/2") == 0) + { + txpkt.coderate = WLIOC_LORA_CR_4_8; + } + else + { + txpkt.coderate = WLIOC_LORA_CR_4_5; + } + + /* Payload */ + + memset(data, 0, sizeof(data)); + ret = lora_fwd_jsonstr(json, "data", data, sizeof(data)); + if (ret > 0) + { + decoded = sizeof(txpkt.payload); + if (base64_decode(data, ret, txpkt.payload, &decoded) == NULL) + { + fprintf(stderr, "lora: cannot decode the downlink payload\n"); + lora_fwd_txack(token, peer, "INVALID_JSON"); + return; + } + + if (txpkt.size == 0 || txpkt.size > decoded) + { + txpkt.size = decoded; + } + } + + if (txpkt.size == 0 || txpkt.freq_hz == 0) + { + lora_fwd_txack(token, peer, "INVALID_JSON"); + return; + } + + ret = write(g_devfd, &txpkt, sizeof(txpkt)); + if (ret == sizeof(txpkt)) + { + g_stats.tx_ok++; + lora_fwd_txack(token, peer, "NONE"); + printf("lora: downlink %" PRIu32 ".%06" PRIu32 " MHz SF%u %u bytes\n", + txpkt.freq_hz / 1000000, txpkt.freq_hz % 1000000, + txpkt.datarate, txpkt.size); + } + else + { + fprintf(stderr, "lora: transmit failed: %d\n", errno); + lora_fwd_txack(token, peer, "TX_FAIL"); + } +} + +/**************************************************************************** + * Name: lora_fwd_downloop + * + * Description: + * Keepalive and downlink handling. + * + ****************************************************************************/ + +static FAR void *lora_fwd_downloop(FAR void *arg) +{ + uint8_t buffer[LORA_FWD_RXBUFSIZE]; + struct sockaddr_in peer; + socklen_t peerlen; + time_t lastpull = 0; + ssize_t nread; + + UNUSED(arg); + + /* Announce ourselves before any uplink is forwarded */ + + lora_fwd_pull(); + lastpull = time(NULL); + + while (!g_stopreq) + { + peerlen = sizeof(peer); + nread = recvfrom(g_sock_down, buffer, sizeof(buffer), 0, + (FAR struct sockaddr *)&peer, &peerlen); + + if (nread >= 4) + { + switch (buffer[3]) + { + case LORA_PKT_PULL_ACK: + g_stats.pull_ack++; + break; + + case LORA_PKT_PULL_RESP: + lora_fwd_pullresp(&buffer[4], nread - 4, &buffer[1], &peer); + break; + + default: + fprintf(stderr, "lora: unexpected downlink type 0x%02x\n", + buffer[3]); + break; + } + } + + if (time(NULL) - lastpull >= g_config.keepalive_sec) + { + lora_fwd_pull(); + lastpull = time(NULL); + } + } + + return NULL; +} + +/**************************************************************************** + * Name: lora_fwd_uploop + * + * Description: + * Forward received packets and push the periodic status. + * + ****************************************************************************/ + +static void lora_fwd_uploop(void) +{ + struct lora_gw_rxpkt_s pkts[LORA_FWD_NPKT]; + uint8_t ack[4]; + time_t laststat; + ssize_t nread; + int npkt; + int len; + + laststat = time(NULL); + + while (!g_stopreq) + { + nread = read(g_devfd, pkts, sizeof(pkts)); + if (nread > 0) + { + npkt = nread / sizeof(struct lora_gw_rxpkt_s); + + len = lora_fwd_buildrxpk(pkts, npkt, LORA_FWD_JSON, + LORA_FWD_JSONSIZE); + if (len > 0 && lora_fwd_push(len) == OK) + { + g_stats.rx_fwd += npkt; + printf("lora: forwarded %d packet(s)\n", npkt); + } + else + { + g_stats.rx_drop += npkt; + } + } + else if (nread < 0 && errno != EAGAIN) + { + fprintf(stderr, "lora: read error %d\n", errno); + break; + } + + if (time(NULL) - laststat >= g_config.stat_sec) + { + len = lora_fwd_buildstat(LORA_FWD_JSON, LORA_FWD_JSONSIZE); + if (len > 0) + { + lora_fwd_push(len); + } + + laststat = time(NULL); + } + + /* Collect the acknowledges of what we sent */ + + while (recv(g_sock_up, ack, sizeof(ack), MSG_DONTWAIT) > 0) + { + g_stats.push_ack++; + } + + usleep(LORA_FWD_POLL_MS * 1000); + } +} + +/**************************************************************************** + * Name: lora_fwd_daemon + ****************************************************************************/ + +static int lora_fwd_daemon(int argc, FAR char *argv[]) +{ + pthread_attr_t attr; + int retries; + int ret; + + UNUSED(argc); + UNUSED(argv); + + g_devfd = open(CONFIG_LORA_PKT_FWD_DEVPATH, + O_RDWR | O_NONBLOCK | O_CLOEXEC); + if (g_devfd < 0) + { + fprintf(stderr, "lora: cannot open %s: %d\n", + CONFIG_LORA_PKT_FWD_DEVPATH, errno); + g_running = false; + return EXIT_FAILURE; + } + + ret = ioctl(g_devfd, WLIOC_GW_START, 0); + if (ret < 0) + { + fprintf(stderr, "lora: cannot start the concentrator: %d\n", errno); + goto errout_dev; + } + + /* The name of the server may not resolve on the first try, for instance + * when the link has just come up. Keep trying rather than tearing the + * concentrator down. + */ + + for (retries = 0; retries < LORA_FWD_RESOLVE_TRIES; retries++) + { + ret = lora_fwd_opensockets(); + if (ret >= 0 || g_stopreq) + { + break; + } + + sleep(LORA_FWD_RESOLVE_DELAY); + } + + if (ret < 0) + { + goto errout_stop; + } + + memset(&g_stats, 0, sizeof(g_stats)); + g_starttime = time(NULL); + + pthread_attr_init(&attr); + pthread_attr_setstacksize(&attr, CONFIG_LORA_PKT_FWD_STACKSIZE); + + ret = pthread_create(&g_downthread, &attr, lora_fwd_downloop, NULL); + pthread_attr_destroy(&attr); + + if (ret != 0) + { + fprintf(stderr, "lora: cannot start the downlink thread: %d\n", ret); + ret = -ret; + goto errout_sockets; + } + + pthread_setname_np(g_downthread, "lora_down"); + + printf("lora: packet forwarder running\n"); + + lora_fwd_uploop(); + + pthread_join(g_downthread, NULL); + +errout_sockets: + lora_fwd_closesockets(); + +errout_stop: + ioctl(g_devfd, WLIOC_GW_STOP, 0); + +errout_dev: + close(g_devfd); + g_devfd = -1; + g_running = false; + g_stopreq = false; + + printf("lora: packet forwarder stopped\n"); + return ret < 0 ? EXIT_FAILURE : EXIT_SUCCESS; +} + +/**************************************************************************** + * Public Functions + ****************************************************************************/ + +/**************************************************************************** + * Name: lora_fwd_init + * + * Description: + * Resolve the compiled in defaults once. Safe to call repeatedly. + * + ****************************************************************************/ + +void lora_fwd_init(void) +{ + static bool initialised; + uint8_t mac[IFHWADDRLEN]; + + if (initialised) + { + return; + } + + initialised = true; + + if (lora_fwd_parse_eui(CONFIG_LORA_PKT_FWD_EUI, g_config.eui) < 0) + { + fprintf(stderr, "lora: bad gateway identifier '%s'\n", + CONFIG_LORA_PKT_FWD_EUI); + } + +#ifdef CONFIG_LORA_PKT_FWD_EUI_FROM_MAC + /* Build an EUI-64 out of the Ethernet address, the usual way: the three + * bytes of the vendor identifier, then FFFE, then the rest. + */ + + if (netlib_getmacaddr(LORA_FWD_IFNAME, mac) == OK) + { + g_config.eui[0] = mac[0]; + g_config.eui[1] = mac[1]; + g_config.eui[2] = mac[2]; + g_config.eui[3] = 0xff; + g_config.eui[4] = 0xfe; + g_config.eui[5] = mac[3]; + g_config.eui[6] = mac[4]; + g_config.eui[7] = mac[5]; + } +#else + UNUSED(mac); +#endif +} + +/**************************************************************************** + * Name: lora_fwd_start + ****************************************************************************/ + +int lora_fwd_start(void) +{ + int pid; + + lora_fwd_init(); + + if (g_running) + { + return -EALREADY; + } + + g_stopreq = false; + g_running = true; + + pid = task_create("lora_fwd", CONFIG_LORA_PKT_FWD_PRIORITY, + CONFIG_LORA_PKT_FWD_STACKSIZE, lora_fwd_daemon, NULL); + if (pid < 0) + { + g_running = false; + return -errno; + } + + return OK; +} + +/**************************************************************************** + * Name: lora_fwd_stop + ****************************************************************************/ + +int lora_fwd_stop(void) +{ + int i; + + if (!g_running) + { + return OK; + } + + g_stopreq = true; + + /* Give both loops time to notice: a socket call may be blocked resolving + * the address of an unreachable server. + */ + + for (i = 0; i < LORA_FWD_STOP_TIMEOUT * 10 && g_running; i++) + { + usleep(100000); + } + + return g_running ? -ETIMEDOUT : OK; +} + +/**************************************************************************** + * Name: lora_fwd_isrunning + ****************************************************************************/ + +bool lora_fwd_isrunning(void) +{ + return g_running; +} + +/**************************************************************************** + * Name: lora_fwd_config + ****************************************************************************/ + +FAR struct lora_fwd_config_s *lora_fwd_config(void) +{ + return &g_config; +} + +/**************************************************************************** + * Name: lora_fwd_getstats + ****************************************************************************/ + +void lora_fwd_getstats(FAR struct lora_fwd_stats_s *stats) +{ + memcpy(stats, &g_stats, sizeof(*stats)); + + stats->uptime_sec = g_running ? (uint32_t)(time(NULL) - g_starttime) : 0; +} + +/**************************************************************************** + * Name: lora_fwd_setserver + * + * Description: + * Change the network server. Takes effect the next time the forwarder is + * started. + * + ****************************************************************************/ + +int lora_fwd_setserver(FAR const char *host, int port_up, int port_down) +{ + if (host != NULL) + { + strlcpy(g_config.server, host, sizeof(g_config.server)); + } + + if (port_up > 0 && port_up <= UINT16_MAX) + { + g_config.port_up = port_up; + } + + if (port_down > 0 && port_down <= UINT16_MAX) + { + g_config.port_down = port_down; + } + + return OK; +} + +/**************************************************************************** + * Name: lora_fwd_parse_eui + * + * Description: + * Convert a sixteen digit hexadecimal string into the eight byte gateway + * identifier. + * + ****************************************************************************/ + +int lora_fwd_parse_eui(FAR const char *str, FAR uint8_t *eui) +{ + char byte[3]; + int i; + + if (str == NULL || strlen(str) < 2 * LORA_FWD_EUILEN) + { + return -EINVAL; + } + + byte[2] = '\0'; + + for (i = 0; i < LORA_FWD_EUILEN; i++) + { + byte[0] = str[2 * i]; + byte[1] = str[2 * i + 1]; + + if (!isxdigit(byte[0]) || !isxdigit(byte[1])) + { + return -EINVAL; + } + + eui[i] = strtoul(byte, NULL, 16); + } + + return OK; +} diff --git a/wireless/lora_pkt_fwd/lora_pkt_fwd.h b/wireless/lora_pkt_fwd/lora_pkt_fwd.h new file mode 100644 index 00000000000..6b6aa2a9cce --- /dev/null +++ b/wireless/lora_pkt_fwd/lora_pkt_fwd.h @@ -0,0 +1,109 @@ +/**************************************************************************** + * apps/wireless/lora_pkt_fwd/lora_pkt_fwd.h + * + * SPDX-License-Identifier: Apache-2.0 + * + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. The + * ASF licenses this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + * + ****************************************************************************/ + +#ifndef __APPS_WIRELESS_LORA_PKT_FWD_LORA_PKT_FWD_H +#define __APPS_WIRELESS_LORA_PKT_FWD_LORA_PKT_FWD_H + +/**************************************************************************** + * Included Files + ****************************************************************************/ + +#include + +#include +#include + +/**************************************************************************** + * Pre-processor Definitions + ****************************************************************************/ + +#define LORA_FWD_HOSTLEN 64 +#define LORA_FWD_EUILEN 8 +#define LORA_FWD_IFNAME "eth0" + +/* Semtech UDP protocol version 2 packet types. Note that the numbering is + * not sequential: PULL_RESP comes before PULL_ACK. Swapping those two makes + * a gateway answer INVALID_JSON to every keepalive. + */ + +#define LORA_PKT_PUSH_DATA 0x00 +#define LORA_PKT_PUSH_ACK 0x01 +#define LORA_PKT_PULL_DATA 0x02 +#define LORA_PKT_PULL_RESP 0x03 +#define LORA_PKT_PULL_ACK 0x04 +#define LORA_PKT_TX_ACK 0x05 + +/**************************************************************************** + * Public Types + ****************************************************************************/ + +/* Runtime configuration of the forwarder ***********************************/ + +struct lora_fwd_config_s +{ + char server[LORA_FWD_HOSTLEN]; /* Host name or address of the server */ + uint16_t port_up; + uint16_t port_down; + uint8_t eui[LORA_FWD_EUILEN]; /* Gateway identifier */ + int keepalive_sec; + int stat_sec; +}; + +/* Counters of the forwarder itself. The radio side counters come from the + * driver with WLIOC_GW_GETSTATUS. + */ + +struct lora_fwd_stats_s +{ + uint32_t rx_fwd; /* Packets forwarded upstream */ + uint32_t rx_drop; /* Packets dropped before forwarding */ + uint32_t tx_req; /* Downlink requests received */ + uint32_t tx_ok; /* Downlinks handed to the concentrator */ + uint32_t push_sent; + uint32_t push_ack; + uint32_t pull_sent; + uint32_t pull_ack; + uint32_t uptime_sec; +}; + +/**************************************************************************** + * Public Function Prototypes + ****************************************************************************/ + +/* Daemon control, lora_pkt_fwd.c *******************************************/ + +void lora_fwd_init(void); +int lora_fwd_start(void); +int lora_fwd_stop(void); +bool lora_fwd_isrunning(void); + +/* Configuration and statistics. The daemon runs as its own task and shares + * this state with the "lora" command through the flat address space, which + * is why the counters are only visible in a flat build. + */ + +FAR struct lora_fwd_config_s *lora_fwd_config(void); +void lora_fwd_getstats(FAR struct lora_fwd_stats_s *stats); +int lora_fwd_setserver(FAR const char *host, int port_up, int port_down); +int lora_fwd_parse_eui(FAR const char *str, FAR uint8_t *eui); + +#endif /* __APPS_WIRELESS_LORA_PKT_FWD_LORA_PKT_FWD_H */