first commit

This commit is contained in:
Jérôme Delacotte
2025-03-06 11:15:32 +01:00
commit 7b30d6e298
5276 changed files with 2108927 additions and 0 deletions

View File

@@ -0,0 +1,128 @@
extern "C" {
void app_loop();
void restartMCU();
}
#include "Settings.h"
#include <BlynkSimpleWioTerminal_SSL.h>
#ifndef BLYNK_NEW_LIBRARY
#error "Old version of Blynk library is in use. Please replace it with the new one."
#endif
#if !defined(BLYNK_TEMPLATE_NAME) && defined(BLYNK_DEVICE_NAME)
#define BLYNK_TEMPLATE_NAME BLYNK_DEVICE_NAME
#endif
#if !defined(BLYNK_TEMPLATE_ID) || !defined(BLYNK_TEMPLATE_NAME)
#error "Please specify your BLYNK_TEMPLATE_ID and BLYNK_TEMPLATE_NAME"
#endif
#if defined(BLYNK_AUTH_TOKEN)
#error "BLYNK_AUTH_TOKEN is assigned automatically when using Blynk.Edgent, please remove it from the configuration"
#endif
BlynkTimer edgentTimer;
#include "BlynkState.h"
#include "ConfigStore.h"
#include "ResetButton.h"
#include "ConfigMode.h"
#include "Indicator.h"
#include "OTA.h"
#include "Console.h"
inline
void BlynkState::set(State m) {
if (state != m && m < MODE_MAX_VALUE) {
DEBUG_PRINT(String(StateStr[state]) + " => " + StateStr[m]);
state = m;
// You can put your state handling here,
// i.e. implement custom indication
}
}
void printDeviceBanner()
{
#ifdef BLYNK_PRINT
Blynk.printBanner();
BLYNK_PRINT.println("----------------------------------------------------");
BLYNK_PRINT.print(" Device: "); BLYNK_PRINT.println(getWiFiName());
BLYNK_PRINT.print(" Firmware: "); BLYNK_PRINT.println(BLYNK_FIRMWARE_VERSION " (build " __DATE__ " " __TIME__ ")");
if (configStore.getFlag(CONFIG_FLAG_VALID)) {
BLYNK_PRINT.print(" Token: ");
BLYNK_PRINT.println(String(configStore.cloudToken).substring(0,4) +
" - •••• - •••• - ••••");
}
BLYNK_PRINT.print(" Platform: "); BLYNK_PRINT.println(String(BLYNK_INFO_DEVICE) + " @ " + (F_CPU/1000000) + "MHz");
BLYNK_PRINT.print(" WiFi FW: "); BLYNK_PRINT.println(rpc_system_version());
BLYNK_PRINT.println("----------------------------------------------------");
#endif
}
void runBlynkWithChecks() {
Blynk.run();
if (BlynkState::get() == MODE_RUNNING) {
if (!Blynk.connected()) {
if (WiFi.status() == WL_CONNECTED) {
BlynkState::set(MODE_CONNECTING_CLOUD);
} else {
BlynkState::set(MODE_CONNECTING_NET);
}
}
}
}
class Edgent {
public:
void begin()
{
//indicator_init();
button_init();
config_init();
printDeviceBanner();
console_init();
if (configStore.getFlag(CONFIG_FLAG_VALID)) {
BlynkState::set(MODE_CONNECTING_NET);
} else if (config_load_blnkopt()) {
DEBUG_PRINT("Firmware is preprovisioned");
BlynkState::set(MODE_CONNECTING_NET);
} else {
BlynkState::set(MODE_WAIT_CONFIG);
}
if (!String(BLYNK_TEMPLATE_ID).startsWith("TMPL") ||
!strlen(BLYNK_TEMPLATE_NAME)
) {
DEBUG_PRINT("Invalid configuration of TEMPLATE_ID / TEMPLATE_NAME");
while (true) { delay(100); }
}
}
void run() {
app_loop();
switch (BlynkState::get()) {
case MODE_WAIT_CONFIG:
case MODE_CONFIGURING: enterConfigMode(); break;
case MODE_CONNECTING_NET: enterConnectNet(); break;
case MODE_CONNECTING_CLOUD: enterConnectCloud(); break;
case MODE_RUNNING: runBlynkWithChecks(); break;
case MODE_OTA_UPGRADE: enterOTA(); break;
case MODE_SWITCH_TO_STA: enterSwitchToSTA(); break;
case MODE_RESET_CONFIG: enterResetConfig(); break;
default: enterError(); break;
}
}
} BlynkEdgent;
void app_loop() {
edgentTimer.run();
edgentConsole.run();
}

View File

@@ -0,0 +1,40 @@
enum State {
MODE_WAIT_CONFIG,
MODE_CONFIGURING,
MODE_CONNECTING_NET,
MODE_CONNECTING_CLOUD,
MODE_RUNNING,
MODE_OTA_UPGRADE,
MODE_SWITCH_TO_STA,
MODE_RESET_CONFIG,
MODE_ERROR,
MODE_MAX_VALUE
};
#if defined(APP_DEBUG)
const char* StateStr[MODE_MAX_VALUE+1] = {
"WAIT_CONFIG",
"CONFIGURING",
"CONNECTING_NET",
"CONNECTING_CLOUD",
"RUNNING",
"OTA_UPGRADE",
"SWITCH_TO_STA",
"RESET_CONFIG",
"ERROR",
"INIT"
};
#endif
namespace BlynkState
{
volatile State state = MODE_MAX_VALUE;
State get() { return state; }
bool is (State m) { return (state == m); }
void set(State m);
};

View File

