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 */