9 Commits

20 changed files with 2593 additions and 84 deletions

26
.clang-format Normal file
View File

@@ -0,0 +1,26 @@
BasedOnStyle: LLVM
Language: Cpp
IndentWidth: 4
UseTab: Never
PointerAlignment: Left
AccessModifierOffset: -4
AlwaysBreakTemplateDeclarations: true
MaxEmptyLinesToKeep: 2
CompactNamespaces: true
FixNamespaceComments: true
LambdaBodyIndentation: Signature
AllowShortFunctionsOnASingleLine: false
AllowShortLambdasOnASingleLine: Empty
AllowShortIfStatementsOnASingleLine: true
AlignConsecutiveAssignments: true
AlignConsecutiveBitFields: true
AlignConsecutiveDeclarations: true
AlignConsecutiveMacros: true

4
.clangd Normal file
View File

@@ -0,0 +1,4 @@
CompileFlags:
Add: [-std=c++20]
Remove: ['-fno-shrink-wrap', '-fstrict-volatile-bitfields', '-fno-tree-switch-conversion']

3
.gitignore vendored
View File

@@ -3,7 +3,6 @@
# https://github.com/espressif/esp-idf # https://github.com/espressif/esp-idf
build/ build/
sdkconfig
sdkconfig.old sdkconfig.old
.idea .idea
@@ -13,3 +12,5 @@ cmake-build-relwithdebinfo
cmake-build-debug-docker/ cmake-build-debug-docker/
cmake-build-release-docker/ cmake-build-release-docker/
cmake-build-relwithdebinfo-docker/ cmake-build-relwithdebinfo-docker/
.cache/

View File

@@ -2,5 +2,7 @@ cmake_minimum_required(VERSION 3.16)
set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD 20)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
include($ENV{IDF_PATH}/tools/cmake/project.cmake) include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(HyperLink) project(HyperLink)

View File

@@ -1,4 +1,4 @@
FROM espressif/idf:release-v5.1 FROM espressif/idf:release-v5.2
LABEL maintainer="andreas.tsouchlos@gmail.com" LABEL maintainer="andreas.tsouchlos@gmail.com"

View File