@@ -0,0 +1,480 @@
#include <WiFiClient.h>
#include <WebServer.h>
#include <DNSServer.h>
const char* config_form = R"html(
<!DOCTYPE HTML>
<html>
<head>
<title>WiFi setup</title>
<style>
body {
background-color: #fcfcfc;
box-sizing: border-box;
}
body, input {
font-family: Roboto, sans-serif;
font-weight: 400;
font-size: 16px;
}
.centered {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
padding: 20px;
background-color: #ccc;
border-radius: 4px;
}
td { padding:0 0 0 5px; }
label { white-space:nowrap; }
input { width: 20em; }
input[name="port"] { width: 5em; }
input[type="submit"], img { margin: auto; display: block; width: 30%; }
</style>
</head>
<body>
<div class="centered">
<form method="get" action="config">
<table>
<tr><td><label for="ssid">WiFi SSID:</label></td> <td><input type="text" name="ssid" length=64 required="required"></td></tr>
<tr><td><label for="pass">Password:</label></td> <td><input type="text" name="pass" length=64></td></tr>
<tr><td><label for="blynk">Auth token:</label></td><td><input type="text" name="blynk" placeholder="a0b1c2d..." pattern="[-_a-zA-Z0-9]{32}" maxlength="32" required="required"></td></tr>
<tr><td><label for="host">Host:</label></td> <td><input type="text" name="host" value="blynk.cloud" length=64></td></tr>
<tr><td><label for="port_ssl">Port:</label></td> <td><input type="number" name="port_ssl" value="443" min="1" max="65535"></td></tr>
</table><br/>
<input type="submit" value="Apply">
</form>
</div>
</body>
</html>
)html";
WebServer server(80);
DNSServer dnsServer;
const byte DNS_PORT = 53;
static int connectNetRetries = WIFI_CLOUD_MAX_RETRIES;
static int connectBlynkRetries = WIFI_CLOUD_MAX_RETRIES;
void restartMCU() {
NVIC_SystemReset();
while(1) {};
}
static
String encodeUniquePart(uint32_t n, unsigned len)
{
static constexpr char alphabet[] = { "0W8N4Y1HP5DF9K6JM3C2UA7R" };
static constexpr int base = sizeof(alphabet)-1;
char buf[16] = { 0, };
char prev = 0;
for (unsigned i = 0; i < len; n /= base) {
char c = alphabet[n % base];
if (c == prev) {
c = alphabet[(n+1) % base];
}
prev = buf[i++] = c;
}
return String(buf);
}
static
String getWiFiName(bool withPrefix = true)
{
static byte mac[6] = { 0, };
static bool needMac = true;
if (needMac) {
WiFi.macAddress(mac);
needMac = false;
}
uint32_t unique = 0;
for (int i=0; i<4; i++) {
unique = BlynkCRC32(&mac, sizeof(mac), unique);
}
String devUnique = encodeUniquePart(unique, 4);
String devPrefix = CONFIG_DEVICE_PREFIX;
String devName = String(BLYNK_TEMPLATE_NAME).substring(0, 31-6-devPrefix.length());
if (withPrefix) {
return devPrefix + " " + devName + "-" + devUnique;
} else {
return devName + "-" + devUnique;
}
}
static
String getWiFiMacAddress() {
return WiFi.macAddress();
}
static
String getWiFiApBSSID() {
return WiFi.softAPmacAddress();
}
static
String getWiFiNetworkSSID() {
return WiFi.SSID();
}
static
String getWiFiNetworkBSSID() {
return WiFi.BSSIDstr();
}
String scanNetworks()
{
DEBUG_PRINT("Scanning networks...");
int wifi_nets = WiFi.scanNetworks(true, true);
const uint32_t t = millis();
while (wifi_nets < 0 &&
millis() - t < 20000)
{
delay(20);
wifi_nets = WiFi.scanComplete();
}
DEBUG_PRINT(String("Found networks: ") + wifi_nets);
if (wifi_nets > 0) {
// Sort networks
int indices[wifi_nets];
for (int i = 0; i < wifi_nets; i++) {
indices[i] = i;
}
for (int i = 0; i < wifi_nets; i++) {
for (int j = i + 1; j < wifi_nets; j++) {
if (WiFi.RSSI(indices[j]) > WiFi.RSSI(indices[i])) {
std::swap(indices[i], indices[j]);
}
}
}
wifi_nets = BlynkMin(15, wifi_nets); // Show top 15 networks
// TODO: skip empty names
String result = "[\n";
char buff[256];
for (int i = 0; i < wifi_nets; i++){
int id = indices[i];
const char* sec;
switch (WiFi.encryptionType(id)) {
case WIFI_AUTH_WEP: sec = "WEP"; break;
case WIFI_AUTH_WPA_PSK: sec = "WPA/PSK"; break;
case WIFI_AUTH_WPA2_PSK: sec = "WPA2/PSK"; break;
case WIFI_AUTH_WPA_WPA2_PSK: sec = "WPA/WPA2/PSK"; break;
case WIFI_AUTH_OPEN: sec = "OPEN"; break;
default: sec = "unknown"; break;
}
snprintf(buff, sizeof(buff),
R"json( {"ssid":"%s","bssid":"%s","rssi":%i,"sec":"%s","ch":%i})json",
WiFi.SSID(id).c_str(),
WiFi.BSSIDstr(id).c_str(),
WiFi.RSSI(id),
sec,
WiFi.channel(id)
);
result += buff;
if (i != wifi_nets-1) result += ",\n";
}
return result + "\n]";
} else {
return "[]";
}
}
void handleRoot() {
server.send(200, "text/html", config_form);
}
String networks = "[]";
void enterConfigMode()
{
networks = scanNetworks();
WiFi.mode(WIFI_OFF);
delay(100);
WiFi.mode(WIFI_AP);
delay(2000);
WiFi.softAPConfig(WIFI_AP_IP, WIFI_AP_IP, WIFI_AP_Subnet);
WiFi.softAP(getWiFiName().c_str());
delay(500);
// Set up DNS Server
dnsServer.setTTL(300); // Time-to-live 300s
dnsServer.setErrorReplyCode(DNSReplyCode::ServerFailure); // Return code for non-accessible domains
#ifdef WIFI_CAPTIVE_PORTAL_ENABLE
dnsServer.start(DNS_PORT, "*", WiFi.softAPIP()); // Point all to our IP
server.onNotFound(handleRoot);
#else
dnsServer.start(DNS_PORT, CONFIG_AP_URL, WiFi.softAPIP());
DEBUG_PRINT(String("AP URL: ") + CONFIG_AP_URL);
#endif
server.on("/config", []() {
DEBUG_PRINT("Applying configuration...");
String ssid = server.arg("ssid");
String ssidManual = server.arg("ssidManual");
String pass = server.arg("pass");
if (ssidManual != "") {
ssid = ssidManual;
}
String token = server.arg("blynk");
String host = server.arg("host");
String port = server.arg("port_ssl");
String ip = server.arg("ip");
String mask = server.arg("mask");
String gw = server.arg("gw");
String dns = server.arg("dns");
String dns2 = server.arg("dns2");
bool forceSave = server.arg("save").toInt();
String content;
DEBUG_PRINT(String("WiFi SSID: ") + ssid + " Pass: " + pass);
DEBUG_PRINT(String("Blynk cloud: ") + token + " @ " + host + ":" + port);
if (token.length() == 32 && ssid.length() > 0) {
configStore = configDefault;
CopyString(ssid, configStore.wifiSSID);
CopyString(pass, configStore.wifiPass);
CopyString(token, configStore.cloudToken);
if (host.length()) {
CopyString(host, configStore.cloudHost);
}
if (port.length()) {
configStore.cloudPort = port.toInt();
}
IPAddress addr;
if (ip.length() && addr.fromString(ip)) {
configStore.staticIP = addr;
configStore.setFlag(CONFIG_FLAG_STATIC_IP, true);
} else {
configStore.setFlag(CONFIG_FLAG_STATIC_IP, false);
}
if (mask.length() && addr.fromString(mask)) {
configStore.staticMask = addr;
}
if (gw.length() && addr.fromString(gw)) {
configStore.staticGW = addr;
}
if (dns.length() && addr.fromString(dns)) {
configStore.staticDNS = addr;
}
if (dns2.length() && addr.fromString(dns2)) {
configStore.staticDNS2 = addr;
}
if (forceSave) {
configStore.setFlag(CONFIG_FLAG_VALID, true);
config_save();
content = R"json({"status":"ok","msg":"Configuration saved"})json";
} else {
content = R"json({"status":"ok","msg":"Trying to connect..."})json";
}
server.send(200, "application/json", content);
connectNetRetries = connectBlynkRetries = 1;
BlynkState::set(MODE_SWITCH_TO_STA);
} else {
DEBUG_PRINT("Configuration invalid");
content = R"json({"status":"error","msg":"Configuration invalid"})json";
server.send(500, "application/json", content);
}
});
server.on("/board_info.json", []() {
// Configuring starts with board info request (may impact indication)
BlynkState::set(MODE_CONFIGURING);
DEBUG_PRINT("Sending board info...");
const char* tmpl = BLYNK_TEMPLATE_ID;
char buff[512];
snprintf(buff, sizeof(buff),
R"json({"board":"%s","tmpl_id":"%s","fw_type":"%s","fw_ver":"%s","ssid":"%s","bssid":"%s","mac":"%s","last_error":%d,"wifi_scan":true,"static_ip":true,"5ghz":true})json",
BLYNK_TEMPLATE_NAME,
tmpl ? tmpl : "Unknown",
BLYNK_FIRMWARE_TYPE,
BLYNK_FIRMWARE_VERSION,
getWiFiName().c_str(),
getWiFiApBSSID().c_str(),
getWiFiMacAddress().c_str(),
configStore.last_error
);
server.send(200, "application/json", buff);
});
server.on("/wifi_scan.json", []() {
server.send(200, "application/json", networks);
});
server.on("/reset", []() {
BlynkState::set(MODE_RESET_CONFIG);
server.send(200, "application/json", R"json({"status":"ok","msg":"Configuration reset"})json");
});
server.on("/reboot", []() {
restartMCU();
});
server.begin();
while (BlynkState::is(MODE_WAIT_CONFIG) || BlynkState::is(MODE_CONFIGURING)) {
delay(10);
dnsServer.processNextRequest();
server.handleClient();
app_loop();
if (BlynkState::is(MODE_CONFIGURING) && WiFi.softAPgetStationNum() == 0) {
BlynkState::set(MODE_WAIT_CONFIG);
}
}
server.stop();
}
void enterConnectNet() {
BlynkState::set(MODE_CONNECTING_NET);
DEBUG_PRINT(String("Connecting to WiFi: ") + configStore.wifiSSID);
String hostname = getWiFiName();
hostname.replace(" ", "-");
WiFi.setHostname(hostname.c_str());
if (configStore.getFlag(CONFIG_FLAG_STATIC_IP)) {
if (!WiFi.config(configStore.staticIP,
configStore.staticGW,
configStore.staticMask,
configStore.staticDNS,
configStore.staticDNS2)
) {
DEBUG_PRINT("Failed to configure Static IP");
config_set_last_error(BLYNK_PROV_ERR_CONFIG);
BlynkState::set(MODE_ERROR);
return;
}
}
if (strlen(configStore.wifiPass)) {
WiFi.begin(configStore.wifiSSID, configStore.wifiPass);
} else {
WiFi.begin(configStore.wifiSSID);
}
unsigned long timeoutMs = millis() + WIFI_NET_CONNECT_TIMEOUT;
while ((timeoutMs > millis()) && (WiFi.status() != WL_CONNECTED))
{
delay(10);
app_loop();
if (!BlynkState::is(MODE_CONNECTING_NET)) {
WiFi.disconnect();
return;
}
}
if (WiFi.status() == WL_CONNECTED) {
IPAddress localip = WiFi.localIP();
if (configStore.getFlag(CONFIG_FLAG_STATIC_IP)) {
BLYNK_LOG_IP("Using Static IP: ", localip);
} else {
BLYNK_LOG_IP("Using Dynamic IP: ", localip);
}
connectNetRetries = WIFI_CLOUD_MAX_RETRIES;
BlynkState::set(MODE_CONNECTING_CLOUD);
} else if (--connectNetRetries <= 0) {
config_set_last_error(BLYNK_PROV_ERR_NETWORK);
BlynkState::set(MODE_ERROR);
}
}
void enterConnectCloud() {
BlynkState::set(MODE_CONNECTING_CLOUD);
Blynk.config(configStore.cloudToken, configStore.cloudHost, configStore.cloudPort);
Blynk.connect(0);
unsigned long timeoutMs = millis() + WIFI_CLOUD_CONNECT_TIMEOUT;
while ((timeoutMs > millis()) &&
(WiFi.status() == WL_CONNECTED) &&
(!Blynk.isTokenInvalid()) &&
(Blynk.connected() == false))
{
delay(10);
Blynk.run();
app_loop();
if (!BlynkState::is(MODE_CONNECTING_CLOUD)) {
Blynk.disconnect();
return;
}
}
if (millis() > timeoutMs) {
DEBUG_PRINT("Timeout");
}
if (Blynk.isTokenInvalid()) {
config_set_last_error(BLYNK_PROV_ERR_TOKEN);
BlynkState::set(MODE_WAIT_CONFIG); // TODO: retry after timeout
} else if (WiFi.status() != WL_CONNECTED) {
BlynkState::set(MODE_CONNECTING_NET);
} else if (Blynk.connected()) {
BlynkState::set(MODE_RUNNING);
connectBlynkRetries = WIFI_CLOUD_MAX_RETRIES;
if (!configStore.getFlag(CONFIG_FLAG_VALID)) {
configStore.last_error = BLYNK_PROV_ERR_NONE;
configStore.setFlag(CONFIG_FLAG_VALID, true);
config_save();
Blynk.sendInternal("meta", "set", "Hotspot Name", getWiFiName());
}
} else if (--connectBlynkRetries <= 0) {
config_set_last_error(BLYNK_PROV_ERR_CLOUD);
BlynkState::set(MODE_ERROR);
}
}
void enterSwitchToSTA() {
BlynkState::set(MODE_SWITCH_TO_STA);
DEBUG_PRINT("Switching to STA...");
delay(1000);
WiFi.mode(WIFI_OFF);
delay(100);
WiFi.mode(WIFI_STA);
BlynkState::set(MODE_CONNECTING_NET);
}
void enterError() {
BlynkState::set(MODE_ERROR);
unsigned long timeoutMs = millis() + 10000;
while (timeoutMs > millis() || g_buttonPressed)
{
delay(10);
app_loop();
if (!BlynkState::is(MODE_ERROR)) {
return;
}
}
DEBUG_PRINT("Restarting after error.");
delay(10);
restartMCU();
}

