diff --git a/.gitignore b/.gitignore index 50f8f64..57ace6f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ build/ sdkconfig +sdkconfig.old dev_keys.txt ttn-esp32 diff --git a/.vscode/c_cpp_properties.json b/.vscode/c_cpp_properties.json index 934642c..199c206 100644 --- a/.vscode/c_cpp_properties.json +++ b/.vscode/c_cpp_properties.json @@ -16,7 +16,9 @@ "${IDF_PATH}/components/soc/include", "${IDF_PATH}/components/soc/esp32/include", "${IDF_PATH}/components/tcpip_adapter/include", + "${workspaceRoot}/examples/provisioning/build/include", "${workspaceRoot}/examples/hello_world/build/include", + "${workspaceRoot}/examples/send_recv/build/include", "${workspaceRoot}/include", "${workspaceRoot}/src" ], diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..28b96ed --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "files.associations": { + "provisioning.h": "c", + "config.h": "c", + "sdkconfig.h": "c", + "oslmic.h": "c", + "hal_esp32.h": "c" + } +} \ No newline at end of file diff --git a/Kconfig b/Kconfig index c6e7035..5850a92 100644 --- a/Kconfig +++ b/Kconfig @@ -2,13 +2,13 @@ menu "The Things Network" choice TTN_LORA_FREQ prompt "TTN LoRa frequency" - default TTN_LORA_FREQ_NONE + default TTN_LORA_FREQ_DISABLED help LoRa frequency must match the geographic region the device is operated in. - Running it with the incorrect frequency most like violates the law. + Running it with the incorrect frequency most likely violates the law. -config TTN_LORA_FREQ_NONE - bool "None" +config TTN_LORA_FREQ_DISABLED + bool "LoRa Disabled" config TTN_LORA_FREQ_EU_868 bool "868 MHz (Europe)" @@ -68,4 +68,75 @@ config TTN_BG_TASK_PRIO the LoRaWAN radio chip. It needs a high priority as the timing is crucial. Higher numbers indicate higher priority. + +choice TTN_PROVISION_UART + prompt "UART for provisioning" + default TTN_PROVISION_UART_DEFAULT + help + Select whether to use UART for listening for provisioning commands. + + - Default is to use UART0 on pins GPIO1(TX) and GPIO3(RX). + - If "Custom" is selected, UART0 or UART1 can be chosen, + and any pins can be selected. + - If "None" is selected, the feature is not available. + +config TTN_PROVISION_UART_DEFAULT + bool "Default: UART0, TX=GPIO1, RX=GPIO3, 115,200 baud" +config TTN_PROVISION_UART_CUSTOM + bool "Custom" +config TTN_PROVISION_UART_NONE + bool "None" +endchoice + +choice TTN_PROVISION_UART_NUM + prompt "UART peripheral for provisioning (0-1)" + depends on TTN_PROVISION_UART_CUSTOM + default TTN_PROVISION_UART_CUSTOM_NUM_0 + +config TTN_PROVISION_UART_CUSTOM_NUM_0 + bool "UART0" +config TTN_PROVISION_UART_CUSTOM_NUM_1 + bool "UART1" +endchoice + +choice TTN_PROVISION_UART_CONFIG + prompt "Configure UART settings" + default TTN_PROVISION_UART_CONFIG_NO + depends on TTN_PROVISION_UART_CUSTOM + help + Select whether to configure the UART, i.e. set the baud rate, the RX and TX + pins. If the UART is shared with other features (e.g. the console), it + should not be configured. + +config TTN_PROVISION_UART_CONFIG_NO + bool "No" +config TTN_PROVISION_UART_CONFIG_YES + bool "Yes" +endchoice + +config TTN_PROVISION_UART_NUM + int + default 0 if TTN_PROVISION_UART_DEFAULT || TTN_PROVISION_UART_NONE + default 0 if TTN_PROVISION_UART_CUSTOM_NUM_0 + default 1 if TTN_PROVISION_UART_CUSTOM_NUM_1 + +config TTN_PROVISION_UART_TX_GPIO + int "Provisioning UART TX on GPIO#" + depends on TTN_PROVISION_UART_CONFIG_YES + range 0 33 + default 1 + +config TTN_PROVISION_UART_RX_GPIO + int "Provisioning UART RX on GPIO#" + depends on TTN_PROVISION_UART_CONFIG_YES + range 0 39 + default 3 + +config TTN_PROVISION_UART_BAUDRATE + int "Provisioning UART baud rate" + depends on TTN_PROVISION_UART_CONFIG_YES + range 1200 4000000 + default 115200 + + endmenu diff --git a/examples/provisioning/Makefile b/examples/provisioning/Makefile new file mode 100644 index 0000000..5866668 --- /dev/null +++ b/examples/provisioning/Makefile @@ -0,0 +1,3 @@ +PROJECT_NAME := provisioning + +include $(IDF_PATH)/make/project.mk diff --git a/examples/provisioning/main/component.mk b/examples/provisioning/main/component.mk new file mode 100644 index 0000000..e69de29 diff --git a/examples/provisioning/main/main.cpp b/examples/provisioning/main/main.cpp new file mode 100644 index 0000000..9c1cad9 --- /dev/null +++ b/examples/provisioning/main/main.cpp @@ -0,0 +1,91 @@ +/******************************************************************************* + * + * ttn-esp32 - The Things Network device library for ESP-IDF / SX127x + * + * Copyright (c) 2018 Manuel Bleichenbacher + * + * Licensed under MIT License + * https://opensource.org/licenses/MIT + * + * Sample program showing how to provision the keys via the console. + *******************************************************************************/ + +#include "freertos/FreeRTOS.h" +#include "esp_event.h" +#include "nvs_flash.h" + +#include "TheThingsNetwork.h" + +// NOTE: +// The LoRaWAN frequency and the radio chip must be configured by running 'make menuconfig'. +// Go to Components / The Things Network, select the appropriate values and save. + +// Pins and other resources +#define TTN_SPI_HOST HSPI_HOST +#define TTN_SPI_DMA_CHAN 1 +#define TTN_PIN_SPI_SCLK 5 +#define TTN_PIN_SPI_MOSI 27 +#define TTN_PIN_SPI_MISO 19 +#define TTN_PIN_NSS 18 +#define TTN_PIN_RXTX TTN_NOT_CONNECTED +#define TTN_PIN_RST 14 +#define TTN_PIN_DIO0 26 +#define TTN_PIN_DIO1 33 + +static TheThingsNetwork ttn; + +const unsigned TX_INTERVAL = 30; +static uint8_t msgData[] = "Hello, world"; + + +void sendMessages(void* pvParameter) +{ + while (1) { + printf("Sending message...\n"); + TTNResponseCode res = ttn.transmitMessage(msgData, sizeof(msgData) - 1); + printf(res == kTTNSuccessfulTransmission ? "Message sent.\n" : "Transmission failed.\n"); + + vTaskDelay(TX_INTERVAL * 1000 / portTICK_PERIOD_MS); + } +} + +extern "C" void app_main(void) +{ + esp_err_t err; + // Initialize the GPIO ISR handler service + err = gpio_install_isr_service(ESP_INTR_FLAG_IRAM); + ESP_ERROR_CHECK(err); + + // Initialize the NVS (non-volatile storage) for saving and restoring the keys + err = nvs_flash_init(); + ESP_ERROR_CHECK(err); + + // Initialize SPI bus + spi_bus_config_t spi_bus_config; + spi_bus_config.miso_io_num = TTN_PIN_SPI_MISO; + spi_bus_config.mosi_io_num = TTN_PIN_SPI_MOSI; + spi_bus_config.sclk_io_num = TTN_PIN_SPI_SCLK; + spi_bus_config.quadwp_io_num = -1; + spi_bus_config.quadhd_io_num = -1; + spi_bus_config.max_transfer_sz = 0; + err = spi_bus_initialize(TTN_SPI_HOST, &spi_bus_config, TTN_SPI_DMA_CHAN); + ESP_ERROR_CHECK(err); + + // Configure the SX127x pins + ttn.configurePins(TTN_SPI_HOST, TTN_PIN_NSS, TTN_PIN_RXTX, TTN_PIN_RST, TTN_PIN_DIO0, TTN_PIN_DIO1); + + ttn.startProvisioningTask(); + + ttn.waitForProvisioning(); + + printf("Joining...\n"); + if (ttn.join()) + { + printf("Joined.\n"); + xTaskCreate(sendMessages, "send_messages", 1024 * 4, (void* )0, 3, NULL); + } + else + { + printf("Join failed. Goodbye\n"); + } +} diff --git a/include/TheThingsNetwork.h b/include/TheThingsNetwork.h index 2d4f906..291ca8c 100644 --- a/include/TheThingsNetwork.h +++ b/include/TheThingsNetwork.h @@ -7,7 +7,7 @@ * Licensed under MIT License * https://opensource.org/licenses/MIT * - * This the hardware abstraction layer to run LMIC in on ESP32 using ESP-iDF. + * High-level API for ttn-esp32. *******************************************************************************/ #ifndef _THETHINGSNETWORK_H_ @@ -104,6 +104,23 @@ public: */ bool provision(const char *devEui, const char *appEui, const char *appKey); + /** + * @brief Start task that listens on configured UART for provisioning commands. + * + * Run 'make menuconfig' to configure it. + */ + void startProvisioningTask(); + + /** + * @brief Wait until the device EUI, app EUI and app key have been provisioned + * via the provisioning task. + * + * If device is already provisioned (stored data in NVS, call to 'provision()' + * or call to 'join(const char*, const char*, const char*)', this function + * immediately returns. + */ + void waitForProvisioning(); + /** * @brief Activate the device via OTAA. * @@ -176,12 +193,8 @@ public: private: TTNMessageCallback messageCallback; - bool haveKeys; bool joinCore(); - bool decodeKeys(const char *devEui, const char *appEui, const char *appKey); - bool saveKeys(); - bool restoreKeys(bool silent); }; #endif diff --git a/src/TheThingsNetwork.cpp b/src/TheThingsNetwork.cpp index 0ca6d79..6eff020 100644 --- a/src/TheThingsNetwork.cpp +++ b/src/TheThingsNetwork.cpp @@ -7,18 +7,19 @@ * Licensed under MIT License * https://opensource.org/licenses/MIT * - * This the hardware abstraction layer to run LMIC in on ESP32 using ESP-iDF. + * High-level API for ttn-esp32. *******************************************************************************/ #include "freertos/FreeRTOS.h" #include "esp_event.h" -#include "nvs_flash.h" -#include "TheThingsNetwork.h" #include "esp_log.h" -#include "oslmic.h" +#include "TheThingsNetwork.h" +#include "config.h" #include "hal.h" #include "hal_esp32.h" #include "lmic.h" +#include "provisioning.h" + enum ClientAction { @@ -28,34 +29,24 @@ enum ClientAction }; static const char *TAG = "ttn"; -static const char *NVS_FLASH_PARTITION = "ttn"; -static const char *NVS_FLASH_KEY_DEV_EUI = "devEui"; -static const char *NVS_FLASH_KEY_APP_EUI = "appEui"; -static const char *NVS_FLASH_KEY_APP_KEY = "appKey"; static TheThingsNetwork* ttnInstance; -static uint8_t devEui[8]; -static uint8_t appEui[8]; -static uint8_t appKey[16]; static QueueHandle_t resultQueue; static ClientAction clientAction = eActionUnrelated; -static bool readNvsValue(nvs_handle handle, const char* key, uint8_t* data, size_t expectedLength, bool silent); -static bool writeNvsValue(nvs_handle handle, const char* key, const uint8_t* data, size_t len); -static bool hexStringToBin(const char *hex, uint8_t *buf, int len); -static int hexTupleToByte(const char *hex); -static int hexDigitToVal(char ch); -static void swapByteOrder(uint8_t* buf, int len); - - TheThingsNetwork::TheThingsNetwork() - : messageCallback(NULL), haveKeys(false) + : messageCallback(NULL) { +#if defined(TTN_IS_DISABLED) + ESP_LOGE(TAG, "TTN is disabled. Configure a frequency plan using 'make menuconfig'"); + ASSERT(0); + esp_restart(); +#endif + ASSERT(ttnInstance == NULL); ttnInstance = this; hal_initCriticalSection(); - } TheThingsNetwork::~TheThingsNetwork() @@ -89,45 +80,38 @@ void TheThingsNetwork::reset() bool TheThingsNetwork::provision(const char *devEui, const char *appEui, const char *appKey) { - if (!decodeKeys(devEui, appEui, appKey)) + if (!provisioning_decode_keys(devEui, appEui, appKey)) return false; - return saveKeys(); + return provisioning_save_keys(); } -bool TheThingsNetwork::decodeKeys(const char *devEui, const char *appEui, const char *appKey) +void TheThingsNetwork::startProvisioningTask() { - haveKeys = false; +#if !defined(CONFIG_TTN_PROVISION_UART_NONE) + provisioning_start_task(); +#endif +} - if (strlen(devEui) != 16 || !hexStringToBin(devEui, ::devEui, 8)) +void TheThingsNetwork::waitForProvisioning() +{ +#if !defined(CONFIG_TTN_PROVISION_UART_NONE) + if (isProvisioned()) { - ESP_LOGW(TAG, "Invalid device EUI: %s", devEui); - return false; + ESP_LOGI(TAG, "Device is already provisioned"); + return; } - swapByteOrder(::devEui, 8); + while (!provisioning_have_keys()) + vTaskDelay(1000 / portTICK_PERIOD_MS); - if (strlen(appEui) != 16 || !hexStringToBin(appEui, ::appEui, 8)) - { - ESP_LOGW(TAG, "Invalid application EUI: %s", appEui); - return false; - } - - swapByteOrder(::appEui, 8); - - if (strlen(appKey) != 32 || !hexStringToBin(appKey, ::appKey, 16)) - { - ESP_LOGW(TAG, "Invalid application key: %s", appEui); - return false; - } - - haveKeys = true; - return true; + ESP_LOGI(TAG, "Device successfully provisioned"); +#endif } bool TheThingsNetwork::join(const char *devEui, const char *appEui, const char *appKey) { - if (!decodeKeys(devEui, appEui, appKey)) + if (!provisioning_decode_keys(devEui, appEui, appKey)) return false; return joinCore(); @@ -135,9 +119,9 @@ bool TheThingsNetwork::join(const char *devEui, const char *appEui, const char * bool TheThingsNetwork::join() { - if (!haveKeys) + if (!provisioning_have_keys()) { - if (!restoreKeys(false)) + if (!provisioning_restore_keys(false)) return false; } @@ -146,7 +130,7 @@ bool TheThingsNetwork::join() bool TheThingsNetwork::joinCore() { - if (!haveKeys) + if (!provisioning_have_keys()) { ESP_LOGW(TAG, "Device EUI, App EUI and/or App key have not been provided"); return false; @@ -205,118 +189,15 @@ void TheThingsNetwork::onMessage(TTNMessageCallback callback) messageCallback = callback; } -bool TheThingsNetwork::saveKeys() -{ - bool result = false; - - nvs_handle handle = 0; - esp_err_t res = nvs_open(NVS_FLASH_PARTITION, NVS_READWRITE, &handle); - if (res == ESP_ERR_NVS_NOT_INITIALIZED) - { - ESP_LOGW(TAG, "NVS storage is not initialized. Call 'nvs_flash_init()' first."); - goto done; - } - ESP_ERROR_CHECK(res); - if (res != ESP_OK) - goto done; - - if (!writeNvsValue(handle, NVS_FLASH_KEY_DEV_EUI, ::devEui, sizeof(::devEui))) - goto done; - - if (!writeNvsValue(handle, NVS_FLASH_KEY_APP_EUI, ::appEui, sizeof(::appEui))) - goto done; - - if (!writeNvsValue(handle, NVS_FLASH_KEY_APP_KEY, ::appKey, sizeof(::appKey))) - goto done; - - res = nvs_commit(handle); - ESP_ERROR_CHECK(res); - - result = true; - ESP_LOGI(TAG, "Dev and app EUI and app key saved in NVS storage"); - -done: - nvs_close(handle); - return result; -} - -bool TheThingsNetwork::restoreKeys(bool silent) -{ - haveKeys = false; - - nvs_handle handle = 0; - esp_err_t res = nvs_open(NVS_FLASH_PARTITION, NVS_READONLY, &handle); - if (res == ESP_ERR_NVS_NOT_FOUND) - return false; // partition does not exist yet - if (res == ESP_ERR_NVS_NOT_INITIALIZED) - { - ESP_LOGW(TAG, "NVS storage is not initialized. Call 'nvs_flash_init()' first."); - goto done; - } - ESP_ERROR_CHECK(res); - if (res != ESP_OK) - goto done; - - if (!readNvsValue(handle, NVS_FLASH_KEY_DEV_EUI, ::devEui, sizeof(::devEui), silent)) - goto done; - - if (!readNvsValue(handle, NVS_FLASH_KEY_APP_EUI, ::appEui, sizeof(::appEui), silent)) - goto done; - - if (!readNvsValue(handle, NVS_FLASH_KEY_APP_KEY, ::appKey, sizeof(::appKey), silent)) - goto done; - - haveKeys = true; - ESP_LOGI(TAG, "Dev and app EUI and app key have been restored from NVS storage"); - -done: - nvs_close(handle); - return haveKeys; -} bool TheThingsNetwork::isProvisioned() { - if (haveKeys) + if (provisioning_have_keys()) return true; - return restoreKeys(true); -} + provisioning_restore_keys(true); -bool readNvsValue(nvs_handle handle, const char* key, uint8_t* data, size_t expectedLength, bool silent) -{ - size_t size = expectedLength; - esp_err_t res = nvs_get_blob(handle, key, data, &size); - if (res == ESP_OK && size == expectedLength) - return true; - - if (res == ESP_OK && size != expectedLength) - { - if (!silent) - ESP_LOGW(TAG, "Invalid size of NVS data for %s", key); - return false; - } - - if (res == ESP_ERR_NVS_NOT_FOUND) - { - if (!silent) - ESP_LOGW(TAG, "No NVS data found for %s", key); - return false; - } - - ESP_ERROR_CHECK(res); - return false; -} - -bool writeNvsValue(nvs_handle handle, const char* key, const uint8_t* data, size_t len) -{ - uint8_t buf[16]; - if (readNvsValue(handle, key, buf, len, true) && memcmp(buf, data, len) == 0) - return true; // unchanged - - esp_err_t res = nvs_set_blob(handle, key, data, len); - ESP_ERROR_CHECK(res); - - return res == ESP_OK; + return provisioning_have_keys(); } @@ -333,29 +214,6 @@ static const char *eventNames[] = { }; #endif -// This EUI must be in little-endian format, so least-significant-byte first. -// When copying an EUI from ttnctl output, this means to reverse the bytes. -// For TTN issued EUIs the last bytes should be 0xD5, 0xB3, 0x70. -// The order is swapped in TheThingsNetwork::provision(). -void os_getArtEui (u1_t* buf) -{ - memcpy(buf, appEui, 8); -} - -// This should also be in little endian format, see above. -void os_getDevEui (u1_t* buf) -{ - memcpy(buf, devEui, 8); -} - -// This key should be in big endian format (or, since it is not really a number -// but a block of memory, endianness does not really apply). In practice, a key -// taken from ttnctl can be copied as-is. -void os_getDevKey (u1_t* buf) -{ - memcpy(buf, appKey, 16); -} - void onEvent (ev_t ev) { #if CONFIG_LOG_DEFAULT_LEVEL >= 3 ESP_LOGI(TAG, "event %s", eventNames[ev]); @@ -372,12 +230,12 @@ void onEvent (ev_t ev) { } else if (clientAction == eActionJoining) { - if (ev != EV_JOINED && EV_REJOIN_FAILED) + if (ev != EV_JOINED && ev != EV_REJOIN_FAILED && ev != EV_RESET) return; } else { - if (ev != EV_TXCOMPLETE && ev != EV_LINK_DEAD) + if (ev != EV_TXCOMPLETE && ev != EV_LINK_DEAD && ev != EV_RESET) return; } @@ -385,56 +243,3 @@ void onEvent (ev_t ev) { clientAction = eActionUnrelated; xQueueSend(resultQueue, &result, 100 / portTICK_PERIOD_MS); } - - -// --- Helper functions --- - -bool hexStringToBin(const char *hex, uint8_t *buf, int len) -{ - const char* ptr = hex; - for (int i = 0; i < len; i++) - { - int val = hexTupleToByte(ptr); - if (val < 0) - return false; - buf[i] = val; - ptr += 2; - } - return true; -} - -int hexTupleToByte(const char *hex) -{ - int nibble1 = hexDigitToVal(hex[0]); - if (nibble1 < 0) - return -1; - int nibble2 = hexDigitToVal(hex[1]); - if (nibble2 < 0) - return -1; - return (nibble1 << 4) | nibble2; -} - -int hexDigitToVal(char ch) -{ - if (ch >= '0' && ch <= '9') - return ch - '0'; - if (ch >= 'A' && ch <= 'F') - return ch + 10 - 'A'; - if (ch >= 'a' && ch <= 'f') - return ch + 10 - 'a'; - return -1; -} - -void swapByteOrder(uint8_t* buf, int len) -{ - uint8_t* p1 = buf; - uint8_t* p2 = buf + len - 1; - while (p1 < p2) - { - uint8_t t = *p1; - *p1 = *p2; - *p2 = t; - p1++; - p2--; - } -} \ No newline at end of file diff --git a/src/config.h b/src/config.h index 478f72b..416d13b 100644 --- a/src/config.h +++ b/src/config.h @@ -24,7 +24,8 @@ extern "C" { #elif defined(CONFIG_TTN_LORA_FREQ_US_915) #define CFG_us915 1 #else -#error TTN LoRa frequency must be configured using 'make menuconfig' +#define TTN_IS_DISABLED 1 +#define CFG_eu868 1 #endif #if defined(CONFIG_TTN_RADIO_SX1272_73) diff --git a/src/hal.h b/src/hal.h index 2acd42b..a1d34c6 100755 --- a/src/hal.h +++ b/src/hal.h @@ -28,6 +28,9 @@ #ifndef _hal_hpp_ #define _hal_hpp_ +#include "oslmic.h" + + #ifdef __cplusplus extern "C" { #endif diff --git a/src/hal_esp32.c b/src/hal_esp32.c index 00cc96a..065a0f8 100755 --- a/src/hal_esp32.c +++ b/src/hal_esp32.c @@ -182,7 +182,8 @@ static void hal_spi_init() .command_bits = 0, .address_bits = 8, .spics_io_num = lmic_pins.nss, - .queue_size = SPI_QUEUE_SIZE + .queue_size = SPI_QUEUE_SIZE, + .cs_ena_posttrans = 2 }; esp_err_t ret = spi_bus_add_device(lmic_pins.spi_host, &spi_device_intf_config, &spi_handle); diff --git a/src/provisioning.c b/src/provisioning.c new file mode 100644 index 0000000..aeedb7c --- /dev/null +++ b/src/provisioning.c @@ -0,0 +1,521 @@ +/******************************************************************************* + * + * ttn-esp32 - The Things Network device library for ESP-IDF / SX127x + * + * Copyright (c) 2018 Manuel Bleichenbacher + * + * Licensed under MIT License + * https://opensource.org/licenses/MIT + * + * Task listening on a UART port for provisioning commands. + *******************************************************************************/ + +#include "freertos/FreeRTOS.h" +#include "driver/uart.h" +#include "esp_event.h" +#include "esp_log.h" +#include "nvs_flash.h" +#include "provisioning.h" +#include "lmic.h" +#include "hal_esp32.h" + +#define UART_NUM CONFIG_TTN_PROVISION_UART_NUM +#define MAX_LINE_LENGTH 128 + +static const char *TAG = "ttn_prov"; +static const char *NVS_FLASH_PARTITION = "ttn"; +static const char *NVS_FLASH_KEY_DEV_EUI = "devEui"; +static const char *NVS_FLASH_KEY_APP_EUI = "appEui"; +static const char *NVS_FLASH_KEY_APP_KEY = "appKey"; + +static void provisioning_task(void* pvParameter); +static void provisioning_add_line_data(int numBytes); +static void provisioning_detect_line_end(int start_at); +static void provisioning_process_line(); +static bool read_nvs_value(nvs_handle handle, const char* key, uint8_t* data, size_t expected_length, bool silent); +static bool write_nvs_value(nvs_handle handle, const char* key, const uint8_t* data, size_t len); +static bool hex_str_to_bin(const char *hex, uint8_t *buf, int len); +static int hex_tuple_to_byte(const char *hex); +static int hex_digit_to_val(char ch); +static void bin_to_hex_str(const uint8_t* buf, int len, char* hex); +static char val_to_hex_digit(int val); +static void swap_bytes(uint8_t* buf, int len); +static bool is_all_zeroes(const uint8_t* buf, int len); + + +static QueueHandle_t uart_queue = NULL; +static char* line_buf; +static int line_length; +static uint8_t last_line_end_char = 0; +static uint8_t global_dev_eui[8]; +static uint8_t global_app_eui[8]; +static uint8_t global_app_key[16]; +static bool have_keys = false; +static bool quit_task = false; + + +#if defined(CONFIG_TTN_PROVISION_UART_CONFIG_YES) +static void provisioning_config_uart(); +#endif + + +// --- LMIC callbacks + +// This EUI must be in little-endian format, so least-significant-byte first. +// When copying an EUI from ttnctl output, this means to reverse the bytes. +// For TTN issued EUIs the last bytes should be 0xD5, 0xB3, 0x70. +// The order is swapped in provisioning_decode_keys(). +void os_getArtEui (u1_t* buf) +{ + memcpy(buf, global_app_eui, 8); +} + +// This should also be in little endian format, see above. +void os_getDevEui (u1_t* buf) +{ + memcpy(buf, global_dev_eui, 8); +} + +// This key should be in big endian format (or, since it is not really a number +// but a block of memory, endianness does not really apply). In practice, a key +// taken from ttnctl can be copied as-is. +void os_getDevKey (u1_t* buf) +{ + memcpy(buf, global_app_key, 16); +} + + +// --- Provisioning task + +void provisioning_start_task() +{ +#if defined(CONFIG_TTN_PROVISION_UART_CONFIG_YES) + provisioning_config_uart(); +#endif + + esp_err_t err = uart_driver_install(UART_NUM, 2048, 2048, 20, &uart_queue, 0); + ESP_ERROR_CHECK(err); + + xTaskCreate(provisioning_task, "provisioning", 2048, NULL, 1, NULL); +} + +void provisioning_task(void* pvParameter) +{ + line_buf = (char*)malloc(MAX_LINE_LENGTH + 1); + line_length = 0; + + uart_event_t event; + + ESP_LOGI(TAG, "Provisioning task started"); + + while (!quit_task) + { + if (!xQueueReceive(uart_queue, &event, portMAX_DELAY)) + continue; + + switch (event.type) + { + case UART_DATA: + provisioning_add_line_data(event.size); + break; + + case UART_FIFO_OVF: + case UART_BUFFER_FULL: + uart_flush_input(UART_NUM); + xQueueReset(uart_queue); + break; + + default: + break; + } + } + + free(line_buf); + uart_driver_delete(UART_NUM); + vTaskDelete(NULL); +} + +void provisioning_add_line_data(int numBytes) +{ + int n; +top: + n = numBytes; + if (line_length + n > MAX_LINE_LENGTH) + n = MAX_LINE_LENGTH - line_length; + + uart_read_bytes(UART_NUM, (uint8_t*)line_buf + line_length, n, portMAX_DELAY); + int start_at = line_length; + line_length += n; + + provisioning_detect_line_end(start_at); + + if (n < numBytes) + { + numBytes -= n; + goto top; + } +} + +void provisioning_detect_line_end(int start_at) +{ +top: + for (int p = start_at; p < line_length; p++) + { + char ch = line_buf[p]; + if (ch == 0x0d || ch == 0x0a) + { + if (p > 0) + uart_write_bytes(UART_NUM, line_buf + start_at, line_length - start_at - 1); + if (p > 0 || ch == 0x0d || last_line_end_char == 0x0a) + uart_write_bytes(UART_NUM, "\r\n", 2); + + line_buf[p] = 0; + last_line_end_char = ch; + + if (p > 0) + provisioning_process_line(); + + memcpy(line_buf, line_buf + p + 1, line_length - p - 1); + line_length -= p + 1; + start_at = 0; + goto top; + } + } + + if (line_length > 0) + uart_write_bytes(UART_NUM, line_buf + start_at, line_length - start_at); + + if (line_length == MAX_LINE_LENGTH) + line_length = 0; // Line too long; flush it +} + +void provisioning_process_line() +{ + bool is_ok = true; + // Expected format: + // AT+PROV? + // AT+PROV=hex16-hex16-hex32 + if (strcmp(line_buf, "AT+PROV?") == 0) + { + uint8_t binbuf[8]; + char hexbuf[16]; + + memcpy(binbuf, global_dev_eui, 8); + swap_bytes(binbuf, 8); + bin_to_hex_str(binbuf, 8, hexbuf); + uart_write_bytes(UART_NUM, hexbuf, 16); + uart_write_bytes(UART_NUM, "-", 1); + + memcpy(binbuf, global_app_eui, 8); + swap_bytes(binbuf, 8); + bin_to_hex_str(binbuf, 8, hexbuf); + uart_write_bytes(UART_NUM, hexbuf, 16); + + uart_write_bytes(UART_NUM, "-00000000000000000000000000000000\r\n", 35); + } + else if (strncmp(line_buf, "AT+PROV=", 8) == 0) + { + if (strlen(line_buf) == 74 && line_buf[24] == '-' && line_buf[41] == '-') + { + line_buf[24] = 0; + line_buf[41] = 0; + is_ok = provisioning_decode_keys(line_buf + 8, line_buf + 25, line_buf + 42); + if (is_ok) + { + hal_enterCriticalSection(); + LMIC_reset(); + hal_leaveCriticalSection(); + onEvent(EV_RESET); + } + } + else + { + is_ok = false; + } + } + else if (strcmp(line_buf, "AT+PROVQ") == 0) + { + quit_task = true; + } + else if (strcmp(line_buf, "AT") != 0) + { + is_ok = false; + } + + uart_write_bytes(UART_NUM, is_ok ? "OK\r\n" : "ERROR\r\n", is_ok ? 4 : 7); +} + +#if defined(CONFIG_TTN_PROVISION_UART_CONFIG_YES) + +void provisioning_config_uart() +{ + esp_err_t err; + + uart_config_t uart_config = { + .baud_rate = CONFIG_TTN_PROVISION_UART_BAUDRATE, + .data_bits = UART_DATA_8_BITS, + .parity = UART_PARITY_DISABLE, + .stop_bits = UART_STOP_BITS_1, + .flow_ctrl = UART_HW_FLOWCTRL_DISABLE + }; + err = uart_param_config(UART_NUM, &uart_config); + ESP_ERROR_CHECK(err); + + err = uart_set_pin(UART_NUM, CONFIG_TTN_PROVISION_UART_TX_GPIO, CONFIG_TTN_PROVISION_UART_RX_GPIO, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE); + ESP_ERROR_CHECK(err); +} + +#endif + + +// --- Key handling + +bool provisioning_have_keys() +{ + return have_keys; +} + +bool provisioning_decode_keys(const char *dev_eui, const char *app_eui, const char *app_key) +{ + uint8_t buf_dev_eui[8]; + uint8_t buf_app_eui[8]; + uint8_t buf_app_key[16]; + + if (strlen(dev_eui) != 16 || !hex_str_to_bin(dev_eui, buf_dev_eui, 8)) + { + ESP_LOGW(TAG, "Invalid device EUI: %s", dev_eui); + return false; + } + + swap_bytes(buf_dev_eui, 8); + + if (strlen(app_eui) != 16 || !hex_str_to_bin(app_eui, buf_app_eui, 8)) + { + ESP_LOGW(TAG, "Invalid application EUI: %s", app_eui); + return false; + } + + swap_bytes(buf_app_eui, 8); + + if (strlen(app_key) != 32 || !hex_str_to_bin(app_key, buf_app_key, 16)) + { + ESP_LOGW(TAG, "Invalid application key: %s", app_key); + return false; + } + + memcpy(global_dev_eui, buf_dev_eui, sizeof(global_dev_eui)); + memcpy(global_app_eui, buf_app_eui, sizeof(global_app_eui)); + memcpy(global_app_key, buf_app_key, sizeof(global_app_key)); + + have_keys = !is_all_zeroes(global_dev_eui, sizeof(global_dev_eui)) + && !is_all_zeroes(global_app_eui, sizeof(global_app_eui)) + && !is_all_zeroes(global_app_key, sizeof(global_app_key)); + + if (!provisioning_save_keys()) + return false; + + return true; +} + + +// --- Non-volatile storage + +bool provisioning_save_keys() +{ + bool result = false; + + nvs_handle handle = 0; + esp_err_t res = nvs_open(NVS_FLASH_PARTITION, NVS_READWRITE, &handle); + if (res == ESP_ERR_NVS_NOT_INITIALIZED) + { + ESP_LOGW(TAG, "NVS storage is not initialized. Call 'nvs_flash_init()' first."); + goto done; + } + ESP_ERROR_CHECK(res); + if (res != ESP_OK) + goto done; + + if (!write_nvs_value(handle, NVS_FLASH_KEY_DEV_EUI, global_dev_eui, sizeof(global_dev_eui))) + goto done; + + if (!write_nvs_value(handle, NVS_FLASH_KEY_APP_EUI, global_app_eui, sizeof(global_app_eui))) + goto done; + + if (!write_nvs_value(handle, NVS_FLASH_KEY_APP_KEY, global_app_key, sizeof(global_app_key))) + goto done; + + res = nvs_commit(handle); + ESP_ERROR_CHECK(res); + + result = true; + ESP_LOGI(TAG, "Dev and app EUI and app key saved in NVS storage"); + +done: + nvs_close(handle); + return result; +} + +bool provisioning_restore_keys(bool silent) +{ + uint8_t buf_dev_eui[8]; + uint8_t buf_app_eui[8]; + uint8_t buf_app_key[16]; + + nvs_handle handle = 0; + esp_err_t res = nvs_open(NVS_FLASH_PARTITION, NVS_READONLY, &handle); + if (res == ESP_ERR_NVS_NOT_FOUND) + return false; // partition does not exist yet + if (res == ESP_ERR_NVS_NOT_INITIALIZED) + { + ESP_LOGW(TAG, "NVS storage is not initialized. Call 'nvs_flash_init()' first."); + goto done; + } + ESP_ERROR_CHECK(res); + if (res != ESP_OK) + goto done; + + if (!read_nvs_value(handle, NVS_FLASH_KEY_DEV_EUI, buf_dev_eui, sizeof(global_dev_eui), silent)) + goto done; + + if (!read_nvs_value(handle, NVS_FLASH_KEY_APP_EUI, buf_app_eui, sizeof(global_app_eui), silent)) + goto done; + + if (!read_nvs_value(handle, NVS_FLASH_KEY_APP_KEY, buf_app_key, sizeof(global_app_key), silent)) + goto done; + + memcpy(global_dev_eui, buf_dev_eui, sizeof(global_dev_eui)); + memcpy(global_app_eui, buf_app_eui, sizeof(global_app_eui)); + memcpy(global_app_key, buf_app_key, sizeof(global_app_key)); + + have_keys = !is_all_zeroes(global_dev_eui, sizeof(global_dev_eui)) + && !is_all_zeroes(global_app_eui, sizeof(global_app_eui)) + && !is_all_zeroes(global_app_key, sizeof(global_app_key)); + + if (have_keys) + { + ESP_LOGI(TAG, "Dev and app EUI and app key have been restored from NVS storage"); + } + else + { + ESP_LOGW(TAG, "Dev and app EUI and app key are invalid (zeroes only)"); + } + +done: + nvs_close(handle); + return true; +} + +bool read_nvs_value(nvs_handle handle, const char* key, uint8_t* data, size_t expected_length, bool silent) +{ + size_t size = expected_length; + esp_err_t res = nvs_get_blob(handle, key, data, &size); + if (res == ESP_OK && size == expected_length) + return true; + + if (res == ESP_OK && size != expected_length) + { + if (!silent) + ESP_LOGW(TAG, "Invalid size of NVS data for %s", key); + return false; + } + + if (res == ESP_ERR_NVS_NOT_FOUND) + { + if (!silent) + ESP_LOGW(TAG, "No NVS data found for %s", key); + return false; + } + + ESP_ERROR_CHECK(res); + return false; +} + +bool write_nvs_value(nvs_handle handle, const char* key, const uint8_t* data, size_t len) +{ + uint8_t buf[16]; + if (read_nvs_value(handle, key, buf, len, true) && memcmp(buf, data, len) == 0) + return true; // unchanged + + esp_err_t res = nvs_set_blob(handle, key, data, len); + ESP_ERROR_CHECK(res); + + return res == ESP_OK; +} + + +// --- Helper functions --- + +bool hex_str_to_bin(const char *hex, uint8_t *buf, int len) +{ + const char* ptr = hex; + for (int i = 0; i < len; i++) + { + int val = hex_tuple_to_byte(ptr); + if (val < 0) + return false; + buf[i] = val; + ptr += 2; + } + return true; +} + +int hex_tuple_to_byte(const char *hex) +{ + int nibble1 = hex_digit_to_val(hex[0]); + if (nibble1 < 0) + return -1; + int nibble2 = hex_digit_to_val(hex[1]); + if (nibble2 < 0) + return -1; + return (nibble1 << 4) | nibble2; +} + +int hex_digit_to_val(char ch) +{ + if (ch >= '0' && ch <= '9') + return ch - '0'; + if (ch >= 'A' && ch <= 'F') + return ch + 10 - 'A'; + if (ch >= 'a' && ch <= 'f') + return ch + 10 - 'a'; + return -1; +} + +void bin_to_hex_str(const uint8_t* buf, int len, char* hex) +{ + for (int i = 0; i < len; i++) + { + uint8_t b = buf[i]; + *hex = val_to_hex_digit((b & 0xf0) >> 4); + hex++; + *hex = val_to_hex_digit(b & 0x0f); + hex++; + } +} + +char val_to_hex_digit(int val) +{ + return "0123456789ABCDEF"[val]; +} + +void swap_bytes(uint8_t* buf, int len) +{ + uint8_t* p1 = buf; + uint8_t* p2 = buf + len - 1; + while (p1 < p2) + { + uint8_t t = *p1; + *p1 = *p2; + *p2 = t; + p1++; + p2--; + } +} + +bool is_all_zeroes(const uint8_t* buf, int len) +{ + for (int i = 0; i < len; i++) + if (buf[i] != 0) + return false; + return true; +} \ No newline at end of file diff --git a/src/provisioning.h b/src/provisioning.h new file mode 100644 index 0000000..3d1900f --- /dev/null +++ b/src/provisioning.h @@ -0,0 +1,35 @@ +/******************************************************************************* + * + * ttn-esp32 - The Things Network device library for ESP-IDF / SX127x + * + * Copyright (c) 2018 Manuel Bleichenbacher + * + * Licensed under MIT License + * https://opensource.org/licenses/MIT + * + * Task listening on a UART port for provisioning commands. + *******************************************************************************/ + +#ifndef _provision_task_h_ +#define _provision_task_h_ + +#include "oslmic.h" + + +#ifdef __cplusplus +extern "C" { +#endif + + +void provisioning_start_task(); +bool provisioning_have_keys(); +bool provisioning_decode_keys(const char *dev_eui, const char *app_eui, const char *app_key); +bool provisioning_save_keys(); +bool provisioning_restore_keys(bool silent); + + +#ifdef __cplusplus +} +#endif + +#endif