Merge branch 'dev'

* dev:
  Check for all zero EUIs and keys
  Minor adjustment to SPI timing
  Fix file description
  Ensure library compiles even if no frequency plan was selected
  Configuration of UART
  AT command for ending provisioning task
  Reset LMIC on AT command provisioning
  provisioning example, waitForProvisioning()
  Add provisioning task, refactor provisioning
This commit is contained in:
Manuel Bleichenbacher 2018-07-28 22:17:55 +02:00
commit cf7ab60f52
14 changed files with 800 additions and 244 deletions

1
.gitignore vendored
View File

@ -1,4 +1,5 @@
build/ build/
sdkconfig sdkconfig
sdkconfig.old
dev_keys.txt dev_keys.txt
ttn-esp32 ttn-esp32

View File

@ -16,7 +16,9 @@
"${IDF_PATH}/components/soc/include", "${IDF_PATH}/components/soc/include",
"${IDF_PATH}/components/soc/esp32/include", "${IDF_PATH}/components/soc/esp32/include",
"${IDF_PATH}/components/tcpip_adapter/include", "${IDF_PATH}/components/tcpip_adapter/include",
"${workspaceRoot}/examples/provisioning/build/include",
"${workspaceRoot}/examples/hello_world/build/include", "${workspaceRoot}/examples/hello_world/build/include",
"${workspaceRoot}/examples/send_recv/build/include",
"${workspaceRoot}/include", "${workspaceRoot}/include",
"${workspaceRoot}/src" "${workspaceRoot}/src"
], ],

9
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,9 @@
{
"files.associations": {
"provisioning.h": "c",
"config.h": "c",
"sdkconfig.h": "c",
"oslmic.h": "c",
"hal_esp32.h": "c"
}
}

79
Kconfig
View File

@ -2,13 +2,13 @@ menu "The Things Network"
choice TTN_LORA_FREQ choice TTN_LORA_FREQ
prompt "TTN LoRa frequency" prompt "TTN LoRa frequency"
default TTN_LORA_FREQ_NONE default TTN_LORA_FREQ_DISABLED
help help
LoRa frequency must match the geographic region the device is operated in. 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 config TTN_LORA_FREQ_DISABLED
bool "None" bool "LoRa Disabled"
config TTN_LORA_FREQ_EU_868 config TTN_LORA_FREQ_EU_868
bool "868 MHz (Europe)" 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. the LoRaWAN radio chip. It needs a high priority as the timing is crucial.
Higher numbers indicate higher priority. 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 endmenu

View File

@ -0,0 +1,3 @@
PROJECT_NAME := provisioning
include $(IDF_PATH)/make/project.mk

View File

View File

@ -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");
}
}

View File

@ -7,7 +7,7 @@
* Licensed under MIT License * Licensed under MIT License
* https://opensource.org/licenses/MIT * 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_ #ifndef _THETHINGSNETWORK_H_
@ -104,6 +104,23 @@ public:
*/ */
bool provision(const char *devEui, const char *appEui, const char *appKey); 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. * @brief Activate the device via OTAA.
* *
@ -176,12 +193,8 @@ public:
private: private:
TTNMessageCallback messageCallback; TTNMessageCallback messageCallback;
bool haveKeys;
bool joinCore(); bool joinCore();
bool decodeKeys(const char *devEui, const char *appEui, const char *appKey);
bool saveKeys();
bool restoreKeys(bool silent);
}; };
#endif #endif

View File