View File

@@ -0,0 +1,162 @@
#define CONFIG_FLAG_VALID 0x01
#define CONFIG_FLAG_STATIC_IP 0x02
#define BLYNK_PROV_ERR_NONE 0 // All good
#define BLYNK_PROV_ERR_CONFIG 700 // Invalid config from app (malformed token,etc)
#define BLYNK_PROV_ERR_NETWORK 701 // Could not connect to the router
#define BLYNK_PROV_ERR_CLOUD 702 // Could not connect to the cloud
#define BLYNK_PROV_ERR_TOKEN 703 // Invalid token error (after connection)
#define BLYNK_PROV_ERR_INTERNAL 704 // Other issues (i.e. hardware failure)
struct ConfigStore {
uint32_t magic;
char version[15];
uint8_t flags;
char wifiSSID[34];
char wifiPass[64];
char cloudToken[34];
char cloudHost[34];
uint16_t cloudPort;
uint32_t staticIP;
uint32_t staticMask;
uint32_t staticGW;
uint32_t staticDNS;
uint32_t staticDNS2;
int last_error;
void setFlag(uint8_t mask, bool value) {
if (value) {
flags |= mask;
} else {
flags &= ~mask;
}
}
bool getFlag(uint8_t mask) {
return (flags & mask) == mask;
}
} __attribute__((packed));
ConfigStore configStore;
const ConfigStore configDefault = {
0x626C6E6B,
BLYNK_FIRMWARE_VERSION,
0x00,
"",
"",
"invalid token",
CONFIG_DEFAULT_SERVER,
CONFIG_DEFAULT_PORT,
0,
BLYNK_PROV_ERR_NONE
};
template<typename T, int size>
void CopyString(const String& s, T(&arr)[size]) {
s.toCharArray(arr, size);
}
static bool config_load_blnkopt()
{
static const char blnkopt[] = "blnkopt\0"
BLYNK_PARAM_KV("ssid" , BLYNK_PARAM_PLACEHOLDER_64
BLYNK_PARAM_PLACEHOLDER_64
BLYNK_PARAM_PLACEHOLDER_64
BLYNK_PARAM_PLACEHOLDER_64)
BLYNK_PARAM_KV("host" , CONFIG_DEFAULT_SERVER)
BLYNK_PARAM_KV("port" , BLYNK_TOSTRING(CONFIG_DEFAULT_PORT))
"\0";
BlynkParam prov(blnkopt+8, sizeof(blnkopt)-8-2);
BlynkParam::iterator ssid = prov["ssid"];
BlynkParam::iterator pass = prov["pass"];
BlynkParam::iterator auth = prov["auth"];
BlynkParam::iterator host = prov["host"];
BlynkParam::iterator port = prov["port"];
if (!(ssid.isValid() && auth.isValid())) {
return false;
}
// reset to defaut before loading values from blnkopt
configStore = configDefault;
if (ssid.isValid()) { CopyString(ssid.asStr(), configStore.wifiSSID); }
if (pass.isValid()) { CopyString(pass.asStr(), configStore.wifiPass); }
if (auth.isValid()) { CopyString(auth.asStr(), configStore.cloudToken); }
if (host.isValid()) { CopyString(host.asStr(), configStore.cloudHost); }
if (port.isValid()) { configStore.cloudPort = port.asInt(); }
return true;
}
#include <sfud.h>
const sfud_flash *_flash = sfud_get_device_table() + 0;
void config_load()
{
memset(&configStore, 0, sizeof(configStore));
sfud_err result = sfud_read(_flash, 0, sizeof(configStore), (uint8_t*)&configStore);
if (result != SFUD_SUCCESS || configStore.magic != 0x626C6E6B)
{
DEBUG_PRINT("Using default config.");
configStore = configDefault;
return;
}
}
bool config_save()
{
sfud_err result = sfud_erase(_flash, 0, sizeof(configStore));
delay(100);
if (!result == SFUD_SUCCESS) { DEBUG_PRINT("Erase flash data failed"); return false; }
result = sfud_write(_flash, 0, sizeof(configStore), (uint8_t*)&configStore);
delay(50);
if (!result == SFUD_SUCCESS) { DEBUG_PRINT("Write the flash data failed"); return false; }
DEBUG_PRINT("Configuration stored to flash");
return true;
}
bool config_init()
{
if (sfud_init() != SFUD_SUCCESS) { DEBUG_PRINT("SFUD init failed"); return false; }
sfud_qspi_fast_read_enable(sfud_get_device(SFUD_W25Q32_DEVICE_INDEX), 2);
config_load();
return true;
}
void enterResetConfig()
{
DEBUG_PRINT("Resetting configuration!");
configStore = configDefault;
config_save();
BlynkState::set(MODE_WAIT_CONFIG);
}
void config_set_last_error(int error) {
// Only set error if not provisioned
if (!configStore.getFlag(CONFIG_FLAG_VALID)) {
configStore = configDefault;
sfud_err result = sfud_erase(_flash, 0, 1);
configStore.last_error = error;
BLYNK_LOG2("Last error code: ", error);
config_save();
}
}