@@ -1,38 +1,23 @@
# HyperLink_SW # HyperLink_SW
Firmware for the HyperLink board. Firmware for the HyperLink project.
## Build and run ## Build and run
This project is built using the [ESP IDF](https://docs.espressif.com/projects/esp-idf/en/release-v5.1/esp32/index.html) toolchain. This project is built using the
In order to ease the build process, a custom `Dockerfile` is provided. [ESP IDF](https://docs.espressif.com/projects/esp-idf/en/release-v5.1/esp32/index.html)
Any ESP IDF toolchain command can be run by replacing `idf.py` by `./idf.sh`, e.g., `./idf.sh build`, toolchain. In order to ease the build process, a custom `Dockerfile` is
after having built the docker image. provided. Any ESP IDF toolchain command can be run by replacing `idf.py` by
`./idf.sh`, e.g., `./idf.sh build`.
As the docker container has to be able to access the serial device for flashing, Before the executing the following commands, make sure the docker daemon is
some configuration may have to take place. running:
Make sure the `USB_GUID` and `USB_DEV` in `idf.py` have the correct values.
1. Build the docker image 1. Build the project
```bash
$ sudo docker build . --tag hyperlink
```
2. Compile the project
```bash ```bash
$ ./idf.sh build $ ./idf.sh build
``` ```
1. Flash the executable
3. Flash the executable
```bash ```bash
$ ./idf.sh flash $ ./idf.sh flash monitor
``` ```
## Misc
### CLion Docker Setup
To configure the project to be built with docker in CLion, create a new docker
toolchain with the following settings:
![image](doc/CLion_docker_settings_screenshot.png)

View File

@@ -0,0 +1 @@
idf_component_register(INCLUDE_DIRS include REQUIRES util esp_http_server)

View File

@@ -0,0 +1,147 @@
#pragma once
#include <algorithm>
#include <stdexcept>
#include <esp_http_server.h>
#include <util/inplace_function.h>
#define RESPONSE_BUFFER_LEN 512
//
//
// Types
//
//
namespace http {
enum class Method { get = HTTP_GET, post = HTTP_POST };
struct response_t {
response_t() = default;
response_t(const char* str) {
std::size_t respLen =
std::min(strlen(str), static_cast<unsigned>(RESPONSE_BUFFER_LEN));
std::copy(str, str + respLen, text.begin());
}
std::array<char, RESPONSE_BUFFER_LEN> text = {0};
};
struct handler_t {
Method method;
const char* path;
inplace_function<response_t()> handler;
};
struct server_settings_t {
int port = 80;
inplace_function<void()> on_error = [] {};
inplace_function<void()> on_not_found = [] {};
};
} // namespace http
//
//
// Implementation details
//
//
namespace http::detail {
inline auto& get_server() {
static httpd_handle_t server = nullptr;
return server;
}
template <std::size_t N>
inline auto& get_handler_storage() {
static std::array<inplace_function<response_t()>, N> handlers;
return handlers;
}
template <std::size_t N>
inline auto& get_esp_uri_storage() {
static std::array<httpd_uri_t, N> uris;
return uris;
}
// TODO: Currently, the set handler is only valid for GET requests
template <std::size_t N, std::size_t I>
inline void set_handlers_impl(const handler_t (&handlers)[N]) {
auto& uriStorage = get_esp_uri_storage<N>();
uriStorage[I] = httpd_uri_t{
.uri = handlers[I].path,
.method = static_cast<httpd_method_t>(handlers[I].method),
.handler =
[](httpd_req_t* req) {
auto resp = get_handler_storage<N>()[I]();
httpd_resp_send(req, resp.text.data(), HTTPD_RESP_USE_STRLEN);
return ESP_OK;
},
.user_ctx = nullptr,
};
httpd_register_uri_handler(get_server(), &(uriStorage[I]));
if constexpr (I < N - 1) set_handlers_impl<N, I + 1>(handlers);
}
} // namespace http::detail
//
//
// Public API
//
//
namespace http {
// TODO: Give request information to handlers
template <std::size_t N>
inline void set_handlers(const handler_t (&handlers)[N]) {
auto& handlerStorage = detail::get_handler_storage<N>();
std::transform(handlers, handlers + N, handlerStorage.begin(),
[](const auto& handler) {
return handler.handler;
});
detail::set_handlers_impl<N, 0>(handlers);
}
inline void start_server(server_settings_t settings) {
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
config.server_port = settings.port;
if (httpd_start(&detail::get_server(), &config) != ESP_OK) {
throw std::runtime_error("Failed to start server");
}
// TODO: Set error and not found handlers
}
inline void stop_server() {
if (httpd_stop(detail::get_server()) != ESP_OK) {
throw std::runtime_error("Failed to stop server");
}
}
} // namespace http

View File

@@ -0,0 +1,2 @@
idf_component_register(SRCS "src/nvs.cpp" INCLUDE_DIRS include REQUIRES
nvs_flash)

View File

@@ -0,0 +1,10 @@
#pragma once
namespace nvs {
void init();
} // namespace nvs

View File

@@ -0,0 +1,26 @@
#include <stdexcept>
#include "esp_err.h"
#include "nvs_flash.h"
namespace nvs {
void init() {
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES ||
ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
if (nvs_flash_erase() != ESP_OK)
throw std::runtime_error("Failed to erase NVS");
ret = nvs_flash_init();
}
if (ret != ESP_OK) throw std::runtime_error("Failed to initialize NVS");
}
} // namespace nvs

View File

@@ -0,0 +1 @@
idf_component_register(INCLUDE_DIRS include)

View File

@@ -0,0 +1,122 @@
#pragma once
#include <cstddef>
#include <functional>
#include <type_traits>
#include <utility>
template <typename func_sig_t, std::size_t storage_size = 8>
class inplace_function;
template <typename return_t, typename... args_t, std::size_t storage_size>
class inplace_function<return_t(args_t...), storage_size> {
using storage_t = std::byte[storage_size];
using wrapper_func_t = return_t (*)(storage_t, args_t...);
public:
inplace_function() = default;
///
/// @brief Constructor for function objects.
///
template <typename F>
inplace_function(F f) {
bind(f);
}
///
/// @brief Construct from free function pointer.
///
template <auto F>
static inplace_function construct() {
inplace_function<return_t(args_t...)> func;
func.template bind<F>();
return func;
}
///
/// @brief Construct form member function pointer.
///
template <auto F, typename class_t>
static inplace_function construct(class_t& c) {
inplace_function<return_t(args_t...)> func;
func.template bind<F>(c);
return func;
}
///
/// @brief Construct from function object.
///
template <typename F>
static inplace_function construct(F f) {
inplace_function<return_t(args_t...)> func;
func.bind(f);
return func;
}
return_t operator()(args_t... args) {
if (mWrapper)
return mWrapper(mStorage, args...);
else
throw std::bad_function_call();
}
operator bool() const {
return !(mWrapper == nullptr);
}
///
/// @brief Bind a free function.
///
/// @tparam F Pointer to free function
///
template <auto F>
requires std::is_invocable_r_v<return_t, decltype(F), args_t...>
void bind() {
mWrapper = [](storage_t, args_t... args) {
return std::invoke(F, std::forward<args_t>(args)...);
};
}
///
/// @brief Bind a member function.
///
/// @tparam class_t Class the member belongs to.
/// @param c Reference to object the member function should be called on.
/// @tparam F Member function pointer.
///
template <auto F, typename class_t>
requires std::is_invocable_r_v<return_t, decltype(F), class_t&,
args_t...> &&
(sizeof(class_t*) <= storage_size)
void bind(class_t& c) {
new (mStorage)(class_t*){&c};
mWrapper = [](storage_t storage, args_t... args) {
return std::invoke(F, reinterpret_cast<class_t*>(storage),
std::forward<args_t>(args)...);
};
}
///
/// @brief Bind a function object.
///
template <typename F>
requires std::is_invocable_r_v<return_t, F, args_t...> &&
(sizeof(F) <= storage_size) &&
std::is_trivially_destructible_v<F>
void bind(F f) {
new (mStorage) F{std::move(f)};
mWrapper = [](storage_t storage, args_t... args) {
return std::invoke(*reinterpret_cast<F*>(storage),
std::forward<args_t>(args)...);
};
}
private:
storage_t mStorage;
wrapper_func_t mWrapper = nullptr;
};

View File

@@ -0,0 +1,10 @@
idf_component_register(
SRCS
"src/wifi.cpp"
INCLUDE_DIRS
include
REQUIRES
esp_wifi
util
nvs
nanofmt)

View File

@@ -0,0 +1,57 @@
#pragma once
#include <span>
#include <string_view>
#include <util/inplace_function.h>
namespace wifi {
enum class Mode { station, soft_ap, /* station_and_soft_ap */ };
// TODO: Authmode
struct soft_ap_settings_t {
uint8_t channel = 11;
uint8_t max_connections = 5;
inplace_function<void(std::array<char, 18>, uint8_t)> connected;
inplace_function<void(std::array<char, 18>, uint8_t)> disconnected;
};
struct station_settings_t {
inplace_function<void()> stationStarted = [] {};
inplace_function<void()> disconnected = [] {};
inplace_function<void(std::array<char, 17>)> gotIp = [](auto...) {};
};
struct scanned_ap_t {
std::array<char, 33> ssid = {0};
int rssi;
};
void init();
void deinit();
void set_mode(Mode mode);
void configure_soft_ap(std::string_view ssid, std::string_view password,
soft_ap_settings_t settings = {});
void configure_station(std::string_view ssid, std::string_view password,
station_settings_t settings = {});
void clear_callbacks();
void station_connect();
void station_disconnect();
void start();
void stop();
std::span<scanned_ap_t> scan(std::span<scanned_ap_t> memory);
} // namespace wifi

View File

@@ -0,0 +1,251 @@
#include "wifi/wifi.h"
#include <cstring>
#include <stdexcept>
#include "esp_event_base.h"
#include "esp_netif.h"
#include "esp_netif_ip_addr.h"
#include "esp_netif_types.h"
#include "esp_wifi.h"
#include "esp_wifi_types.h"
#include <util/inplace_function.h>
namespace {
esp_netif_t* gSoftApNetif;
esp_netif_t* gStationNetif;
inplace_function<void(std::array<char, 18>, uint8_t)> gAPConnectCallback =
[](auto...) {};
inplace_function<void(std::array<char, 18>, uint8_t)> gAPDisconnectCallback =
[](auto...) {};
inplace_function<void()> gStationStartCallback = [] {};
inplace_function<void()> gStationDisconnectCallback = [] {};
inplace_function<void(std::array<char, 17>)> gStationGotIpCallback =
[](auto...) {};
void wifi_event_handler(void*, esp_event_base_t event_base, int32_t event_id,
void* event_data) {
if (event_base == WIFI_EVENT) {
if (event_id == WIFI_EVENT_STA_START) {
gStationStartCallback();
} else if (event_id == WIFI_EVENT_STA_DISCONNECTED) {
gStationDisconnectCallback();
} else if (event_id == WIFI_EVENT_AP_STACONNECTED) {
auto disconnectEvent =
reinterpret_cast<wifi_event_ap_stadisconnected_t*>(event_data);
std::array<char, 18> macStr = {0};
std::snprintf(macStr.data(), macStr.size(),
"%02x:%02x:%02x:%02x:%02x:%02x",
disconnectEvent->mac[0], disconnectEvent->mac[1],
disconnectEvent->mac[2], disconnectEvent->mac[3],
disconnectEvent->mac[4], disconnectEvent->mac[5]);
gAPConnectCallback(macStr, disconnectEvent->aid);
} else if (event_id == WIFI_EVENT_AP_STADISCONNECTED) {
auto disconnectEvent =
reinterpret_cast<wifi_event_ap_stadisconnected_t*>(event_data);
std::array<char, 18> macStr = {0};
std::snprintf(macStr.data(), macStr.size(),
"%02x:%02x:%02x:%02x:%02x:%02x",
disconnectEvent->mac[0], disconnectEvent->mac[1],
disconnectEvent->mac[2], disconnectEvent->mac[3],
disconnectEvent->mac[4], disconnectEvent->mac[5]);
gAPDisconnectCallback(macStr, disconnectEvent->aid);
}
} else if (event_base == IP_EVENT) {
if (event_id == IP_EVENT_STA_GOT_IP) {
auto event = reinterpret_cast<ip_event_got_ip_t*>(event_data);
auto ipAddr = &event->ip_info.ip;
std::array<char, 17> ipStr = {0};
std::snprintf(ipStr.data(), ipStr.size(), "%d.%d.%d.%d",
esp_ip4_addr1_16(ipAddr), esp_ip4_addr2_16(ipAddr),
esp_ip4_addr3_16(ipAddr), esp_ip4_addr4_16(ipAddr));
gStationGotIpCallback(ipStr);
}
}
}
} // namespace
namespace wifi {
void init() {
if (esp_netif_init() != ESP_OK)
throw std::runtime_error("Failed to initialize netif");
if (esp_event_loop_create_default() != ESP_OK)
throw std::runtime_error("Failed to create event loop");
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
if (esp_wifi_init(&cfg) != ESP_OK)
throw std::runtime_error("Failed to initialize wifi");
if (esp_event_handler_instance_register(
ESP_EVENT_ANY_BASE, ESP_EVENT_ANY_ID, &wifi_event_handler, NULL,
NULL) != ESP_OK)
throw std::runtime_error("Failed to register wifi event handler");
}
void deinit() {
if (esp_wifi_deinit() != ESP_OK)
throw std::runtime_error("Failed to deinitialize wifi");
if (esp_event_loop_delete_default() != ESP_OK)
throw std::runtime_error("Failed to delete event loop");
// Apparently, this is not implemented in the esp idf yet
// esp_netif_deinit();
}
void set_mode(Mode mode) {
wifi_mode_t esp_mode = WIFI_MODE_NULL;
switch (mode) {
case Mode::station:
esp_mode = WIFI_MODE_STA;
break;
case Mode::soft_ap:
esp_mode = WIFI_MODE_AP;
break;
// case Mode::station_and_soft_ap:
// esp_mode = WIFI_MODE_APSTA;
// break;
}
if (esp_wifi_set_mode(esp_mode) != ESP_OK)
throw std::runtime_error("Failed to set wifi mode");
}
void configure_soft_ap(std::string_view ssid, std::string_view password,
soft_ap_settings_t settings) {
// Create network interface
if (gSoftApNetif != nullptr) esp_netif_destroy_default_wifi(gSoftApNetif);
gSoftApNetif = esp_netif_create_default_wifi_ap();
// Configure wifi
wifi_config_t wifi_config = {
.ap =
{
.ssid_len = static_cast<uint8_t>(ssid.size()),
.channel = settings.channel,
.authmode =
password.size() != 0 ? WIFI_AUTH_WPA3_PSK : WIFI_AUTH_OPEN,
.max_connection = settings.max_connections,
.pmf_cfg =
{
.required = true,
},
.sae_pwe_h2e = WPA3_SAE_PWE_BOTH,
},
};
std::copy(ssid.begin(), ssid.end(), wifi_config.ap.ssid);
std::copy(password.begin(), password.end(), wifi_config.ap.password);
if (esp_wifi_set_config(WIFI_IF_AP, &wifi_config) != ESP_OK)
throw std::runtime_error("Failed to set wifi config");
// Register event handlers
gAPConnectCallback = settings.connected;
gAPDisconnectCallback = settings.disconnected;
}
void configure_station(std::string_view ssid, std::string_view password,
station_settings_t settings) {
// Create network interface
if (gStationNetif != nullptr) esp_netif_destroy_default_wifi(gStationNetif);
gStationNetif = esp_netif_create_default_wifi_sta();
// Configure wifi
// TODO: Are there really no other settings necessary?
wifi_config_t wifi_config = {};
std::copy(ssid.begin(), ssid.end(), wifi_config.sta.ssid);
std::copy(password.begin(), password.end(), wifi_config.sta.password);
if (esp_wifi_set_config(WIFI_IF_STA, &wifi_config) != ESP_OK)
throw std::runtime_error("Failed to configure wifi station");
// Set event handlers
gStationStartCallback = settings.stationStarted;
gStationDisconnectCallback = settings.disconnected;
gStationGotIpCallback = settings.gotIp;
}
void clear_callbacks() {
gAPConnectCallback = [](auto...) {};
gAPDisconnectCallback = [](auto...) {};
gStationStartCallback = [] {};
gStationGotIpCallback = [](auto...) {};
gStationDisconnectCallback = [] {};
}
void station_connect() {
if (esp_wifi_connect() != ESP_OK)
throw std::runtime_error("Failed to connect to wifi");
}
void station_disconnect() {
if (esp_wifi_disconnect() != ESP_OK)
throw std::runtime_error("Failed to disconnect from wifi");
}
void start() {
if (esp_wifi_start() != ESP_OK)
throw std::runtime_error("Failed to start wifi");
}
void stop() {
if (esp_wifi_stop() != ESP_OK)
throw std::runtime_error("Failed to stop wifi");
}
std::span<scanned_ap_t> scan(std::span<scanned_ap_t> memory) {
uint16_t number = memory.size();
wifi_ap_record_t ap_info[number];
esp_err_t err;
if ((err = esp_wifi_scan_start(NULL, true)) != ESP_OK) {
throw std::runtime_error("Failed to start scan");
}
if (esp_wifi_scan_get_ap_records(&number, ap_info) != ESP_OK)
throw std::runtime_error("Failed to get wifi ap records");
std::transform(&ap_info[0], &ap_info[0] + number, memory.begin(),
[](const wifi_ap_record_t& record) {
scanned_ap_t result;
std::copy(&record.ssid[0], &record.ssid[0] + 32,
result.ssid.begin());
result.rssi = record.rssi;
return result;
});
return std::span{memory.begin(), number};
}
} // namespace wifi

2
idf.sh
View File

@@ -35,4 +35,4 @@ done
# #
docker run ${DEVICE_FLAGS} --rm --user $(id -u):${USB_GID} -v $PWD:/project -w /project -it hyperlink idf.py $@ docker build . && docker run ${DEVICE_FLAGS} --rm --user $(id -u):${USB_GID} -v $PWD:/project -w /project -it $(docker build . -q) idf.py $@

View File

@@ -1,4 +1,4 @@
set(SOURCES src/main.cpp) set(SOURCES src/main.cpp)
set(INCLUDES include) set(INCLUDES include)
idf_component_register(SRCS ${SOURCES} INCLUDE_DIRS ${INCLUDES}) idf_component_register(SRCS ${SOURCES} INCLUDE_DIRS ${INCLUDES} REQUIRES wifi nvs nanofmt http_server)

View File

@@ -1,54 +1,61 @@
/* #include "esp_log.h"
* SPDX-FileCopyrightText: 2010-2022 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: CC0-1.0
*/
#include <stdio.h> #include <http/server.h>
#include <inttypes.h> #include <nvs/nvs.h>
#include "sdkconfig.h" #include <wifi/wifi.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_chip_info.h"
#include "esp_flash.h"
#include "esp_system.h"
#include "nanofmt/format.h"
extern "C" void app_main(void) void start_ap() {
{ nvs::init();
printf("Hello world!\n"); wifi::init();
/* Print chip information */ wifi::set_mode(wifi::Mode::soft_ap);
esp_chip_info_t chip_info; wifi::configure_soft_ap(
uint32_t flash_size; "HyperLink", "1234567890",
esp_chip_info(&chip_info); {.connected =
printf("This is %s chip with %d CPU core(s), %s%s%s%s, ", [](auto mac, auto aid) {
CONFIG_IDF_TARGET, ESP_LOGI("DEBUG - ap callback", "Connected to mac %s",
chip_info.cores, mac.data());
(chip_info.features & CHIP_FEATURE_WIFI_BGN) ? "WiFi/" : "", },
(chip_info.features & CHIP_FEATURE_BT) ? "BT" : "", .disconnected =
(chip_info.features & CHIP_FEATURE_BLE) ? "BLE" : "", [](auto mac, auto aid) {
(chip_info.features & CHIP_FEATURE_IEEE802154) ? ", 802.15.4 (Zigbee/Thread)" : ""); ESP_LOGI("DEBUG - ap callback", "Disconnected from mac %s",
mac.data());
unsigned major_rev = chip_info.revision / 100; }});
unsigned minor_rev = chip_info.revision % 100; wifi::start();
printf("silicon revision v%d.%d, ", major_rev, minor_rev);
if(esp_flash_get_size(NULL, &flash_size) != ESP_OK) {
printf("Get flash size failed");
return;
} }
printf("%" PRIu32 "MB %s flash\n", flash_size / (uint32_t)(1024 * 1024), void start_http_server() {
(chip_info.features & CHIP_FEATURE_EMB_FLASH) ? "embedded" : "external"); http::start_server({.port = 80,
.on_error =
printf("Minimum free heap size: %" PRIu32 " bytes\n", esp_get_minimum_free_heap_size()); []() {
// TODO
for (int i = 10; i >= 0; i--) { },
printf("Restarting in %d seconds...\n", i); .on_not_found =
vTaskDelay(1000 / portTICK_PERIOD_MS); []() {
// TODO
}});
http::set_handlers({{http::Method::get, "/",
[]() {
return http::response_t{
"<html><body style=\"background:black; color: "
"white\">Home</body></html>"};
}},
{http::Method::get, "/hello1",
[]() {
return http::response_t{"Hello 1"};
}},
{http::Method::get, "/hello2", []() {
return http::response_t{"Hello 2"};
}}});
}
extern "C" void app_main(void) {
try {
start_ap();
start_http_server();
} catch (const std::exception& e) {
ESP_LOGE("main", "Exception: %s", e.what());
} }
printf("Restarting now.\n");
fflush(stdout);
esp_restart();
} }

1857
sdkconfig Normal file

File diff suppressed because it is too large Load Diff