@ -7,18 +7,19 @@
* Licensed under MIT License * Licensed under MIT License
* https://opensource.org/licenses/MIT * 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 "freertos/FreeRTOS.h"
#include "esp_event.h" #include "esp_event.h"
#include "nvs_flash.h"
#include "TheThingsNetwork.h"
#include "esp_log.h" #include "esp_log.h"
#include "oslmic.h" #include "TheThingsNetwork.h"
#include "config.h"
#include "hal.h" #include "hal.h"
#include "hal_esp32.h" #include "hal_esp32.h"
#include "lmic.h" #include "lmic.h"
#include "provisioning.h"
enum ClientAction enum ClientAction
{ {
@ -28,34 +29,24 @@ enum ClientAction
}; };
static const char *TAG = "ttn"; 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 TheThingsNetwork* ttnInstance;
static uint8_t devEui[8];
static uint8_t appEui[8];
static uint8_t appKey[16];
static QueueHandle_t resultQueue; static QueueHandle_t resultQueue;
static ClientAction clientAction = eActionUnrelated; 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() 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); ASSERT(ttnInstance == NULL);
ttnInstance = this; ttnInstance = this;
hal_initCriticalSection(); hal_initCriticalSection();
} }
TheThingsNetwork::~TheThingsNetwork() TheThingsNetwork::~TheThingsNetwork()
@ -89,45 +80,38 @@ void TheThingsNetwork::reset()
bool TheThingsNetwork::provision(const char *devEui, const char *appEui, const char *appKey) 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 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();
if (strlen(devEui) != 16 || !hexStringToBin(devEui, ::devEui, 8)) #endif
{
ESP_LOGW(TAG, "Invalid device EUI: %s", devEui);
return false;
} }
swapByteOrder(::devEui, 8); void TheThingsNetwork::waitForProvisioning()
if (strlen(appEui) != 16 || !hexStringToBin(appEui, ::appEui, 8))
{ {
ESP_LOGW(TAG, "Invalid application EUI: %s", appEui); #if !defined(CONFIG_TTN_PROVISION_UART_NONE)
return false; if (isProvisioned())
{
ESP_LOGI(TAG, "Device is already provisioned");
return;
} }
swapByteOrder(::appEui, 8); while (!provisioning_have_keys())
vTaskDelay(1000 / portTICK_PERIOD_MS);
if (strlen(appKey) != 32 || !hexStringToBin(appKey, ::appKey, 16)) ESP_LOGI(TAG, "Device successfully provisioned");
{ #endif
ESP_LOGW(TAG, "Invalid application key: %s", appEui);
return false;
}
haveKeys = true;
return true;
} }
bool TheThingsNetwork::join(const char *devEui, const char *appEui, const char *appKey) 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 false;
return joinCore(); return joinCore();
@ -135,9 +119,9 @@ bool TheThingsNetwork::join(const char *devEui, const char *appEui, const char *
bool TheThingsNetwork::join() bool TheThingsNetwork::join()
{ {
if (!haveKeys) if (!provisioning_have_keys())
{ {
if (!restoreKeys(false)) if (!provisioning_restore_keys(false))
return false; return false;
} }
@ -146,7 +130,7 @@ bool TheThingsNetwork::join()
bool TheThingsNetwork::joinCore() bool TheThingsNetwork::joinCore()
{ {
if (!haveKeys) if (!provisioning_have_keys())
{ {
ESP_LOGW(TAG, "Device EUI, App EUI and/or App key have not been provided"); ESP_LOGW(TAG, "Device EUI, App EUI and/or App key have not been provided");
return false; return false;
@ -205,118 +189,15 @@ void TheThingsNetwork::onMessage(TTNMessageCallback callback)
messageCallback = 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() bool TheThingsNetwork::isProvisioned()
{ {
if (haveKeys) if (provisioning_have_keys())
return true; 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) return provisioning_have_keys();
{
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;
} }
@ -333,29 +214,6 @@ static const char *eventNames[] = {
}; };
#endif #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) { void onEvent (ev_t ev) {
#if CONFIG_LOG_DEFAULT_LEVEL >= 3 #if CONFIG_LOG_DEFAULT_LEVEL >= 3
ESP_LOGI(TAG, "event %s", eventNames[ev]); ESP_LOGI(TAG, "event %s", eventNames[ev]);
@ -372,12 +230,12 @@ void onEvent (ev_t ev) {
} }
else if (clientAction == eActionJoining) else if (clientAction == eActionJoining)
{ {
if (ev != EV_JOINED && EV_REJOIN_FAILED) if (ev != EV_JOINED && ev != EV_REJOIN_FAILED && ev != EV_RESET)
return; return;
} }
else else
{ {
if (ev != EV_TXCOMPLETE && ev != EV_LINK_DEAD) if (ev != EV_TXCOMPLETE && ev != EV_LINK_DEAD && ev != EV_RESET)
return; return;
} }
@ -385,56 +243,3 @@ void onEvent (ev_t ev) {
clientAction = eActionUnrelated; clientAction = eActionUnrelated;
xQueueSend(resultQueue, &result, 100 / portTICK_PERIOD_MS); 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--;
}
}

View File

@ -24,7 +24,8 @@ extern "C" {
#elif defined(CONFIG_TTN_LORA_FREQ_US_915) #elif defined(CONFIG_TTN_LORA_FREQ_US_915)
#define CFG_us915 1 #define CFG_us915 1
#else #else
#error TTN LoRa frequency must be configured using 'make menuconfig' #define TTN_IS_DISABLED 1
#define CFG_eu868 1
#endif #endif
#if defined(CONFIG_TTN_RADIO_SX1272_73) #if defined(CONFIG_TTN_RADIO_SX1272_73)

View File

@ -28,6 +28,9 @@
#ifndef _hal_hpp_ #ifndef _hal_hpp_
#define _hal_hpp_ #define _hal_hpp_
#include "oslmic.h"
#ifdef __cplusplus #ifdef __cplusplus
extern "C" { extern "C" {
#endif #endif

View File

@ -182,7 +182,8 @@ static void hal_spi_init()
.command_bits = 0, .command_bits = 0,
.address_bits = 8, .address_bits = 8,
.spics_io_num = lmic_pins.nss, .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); esp_err_t ret = spi_bus_add_device(lmic_pins.spi_host, &spi_device_intf_config, &spi_handle);

521
src/provisioning.c Normal file
View File

@ -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;
}

35
src/provisioning.h Normal file
View File

@ -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