View File

@@ -0,0 +1,69 @@
#include <Blynk/BlynkConsole.h>
BlynkConsole edgentConsole;
void console_init()
{
#ifdef BLYNK_PRINT
edgentConsole.begin(BLYNK_PRINT);
#endif
edgentConsole.print("\n>");
edgentConsole.addCommand("reboot", []() {
edgentConsole.print(R"json({"status":"OK","msg":"rebooting wifi module"})json" "\n");
delay(100);
restartMCU();
});
edgentConsole.addCommand("config", [](int argc, const char** argv) {
if (argc < 1 || 0 == strcmp(argv[0], "start")) {
BlynkState::set(MODE_WAIT_CONFIG);
} else if (0 == strcmp(argv[0], "erase")) {
BlynkState::set(MODE_RESET_CONFIG);
}
});
edgentConsole.addCommand("devinfo", []() {
edgentConsole.printf(
R"json({"name":"%s","board":"%s","tmpl_id":"%s","fw_type":"%s","fw_ver":"%s"})json" "\n",
getWiFiName().c_str(),
BLYNK_TEMPLATE_NAME,
BLYNK_TEMPLATE_ID,
BLYNK_FIRMWARE_TYPE,
BLYNK_FIRMWARE_VERSION
);
});
edgentConsole.addCommand("connect", [](int argc, const char** argv) {
if (argc < 2) {
edgentConsole.print(R"json({"status":"error","msg":"invalid arguments. expected: <auth> <ssid> <pass>"})json" "\n");
return;
}
String auth = argv[0];
String ssid = argv[1];
String pass = (argc >= 3) ? argv[2] : "";
if (auth.length() != 32) {
edgentConsole.print(R"json({"status":"error","msg":"invalid token size"})json" "\n");
return;
}
edgentConsole.print(R"json({"status":"OK","msg":"trying to connect..."})json" "\n");
configStore = configDefault;
CopyString(ssid, configStore.wifiSSID);
CopyString(pass, configStore.wifiPass);
CopyString(auth, configStore.cloudToken);
BlynkState::set(MODE_SWITCH_TO_STA);
});
}
BLYNK_WRITE(InternalPinDBG) {
String cmd = String(param.asStr()) + "\n";
edgentConsole.runCommand((char*)cmd.c_str());
}

View File

@@ -0,0 +1,59 @@
/*************************************************************
Blynk is a platform with iOS and Android apps to control
ESP32, Arduino, Raspberry Pi and the likes over the Internet.
You can easily build mobile and web interfaces for any
projects by simply dragging and dropping widgets.
Downloads, docs, tutorials: https://www.blynk.io
Sketch generator: https://examples.blynk.cc
Blynk community: https://community.blynk.cc
Follow us: https://www.fb.com/blynkapp
https://twitter.com/blynk_app
Blynk library is licensed under MIT license
*************************************************************
Blynk.Edgent implements:
- Blynk.Inject - Dynamic WiFi credentials provisioning
- Blynk.Air - Over The Air firmware updates
- Device state indication using a physical LED
- Credentials reset using a physical Button
Required libraries:
- Seeed Arduino rpcUnified
- Seeed Arduino rpcWiFi
- Seeed Arduino SFUD
- Seeed Arduino FS
- Seeed Arduino mbedtls
- Seeed Arduino FreeRTOS
- ArduinoOTA
- ArduinoHttpClient
NOTE: Please also update the WiFi module firmware:
https://wiki.seeedstudio.com/Wio-Terminal-Network-Overview
*************************************************************/
/* Fill in information from your Blynk Template here */
/* Read more: https://bit.ly/BlynkInject */
//#define BLYNK_TEMPLATE_ID "TMPxxxxxx"
//#define BLYNK_TEMPLATE_NAME "Device"
#define BLYNK_FIRMWARE_VERSION "0.1.0"
#define BLYNK_PRINT Serial
//#define BLYNK_DEBUG
#define APP_DEBUG
#include "BlynkEdgent.h"
void setup()
{
Serial.begin(115200);
delay(100);
BlynkEdgent.begin();
}
void loop() {
BlynkEdgent.run();
}

View File

@@ -0,0 +1,250 @@
#if defined(BOARD_LED_PIN_WS2812)
#include <Adafruit_NeoPixel.h> // Library: https://github.com/adafruit/Adafruit_NeoPixel
Adafruit_NeoPixel rgb = Adafruit_NeoPixel(1, BOARD_LED_PIN_WS2812, NEO_GRB + NEO_KHZ800);
#endif
void indicator_run();
#if !defined(BOARD_LED_BRIGHTNESS)
#define BOARD_LED_BRIGHTNESS 255
#endif
#if defined(BOARD_LED_PIN_WS2812) || defined(BOARD_LED_PIN_R)
#define BOARD_LED_IS_RGB
#endif
#define DIMM(x) ((uint32_t)(x)*(BOARD_LED_BRIGHTNESS)/255)
#define RGB(r,g,b) (DIMM(r) << 16 | DIMM(g) << 8 | DIMM(b) << 0)
#define TO_PWM(x) ((uint32_t)(x)*(BOARD_PWM_MAX)/255)
class Indicator {
public:
enum Colors {
COLOR_BLACK = RGB(0x00, 0x00, 0x00),
COLOR_WHITE = RGB(0xFF, 0xFF, 0xE7),
COLOR_BLUE = RGB(0x0D, 0x36, 0xFF),
COLOR_BLYNK = RGB(0x2E, 0xFF, 0xB9),
COLOR_RED = RGB(0xFF, 0x10, 0x08),
COLOR_MAGENTA = RGB(0xA7, 0x00, 0xFF),
};
Indicator() {
}
void init() {
m_Counter = 0;
initLED();
}
uint32_t run() {
State currState = BlynkState::get();
// Reset counter if indicator state changes
if (m_PrevState != currState) {
m_PrevState = currState;
m_Counter = 0;
}
const long t = millis();
if (g_buttonPressed) {
if (t - g_buttonPressTime > BUTTON_HOLD_TIME_ACTION) { return beatLED(COLOR_WHITE, (int[]){ 100, 100 }); }
if (t - g_buttonPressTime > BUTTON_HOLD_TIME_INDICATION) { return waveLED(COLOR_WHITE, 1000); }
}
switch (currState) {
case MODE_RESET_CONFIG:
case MODE_WAIT_CONFIG: return beatLED(COLOR_BLUE, (int[]){ 50, 500 });
case MODE_CONFIGURING: return beatLED(COLOR_BLUE, (int[]){ 200, 200 });
case MODE_CONNECTING_NET: return beatLED(COLOR_BLYNK, (int[]){ 50, 500 });
case MODE_CONNECTING_CLOUD: return beatLED(COLOR_BLYNK, (int[]){ 100, 100 });
case MODE_RUNNING: return waveLED(COLOR_BLYNK, 5000);
case MODE_OTA_UPGRADE: return beatLED(COLOR_MAGENTA, (int[]){ 50, 50 });
default: return beatLED(COLOR_RED, (int[]){ 80, 100, 80, 1000 } );
}
}
protected:
/*
* LED drivers
*/
#if defined(BOARD_LED_PIN_WS2812) // Addressable, NeoPixel RGB LED
void initLED() {
rgb.begin();
setRGB(COLOR_BLACK);
}
void setRGB(uint32_t color) {
rgb.setPixelColor(0, color);
rgb.show();
}
#elif defined(BOARD_LED_PIN_R) // Normal RGB LED (common anode or common cathode)
void initLED() {
pinMode(BOARD_LED_PIN_R, OUTPUT);
pinMode(BOARD_LED_PIN_G, OUTPUT);
pinMode(BOARD_LED_PIN_B, OUTPUT);
}
void setRGB(uint32_t color) {
uint8_t r = (color & 0xFF0000) >> 16;
uint8_t g = (color & 0x00FF00) >> 8;
uint8_t b = (color & 0x0000FF);
#if BOARD_LED_INVERSE
analogWrite(BOARD_LED_PIN_R, TO_PWM(255 - r));
analogWrite(BOARD_LED_PIN_G, TO_PWM(255 - g));
analogWrite(BOARD_LED_PIN_B, TO_PWM(255 - b));
#else
analogWrite(BOARD_LED_PIN_R, TO_PWM(r));
analogWrite(BOARD_LED_PIN_G, TO_PWM(g));
analogWrite(BOARD_LED_PIN_B, TO_PWM(b));
#endif
}
#elif defined(BOARD_LED_PIN) // Single color LED
void initLED() {
pinMode(BOARD_LED_PIN, OUTPUT);
}
void setLED(uint32_t color) {
#if BOARD_LED_INVERSE
analogWrite(BOARD_LED_PIN, TO_PWM(255 - color));
#else
analogWrite(BOARD_LED_PIN, TO_PWM(color));
#endif
}
#else
#warning Invalid LED configuration.
void initLED() {
}
void setLED(uint32_t color) {
}
#endif
/*
* Animations
*/
uint32_t skipLED() {
return 20;
}
#if defined(BOARD_LED_IS_RGB)
template<typename T>
uint32_t beatLED(uint32_t onColor, const T& beat) {
const uint8_t cnt = sizeof(beat)/sizeof(beat[0]);
setRGB((m_Counter % 2 == 0) ? onColor : (uint32_t)COLOR_BLACK);
uint32_t next = beat[m_Counter % cnt];
m_Counter = (m_Counter+1) % cnt;
return next;
}
uint32_t waveLED(uint32_t colorMax, unsigned breathePeriod) {
uint8_t redMax = (colorMax & 0xFF0000) >> 16;
uint8_t greenMax = (colorMax & 0x00FF00) >> 8;
uint8_t blueMax = (colorMax & 0x0000FF);
// Brightness will rise from 0 to 128, then fall back to 0
uint8_t brightness = (m_Counter < 128) ? m_Counter : 255 - m_Counter;
// Multiply our three colors by the brightness:
redMax *= ((float)brightness / 128.0);
greenMax *= ((float)brightness / 128.0);
blueMax *= ((float)brightness / 128.0);
// And turn the LED to that color:
setRGB((redMax << 16) | (greenMax << 8) | blueMax);
// This function relies on the 8-bit, unsigned m_Counter rolling over.
m_Counter = (m_Counter+1) % 256;
return breathePeriod / 256;
}
#else
template<typename T>
uint32_t beatLED(uint32_t, const T& beat) {
const uint8_t cnt = sizeof(beat)/sizeof(beat[0]);
setLED((m_Counter % 2 == 0) ? BOARD_LED_BRIGHTNESS : 0);
uint32_t next = beat[m_Counter % cnt];
m_Counter = (m_Counter+1) % cnt;
return next;
}
uint32_t waveLED(uint32_t, unsigned breathePeriod) {
uint32_t brightness = (m_Counter < 128) ? m_Counter : 255 - m_Counter;
setLED(DIMM(brightness*2));
// This function relies on the 8-bit, unsigned m_Counter rolling over.
m_Counter = (m_Counter+1) % 256;
return breathePeriod / 256;
}
#endif
private:
uint8_t m_Counter;
State m_PrevState;
};
Indicator indicator;
/*
* Animation timers
*/
#if defined(USE_TC3)
#include <TimerTC3.h>
void indicator_run() {
uint32_t returnTime = indicator.run();
if (returnTime) {
TimerTc3.initialize(returnTime*1000);
}
}
void indicator_init() {
indicator.init();
TimerTc3.initialize(100*1000);
TimerTc3.attachInterrupt(indicator_run);
}
#elif defined(USE_TCC0)
#include <TimerThree.h>
void indicator_run() {
uint32_t returnTime = indicator.run();
if (returnTime) {
Timer3.initialize(returnTime*1000);
}
}
void indicator_init() {
indicator.init();
Timer3.initialize(100*1000);
Timer3.attachInterrupt(indicator_run);
}
#else
#warning LED indicator needs a functional timer!
void indicator_run() {}
void indicator_init() {}
#endif

View File

@@ -0,0 +1,151 @@
#include <ArduinoOTA.h> // only for InternalStorage
#include <ArduinoHttpClient.h>
#define OTA_FATAL(...) { BLYNK_LOG1(__VA_ARGS__); delay(1000); restartMCU(); }
#define USE_SSL
String overTheAirURL;
extern BlynkTimer edgentTimer;
BLYNK_WRITE(InternalPinOTA) {
overTheAirURL = param.asString();
// Force HTTP update
overTheAirURL.replace("https://", "http://");
edgentTimer.setTimeout(2000L, [](){
// Start OTA
Blynk.logEvent("sys_ota", "OTA started");
// Disconnect, not to interfere with OTA process
Blynk.disconnect();
BlynkState::set(MODE_OTA_UPGRADE);
});
}
bool parseURL(String url, String& protocol, String& host, int& port, String& uri)
{
int index = url.indexOf(':');
if(index < 0) {
return false;
}
protocol = url.substring(0, index);
url.remove(0, (index + 3)); // remove protocol part
index = url.indexOf('/');
String server = url.substring(0, index);
url.remove(0, index); // remove server part
index = server.indexOf(':');
if(index >= 0) {
host = server.substring(0, index); // hostname
port = server.substring(index + 1).toInt(); // port
} else {
host = server;
if (protocol == "http") {
port = 80;
} else if (protocol == "https") {
port = 443;
}
}
if (url.length()) {
uri = url;
} else {
uri = "/";
}
return true;
}
void enterOTA() {
BlynkState::set(MODE_OTA_UPGRADE);
// Disconnect, not to interfere with OTA process
Blynk.disconnect();
String protocol, host, url;
int port;
DEBUG_PRINT(String("OTA: ") + overTheAirURL);
if (!parseURL(overTheAirURL, protocol, host, port, url)) {
OTA_FATAL(F("Cannot parse URL"));
}
DEBUG_PRINT(String("Connecting to ") + host + ":" + port);
Client* client = NULL;
if (protocol == "http") {
client = new WiFiClient();
#ifdef USE_SSL
} else if (protocol == "https") {
client = &_blynkWifiClient;
//client = new WiFiClientSecure();
#endif
} else {
OTA_FATAL(String("Unsupported protocol: ") + protocol);
}
HttpClient http(*client, host, port);
http.get(url);
int statusCode = http.responseStatusCode();
if (statusCode != 200) {
http.stop();
OTA_FATAL(String("HTTP status code: ") + statusCode);
}
int contentLength = http.contentLength();
if (contentLength == HttpClient::kNoContentLengthHeader) {
http.stop();
OTA_FATAL("Content-Length not defined");
}
if (!InternalStorage.open(contentLength)) {
http.stop();
OTA_FATAL("Not enough space to store the update");
}
//InternalStorage.debugPrint();
DEBUG_PRINT("Flashing...");
int written = 0;
int prevProgress = 0;
uint8_t buff[256];
while (client->connected() && written < contentLength) {
int len = http.readBytes(buff, sizeof(buff));
if (len <= 0) continue;
for (int i = 0; i<len; i++) {
InternalStorage.write(buff[i]);
}
written += len;
const int progress = (written*100)/contentLength;
if (progress - prevProgress >= 10 || progress == 100) {
#ifdef BLYNK_PRINT
BLYNK_PRINT.print(String("\r ") + progress + "%");
#endif
prevProgress = progress;
}
}
#ifdef BLYNK_PRINT
BLYNK_PRINT.println();
#endif
InternalStorage.close();
http.stop();
if (written != contentLength) {
OTA_FATAL(String("Interrupted at ") + written + " / " + contentLength + " bytes");
}
DEBUG_PRINT("=== Update successfully completed. Rebooting.");
InternalStorage.apply();
}

View File

@@ -0,0 +1,53 @@
#ifdef BOARD_BUTTON_PIN
volatile bool g_buttonPressed = false;
volatile uint32_t g_buttonPressTime = -1;
void button_action(void)
{
BlynkState::set(MODE_RESET_CONFIG);
}
void button_change(void)
{
#if BOARD_BUTTON_ACTIVE_LOW
bool buttonState = !digitalRead(BOARD_BUTTON_PIN);
#else
bool buttonState = digitalRead(BOARD_BUTTON_PIN);
#endif
if (buttonState && !g_buttonPressed) {
g_buttonPressTime = millis();
g_buttonPressed = true;
DEBUG_PRINT("Hold the button for 10 seconds to reset configuration...");
} else if (!buttonState && g_buttonPressed) {
g_buttonPressed = false;
uint32_t buttonHoldTime = millis() - g_buttonPressTime;
if (buttonHoldTime >= BUTTON_HOLD_TIME_ACTION) {
button_action();
} else if (buttonHoldTime >= BUTTON_PRESS_TIME_ACTION) {
// User action
}
g_buttonPressTime = -1;
}
}
void button_init()
{
#if BOARD_BUTTON_ACTIVE_LOW
pinMode(BOARD_BUTTON_PIN, INPUT_PULLUP);
#else
pinMode(BOARD_BUTTON_PIN, INPUT_PULLDOWN);
#endif
attachInterrupt(BOARD_BUTTON_PIN, button_change, CHANGE);
}
#else
#define g_buttonPressed false
#define g_buttonPressTime 0
void button_init() {}
#endif

View File

@@ -0,0 +1,61 @@
/*
* Board configuration (see examples below).
*/
// Example configuration for Wio Terminal Board
#define BOARD_BUTTON_PIN WIO_KEY_A // Pin where user button is attached
#define BOARD_BUTTON_ACTIVE_LOW true // true if button is "active-low"
#define BOARD_LED_PIN LED_BUILTIN // Set LED pin - if you have a single-color LED attached
//#define BOARD_LED_PIN_R 27 // Set R,G,B pins - if your LED is PWM RGB
//#define BOARD_LED_PIN_G 26
//#define BOARD_LED_PIN_B 25
//#define BOARD_LED_PIN_WS2812 33 // Set if your LED is WS2812 RGB
#define BOARD_LED_INVERSE false // true if LED is common anode, false if common cathode
#define BOARD_LED_BRIGHTNESS 255 // 0..255 brightness control
/*
* Advanced options
*/
#define BUTTON_HOLD_TIME_INDICATION 3000
#define BUTTON_HOLD_TIME_ACTION 10000
#define BUTTON_PRESS_TIME_ACTION 50
#define BOARD_PWM_MAX 1023
#if !defined(CONFIG_DEVICE_PREFIX)
#define CONFIG_DEVICE_PREFIX "Blynk"
#endif
#if !defined(CONFIG_AP_URL)
#define CONFIG_AP_URL "blynk.setup"
#endif
#if !defined(CONFIG_DEFAULT_SERVER)
#define CONFIG_DEFAULT_SERVER "blynk.cloud"
#endif
#if !defined(CONFIG_DEFAULT_PORT)
#define CONFIG_DEFAULT_PORT 443
#endif
#define WIFI_CLOUD_MAX_RETRIES 500
#define WIFI_NET_CONNECT_TIMEOUT 50000
#define WIFI_CLOUD_CONNECT_TIMEOUT 50000
#define WIFI_AP_IP IPAddress(192, 168, 4, 1)
#define WIFI_AP_Subnet IPAddress(255, 255, 255, 0)
//#define WIFI_CAPTIVE_PORTAL_ENABLE 1
//#define USE_TC3
//#define USE_TCC0
#define BLYNK_NO_DEFAULT_BANNER
#if defined(APP_DEBUG)
#define DEBUG_PRINT(...) BLYNK_LOG1(__VA_ARGS__)
#define DEBUG_PRINTF(...) BLYNK_LOG(__VA_ARGS__)
#else
#define DEBUG_PRINT(...)
#define DEBUG_PRINTF(...)
#endif