Merge pull request #17 from lorow/feature/replace-json-libs

Removed heartbeats in favor of sending a ping command from time to time on the PC side
Reworked the response structure - both in code and in JSON - we're now getting a proper JSON resposne that's easier to parse, handling them in code now also becomes a bit easier.
Removed unused reset camera command
Reworked how scanned networks get returned
Reworked how the response gets returned - no more raw printf, instead we're getting chunked serial response with proper content
This commit is contained in:
Lorow
2025-10-19 19:30:45 +02:00
committed by GitHub
40 changed files with 26760 additions and 5655 deletions

View File

@@ -8,10 +8,11 @@ Firmware and tools for OpenIris — WiFi, UVC streaming, and a Python setup C
---
## Whats inside
- ESPIDF firmware (C/C++) with modules for Camera, WiFi, UVC, REST/Serial commands, and more
- Python tools for setup over USB serial:
- `tools/switchBoardType.py` — choose a board profile (builds the right sdkconfig)
- `tools/openiris_setup.py` — interactive CLI for WiFi, MDNS/Name, Mode, LED PWM, Logs, and a Settings Summary
- `tools/setup_openiris.py` — interactive CLI for WiFi, MDNS/Name, Mode, LED PWM, Logs, and a Settings Summary
- Composite USB (UVC + CDC) when UVC mode is enabled (`GENERAL_INCLUDE_UVC_MODE`) for simultaneous video streaming and command channel
- LED current monitoring (if enabled via `MONITORING_LED_CURRENT`) with filtered mA readings
- Configurable debug LED + external IR LED control with optional error mirroring (`LED_DEBUG_ENABLE`, `LED_EXTERNAL_AS_DEBUG`)
@@ -22,29 +23,36 @@ Firmware and tools for OpenIris — WiFi, UVC streaming, and a Python setup C
---
## First-time setup on Windows (VS Code + ESPIDF extension)
If youre starting fresh on Windows, this workflow is smooth and reliable:
1) Install tooling
1. Install tooling
- Git: https://git-scm.com/downloads/win
- Visual Studio Code: https://code.visualstudio.com/
2) Get the source code
2. Get the source code
- Create a folder where you want the repo (e.g., `D:\OpenIris-ESPIDF\`). In File Explorer, rightclick the folder and choose “Open in Terminal”.
- Clone and open in VS Code:
```cmd
git clone https://github.com/lorow/OpenIris-ESPIDF.git
cd OpenIris-ESPIDF
code .
```
3) Install the ESPIDF VS Code extension
3. Install the ESPIDF VS Code extension
- In VS Code, open the Extensions tab and install: https://marketplace.visualstudio.com/items?itemName=espressif.esp-idf-extension
4) Set the default terminal profile to Command Prompt
4. Set the default terminal profile to Command Prompt
- Press Ctrl+Shift+P → search “Terminal: Select Default Profile” → choose “Command Prompt”.
- Restart VS Code from its normal shortcut (not from Git Bash). This avoids running ESPIDF in the wrong shell.
5) Configure ESPIDF in the extension
5. Configure ESPIDF in the extension
- On first launch, the extension may prompt to install ESPIDF and tools — follow the steps. It can take a while.
- If you see the extensions home page instead, click “Configure extension”, pick “EXPRESS”, choose “GitHub” as the server and version “v5.4.2”.
- Then open the ESPIDF Explorer tab and click “Open ESPIDF Terminal”. Well use that for builds.
@@ -59,11 +67,14 @@ After this, youre ready for the Quick start below.
Boards are autodiscovered from the `boards/` directory. First list them, then pick one:
Windows (cmd):
```cmd
python .\tools\switchBoardType.py --list
python .\tools\switchBoardType.py --board seed_studio_xiao_esp32s3 --diff
```
macOS/Linux (bash):
```bash
python3 ./tools/switchBoardType.py --list
python3 ./tools/switchBoardType.py --board seed_studio_xiao_esp32s3 --diff
@@ -75,28 +86,35 @@ Notes:
- You can also pass partial or pathlike inputs (e.g. `facefocusvr/eye_L`), the tool normalizes them.
### 2) Build & flash
- Set the target (e.g., ESP32S3).
- Build, flash, and open the serial monitor.
- (Optional) For UVC mode ensure `GENERAL_INCLUDE_UVC_MODE=y`. If you want device to boot directly into UVC: also set `START_IN_UVC_MODE=y`.
- Disable WiFi services for pure wired builds: `GENERAL_ENABLE_WIRELESS=n`.
### 3) Use the Python setup CLI (recommended)
Configure the device over USB serial.
Before you run it:
- If you still have the serial monitor open, close it (the port must be free).
- In VS Code, open the sidebar “ESPIDF: Explorer” and click “Open ESPIDF Terminal”. Well run the CLI there so Python and packages are in the right environment.
Then run:
```cmd
python .\tools\openiris_setup.py --port COMxx
python .\tools\setup_openiris.py --port COMxx
```
Examples:
- Windows: `python .\tools\openiris_setup.py --port COM69`, …
- Windows: `python .\tools\setup_openiris.py --port COM69`, …
- macOS: idk
- Linux: idk
What the CLI can do:
- WiFi menu: automatic (scan → pick → password → connect → wait for IP) or manual (scan, show, configure, connect, status)
- Set MDNS/Device name (also used for the UVC device name)
- Switch mode (WiFi / UVC / Setup)
@@ -107,6 +125,7 @@ What the CLI can do:
---
## Serial number & MAC
- Internally, the serial number is derived from the WiFi MAC address.
- The CLI displays the MAC by default (clearer); its the value used as the serial number.
- The UVC device name is based on the MDNS hostname.
@@ -121,6 +140,7 @@ Runtime override: If the setup CLI (or a JSON command) provides a new device nam
---
## Common workflows
- Fast WiFi setup: in the CLI, go to “WiFi settings” → “Automatic setup”, then check “status”.
- Change name/MDNS: set the device name in the CLI, then replug USB — UVC will show the new name.
- Adjust brightness/LED: set LED PWM in the CLI.
@@ -132,6 +152,7 @@ Runtime override: If the setup CLI (or a JSON command) provides a new device nam
---
## Project layout (short)
- `main/` — entry point
- `components/` — modules (Camera, WiFi, UVC, CommandManager, …)
- `tools/` — Python helper tools (board switch, setup CLI, scanner)
@@ -140,8 +161,10 @@ If you want to dig deeper: commands are mapped via the `CommandManager` under `c
---
## Troubleshooting
### USB Composite (UVC + CDC)
When UVC support is compiled in the device enumerates as a composite USB device:
When UVC support is compiled in, the device enumerates as a composite USB device:
- UVC interface: video streaming (JPEG frames)
- CDC (virtual COM): command channel accepting newlineterminated JSON objects
@@ -209,4 +232,4 @@ The firmware uses a small set of LED patterns to indicate status and blocking er
---
Feedback, issues, and PRs are welcome.
Feedback, issues, and PRs are welcome.

View File

@@ -255,28 +255,4 @@ int CameraManager::setVieWindow(int offsetX,
// todo safariMonkey made a PoC, implement it here
return 0;
}
//! either hardware(1) or software(0)
void CameraManager::resetCamera(bool type)
{
// TODO add camera reset
// if (type)
// {
// // power cycle the camera module (handy if camera stops responding)
// digitalWrite(PWDN_GPIO_NUM, HIGH); // turn power off to camera module
// Network_Utilities::my_delay(0.3); // a for loop with a delay of 300ms
// digitalWrite(PWDN_GPIO_NUM, LOW);
// Network_Utilities::my_delay(0.3);
// setupCamera();
// }
// else
// {
// // reset via software (handy if you wish to change resolution or image type
// // etc. - see test procedure)
// esp_camera_deinit();
// Network_Utilities::my_delay(0.05);
// setupCamera();
// }
}
}

View File

@@ -31,7 +31,6 @@ public:
int setVFlip(int direction);
int setHFlip(int direction);
int setVieWindow(int offsetX, int offsetY, int outputX, int outputY);
void resetCamera(bool type);
private:
void loadConfigData();

View File

@@ -1,6 +1,8 @@
idf_component_register(
SRCS
"CommandManager/CommandManager.cpp"
"CommandManager/CommandResult.cpp"
"CommandManager/CommandSchema.cpp"
"CommandManager/commands/simple_commands.cpp"
"CommandManager/commands/camera_commands.cpp"
"CommandManager/commands/wifi_commands.cpp"
@@ -11,5 +13,5 @@ idf_component_register(
INCLUDE_DIRS
"CommandManager"
"CommandManager/commands"
REQUIRES ProjectConfig cJSON CameraManager OpenIrisTasks wifiManager Helpers LEDManager Monitoring
REQUIRES ProjectConfig nlohmann-json CameraManager OpenIrisTasks wifiManager Helpers LEDManager Monitoring
)

View File

@@ -12,7 +12,6 @@ std::unordered_map<std::string, CommandType> commandTypeMap = {
{"set_mdns", CommandType::SET_MDNS},
{"get_mdns_name", CommandType::GET_MDNS_NAME},
{"update_camera", CommandType::UPDATE_CAMERA},
{"restart_camera", CommandType::RESTART_CAMERA},
{"save_config", CommandType::SAVE_CONFIG},
{"get_config", CommandType::GET_CONFIG},
{"reset_config", CommandType::RESET_CONFIG},
@@ -30,7 +29,7 @@ std::unordered_map<std::string, CommandType> commandTypeMap = {
{"get_who_am_i", CommandType::GET_WHO_AM_I},
};
std::function<CommandResult()> CommandManager::createCommand(const CommandType type, std::string_view json) const
std::function<CommandResult()> CommandManager::createCommand(const CommandType type, const nlohmann::json &json) const
{
switch (type)
{
@@ -39,8 +38,6 @@ std::function<CommandResult()> CommandManager::createCommand(const CommandType t
case CommandType::PAUSE:
return [json]
{ return PauseCommand(json); };
return [json]
{ return PauseCommand(json); };
case CommandType::UPDATE_OTA_CREDENTIALS:
return [this, json]
{ return updateOTACredentialsCommand(this->registry, json); };
@@ -65,9 +62,6 @@ std::function<CommandResult()> CommandManager::createCommand(const CommandType t
case CommandType::UPDATE_CAMERA:
return [this, json]
{ return updateCameraCommand(this->registry, json); };
case CommandType::RESTART_CAMERA:
return [this, json]
{ return restartCameraCommand(this->registry, json); };
case CommandType::GET_CONFIG:
return [this]
{ return getConfigCommand(this->registry); };
@@ -116,70 +110,56 @@ std::function<CommandResult()> CommandManager::createCommand(const CommandType t
}
}
CommandResult CommandManager::executeFromJson(const std::string_view json) const
CommandManagerResponse CommandManager::executeFromJson(const std::string_view json) const
{
cJSON *parsedJson = cJSON_Parse(json.data());
if (parsedJson == nullptr)
return CommandResult::getErrorResult("Initial JSON Parse: Invalid JSON");
const cJSON *commandData = nullptr;
const cJSON *commands = cJSON_GetObjectItem(parsedJson, "commands");
cJSON *responseDocument = cJSON_CreateObject();
cJSON *responses = cJSON_CreateArray();
cJSON *response = nullptr;
cJSON_AddItemToObject(responseDocument, "results", responses);
if (cJSON_GetArraySize(commands) == 0)
if (!nlohmann::json::accept(json))
{
cJSON_Delete(parsedJson);
return CommandResult::getErrorResult("Commands missing");
return CommandManagerResponse(nlohmann::json{{"error", "Initial JSON Parse - Invalid JSON"}});
}
cJSON_ArrayForEach(commandData, commands)
nlohmann::json parsedJson = nlohmann::json::parse(json);
if (!parsedJson.contains("commands") || !parsedJson["commands"].is_array() || parsedJson["commands"].empty())
{
const cJSON *commandTypeString = cJSON_GetObjectItem(commandData, "command");
if (commandTypeString == nullptr)
{
return CommandResult::getErrorResult("Unknown command - missing command type");
}
const cJSON *commandPayload = cJSON_GetObjectItem(commandData, "data");
const auto commandType = commandTypeMap.at(std::string(commandTypeString->valuestring));
std::string commandPayloadString;
if (commandPayload != nullptr)
commandPayloadString = std::string(cJSON_Print(commandPayload));
auto command = createCommand(commandType, commandPayloadString);
if (command == nullptr)
{
cJSON_Delete(parsedJson);
return CommandResult::getErrorResult("Unknown command");
}
response = cJSON_CreateString(command().getResult().c_str());
cJSON_AddItemToArray(responses, response);
return CommandManagerResponse(CommandResult::getErrorResult("Commands missing"));
}
char *jsonString = cJSON_Print(responseDocument);
cJSON_Delete(responseDocument);
cJSON_Delete(parsedJson);
nlohmann::json results = nlohmann::json::array();
// Return the JSON response directly without wrapping it
// The responseDocument already contains the proper format: {"results": [...]}
CommandResult result = CommandResult::getRawJsonResult(jsonString);
free(jsonString);
return result;
for (auto &commandObject : parsedJson["commands"].items())
{
auto commandData = commandObject.value();
if (!commandData.contains("command"))
{
return CommandManagerResponse({{"command", "Unknown command"}, {"error", "Missing command type"}});
}
const auto commandName = commandData["command"].get<std::string>();
if (!commandTypeMap.contains(commandName))
{
return CommandManagerResponse({{"command", commandName}, {"error", "Unknown command"}});
}
const auto commandType = commandTypeMap.at(commandName);
const auto commandPayload = commandData.contains("data") ? commandData["data"] : nlohmann::json::object();
auto command = createCommand(commandType, commandPayload);
results.push_back({
{"command", commandName},
{"result", command()},
});
}
auto response = nlohmann::json{{"results", results}};
return CommandManagerResponse(response);
}
CommandResult CommandManager::executeFromType(const CommandType type, const std::string_view json) const
CommandManagerResponse CommandManager::executeFromType(const CommandType type, const std::string_view json) const
{
const auto command = createCommand(type, json);
if (command == nullptr)
{
return CommandResult::getErrorResult("Unknown command");
return CommandManagerResponse({{"command", type}, {"error", "Unknown command"}});
}
return command();
return CommandManagerResponse({"result", command()});
}

View File

@@ -18,7 +18,7 @@
#include "commands/wifi_commands.hpp"
#include "commands/device_commands.hpp"
#include "commands/scan_commands.hpp"
#include <cJSON.h>
#include <nlohmann-json.hpp>
enum class CommandType
{
@@ -33,7 +33,6 @@ enum class CommandType
SET_MDNS,
GET_MDNS_NAME,
UPDATE_CAMERA,
RESTART_CAMERA,
SAVE_CONFIG,
GET_CONFIG,
RESET_CONFIG,
@@ -57,10 +56,10 @@ class CommandManager
public:
explicit CommandManager(const std::shared_ptr<DependencyRegistry> &DependencyRegistry) : registry(DependencyRegistry) {};
std::function<CommandResult()> createCommand(CommandType type, std::string_view json) const;
std::function<CommandResult()> createCommand(const CommandType type, const nlohmann::json &json) const;
CommandResult executeFromJson(std::string_view json) const;
CommandResult executeFromType(CommandType type, std::string_view json) const;
CommandManagerResponse executeFromJson(std::string_view json) const;
CommandManagerResponse executeFromType(CommandType type, std::string_view json) const;
};
#endif

View File

@@ -0,0 +1,24 @@
#include "CommandResult.hpp"
void to_json(nlohmann::json &j, const CommandResult &result)
{
j = nlohmann::json{{"status", result.isSuccess() ? "success" : "error"}, {"data", result.getData()}};
}
// defined only for interface compatibility, should not be used directly
void from_json(const nlohmann::json &j, CommandResult &result)
{
auto message = j.at("message");
j.at("status") == "success" ? result = CommandResult::getSuccessResult(message) : result = CommandResult::getErrorResult(message);
}
void to_json(nlohmann::json &j, const CommandManagerResponse &result)
{
j = result.getData();
}
// defined only for interface compatibility, should not be used directly
void from_json(const nlohmann::json &j, CommandManagerResponse &result)
{
result = CommandManagerResponse(j.at("result"));
}

View File

@@ -1,9 +1,13 @@
#pragma once
#ifndef COMMAND_RESULT
#define COMMAND_RESULT
#include <format>
#include <string>
#include <algorithm>
#include <nlohmann-json.hpp>
using json = nlohmann::json;
class CommandResult
{
@@ -15,60 +19,41 @@ public:
};
private:
nlohmann::json data;
Status status;
std::string message;
public:
CommandResult(std::string message, const Status status)
{
this->status = status;
// Escape quotes and backslashes in the message for JSON
std::string escapedMessage = message;
size_t pos = 0;
// First escape backslashes
while ((pos = escapedMessage.find('\\', pos)) != std::string::npos) {
escapedMessage.replace(pos, 1, "\\\\");
pos += 2;
}
// Then escape quotes
pos = 0;
while ((pos = escapedMessage.find('"', pos)) != std::string::npos) {
escapedMessage.replace(pos, 1, "\\\"");
pos += 2;
}
if (status == Status::SUCCESS)
{
this->message = std::format("{{\"result\":\"{}\"}}", escapedMessage);
}
else
{
this->message = std::format("{{\"error\":\"{}\"}}", escapedMessage);
}
}
CommandResult(nlohmann::json data, const Status status) : data(data), status(status) {}
bool isSuccess() const { return status == Status::SUCCESS; }
static CommandResult getSuccessResult(const std::string &message)
static CommandResult getSuccessResult(nlohmann::json message)
{
return CommandResult(message, Status::SUCCESS);
}
static CommandResult getErrorResult(const std::string &message)
static CommandResult getErrorResult(nlohmann::json message)
{
return CommandResult(message, Status::FAILURE);
}
// Create a result that returns raw JSON without wrapper
static CommandResult getRawJsonResult(const std::string &jsonMessage)
{
CommandResult result("", Status::SUCCESS);
result.message = jsonMessage;
return result;
}
std::string getResult() const { return this->message; }
nlohmann::json getData() const { return this->data; }
};
void to_json(nlohmann::json &j, const CommandResult &result);
void from_json(const nlohmann::json &j, CommandResult &result);
class CommandManagerResponse
{
private:
nlohmann::json data;
public:
CommandManagerResponse(nlohmann::json data) : data(data) {}
nlohmann::json getData() const { return this->data; }
};
void to_json(nlohmann::json &j, const CommandManagerResponse &result);
void from_json(const nlohmann::json &j, CommandManagerResponse &result);
#endif

View File

@@ -0,0 +1,81 @@
#include "CommandSchema.hpp"
void to_json(nlohmann::json &j, const UpdateWifiPayload &payload)
{
j = nlohmann::json{{"name", payload.name}, {"ssid", payload.ssid}, {"password", payload.password}, {"channel", payload.channel}, {"power", payload.power}};
}
void from_json(const nlohmann::json &j, UpdateWifiPayload &payload)
{
payload.name = j.at("name").get<std::string>();
if (j.contains("ssid"))
{
payload.ssid = j.at("ssid").get<std::string>();
}
if (j.contains("password"))
{
payload.password = j.at("password").get<std::string>();
}
if (j.contains("channel"))
{
payload.channel = j.at("channel").get<uint8_t>();
}
if (j.contains("power"))
{
payload.power = j.at("power").get<uint8_t>();
}
}
void to_json(nlohmann::json &j, const UpdateAPWiFiPayload &payload)
{
j = nlohmann::json{{"ssid", payload.ssid}, {"password", payload.password}, {"channel", payload.channel}};
}
void from_json(const nlohmann::json &j, UpdateAPWiFiPayload &payload)
{
if (j.contains("ssid"))
{
payload.ssid = j.at("ssid").get<std::string>();
}
if (j.contains("password"))
{
payload.password = j.at("password").get<std::string>();
}
if (j.contains("channel"))
{
payload.channel = j.at("channel").get<uint8_t>();
}
}
void to_json(nlohmann::json &j, const UpdateCameraConfigPayload &payload)
{
j = nlohmann::json{{"vflip", payload.vflip}, {"href", payload.href}, {"framesize", payload.framesize}, {"quality", payload.quality}, {"brightness", payload.brightness}};
}
void from_json(const nlohmann::json &j, UpdateCameraConfigPayload &payload)
{
if (j.contains("vflip"))
{
payload.vflip = j.at("vflip").get<uint8_t>();
}
if (j.contains("href"))
{
payload.href = j.at("href").get<uint8_t>();
}
if (j.contains("framesize"))
{
payload.framesize = j.at("framesize").get<uint8_t>();
}
if (j.contains("quality"))
{
payload.quality = j.at("quality").get<uint8_t>();
}
if (j.contains("brightness"))
{
payload.brightness = j.at("brightness").get<uint8_t>();
}
}

View File

@@ -1,31 +1,40 @@
#ifndef COMMAND_SCHEMA_HPP
#define COMMAND_SCHEMA_HPP
#include <nlohmann-json.hpp>
struct BasePayload {};
struct BasePayload
{
};
struct WifiPayload : BasePayload
{
std::string networkName;
std::string name;
std::string ssid;
std::string password;
uint8_t channel;
uint8_t power;
};
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(WifiPayload, name, ssid, password, channel, power)
struct UpdateWifiPayload : BasePayload
{
std::string networkName;
std::string name;
std::optional<std::string> ssid;
std::optional<std::string> password;
std::optional<uint8_t> channel;
std::optional<uint8_t> power;
};
void to_json(nlohmann::json &j, const UpdateWifiPayload &payload);
void from_json(const nlohmann::json &j, UpdateWifiPayload &payload);
struct deleteNetworkPayload : BasePayload
{
std::string networkName;
std::string name;
};
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(deleteNetworkPayload, name)
struct UpdateAPWiFiPayload : BasePayload
{
std::optional<std::string> ssid;
@@ -33,11 +42,15 @@ struct UpdateAPWiFiPayload : BasePayload
std::optional<uint8_t> channel;
};
void to_json(nlohmann::json &j, const UpdateAPWiFiPayload &payload);
void from_json(const nlohmann::json &j, UpdateAPWiFiPayload &payload);
struct MDNSPayload : BasePayload
{
std::string hostname;
};
NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(MDNSPayload, hostname)
struct UpdateCameraConfigPayload : BasePayload
{
std::optional<uint8_t> vflip;
@@ -48,18 +61,6 @@ struct UpdateCameraConfigPayload : BasePayload
// TODO add more options here
};
struct ResetConfigPayload : BasePayload
{
std::string section;
};
struct RestartCameraPayload : BasePayload
{
bool mode;
};
struct PausePayload : BasePayload
{
bool pause;
};
void to_json(nlohmann::json &j, const UpdateCameraConfigPayload &payload);
void from_json(const nlohmann::json &j, UpdateCameraConfigPayload &payload);
#endif

View File

@@ -1,86 +1,17 @@
#include "camera_commands.hpp"
std::optional<UpdateCameraConfigPayload> parseUpdateCameraPayload(std::string_view jsonPayload)
CommandResult updateCameraCommand(std::shared_ptr<DependencyRegistry> registry, const nlohmann::json &json)
{
UpdateCameraConfigPayload payload;
cJSON *parsedJson = cJSON_Parse(jsonPayload.data());
if (parsedJson == nullptr)
return std::nullopt;
cJSON *vflipObject = cJSON_GetObjectItem(parsedJson, "vflip");
cJSON *hrefObject = cJSON_GetObjectItem(parsedJson, "href");
cJSON *framesize = cJSON_GetObjectItem(parsedJson, "framesize");
cJSON *quality = cJSON_GetObjectItem(parsedJson, "quality");
cJSON *brightness = cJSON_GetObjectItem(parsedJson, "brightness");
if (vflipObject != nullptr)
payload.vflip = vflipObject->valueint;
if (hrefObject != nullptr)
payload.href = hrefObject->valueint;
if (framesize != nullptr)
payload.framesize = framesize->valueint;
if (quality != nullptr)
payload.quality = quality->valueint;
if (brightness != nullptr)
payload.brightness = brightness->valueint;
cJSON_Delete(parsedJson);
return payload;
}
std::optional<RestartCameraPayload> parseRestartCameraPayload(std::string_view jsonPayload)
{
RestartCameraPayload payload;
cJSON *parsedJson = cJSON_Parse(jsonPayload.data());
if (parsedJson == nullptr)
return std::nullopt;
cJSON *mode = cJSON_GetObjectItem(parsedJson, "mode");
if (mode == nullptr)
{
cJSON_Delete(parsedJson);
}
payload.mode = (bool)mode->valueint;
cJSON_Delete(parsedJson);
return payload;
}
CommandResult updateCameraCommand(std::shared_ptr<DependencyRegistry> registry, std::string_view jsonPayload)
{
auto payload = parseUpdateCameraPayload(jsonPayload);
if (!payload.has_value())
{
return CommandResult::getErrorResult("Invalid payload");
}
auto updatedConfig = payload.value();
auto payload = json.get<UpdateCameraConfigPayload>();
std::shared_ptr<ProjectConfig> projectConfig = registry->resolve<ProjectConfig>(DependencyType::project_config);
auto oldConfig = projectConfig->getCameraConfig();
projectConfig->setCameraConfig(
updatedConfig.vflip.has_value() ? updatedConfig.vflip.value() : oldConfig.vflip,
updatedConfig.framesize.has_value() ? updatedConfig.framesize.value() : oldConfig.framesize,
updatedConfig.href.has_value() ? updatedConfig.href.value() : oldConfig.href,
updatedConfig.quality.has_value() ? updatedConfig.quality.value() : oldConfig.quality,
updatedConfig.brightness.has_value() ? updatedConfig.brightness.value() : oldConfig.brightness);
payload.vflip.has_value() ? payload.vflip.value() : oldConfig.vflip,
payload.framesize.has_value() ? payload.framesize.value() : oldConfig.framesize,
payload.href.has_value() ? payload.href.value() : oldConfig.href,
payload.quality.has_value() ? payload.quality.value() : oldConfig.quality,
payload.brightness.has_value() ? payload.brightness.value() : oldConfig.brightness);
return CommandResult::getSuccessResult("Config updated");
}
CommandResult restartCameraCommand(std::shared_ptr<DependencyRegistry> registry, std::string_view jsonPayload)
{
auto payload = parseRestartCameraPayload(jsonPayload);
if (!payload.has_value())
{
return CommandResult::getErrorResult("Invalid payload");
}
std::shared_ptr<CameraManager> cameraManager = registry->resolve<CameraManager>(DependencyType::camera_manager);
cameraManager->resetCamera(payload.value().mode);
return CommandResult::getSuccessResult("Camera restarted");
}

View File

@@ -4,17 +4,14 @@
#include <memory>
#include <string>
#include <optional>
#include <cJSON.h>
#include "CommandResult.hpp"
#include "CommandSchema.hpp"
#include "DependencyRegistry.hpp"
#include <CameraManager.hpp>
#include <nlohmann-json.hpp>
std::optional<UpdateCameraConfigPayload> parseUpdateCameraPayload(std::string_view jsonPayload);
CommandResult updateCameraCommand(std::shared_ptr<DependencyRegistry> registry, std::string_view jsonPayload);
CommandResult updateCameraCommand(std::shared_ptr<DependencyRegistry> registry, const nlohmann::json &json);
std::optional<RestartCameraPayload> parseRestartCameraPayload(std::string_view jsonPayload);
CommandResult restartCameraCommand(std::shared_ptr<DependencyRegistry> registry, std::string_view jsonPayload);
#endif
// add cropping command

View File

@@ -1,27 +1,5 @@
#include "config_commands.hpp"
std::optional<ResetConfigPayload> parseResetConfigCommandPayload(std::string_view jsonPayload)
{
ResetConfigPayload payload;
cJSON *parsedJson = cJSON_Parse(jsonPayload.data());
if (parsedJson == nullptr)
return std::nullopt;
cJSON *section = cJSON_GetObjectItem(parsedJson, "section");
if (section == nullptr)
{
cJSON_Delete(parsedJson);
return std::nullopt;
}
payload.section = std::string(section->valuestring);
cJSON_Delete(parsedJson);
return payload;
}
CommandResult saveConfigCommand(std::shared_ptr<DependencyRegistry> registry)
{
std::shared_ptr<ProjectConfig> projectConfig = registry->resolve<ProjectConfig>(DependencyType::project_config);
@@ -38,29 +16,27 @@ CommandResult getConfigCommand(std::shared_ptr<DependencyRegistry> registry)
return CommandResult::getSuccessResult(configRepresentation);
}
CommandResult resetConfigCommand(std::shared_ptr<DependencyRegistry> registry, std::string_view jsonPayload)
CommandResult resetConfigCommand(std::shared_ptr<DependencyRegistry> registry, const nlohmann::json &json)
{
std::array<std::string, 4> supported_sections = {
"all",
};
auto payload = parseResetConfigCommandPayload(jsonPayload);
if (!payload.has_value())
if (!json.contains("section"))
{
return CommandResult::getErrorResult("Invalid payload or missing section");
return CommandResult::getErrorResult("Invalid payload - missing section");
}
auto sectionPayload = payload.value();
auto section = json["section"].get<std::string>();
if (std::find(supported_sections.begin(), supported_sections.end(), sectionPayload.section) == supported_sections.end())
if (std::find(supported_sections.begin(), supported_sections.end(), section) == supported_sections.end())
{
return CommandResult::getErrorResult("Selected section is unsupported");
}
// we cannot match on string, and making a map would be overkill right now, sooo
// todo, add more granual control for other sections, like only reset camera, or only reset wifi
if (sectionPayload.section == "all")
// todo, add more granular control for other sections, like only reset camera, or only reset wifi
if (section == "all")
{
std::shared_ptr<ProjectConfig> projectConfig = registry->resolve<ProjectConfig>(DependencyType::project_config);
projectConfig->reset();

View File

@@ -2,13 +2,12 @@
#include <memory>
#include <string>
#include <optional>
#include <cJSON.h>
#include "CommandResult.hpp"
#include "CommandSchema.hpp"
#include "DependencyRegistry.hpp"
#include <nlohmann-json.hpp>
CommandResult saveConfigCommand(std::shared_ptr<DependencyRegistry> registry);
CommandResult getConfigCommand(std::shared_ptr<DependencyRegistry> registry);
std::optional<ResetConfigPayload> parseresetConfigCommandPayload(std::string_view jsonPayload);
CommandResult resetConfigCommand(std::shared_ptr<DependencyRegistry> registry, std::string_view jsonPayload);
CommandResult resetConfigCommand(std::shared_ptr<DependencyRegistry> registry, const nlohmann::json &json);

View File

@@ -4,23 +4,14 @@
#include "esp_mac.h"
#include <cstdio>
// Implementation inspired by SummerSigh work, initial PR opened in openiris repo, adapted to this rewrite
CommandResult setDeviceModeCommand(std::shared_ptr<DependencyRegistry> registry, std::string_view jsonPayload)
CommandResult setDeviceModeCommand(std::shared_ptr<DependencyRegistry> registry, const nlohmann::json &json)
{
const auto parsedJson = cJSON_Parse(jsonPayload.data());
if (parsedJson == nullptr)
if (!json.contains("mode") || !json["mode"].is_number_integer())
{
return CommandResult::getErrorResult("Invalid payload");
return CommandResult::getErrorResult("Invalid payload - missing or unsupported mode");
}
const auto modeObject = cJSON_GetObjectItem(parsedJson, "mode");
if (modeObject == nullptr)
{
return CommandResult::getErrorResult("Invalid payload - missing mode");
}
const auto mode = modeObject->valueint;
const auto mode = json["mode"].get<int>();
if (mode < 0 || mode > 2)
{
return CommandResult::getErrorResult("Invalid payload - unsupported mode");
@@ -29,19 +20,11 @@ CommandResult setDeviceModeCommand(std::shared_ptr<DependencyRegistry> registry,
const auto projectConfig = registry->resolve<ProjectConfig>(DependencyType::project_config);
projectConfig->setDeviceMode(static_cast<StreamingMode>(mode));
cJSON_Delete(parsedJson);
return CommandResult::getSuccessResult("Device mode set");
}
CommandResult updateOTACredentialsCommand(std::shared_ptr<DependencyRegistry> registry, std::string_view jsonPayload)
CommandResult updateOTACredentialsCommand(std::shared_ptr<DependencyRegistry> registry, const nlohmann::json &json)
{
const auto parsedJson = cJSON_Parse(jsonPayload.data());
if (parsedJson == nullptr)
{
return CommandResult::getErrorResult("Invalid payload");
}
const auto projectConfig = registry->resolve<ProjectConfig>(DependencyType::project_config);
const auto oldDeviceConfig = projectConfig->getDeviceConfig();
@@ -49,48 +32,39 @@ CommandResult updateOTACredentialsCommand(std::shared_ptr<DependencyRegistry> re
auto OTAPassword = oldDeviceConfig.OTAPassword;
auto OTAPort = oldDeviceConfig.OTAPort;
if (const auto OTALoginObject = cJSON_GetObjectItem(parsedJson, "login"); OTALoginObject != nullptr)
if (json.contains("login") && json["login"].is_string())
{
if (const auto newLogin = OTALoginObject->valuestring; strcmp(newLogin, "") != 0)
if (const auto newLogin = json["login"].get<std::string>(); strcmp(newLogin.c_str(), "") != 0)
{
OTALogin = newLogin;
}
}
if (const auto OTAPasswordObject = cJSON_GetObjectItem(parsedJson, "password"); OTAPasswordObject != nullptr)
if (json.contains("password") && json["password"].is_string())
{
OTAPassword = OTAPasswordObject->valuestring;
OTAPassword = json["password"].get<std::string>();
}
if (const auto OTAPortObject = cJSON_GetObjectItem(parsedJson, "port"); OTAPortObject != nullptr)
if (json.contains("port") && json["port"].is_number_integer())
{
if (const auto newPort = OTAPortObject->valueint; newPort >= 82)
if (const auto newPort = json["port"].get<int>(); newPort >= 82)
{
OTAPort = newPort;
}
}
cJSON_Delete(parsedJson);
projectConfig->setOTAConfig(OTALogin, OTAPassword, OTAPort);
return CommandResult::getSuccessResult("OTA Config set");
}
CommandResult updateLEDDutyCycleCommand(std::shared_ptr<DependencyRegistry> registry, std::string_view jsonPayload)
CommandResult updateLEDDutyCycleCommand(std::shared_ptr<DependencyRegistry> registry, const nlohmann::json &json)
{
const auto parsedJson = cJSON_Parse(jsonPayload.data());
if (parsedJson == nullptr)
{
return CommandResult::getErrorResult("Invalid payload");
}
const auto dutyCycleObject = cJSON_GetObjectItem(parsedJson, "dutyCycle");
if (dutyCycleObject == nullptr)
if (!json.contains("dutyCycle") || !json["dutyCycle"].is_number_integer())
{
return CommandResult::getErrorResult("Invalid payload - missing dutyCycle");
}
const auto dutyCycle = dutyCycleObject->valueint;
const auto dutyCycle = json["dutyCycle"].get<int>();
if (dutyCycle < 0 || dutyCycle > 100)
{
@@ -107,8 +81,6 @@ CommandResult updateLEDDutyCycleCommand(std::shared_ptr<DependencyRegistry> regi
ledMgr->setExternalLEDDutyCycle(static_cast<uint8_t>(dutyCycle));
}
cJSON_Delete(parsedJson);
return CommandResult::getSuccessResult("LED duty cycle set");
}
@@ -123,8 +95,8 @@ CommandResult getLEDDutyCycleCommand(std::shared_ptr<DependencyRegistry> registr
const auto projectConfig = registry->resolve<ProjectConfig>(DependencyType::project_config);
const auto deviceCfg = projectConfig->getDeviceConfig();
int duty = deviceCfg.led_external_pwm_duty_cycle;
auto result = std::format("{{ \"led_external_pwm_duty_cycle\": {} }}", duty);
return CommandResult::getSuccessResult(result);
const auto json = nlohmann::json{{"led_external_pwm_duty_cycle", duty}};
return CommandResult::getSuccessResult(json);
}
CommandResult startStreamingCommand()
@@ -145,34 +117,28 @@ CommandResult startStreamingCommand()
return CommandResult::getSuccessResult("Streaming starting");
}
CommandResult switchModeCommand(std::shared_ptr<DependencyRegistry> registry, std::string_view jsonPayload)
CommandResult switchModeCommand(std::shared_ptr<DependencyRegistry> registry, const nlohmann::json &json)
{
const auto parsedJson = cJSON_Parse(jsonPayload.data());
if (parsedJson == nullptr)
{
return CommandResult::getErrorResult("Invalid payload");
}
const auto modeObject = cJSON_GetObjectItem(parsedJson, "mode");
if (modeObject == nullptr)
if (!json.contains("mode") || !json["mode"].is_string())
{
return CommandResult::getErrorResult("Invalid payload - missing mode");
}
const char *modeStr = modeObject->valuestring;
auto modeStr = json["mode"].get<std::string>();
StreamingMode newMode;
ESP_LOGI("[DEVICE_COMMANDS]", "Switch mode command received with mode: %s", modeStr);
ESP_LOGI("[DEVICE_COMMANDS]", "Switch mode command received with mode: %s", modeStr.c_str());
if (strcmp(modeStr, "uvc") == 0)
if (modeStr == "uvc")
{
newMode = StreamingMode::UVC;
}
else if (strcmp(modeStr, "wifi") == 0)
else if (modeStr == "wifi")
{
newMode = StreamingMode::WIFI;
}
else if (strcmp(modeStr, "setup") == 0 || strcmp(modeStr, "auto") == 0)
else if (modeStr == "setup" || modeStr == "auto")
{
newMode = StreamingMode::SETUP;
}
@@ -185,8 +151,6 @@ CommandResult switchModeCommand(std::shared_ptr<DependencyRegistry> registry, st
ESP_LOGI("[DEVICE_COMMANDS]", "Setting device mode to: %d", (int)newMode);
projectConfig->setDeviceMode(newMode);
cJSON_Delete(parsedJson);
return CommandResult::getSuccessResult("Device mode switched, restart to apply");
}
@@ -209,8 +173,11 @@ CommandResult getDeviceModeCommand(std::shared_ptr<DependencyRegistry> registry)
break;
}
auto result = std::format("{{ \"mode\": \"{}\", \"value\": {} }}", modeStr, static_cast<int>(currentMode));
return CommandResult::getSuccessResult(result);
const auto json = nlohmann::json{
{"mode", modeStr},
{"value", static_cast<int>(currentMode)},
};
return CommandResult::getSuccessResult(json);
}
CommandResult getSerialNumberCommand(std::shared_ptr<DependencyRegistry> /*registry*/)
@@ -229,8 +196,11 @@ CommandResult getSerialNumberCommand(std::shared_ptr<DependencyRegistry> /*regis
std::snprintf(mac_colon, sizeof(mac_colon), "%02X:%02X:%02X:%02X:%02X:%02X",
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
auto result = std::format("{{ \"serial\": \"{}\", \"mac\": \"{}\" }}", serial_no_sep, mac_colon);
return CommandResult::getSuccessResult(result);
const auto json = nlohmann::json{
{"serial", serial_no_sep},
{"mac", mac_colon},
};
return CommandResult::getSuccessResult(json);
}
CommandResult getLEDCurrentCommand(std::shared_ptr<DependencyRegistry> registry)
@@ -242,8 +212,8 @@ CommandResult getLEDCurrentCommand(std::shared_ptr<DependencyRegistry> registry)
return CommandResult::getErrorResult("MonitoringManager unavailable");
}
float ma = mon->getCurrentMilliAmps();
auto result = std::format("{{ \"led_current_ma\": {:.3f} }}", static_cast<double>(ma));
return CommandResult::getSuccessResult(result);
const auto json = nlohmann::json{{"led_current_ma", std::format("{:.3f}", static_cast<double>(ma))}};
return CommandResult::getSuccessResult(json);
#else
return CommandResult::getErrorResult("Monitoring disabled");
#endif
@@ -251,11 +221,17 @@ CommandResult getLEDCurrentCommand(std::shared_ptr<DependencyRegistry> registry)
CommandResult getInfoCommand(std::shared_ptr<DependencyRegistry> /*registry*/)
{
const char* who = CONFIG_GENERAL_BOARD;
const char* ver = CONFIG_GENERAL_VERSION;
const char *who = CONFIG_GENERAL_BOARD;
const char *ver = CONFIG_GENERAL_VERSION;
// Ensure non-null strings
if (!who) who = "";
if (!ver) ver = "";
auto result = std::format("{{ \"who_am_i\": \"{}\", \"version\": \"{}\" }}", who, ver);
return CommandResult::getSuccessResult(result);
if (!who)
who = "";
if (!ver)
ver = "";
const auto json = nlohmann::json{
{"who_am_i", who},
{"version", ver},
};
return CommandResult::getSuccessResult(json);
}

View File

@@ -3,22 +3,22 @@
#include "OpenIrisTasks.hpp"
#include "DependencyRegistry.hpp"
#include "esp_timer.h"
#include "cJSON.h"
#include "main_globals.hpp"
#include <format>
#include <string>
#include <nlohmann-json.hpp>
CommandResult updateOTACredentialsCommand(std::shared_ptr<DependencyRegistry> registry, std::string_view jsonPayload);
CommandResult updateOTACredentialsCommand(std::shared_ptr<DependencyRegistry> registry, const nlohmann::json &json);
CommandResult updateLEDDutyCycleCommand(std::shared_ptr<DependencyRegistry> registry, std::string_view jsonPayload);
CommandResult updateLEDDutyCycleCommand(std::shared_ptr<DependencyRegistry> registry, const nlohmann::json &json);
CommandResult getLEDDutyCycleCommand(std::shared_ptr<DependencyRegistry> registry);
CommandResult restartDeviceCommand();
CommandResult startStreamingCommand();
CommandResult switchModeCommand(std::shared_ptr<DependencyRegistry> registry, std::string_view jsonPayload);
CommandResult switchModeCommand(std::shared_ptr<DependencyRegistry> registry, const nlohmann::json &json);
CommandResult getDeviceModeCommand(std::shared_ptr<DependencyRegistry> registry);

View File

@@ -1,33 +1,13 @@
#include "mdns_commands.hpp"
std::optional<MDNSPayload> parseMDNSCommandPayload(std::string_view jsonPayload)
CommandResult setMDNSCommand(std::shared_ptr<DependencyRegistry> registry, const nlohmann::json &json)
{
MDNSPayload payload;
cJSON *parsedJson = cJSON_Parse(jsonPayload.data());
if (parsedJson == nullptr)
return std::nullopt;
cJSON *hostnameObject = cJSON_GetObjectItem(parsedJson, "hostname");
if (hostnameObject == nullptr)
{
cJSON_Delete(parsedJson);
return std::nullopt;
}
payload.hostname = std::string(hostnameObject->valuestring);
cJSON_Delete(parsedJson);
return payload;
}
CommandResult setMDNSCommand(std::shared_ptr<DependencyRegistry> registry, std::string_view jsonPayload)
{
auto payload = parseMDNSCommandPayload(jsonPayload);
if (!payload.has_value())
return CommandResult::getErrorResult("Invalid payload");
const auto payload = json.get<MDNSPayload>();
if (payload.hostname.empty())
return CommandResult::getErrorResult("Invalid payload - empty hostname");
std::shared_ptr<ProjectConfig> projectConfig = registry->resolve<ProjectConfig>(DependencyType::project_config);
projectConfig->setMDNSConfig(payload.value().hostname);
projectConfig->setMDNSConfig(payload.hostname);
return CommandResult::getSuccessResult("Config updated");
}
@@ -37,6 +17,6 @@ CommandResult getMDNSNameCommand(std::shared_ptr<DependencyRegistry> registry)
const auto projectConfig = registry->resolve<ProjectConfig>(DependencyType::project_config);
auto mdnsConfig = projectConfig->getMDNSConfig();
auto result = std::format("{{ \"hostname\": \"{}\" }}", mdnsConfig.hostname);
return CommandResult::getSuccessResult(result);
const auto json = nlohmann::json{{"hostname", mdnsConfig.hostname}};
return CommandResult::getSuccessResult(json);
}

View File

@@ -2,11 +2,10 @@
#include <memory>
#include <string>
#include <optional>
#include <cJSON.h>
#include "CommandResult.hpp"
#include "CommandSchema.hpp"
#include "DependencyRegistry.hpp"
#include <nlohmann-json.hpp>
std::optional<MDNSPayload> parseMDNSCommandPayload(std::string_view jsonPayload);
CommandResult setMDNSCommand(std::shared_ptr<DependencyRegistry> registry, std::string_view jsonPayload);
CommandResult setMDNSCommand(std::shared_ptr<DependencyRegistry> registry, const nlohmann::json &json);
CommandResult getMDNSNameCommand(std::shared_ptr<DependencyRegistry> registry);

View File

@@ -14,29 +14,24 @@ CommandResult scanNetworksCommand(std::shared_ptr<DependencyRegistry> registry)
auto networks = wifiManager->ScanNetworks();
cJSON *root = cJSON_CreateObject();
cJSON *networksArray = cJSON_CreateArray();
cJSON_AddItemToObject(root, "networks", networksArray);
nlohmann::json result;
std::vector<nlohmann::json> networksJson;
for (const auto &network : networks)
{
cJSON *networkObject = cJSON_CreateObject();
cJSON_AddStringToObject(networkObject, "ssid", network.ssid.c_str());
cJSON_AddNumberToObject(networkObject, "channel", network.channel);
cJSON_AddNumberToObject(networkObject, "rssi", network.rssi);
nlohmann::json networkItem;
networkItem["ssid"] = network.ssid;
networkItem["channel"] = network.channel;
networkItem["rssi"] = network.rssi;
char mac_str[18];
sprintf(mac_str, "%02x:%02x:%02x:%02x:%02x:%02x",
network.mac[0], network.mac[1], network.mac[2],
network.mac[3], network.mac[4], network.mac[5]);
cJSON_AddStringToObject(networkObject, "mac_address", mac_str);
cJSON_AddNumberToObject(networkObject, "auth_mode", network.auth_mode);
cJSON_AddItemToArray(networksArray, networkObject);
networkItem["mac_address"] = mac_str;
networkItem["auth_mode"] = network.auth_mode;
networksJson.push_back(networkItem);
}
char *json_string = cJSON_PrintUnformatted(root);
printf("%s\n", json_string);
cJSON_Delete(root);
free(json_string);
return CommandResult::getSuccessResult("Networks scanned");
result["networks"] = networksJson;
return CommandResult::getSuccessResult(result);
}

View File

@@ -4,9 +4,9 @@
#include "CommandResult.hpp"
#include "DependencyRegistry.hpp"
#include "esp_log.h"
#include <cJSON.h>
#include <wifiManager.hpp>
#include <string>
#include <nlohmann-json.hpp>
CommandResult scanNetworksCommand(std::shared_ptr<DependencyRegistry> registry);

View File

@@ -7,27 +7,19 @@ CommandResult PingCommand()
return CommandResult::getSuccessResult("pong");
};
CommandResult PauseCommand(std::string_view jsonPayload)
CommandResult PauseCommand(const nlohmann::json &json)
{
PausePayload payload;
// pause by default if this command gets executed, even if the payload was invalid
payload.pause = true;
auto pause = true;
cJSON *root = cJSON_Parse(std::string(jsonPayload).c_str());
if (root)
if (json.contains("pause") && json["pause"].is_boolean())
{
cJSON *pauseItem = cJSON_GetObjectItem(root, "pause");
if (pauseItem && cJSON_IsBool(pauseItem))
{
payload.pause = cJSON_IsTrue(pauseItem);
}
cJSON_Delete(root);
pause = json["pause"].get<bool>();
}
ESP_LOGI(TAG, "Pause command received: %s", payload.pause ? "true" : "false");
ESP_LOGI(TAG, "Pause command received: %s", pause ? "true" : "false");
setStartupPaused(payload.pause);
if (payload.pause)
setStartupPaused(pause);
if (pause)
{
ESP_LOGI(TAG, "Startup paused - device will remain in configuration mode");
return CommandResult::getSuccessResult("Startup paused");

View File

@@ -3,12 +3,11 @@
#include <string>
#include "CommandResult.hpp"
#include "CommandSchema.hpp"
#include "main_globals.hpp"
#include "esp_log.h"
#include <cJSON.h>
#include <nlohmann-json.hpp>
CommandResult PingCommand();
CommandResult PauseCommand(std::string_view jsonPayload);
CommandResult PauseCommand(const nlohmann::json &json);
#endif

View File

@@ -2,212 +2,76 @@
#include "esp_netif.h"
#include "sdkconfig.h"
std::optional<WifiPayload> parseSetWiFiCommandPayload(std::string_view jsonPayload)
{
WifiPayload payload;
cJSON *parsedJson = cJSON_Parse(jsonPayload.data());
if (parsedJson == nullptr)
return std::nullopt;
const cJSON *networkName = cJSON_GetObjectItem(parsedJson, "name");
const cJSON *ssidPtr = cJSON_GetObjectItem(parsedJson, "ssid");
const cJSON *passwordPtr = cJSON_GetObjectItem(parsedJson, "password");
const cJSON *channelPtr = cJSON_GetObjectItem(parsedJson, "channel");
const cJSON *powerPtr = cJSON_GetObjectItem(parsedJson, "power");
if (ssidPtr == nullptr)
{
// without an SSID we can't do anything
cJSON_Delete(parsedJson);
return std::nullopt;
}
if (networkName == nullptr)
payload.networkName = "main";
else
payload.networkName = std::string(networkName->valuestring);
payload.ssid = std::string(ssidPtr->valuestring);
if (passwordPtr == nullptr)
payload.password = "";
else
payload.password = std::string(passwordPtr->valuestring);
if (channelPtr == nullptr)
payload.channel = 0;
else
payload.channel = channelPtr->valueint;
if (powerPtr == nullptr)
payload.power = 0;
else
payload.power = powerPtr->valueint;
cJSON_Delete(parsedJson);
return payload;
}
std::optional<deleteNetworkPayload> parseDeleteWifiCommandPayload(std::string_view jsonPayload)
{
deleteNetworkPayload payload;
auto *parsedJson = cJSON_Parse(jsonPayload.data());
if (parsedJson == nullptr)
return std::nullopt;
const auto *networkName = cJSON_GetObjectItem(parsedJson, "name");
if (networkName == nullptr)
{
cJSON_Delete(parsedJson);
return std::nullopt;
}
payload.networkName = std::string(networkName->valuestring);
cJSON_Delete(parsedJson);
return payload;
}
std::optional<UpdateWifiPayload> parseUpdateWifiCommandPayload(std::string_view jsonPayload)
{
UpdateWifiPayload payload;
cJSON *parsedJson = cJSON_Parse(jsonPayload.data());
if (parsedJson == nullptr)
return std::nullopt;
const cJSON *networkName = cJSON_GetObjectItem(parsedJson, "name");
if (networkName == nullptr)
{
cJSON_Delete(parsedJson);
return std::nullopt;
}
payload.networkName = std::string(networkName->valuestring);
const auto *ssidObject = cJSON_GetObjectItem(parsedJson, "ssid");
const auto *passwordObject = cJSON_GetObjectItem(parsedJson, "password");
const auto *channelObject = cJSON_GetObjectItem(parsedJson, "channel");
const auto *powerObject = cJSON_GetObjectItem(parsedJson, "power");
if (ssidObject != nullptr && (strcmp(ssidObject->valuestring, "") == 0))
{
// we need ssid to actually connect
cJSON_Delete(parsedJson);
return std::nullopt;
}
if (ssidObject != nullptr)
payload.ssid = std::string(ssidObject->valuestring);
if (passwordObject != nullptr)
payload.password = std::string(passwordObject->valuestring);
if (channelObject != nullptr)
payload.channel = channelObject->valueint;
if (powerObject != nullptr)
payload.power = powerObject->valueint;
cJSON_Delete(parsedJson);
return payload;
}
std::optional<UpdateAPWiFiPayload> parseUpdateAPWiFiCommandPayload(const std::string_view jsonPayload)
{
UpdateAPWiFiPayload payload;
cJSON *parsedJson = cJSON_Parse(jsonPayload.data());
if (parsedJson == nullptr)
{
return std::nullopt;
}
const cJSON *ssidObject = cJSON_GetObjectItem(parsedJson, "ssid");
const cJSON *passwordObject = cJSON_GetObjectItem(parsedJson, "password");
const cJSON *channelObject = cJSON_GetObjectItem(parsedJson, "channel");
if (ssidObject != nullptr)
payload.ssid = std::string(ssidObject->valuestring);
if (passwordObject != nullptr)
payload.password = std::string(passwordObject->valuestring);
if (channelObject != nullptr)
payload.channel = channelObject->valueint;
cJSON_Delete(parsedJson);
return payload;
}
CommandResult setWiFiCommand(std::shared_ptr<DependencyRegistry> registry, std::string_view jsonPayload)
CommandResult setWiFiCommand(std::shared_ptr<DependencyRegistry> registry, const nlohmann::json &json)
{
#if !CONFIG_GENERAL_ENABLE_WIRELESS
return CommandResult::getErrorResult("Not supported by current firmware");
return CommandResult::getErrorResult("Not supported by current firmware");
#endif
const auto payload = parseSetWiFiCommandPayload(jsonPayload);
if (!payload.has_value())
auto payload = json.get<WifiPayload>();
if (payload.name.empty())
{
return CommandResult::getErrorResult("Invalid payload");
payload.name = std::string("main");
}
if (payload.ssid.empty())
{
return CommandResult::getErrorResult("Invalid payload: missing SSID");
}
const auto wifiConfig = payload.value();
std::shared_ptr<ProjectConfig> projectConfig = registry->resolve<ProjectConfig>(DependencyType::project_config);
projectConfig->setWifiConfig(
wifiConfig.networkName,
wifiConfig.ssid,
wifiConfig.password,
wifiConfig.channel,
wifiConfig.power);
payload.name,
payload.ssid,
payload.password,
payload.channel,
payload.power);
return CommandResult::getSuccessResult("Config updated");
}
CommandResult deleteWiFiCommand(std::shared_ptr<DependencyRegistry> registry, std::string_view jsonPayload)
CommandResult deleteWiFiCommand(std::shared_ptr<DependencyRegistry> registry, const nlohmann::json &json)
{
#if !CONFIG_GENERAL_ENABLE_WIRELESS
return CommandResult::getErrorResult("Not supported by current firmware");
return CommandResult::getErrorResult("Not supported by current firmware");
#endif
const auto payload = parseDeleteWifiCommandPayload(jsonPayload);
if (!payload.has_value())
const auto payload = json.get<deleteNetworkPayload>();
if (payload.name.empty())
return CommandResult::getErrorResult("Invalid payload");
auto projectConfig = registry->resolve<ProjectConfig>(DependencyType::project_config);
projectConfig->deleteWifiConfig(payload.value().networkName);
projectConfig->deleteWifiConfig(payload.name);
return CommandResult::getSuccessResult("Config updated");
}
CommandResult updateWiFiCommand(std::shared_ptr<DependencyRegistry> registry, std::string_view jsonPayload)
CommandResult updateWiFiCommand(std::shared_ptr<DependencyRegistry> registry, const nlohmann::json &json)
{
#if !CONFIG_GENERAL_ENABLE_WIRELESS
return CommandResult::getErrorResult("Not supported by current firmware");
return CommandResult::getErrorResult("Not supported by current firmware");
#endif
const auto payload = parseUpdateWifiCommandPayload(jsonPayload);
if (!payload.has_value())
{
return CommandResult::getErrorResult("Invalid payload");
}
auto updatedConfig = payload.value();
auto payload = json.get<UpdateWifiPayload>();
if (payload.name.empty())
{
return CommandResult::getErrorResult("Invalid payload - missing network name");
}
auto projectConfig = registry->resolve<ProjectConfig>(DependencyType::project_config);
auto storedNetworks = projectConfig->getWifiConfigs();
if (const auto networkToUpdate = std::ranges::find_if(
storedNetworks,
[&](auto &network){ return network.name == updatedConfig.networkName; }
);
storedNetworks,
[&](auto &network)
{ return network.name == payload.name; });
networkToUpdate != storedNetworks.end())
{
projectConfig->setWifiConfig(
updatedConfig.networkName,
updatedConfig.ssid.has_value() ? updatedConfig.ssid.value() : networkToUpdate->ssid,
updatedConfig.password.has_value() ? updatedConfig.password.value() : networkToUpdate->password,
updatedConfig.channel.has_value() ? updatedConfig.channel.value() : networkToUpdate->channel,
updatedConfig.power.has_value() ? updatedConfig.power.value() : networkToUpdate->power);
payload.name,
payload.ssid.has_value() ? payload.ssid.value() : networkToUpdate->ssid,
payload.password.has_value() ? payload.password.value() : networkToUpdate->password,
payload.channel.has_value() ? payload.channel.value() : networkToUpdate->channel,
payload.power.has_value() ? payload.power.value() : networkToUpdate->power);
return CommandResult::getSuccessResult("Config updated");
}
@@ -215,113 +79,99 @@ CommandResult updateWiFiCommand(std::shared_ptr<DependencyRegistry> registry, st
return CommandResult::getErrorResult("Requested network does not exist");
}
CommandResult updateAPWiFiCommand(std::shared_ptr<DependencyRegistry> registry, std::string_view jsonPayload)
CommandResult updateAPWiFiCommand(std::shared_ptr<DependencyRegistry> registry, const nlohmann::json &json)
{
#if !CONFIG_GENERAL_ENABLE_WIRELESS
return CommandResult::getErrorResult("Not supported by current firmware");
return CommandResult::getErrorResult("Not supported by current firmware");
#endif
const auto payload = parseUpdateAPWiFiCommandPayload(jsonPayload);
if (!payload.has_value())
return CommandResult::getErrorResult("Invalid payload");
auto updatedConfig = payload.value();
const auto payload = json.get<UpdateAPWiFiPayload>();
auto projectConfig = registry->resolve<ProjectConfig>(DependencyType::project_config);
const auto previousAPConfig = projectConfig->getAPWifiConfig();
projectConfig->setAPWifiConfig(
updatedConfig.ssid.has_value() ? updatedConfig.ssid.value() : previousAPConfig.ssid,
updatedConfig.password.has_value() ? updatedConfig.password.value() : previousAPConfig.password,
updatedConfig.channel.has_value() ? updatedConfig.channel.value() : previousAPConfig.channel);
payload.ssid.has_value() ? payload.ssid.value() : previousAPConfig.ssid,
payload.password.has_value() ? payload.password.value() : previousAPConfig.password,
payload.channel.has_value() ? payload.channel.value() : previousAPConfig.channel);
return CommandResult::getSuccessResult("Config updated");
}
CommandResult getWiFiStatusCommand(std::shared_ptr<DependencyRegistry> registry) {
CommandResult getWiFiStatusCommand(std::shared_ptr<DependencyRegistry> registry)
{
#if !CONFIG_GENERAL_ENABLE_WIRELESS
return CommandResult::getErrorResult("Not supported by current firmware");
#endif
auto wifiManager = registry->resolve<WiFiManager>(DependencyType::wifi_manager);
if (!wifiManager) {
return CommandResult::getErrorResult("Not supported by current firmware");
auto projectConfig = registry->resolve<ProjectConfig>(DependencyType::project_config);
auto wifiState = wifiManager->GetCurrentWiFiState();
auto networks = projectConfig->getWifiConfigs();
nlohmann::json result;
switch (wifiState)
{
case WiFiState_e::WiFiState_NotInitialized:
result["status"] = "not_initialized";
break;
case WiFiState_e::WiFiState_Initialized:
result["status"] = "initialized";
break;
case WiFiState_e::WiFiState_ReadyToConnect:
result["status"] = "ready";
break;
case WiFiState_e::WiFiState_Connecting:
result["status"] = "connecting";
break;
case WiFiState_e::WiFiState_WaitingForIp:
result["status"] = "waiting_for_ip";
break;
case WiFiState_e::WiFiState_Connected:
result["status"] = "connected";
break;
case WiFiState_e::WiFiState_Disconnected:
result["status"] = "disconnected";
break;
case WiFiState_e::WiFiState_Error:
result["status"] = "error";
break;
}
auto projectConfig = registry->resolve<ProjectConfig>(DependencyType::project_config);
// Get current WiFi state
auto wifiState = wifiManager->GetCurrentWiFiState();
auto networks = projectConfig->getWifiConfigs();
cJSON* statusJson = cJSON_CreateObject();
// Add WiFi state
const char* stateStr = "unknown";
switch(wifiState) {
case WiFiState_e::WiFiState_NotInitialized:
stateStr = "not_initialized";
break;
case WiFiState_e::WiFiState_Initialized:
stateStr = "initialized";
break;
case WiFiState_e::WiFiState_ReadyToConnect:
stateStr = "ready";
break;
case WiFiState_e::WiFiState_Connecting:
stateStr = "connecting";
break;
case WiFiState_e::WiFiState_WaitingForIp:
stateStr = "waiting_for_ip";
break;
case WiFiState_e::WiFiState_Connected:
stateStr = "connected";
break;
case WiFiState_e::WiFiState_Disconnected:
stateStr = "disconnected";
break;
case WiFiState_e::WiFiState_Error:
stateStr = "error";
break;
if (wifiState == WiFiState_e::WiFiState_Connected)
{
// Get IP address from ESP32
esp_netif_ip_info_t ip_info;
esp_netif_t *netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
if (netif && esp_netif_get_ip_info(netif, &ip_info) == ESP_OK)
{
char ip_str[16];
sprintf(ip_str, IPSTR, IP2STR(&ip_info.ip));
result["ip_address"] = ip_str;
}
cJSON_AddStringToObject(statusJson, "status", stateStr);
cJSON_AddNumberToObject(statusJson, "networks_configured", networks.size());
// Add IP info if connected
if (wifiState == WiFiState_e::WiFiState_Connected) {
// Get IP address from ESP32
esp_netif_ip_info_t ip_info;
esp_netif_t* netif = esp_netif_get_handle_from_ifkey("WIFI_STA_DEF");
if (netif && esp_netif_get_ip_info(netif, &ip_info) == ESP_OK) {
char ip_str[16];
sprintf(ip_str, IPSTR, IP2STR(&ip_info.ip));
cJSON_AddStringToObject(statusJson, "ip_address", ip_str);
}
}
char* statusString = cJSON_PrintUnformatted(statusJson);
std::string result(statusString);
free(statusString);
cJSON_Delete(statusJson);
return CommandResult::getSuccessResult(result);
}
return CommandResult::getSuccessResult(result);
}
CommandResult connectWiFiCommand(std::shared_ptr<DependencyRegistry> registry) {
CommandResult connectWiFiCommand(std::shared_ptr<DependencyRegistry> registry)
{
#if !CONFIG_GENERAL_ENABLE_WIRELESS
return CommandResult::getErrorResult("Not supported by current firmware");
#endif
auto wifiManager = registry->resolve<WiFiManager>(DependencyType::wifi_manager);
if (!wifiManager) {
return CommandResult::getErrorResult("Not supported by current firmware");
auto projectConfig = registry->resolve<ProjectConfig>(DependencyType::project_config);
auto networks = projectConfig->getWifiConfigs();
if (networks.empty())
{
return CommandResult::getErrorResult("No WiFi networks configured");
}
auto projectConfig = registry->resolve<ProjectConfig>(DependencyType::project_config);
auto networks = projectConfig->getWifiConfigs();
if (networks.empty()) {
return CommandResult::getErrorResult("No WiFi networks configured");
}
// Trigger WiFi connection attempt
wifiManager->TryConnectToStoredNetworks();
return CommandResult::getSuccessResult("WiFi connection attempt started");
// Trigger WiFi connection attempt
wifiManager->TryConnectToStoredNetworks();
return CommandResult::getSuccessResult("WiFi connection attempt started");
}

View File

@@ -4,22 +4,18 @@
#include <memory>
#include <string>
#include <optional>
#include <cJSON.h>
#include "CommandResult.hpp"
#include "CommandSchema.hpp"
#include "DependencyRegistry.hpp"
#include <nlohmann-json.hpp>
std::optional<WifiPayload> parseSetWiFiCommandPayload(std::string_view jsonPayload);
CommandResult setWiFiCommand(std::shared_ptr<DependencyRegistry> registry, std::string_view jsonPayload);
CommandResult setWiFiCommand(std::shared_ptr<DependencyRegistry> registry, const nlohmann::json &json);
std::optional<deleteNetworkPayload> parseDeleteWifiCommandPayload(std::string_view jsonPayload);
CommandResult deleteWiFiCommand(std::shared_ptr<DependencyRegistry> registry, std::string_view jsonPayload);
CommandResult deleteWiFiCommand(std::shared_ptr<DependencyRegistry> registry, const nlohmann::json &json);
std::optional<UpdateWifiPayload> parseUpdateWifiCommandPayload(std::string_view jsonPayload);
CommandResult updateWiFiCommand(std::shared_ptr<DependencyRegistry> registry, std::string_view jsonPayload);
CommandResult updateWiFiCommand(std::shared_ptr<DependencyRegistry> registry, const nlohmann::json &json);
std::optional<UpdateAPWiFiPayload> parseUpdateAPWiFiCommandPayload(std::string_view jsonPayload);
CommandResult updateAPWiFiCommand(std::shared_ptr<DependencyRegistry> registry, std::string_view jsonPayload);
CommandResult updateAPWiFiCommand(std::shared_ptr<DependencyRegistry> registry, const nlohmann::json &json);
CommandResult getWiFiStatusCommand(std::shared_ptr<DependencyRegistry> registry);
CommandResult connectWiFiCommand(std::shared_ptr<DependencyRegistry> registry);

View File

@@ -4,6 +4,19 @@
#define POST_METHOD "POST"
bool getIsSuccess(const nlohmann::json &response)
{
// since the commandManager will be returning CommandManagerResponse to simplify parsing on the clients end
// we can slightly its json representation, and extract the status from there
// note: This will only work for commands executed with CommandManager::executeFromType().
if (!response.contains("result"))
{
return false;
}
return response.at("result").at("status").get<std::string>() == "success";
}
RestAPI::RestAPI(std::string url, std::shared_ptr<CommandManager> commandManager) : command_manager(commandManager)
{
this->url = std::move(url);
@@ -84,66 +97,73 @@ void HandleRestAPIPollTask(void *pvParameter)
// COMMANDS
// updates
void RestAPI::handle_update_wifi(RequestContext *context) {
void RestAPI::handle_update_wifi(RequestContext *context)
{
if (context->method != POST_METHOD)
{
mg_http_reply(context->connection, 401, JSON_RESPONSE, "{%m:%m}", MG_ESC("error"), "Method not allowed");
return;
}
auto const result = command_manager->executeFromType(CommandType::UPDATE_WIFI, context->body);
auto const code = result.isSuccess() ? 200 : 500;
mg_http_reply(context->connection, code, JSON_RESPONSE, result.getResult().c_str());
const nlohmann::json result = command_manager->executeFromType(CommandType::UPDATE_WIFI, context->body);
const auto code = getIsSuccess(result) ? 200 : 400;
mg_http_reply(context->connection, code, JSON_RESPONSE, result.dump().c_str());
}
void RestAPI::handle_update_device(RequestContext *context) {
void RestAPI::handle_update_device(RequestContext *context)
{
if (context->method != POST_METHOD)
{
mg_http_reply(context->connection, 401, JSON_RESPONSE, "{%m:%m}", MG_ESC("error"), "Method not allowed");
return;
}
auto const result = command_manager->executeFromType(CommandType::UPDATE_OTA_CREDENTIALS, context->body);
auto const code = result.isSuccess() ? 200 : 500;
mg_http_reply(context->connection, code, JSON_RESPONSE, result.getResult().c_str());
const nlohmann::json result = command_manager->executeFromType(CommandType::UPDATE_OTA_CREDENTIALS, context->body);
const auto code = getIsSuccess(result) ? 200 : 500;
mg_http_reply(context->connection, code, JSON_RESPONSE, result.dump().c_str());
}
void RestAPI::handle_update_camera(RequestContext *context) {
void RestAPI::handle_update_camera(RequestContext *context)
{
if (context->method != POST_METHOD)
{
mg_http_reply(context->connection, 401, JSON_RESPONSE, "{%m:%m}", MG_ESC("error"), "Method not allowed");
return;
}
auto const result = command_manager->executeFromType(CommandType::UPDATE_CAMERA, context->body);
auto const code = result.isSuccess() ? 200 : 500;
mg_http_reply(context->connection, code, JSON_RESPONSE, result.getResult().c_str());
const nlohmann::json result = command_manager->executeFromType(CommandType::UPDATE_CAMERA, context->body);
const auto code = getIsSuccess(result) ? 200 : 500;
mg_http_reply(context->connection, code, JSON_RESPONSE, result.dump().c_str());
}
// gets
void RestAPI::handle_get_config(RequestContext *context) {
void RestAPI::handle_get_config(RequestContext *context)
{
auto const result = this->command_manager->executeFromType(CommandType::GET_CONFIG, "");
mg_http_reply(context->connection, 200, JSON_RESPONSE, "{%m:%m}", MG_ESC("result"), result.getResult());
const nlohmann::json jsonResult = result;
mg_http_reply(context->connection, 200, JSON_RESPONSE, "{%m:%m}", MG_ESC("result"), jsonResult.dump().c_str());
}
// resets
void RestAPI::handle_reset_config(RequestContext *context) {
void RestAPI::handle_reset_config(RequestContext *context)
{
if (context->method != POST_METHOD)
{
mg_http_reply(context->connection, 401, JSON_RESPONSE, "{%m:%m}", MG_ESC("error"), "Method not allowed");
return;
}
auto const result = this->command_manager->executeFromType(CommandType::RESET_CONFIG, "{\"section\": \"all\"}");
auto const code = result.isSuccess() ? 200 : 500;
mg_http_reply(context->connection, code, JSON_RESPONSE, "{%m:%m}", MG_ESC("result"), result.getResult());
const nlohmann::json result = this->command_manager->executeFromType(CommandType::RESET_CONFIG, "{\"section\": \"all\"}");
const auto code = getIsSuccess(result) ? 200 : 500;
mg_http_reply(context->connection, code, JSON_RESPONSE, "{%m:%m}", MG_ESC("result"), result.dump().c_str());
}
// reboots
void RestAPI::handle_reboot(RequestContext *context) {
auto const result = this->command_manager->executeFromType(CommandType::RESTART_DEVICE, "");
void RestAPI::handle_reboot(RequestContext *context)
{
const auto result = this->command_manager->executeFromType(CommandType::RESTART_DEVICE, "");
mg_http_reply(context->connection, 200, JSON_RESPONSE, "{%m:%m}", MG_ESC("result"), "Ok");
}
@@ -154,16 +174,18 @@ void RestAPI::handle_camera_reboot(RequestContext *context)
// heartbeat
void RestAPI::pong(RequestContext *context) {
auto const result = this->command_manager->executeFromType(CommandType::PING, "");
auto const code = result.isSuccess() ? 200 : 500;
mg_http_reply(context->connection, code, JSON_RESPONSE, result.getResult().c_str());
void RestAPI::pong(RequestContext *context)
{
const nlohmann::json result = this->command_manager->executeFromType(CommandType::PING, "");
const auto code = getIsSuccess(result) ? 200 : 500;
mg_http_reply(context->connection, code, JSON_RESPONSE, result.dump().c_str());
}
// special
void RestAPI::handle_save(RequestContext *context) {
auto const result = this->command_manager->executeFromType(CommandType::SAVE_CONFIG, "");
auto const code = result.isSuccess() ? 200 : 500;
mg_http_reply(context->connection, code, JSON_RESPONSE, result.getResult().c_str());
void RestAPI::handle_save(RequestContext *context)
{
const nlohmann::json result = this->command_manager->executeFromType(CommandType::SAVE_CONFIG, "");
const auto code = getIsSuccess(result) ? 200 : 500;
mg_http_reply(context->connection, code, JSON_RESPONSE, result.dump().c_str());
}

View File

@@ -5,8 +5,8 @@
#define BUF_SIZE (1024)
SerialManager::SerialManager(std::shared_ptr<CommandManager> commandManager, esp_timer_handle_t *timerHandle, std::shared_ptr<ProjectConfig> deviceConfig)
: commandManager(commandManager), timerHandle(timerHandle), deviceConfig(deviceConfig)
SerialManager::SerialManager(std::shared_ptr<CommandManager> commandManager, esp_timer_handle_t *timerHandle)
: commandManager(commandManager), timerHandle(timerHandle)
{
this->data = static_cast<uint8_t *>(malloc(BUF_SIZE));
this->temp_data = static_cast<uint8_t *>(malloc(256));
@@ -50,14 +50,24 @@ void SerialManager::try_receive()
data[current_position] = '\0';
current_position = 0;
const auto result = this->commandManager->executeFromJson(std::string_view(reinterpret_cast<const char *>(this->data)));
const auto resultMessage = result.getResult();
int written = usb_serial_jtag_write_bytes(resultMessage.c_str(), resultMessage.length(), 1000 / 20);
(void)written; // ignore errors if driver already uninstalled
const nlohmann::json result = this->commandManager->executeFromJson(std::string_view(reinterpret_cast<const char *>(this->data)));
const auto resultMessage = result.dump();
usb_serial_jtag_write_bytes_chunked(resultMessage.c_str(), resultMessage.length(), 1000 / 20);
}
}
}
void SerialManager::usb_serial_jtag_write_bytes_chunked(const char *data, size_t len, size_t timeout)
{
while (len > 0)
{
auto to_write = len > BUF_SIZE ? BUF_SIZE : len;
auto written = usb_serial_jtag_write_bytes(data, to_write, timeout);
data += written;
len -= written;
}
}
// Function to notify that a command was received during startup
void SerialManager::notify_startup_command_received()
{
@@ -73,46 +83,6 @@ void SerialManager::notify_startup_command_received()
}
}
void SerialManager::send_heartbeat()
{
// Get the MAC address as unique identifier
uint8_t mac[6];
esp_read_mac(mac, ESP_MAC_WIFI_STA);
// Format as serial number string
char serial_number[18];
sprintf(serial_number, "%02X%02X%02X%02X%02X%02X",
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
// Create heartbeat JSON with serial number
char heartbeat[128];
sprintf(heartbeat, "{\"heartbeat\":\"openiris_setup_mode\",\"serial\":\"%s\"}\n", serial_number);
usb_serial_jtag_write_bytes(heartbeat, strlen(heartbeat), 1000 / 20);
// Ignore return value; if the driver was uninstalled, this is a no-op
}
bool SerialManager::should_send_heartbeat()
{
// Always send heartbeat during startup delay or if no WiFi configured
// If startup timer is still running, always send heartbeat
if (timerHandle != nullptr && *timerHandle != nullptr)
{
return true;
}
// If in heartbeat mode after startup, continue sending
if (getStartupCommandReceived())
{
return true;
}
// Otherwise, only send if no WiFi credentials configured
const auto wifiConfigs = deviceConfig->getWifiConfigs();
return wifiConfigs.empty();
}
void SerialManager::shutdown()
{
// Stop heartbeats; timer will be deleted by main if needed.
@@ -132,23 +102,9 @@ void SerialManager::shutdown()
void HandleSerialManagerTask(void *pvParameters)
{
auto const serialManager = static_cast<SerialManager *>(pvParameters);
TickType_t lastHeartbeat = xTaskGetTickCount();
const TickType_t heartbeatInterval = pdMS_TO_TICKS(2000); // 2 second heartbeat
while (true)
{
serialManager->try_receive();
// Send heartbeat every 2 seconds, but only if no WiFi credentials are set
TickType_t currentTime = xTaskGetTickCount();
if ((currentTime - lastHeartbeat) >= heartbeatInterval)
{
if (serialManager->should_send_heartbeat())
{
serialManager->send_heartbeat();
}
lastHeartbeat = currentTime;
}
}
}
@@ -171,8 +127,8 @@ void HandleCDCSerialManagerTask(void *pvParameters)
if (idx >= BUF_SIZE || buffer[idx - 1] == '\n' || buffer[idx - 1] == '\r')
{
buffer[idx - 1] = '\0';
const auto result = commandManager->executeFromJson(std::string_view(reinterpret_cast<const char *>(buffer)));
const auto resultMessage = result.getResult();
const nlohmann::json result = commandManager->executeFromJson(std::string_view(reinterpret_cast<const char *>(buffer)));
const auto resultMessage = result.dump();
tud_cdc_write(resultMessage.c_str(), resultMessage.length());
tud_cdc_write_flush();
idx = 0;

View File

@@ -32,18 +32,17 @@ struct cdc_command_packet_t
class SerialManager
{
public:
explicit SerialManager(std::shared_ptr<CommandManager> commandManager, esp_timer_handle_t *timerHandle, std::shared_ptr<ProjectConfig> deviceConfig);
explicit SerialManager(std::shared_ptr<CommandManager> commandManager, esp_timer_handle_t *timerHandle);
void setup();
void try_receive();
void send_heartbeat();
bool should_send_heartbeat();
void notify_startup_command_received();
void shutdown();
private:
void usb_serial_jtag_write_bytes_chunked(const char *data, size_t len, size_t timeout);
std::shared_ptr<CommandManager> commandManager;
esp_timer_handle_t *timerHandle;
std::shared_ptr<ProjectConfig> deviceConfig;
uint8_t *data;
uint8_t *temp_data;
};

View File

@@ -65,7 +65,6 @@ static esp_err_t UVCStreamHelpers::camera_start_cb(uvc_format_t format, int widt
}
cameraHandler->setCameraResolution(frame_size);
cameraHandler->resetCamera(false);
constexpr SystemEvent event = {EventSource::STREAM, StreamState_e::Stream_ON};
xQueueSend(eventQueue, &event, 10);

View File

@@ -1,3 +0,0 @@
idf_component_register(SRCS "cJSON/cJSON.c"
INCLUDE_DIRS "cJSON"
)

File diff suppressed because it is too large Load Diff

View File

@@ -1,300 +0,0 @@
/*
Copyright (c) 2009-2017 Dave Gamble and cJSON contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
#ifndef cJSON__h
#define cJSON__h
#ifdef __cplusplus
extern "C"
{
#endif
#if !defined(__WINDOWS__) && (defined(WIN32) || defined(WIN64) || defined(_MSC_VER) || defined(_WIN32))
#define __WINDOWS__
#endif
#ifdef __WINDOWS__
/* When compiling for windows, we specify a specific calling convention to avoid issues where we are being called from a project with a different default calling convention. For windows you have 3 define options:
CJSON_HIDE_SYMBOLS - Define this in the case where you don't want to ever dllexport symbols
CJSON_EXPORT_SYMBOLS - Define this on library build when you want to dllexport symbols (default)
CJSON_IMPORT_SYMBOLS - Define this if you want to dllimport symbol
For *nix builds that support visibility attribute, you can define similar behavior by
setting default visibility to hidden by adding
-fvisibility=hidden (for gcc)
or
-xldscope=hidden (for sun cc)
to CFLAGS
then using the CJSON_API_VISIBILITY flag to "export" the same symbols the way CJSON_EXPORT_SYMBOLS does
*/
#define CJSON_CDECL __cdecl
#define CJSON_STDCALL __stdcall
/* export symbols by default, this is necessary for copy pasting the C and header file */
#if !defined(CJSON_HIDE_SYMBOLS) && !defined(CJSON_IMPORT_SYMBOLS) && !defined(CJSON_EXPORT_SYMBOLS)
#define CJSON_EXPORT_SYMBOLS
#endif
#if defined(CJSON_HIDE_SYMBOLS)
#define CJSON_PUBLIC(type) type CJSON_STDCALL
#elif defined(CJSON_EXPORT_SYMBOLS)
#define CJSON_PUBLIC(type) __declspec(dllexport) type CJSON_STDCALL
#elif defined(CJSON_IMPORT_SYMBOLS)
#define CJSON_PUBLIC(type) __declspec(dllimport) type CJSON_STDCALL
#endif
#else /* !__WINDOWS__ */
#define CJSON_CDECL
#define CJSON_STDCALL
#if (defined(__GNUC__) || defined(__SUNPRO_CC) || defined (__SUNPRO_C)) && defined(CJSON_API_VISIBILITY)
#define CJSON_PUBLIC(type) __attribute__((visibility("default"))) type
#else
#define CJSON_PUBLIC(type) type
#endif
#endif
/* project version */
#define CJSON_VERSION_MAJOR 1
#define CJSON_VERSION_MINOR 7
#define CJSON_VERSION_PATCH 18
#include <stddef.h>
/* cJSON Types: */
#define cJSON_Invalid (0)
#define cJSON_False (1 << 0)
#define cJSON_True (1 << 1)
#define cJSON_NULL (1 << 2)
#define cJSON_Number (1 << 3)
#define cJSON_String (1 << 4)
#define cJSON_Array (1 << 5)
#define cJSON_Object (1 << 6)
#define cJSON_Raw (1 << 7) /* raw json */
#define cJSON_IsReference 256
#define cJSON_StringIsConst 512
/* The cJSON structure: */
typedef struct cJSON
{
/* next/prev allow you to walk array/object chains. Alternatively, use GetArraySize/GetArrayItem/GetObjectItem */
struct cJSON *next;
struct cJSON *prev;
/* An array or object item will have a child pointer pointing to a chain of the items in the array/object. */
struct cJSON *child;
/* The type of the item, as above. */
int type;
/* The item's string, if type==cJSON_String and type == cJSON_Raw */
char *valuestring;
/* writing to valueint is DEPRECATED, use cJSON_SetNumberValue instead */
int valueint;
/* The item's number, if type==cJSON_Number */
double valuedouble;
/* The item's name string, if this item is the child of, or is in the list of subitems of an object. */
char *string;
} cJSON;
typedef struct cJSON_Hooks
{
/* malloc/free are CDECL on Windows regardless of the default calling convention of the compiler, so ensure the hooks allow passing those functions directly. */
void *(CJSON_CDECL *malloc_fn)(size_t sz);
void (CJSON_CDECL *free_fn)(void *ptr);
} cJSON_Hooks;
typedef int cJSON_bool;
/* Limits how deeply nested arrays/objects can be before cJSON rejects to parse them.
* This is to prevent stack overflows. */
#ifndef CJSON_NESTING_LIMIT
#define CJSON_NESTING_LIMIT 1000
#endif
/* returns the version of cJSON as a string */
CJSON_PUBLIC(const char*) cJSON_Version(void);
/* Supply malloc, realloc and free functions to cJSON */
CJSON_PUBLIC(void) cJSON_InitHooks(cJSON_Hooks* hooks);
/* Memory Management: the caller is always responsible to free the results from all variants of cJSON_Parse (with cJSON_Delete) and cJSON_Print (with stdlib free, cJSON_Hooks.free_fn, or cJSON_free as appropriate). The exception is cJSON_PrintPreallocated, where the caller has full responsibility of the buffer. */
/* Supply a block of JSON, and this returns a cJSON object you can interrogate. */
CJSON_PUBLIC(cJSON *) cJSON_Parse(const char *value);
CJSON_PUBLIC(cJSON *) cJSON_ParseWithLength(const char *value, size_t buffer_length);
/* ParseWithOpts allows you to require (and check) that the JSON is null terminated, and to retrieve the pointer to the final byte parsed. */
/* If you supply a ptr in return_parse_end and parsing fails, then return_parse_end will contain a pointer to the error so will match cJSON_GetErrorPtr(). */
CJSON_PUBLIC(cJSON *) cJSON_ParseWithOpts(const char *value, const char **return_parse_end, cJSON_bool require_null_terminated);
CJSON_PUBLIC(cJSON *) cJSON_ParseWithLengthOpts(const char *value, size_t buffer_length, const char **return_parse_end, cJSON_bool require_null_terminated);
/* Render a cJSON entity to text for transfer/storage. */
CJSON_PUBLIC(char *) cJSON_Print(const cJSON *item);
/* Render a cJSON entity to text for transfer/storage without any formatting. */
CJSON_PUBLIC(char *) cJSON_PrintUnformatted(const cJSON *item);
/* Render a cJSON entity to text using a buffered strategy. prebuffer is a guess at the final size. guessing well reduces reallocation. fmt=0 gives unformatted, =1 gives formatted */
CJSON_PUBLIC(char *) cJSON_PrintBuffered(const cJSON *item, int prebuffer, cJSON_bool fmt);
/* Render a cJSON entity to text using a buffer already allocated in memory with given length. Returns 1 on success and 0 on failure. */
/* NOTE: cJSON is not always 100% accurate in estimating how much memory it will use, so to be safe allocate 5 bytes more than you actually need */
CJSON_PUBLIC(cJSON_bool) cJSON_PrintPreallocated(cJSON *item, char *buffer, const int length, const cJSON_bool format);
/* Delete a cJSON entity and all subentities. */
CJSON_PUBLIC(void) cJSON_Delete(cJSON *item);
/* Returns the number of items in an array (or object). */
CJSON_PUBLIC(int) cJSON_GetArraySize(const cJSON *array);
/* Retrieve item number "index" from array "array". Returns NULL if unsuccessful. */
CJSON_PUBLIC(cJSON *) cJSON_GetArrayItem(const cJSON *array, int index);
/* Get item "string" from object. Case insensitive. */
CJSON_PUBLIC(cJSON *) cJSON_GetObjectItem(const cJSON * const object, const char * const string);
CJSON_PUBLIC(cJSON *) cJSON_GetObjectItemCaseSensitive(const cJSON * const object, const char * const string);
CJSON_PUBLIC(cJSON_bool) cJSON_HasObjectItem(const cJSON *object, const char *string);
/* For analysing failed parses. This returns a pointer to the parse error. You'll probably need to look a few chars back to make sense of it. Defined when cJSON_Parse() returns 0. 0 when cJSON_Parse() succeeds. */
CJSON_PUBLIC(const char *) cJSON_GetErrorPtr(void);
/* Check item type and return its value */
CJSON_PUBLIC(char *) cJSON_GetStringValue(const cJSON * const item);
CJSON_PUBLIC(double) cJSON_GetNumberValue(const cJSON * const item);
/* These functions check the type of an item */
CJSON_PUBLIC(cJSON_bool) cJSON_IsInvalid(const cJSON * const item);
CJSON_PUBLIC(cJSON_bool) cJSON_IsFalse(const cJSON * const item);
CJSON_PUBLIC(cJSON_bool) cJSON_IsTrue(const cJSON * const item);
CJSON_PUBLIC(cJSON_bool) cJSON_IsBool(const cJSON * const item);
CJSON_PUBLIC(cJSON_bool) cJSON_IsNull(const cJSON * const item);
CJSON_PUBLIC(cJSON_bool) cJSON_IsNumber(const cJSON * const item);
CJSON_PUBLIC(cJSON_bool) cJSON_IsString(const cJSON * const item);
CJSON_PUBLIC(cJSON_bool) cJSON_IsArray(const cJSON * const item);
CJSON_PUBLIC(cJSON_bool) cJSON_IsObject(const cJSON * const item);
CJSON_PUBLIC(cJSON_bool) cJSON_IsRaw(const cJSON * const item);
/* These calls create a cJSON item of the appropriate type. */
CJSON_PUBLIC(cJSON *) cJSON_CreateNull(void);
CJSON_PUBLIC(cJSON *) cJSON_CreateTrue(void);
CJSON_PUBLIC(cJSON *) cJSON_CreateFalse(void);
CJSON_PUBLIC(cJSON *) cJSON_CreateBool(cJSON_bool boolean);
CJSON_PUBLIC(cJSON *) cJSON_CreateNumber(double num);
CJSON_PUBLIC(cJSON *) cJSON_CreateString(const char *string);
/* raw json */
CJSON_PUBLIC(cJSON *) cJSON_CreateRaw(const char *raw);
CJSON_PUBLIC(cJSON *) cJSON_CreateArray(void);
CJSON_PUBLIC(cJSON *) cJSON_CreateObject(void);
/* Create a string where valuestring references a string so
* it will not be freed by cJSON_Delete */
CJSON_PUBLIC(cJSON *) cJSON_CreateStringReference(const char *string);
/* Create an object/array that only references it's elements so
* they will not be freed by cJSON_Delete */
CJSON_PUBLIC(cJSON *) cJSON_CreateObjectReference(const cJSON *child);
CJSON_PUBLIC(cJSON *) cJSON_CreateArrayReference(const cJSON *child);
/* These utilities create an Array of count items.
* The parameter count cannot be greater than the number of elements in the number array, otherwise array access will be out of bounds.*/
CJSON_PUBLIC(cJSON *) cJSON_CreateIntArray(const int *numbers, int count);
CJSON_PUBLIC(cJSON *) cJSON_CreateFloatArray(const float *numbers, int count);
CJSON_PUBLIC(cJSON *) cJSON_CreateDoubleArray(const double *numbers, int count);
CJSON_PUBLIC(cJSON *) cJSON_CreateStringArray(const char *const *strings, int count);
/* Append item to the specified array/object. */
CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToArray(cJSON *array, cJSON *item);
CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToObject(cJSON *object, const char *string, cJSON *item);
/* Use this when string is definitely const (i.e. a literal, or as good as), and will definitely survive the cJSON object.
* WARNING: When this function was used, make sure to always check that (item->type & cJSON_StringIsConst) is zero before
* writing to `item->string` */
CJSON_PUBLIC(cJSON_bool) cJSON_AddItemToObjectCS(cJSON *object, const char *string, cJSON *item);
/* Append reference to item to the specified array/object. Use this when you want to add an existing cJSON to a new cJSON, but don't want to corrupt your existing cJSON. */
CJSON_PUBLIC(cJSON_bool) cJSON_AddItemReferenceToArray(cJSON *array, cJSON *item);
CJSON_PUBLIC(cJSON_bool) cJSON_AddItemReferenceToObject(cJSON *object, const char *string, cJSON *item);
/* Remove/Detach items from Arrays/Objects. */
CJSON_PUBLIC(cJSON *) cJSON_DetachItemViaPointer(cJSON *parent, cJSON * const item);
CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromArray(cJSON *array, int which);
CJSON_PUBLIC(void) cJSON_DeleteItemFromArray(cJSON *array, int which);
CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromObject(cJSON *object, const char *string);
CJSON_PUBLIC(cJSON *) cJSON_DetachItemFromObjectCaseSensitive(cJSON *object, const char *string);
CJSON_PUBLIC(void) cJSON_DeleteItemFromObject(cJSON *object, const char *string);
CJSON_PUBLIC(void) cJSON_DeleteItemFromObjectCaseSensitive(cJSON *object, const char *string);
/* Update array items. */
CJSON_PUBLIC(cJSON_bool) cJSON_InsertItemInArray(cJSON *array, int which, cJSON *newitem); /* Shifts pre-existing items to the right. */
CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemViaPointer(cJSON * const parent, cJSON * const item, cJSON * replacement);
CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInArray(cJSON *array, int which, cJSON *newitem);
CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInObject(cJSON *object,const char *string,cJSON *newitem);
CJSON_PUBLIC(cJSON_bool) cJSON_ReplaceItemInObjectCaseSensitive(cJSON *object,const char *string,cJSON *newitem);
/* Duplicate a cJSON item */
CJSON_PUBLIC(cJSON *) cJSON_Duplicate(const cJSON *item, cJSON_bool recurse);
/* Duplicate will create a new, identical cJSON item to the one you pass, in new memory that will
* need to be released. With recurse!=0, it will duplicate any children connected to the item.
* The item->next and ->prev pointers are always zero on return from Duplicate. */
/* Recursively compare two cJSON items for equality. If either a or b is NULL or invalid, they will be considered unequal.
* case_sensitive determines if object keys are treated case sensitive (1) or case insensitive (0) */
CJSON_PUBLIC(cJSON_bool) cJSON_Compare(const cJSON * const a, const cJSON * const b, const cJSON_bool case_sensitive);
/* Minify a strings, remove blank characters(such as ' ', '\t', '\r', '\n') from strings.
* The input pointer json cannot point to a read-only address area, such as a string constant,
* but should point to a readable and writable address area. */
CJSON_PUBLIC(void) cJSON_Minify(char *json);
/* Helper functions for creating and adding items to an object at the same time.
* They return the added item or NULL on failure. */
CJSON_PUBLIC(cJSON*) cJSON_AddNullToObject(cJSON * const object, const char * const name);
CJSON_PUBLIC(cJSON*) cJSON_AddTrueToObject(cJSON * const object, const char * const name);
CJSON_PUBLIC(cJSON*) cJSON_AddFalseToObject(cJSON * const object, const char * const name);
CJSON_PUBLIC(cJSON*) cJSON_AddBoolToObject(cJSON * const object, const char * const name, const cJSON_bool boolean);
CJSON_PUBLIC(cJSON*) cJSON_AddNumberToObject(cJSON * const object, const char * const name, const double number);
CJSON_PUBLIC(cJSON*) cJSON_AddStringToObject(cJSON * const object, const char * const name, const char * const string);
CJSON_PUBLIC(cJSON*) cJSON_AddRawToObject(cJSON * const object, const char * const name, const char * const raw);
CJSON_PUBLIC(cJSON*) cJSON_AddObjectToObject(cJSON * const object, const char * const name);
CJSON_PUBLIC(cJSON*) cJSON_AddArrayToObject(cJSON * const object, const char * const name);
/* When assigning an integer value, it needs to be propagated to valuedouble too. */
#define cJSON_SetIntValue(object, number) ((object) ? (object)->valueint = (object)->valuedouble = (number) : (number))
/* helper for the cJSON_SetNumberValue macro */
CJSON_PUBLIC(double) cJSON_SetNumberHelper(cJSON *object, double number);
#define cJSON_SetNumberValue(object, number) ((object != NULL) ? cJSON_SetNumberHelper(object, (double)number) : (number))
/* Change the valuestring of a cJSON_String object, only takes effect when type of object is cJSON_String */
CJSON_PUBLIC(char*) cJSON_SetValuestring(cJSON *object, const char *valuestring);
/* If the object is not a boolean type this does nothing and returns cJSON_Invalid else it returns the new type*/
#define cJSON_SetBoolValue(object, boolValue) ( \
(object != NULL && ((object)->type & (cJSON_False|cJSON_True))) ? \
(object)->type=((object)->type &(~(cJSON_False|cJSON_True)))|((boolValue)?cJSON_True:cJSON_False) : \
cJSON_Invalid\
)
/* Macro for iterating over an array or object */
#define cJSON_ArrayForEach(element, array) for(element = (array != NULL) ? (array)->child : NULL; element != NULL; element = element->next)
/* malloc/free objects using the malloc/free functions that have been set with cJSON_InitHooks */
CJSON_PUBLIC(void *) cJSON_malloc(size_t size);
CJSON_PUBLIC(void) cJSON_free(void *object);
#ifdef __cplusplus
}
#endif
#endif

View File

@@ -0,0 +1,3 @@
idf_component_register(
INCLUDE_DIRS "nlohmann-json"
)

File diff suppressed because it is too large Load Diff

View File

@@ -28,11 +28,15 @@
#include <UVCStream.hpp>
#endif
// defines to configure nlohmann-json for esp32
#define JSON_NO_IO 1
#define JSON_NOEXCEPTION 1
#ifdef CONFIG_LED_DEBUG_ENABLE
#define BLINK_GPIO (gpio_num_t) CONFIG_LED_DEBUG_GPIO
#else
// Use an invalid / unused GPIO when debug LED disabled to avoid accidental toggles
#define BLINK_GPIO (gpio_num_t) -1
#define BLINK_GPIO (gpio_num_t) - 1
#endif
#define CONFIG_LED_C_PIN_GPIO (gpio_num_t) CONFIG_LED_EXTERNAL_GPIO
@@ -64,8 +68,8 @@ UVCStreamManager uvcStream;
#endif
auto ledManager = std::make_shared<LEDManager>(BLINK_GPIO, CONFIG_LED_C_PIN_GPIO, ledStateQueue, deviceConfig);
auto *serialManager = new SerialManager(commandManager, &timerHandle, deviceConfig);
std::shared_ptr<MonitoringManager> monitoringManager = std::make_shared<MonitoringManager>();
auto *serialManager = new SerialManager(commandManager, &timerHandle);
void startWiFiMode();
void startWiredMode(bool shouldCloseSerialManager);
@@ -108,8 +112,8 @@ void launch_streaming()
}
else if (deviceMode == StreamingMode::SETUP)
{
// we're still in setup, the user didn't select anything yet, let's give a bit of time for them to make a choice
ESP_LOGI("[MAIN]", "No mode was selected, staying in SETUP mode. WiFi streaming will be enabled still. \nPlease select another mode if you'd like.");
// we're still in setup, the user didn't select anything yet, let's give a bit of time for them to make a choice
ESP_LOGI("[MAIN]", "No mode was selected, staying in SETUP mode. WiFi streaming will be enabled still. \nPlease select another mode if you'd like.");
}
else
{
@@ -260,11 +264,8 @@ extern "C" void app_main(void)
dependencyRegistry->registerService<MonitoringManager>(DependencyType::monitoring_manager, monitoringManager);
// add endpoint to check firmware version
// add firmware version somewhere
// setup CI and building for other boards
// finish todos, overhaul stuff a bit
// todo - do we need logs over CDC? Or just commands and their results?
// esp_log_set_vprintf(&websocket_logger);
Logo::printASCII();
initNVSStorage();

File diff suppressed because it is too large Load Diff

View File

@@ -1 +0,0 @@
pyserial>=3.5

View File

@@ -1,7 +0,0 @@
@echo off
echo Installing OpenIris Setup Tool dependencies...
pip install -r requirements.txt
echo.
echo Setup complete! Run the tool with:
echo python openiris_setup.py
pause

View File

@@ -1,6 +0,0 @@
#!/bin/bash
echo "Installing OpenIris Setup Tool dependencies..."
pip3 install -r requirements.txt
echo ""
echo "Setup complete! Run the tool with:"
echo "python3 openiris_setup.py"

717
tools/setup_openiris.py Normal file
View File

@@ -0,0 +1,717 @@
# /// script
# dependencies = [
# "pyserial>=3.5",
# ]
# ///
import json
import time
import argparse
import sys
import serial
import string
from dataclasses import dataclass
def is_back(choice: str):
return choice.lower() in ["back", "b", "exit"]
class SubMenu:
def __init__(self, title, context: dict, parent_menu=None):
self.title = title
self.context = context
self.items = []
if parent_menu:
parent_menu.add_submenu(self)
def render(self, idx: int):
print(f"{str(idx + 1):>2} {self.title}")
def add_submenu(self, submenu):
self.items.append(submenu)
def add_action(self, title, action):
self.items.append((title, action))
def validate_choice(self, choice: str):
try:
parsed_choice = int(choice)
if parsed_choice >= 0 and parsed_choice <= len(self.items):
return parsed_choice
except ValueError:
pass
# we'll print the error regardless if it was an exception or wrong choice
print("❌ Invalid choice")
print("-" * 50)
def show(self):
while True:
for idx, item in enumerate(self.items):
if isinstance(item, SubMenu):
item.render(idx)
else:
print(f"{str(idx + 1):>2} {item[0]}")
print("[Back] To go back")
choice = input(">> ")
if is_back(choice):
break
choice = self.validate_choice(choice)
if not choice:
continue
selected_element = self.items[int(choice) - 1]
if isinstance(selected_element, SubMenu):
selected_element.show()
else:
print("-" * 50)
selected_element[1](**self.context)
print("-" * 50)
class Menu(SubMenu):
def __init__(self, title, context=None, parent_menu=None):
super().__init__(title, context, parent_menu)
class OpenIrisDevice:
def __init__(self, port: str, debug: bool, debug_commands: bool):
self.port = port
self.debug = debug
self.debug_commands = debug_commands
self.connection: serial.Serial | None = None
self.connected = False
def __enter__(self):
self.connected = self.__connect()
return self
def __exit__(self, type, value, traceback):
self.__disconnect()
def __connect(self) -> bool:
print(f"📡 Connecting directly to {self.port}...")
try:
self.connection = serial.Serial(
port=self.port, baudrate=115200, timeout=1, write_timeout=1
)
print(f"✅ Connected to the device on {self.port}")
return True
except Exception as e:
print(f"❌ Failed to connect to {self.port}: {e}")
return False
def __disconnect(self):
if self.connection and self.connection.is_open:
self.connection.close()
print(f"🔌 Disconnected from {self.port}")
def __check_if_response_is_complete(self, response) -> dict | None:
try:
return json.loads(response)
except ValueError:
return None
def __read_response(self, timeout: int | None = None) -> dict | None:
# we can try and retrieve the response now.
# it should be more or less immediate, but some commands may take longer
# so we gotta timeout
timeout = timeout if timeout is not None else 15
start_time = time.time()
response_buffer = ""
while time.time() - start_time < timeout:
if self.connection.in_waiting:
packet = self.connection.read_all().decode("utf-8", errors="ignore")
if self.debug and packet.strip():
print(f"Received: {packet}")
print("-" * 10)
print(f"Current buffer: {response_buffer}")
print("-" * 10)
# we can't rely on new lines to detect if we're done
# nor can we assume that we're always gonna get valid json response
# but we can assume that if we're to get a valid response, it's gonna be json
# so we can start actually building the buffer only when
# some part of the packet starts with "{", and start building from there
# we can assume that no further data will be returned, so we can validate
# right after receiving the last packet
if (not response_buffer and "{" in packet) or response_buffer:
# assume we just started building the buffer and we've received the first packet
# alternative approach in case this doesn't work - we're always sending a valid json
# so we can start building the buffer from the first packet and keep trying to find the
# starting and ending brackets, extract that part and validate, if the message is complete, return
if not response_buffer:
starting_idx = packet.find("{")
response_buffer = packet[starting_idx:]
else:
response_buffer += packet
# and once we get something, we can validate if it's a valid json
if parsed_response := self.__check_if_response_is_complete(
response_buffer
):
return parsed_response
else:
time.sleep(0.1)
return None
def is_connected(self) -> bool:
return self.connected
def send_command(
self, command: str, params: dict | None = None, timeout: int | None = None
) -> dict:
if not self.connection or not self.connection.is_open:
return {"error": "Device Not Connected"}
cmd_obj = {"commands": [{"command": command}]}
if params:
cmd_obj["commands"][0]["data"] = params
# we're expecting the json string to end with a new line
# to signify we've finished sending the command
cmd_str = json.dumps(cmd_obj) + "\n"
try:
# clean it out first, just to be sure we're starting fresh
self.connection.reset_input_buffer()
if self.debug or self.debug_commands:
print(f"Sending command: {cmd_str}")
self.connection.write(cmd_str.encode())
response = self.__read_response(timeout)
if self.debug:
print(f"Received response: {response}")
return response or {"error": "Command timeout"}
except Exception as e:
return {"error": f"Communication error: {e}"}
@dataclass
class WiFiNetwork:
ssid: str
channel: int
rssi: int
mac_address: str
auth_mode: int
@property
def security_type(self) -> str:
"""Convert auth_mode to human readable string"""
auth_modes = {
0: "Open",
1: "WEP",
2: "WPA PSK",
3: "WPA2 PSK",
4: "WPA WPA2 PSK",
5: "WPA2 Enterprise",
6: "WPA3 PSK",
7: "WPA2 WPA3 PSK",
}
return auth_modes.get(self.auth_mode, f"Unknown ({self.auth_mode})")
class WiFiScanner:
def __init__(self, device: OpenIrisDevice):
self.device = device
self.networks = []
def scan_networks(self, timeout: int = 30):
print(
f"🔍 Scanning for WiFi networks (this may take up to {timeout} seconds)..."
)
response = self.device.send_command("scan_networks", timeout=timeout)
if has_command_failed(response):
print(f"❌ Scan failed: {response['error']}")
return
channels_found = set()
networks = response["results"][0]["result"]["data"]["networks"]
# after each scan, clear the network list to avoid duplication
self.networks = []
for net in networks:
network = WiFiNetwork(
ssid=net["ssid"],
channel=net["channel"],
rssi=net["rssi"],
mac_address=net["mac_address"],
auth_mode=net["auth_mode"],
)
self.networks.append(network)
channels_found.add(net["channel"])
# Sort networks by RSSI (strongest first)
self.networks.sort(key=lambda x: x.rssi, reverse=True)
print(
f"✅ Found {len(self.networks)} networks on channels: {sorted(channels_found)}"
)
def get_networks(self) -> list:
return self.networks
def has_command_failed(result) -> bool:
return "error" in result or result["results"][0]["result"]["status"] != "success"
def get_device_mode(device: OpenIrisDevice) -> dict:
command_result = device.send_command("get_device_mode")
if has_command_failed(command_result):
return {"mode": "unknown"}
return {"mode": command_result["results"][0]["result"]["data"]["mode"].lower()}
def get_led_duty_cycle(device: OpenIrisDevice) -> dict:
command_result = device.send_command("get_led_duty_cycle")
if has_command_failed(command_result):
print(f"❌ Failed to get LED duty cycle: {command_result['error']}")
return {"duty_cycle": "unknown"}
try:
return {
"duty_cycle": int(
command_result["results"][0]["result"]["data"][
"led_external_pwm_duty_cycle"
]
)
}
except ValueError as e:
print(f"❌ Failed to parse LED duty cycle: {e}")
return {"duty_cycle": "unknown"}
def get_mdns_name(device: OpenIrisDevice) -> dict:
response = device.send_command("get_mdns_name")
if "error" in response:
print(f"❌ Failed to get device name: {response['error']}")
return {"name": "unknown"}
return {"name": response["results"][0]["result"]["data"]["hostname"]}
def get_serial_info(device: OpenIrisDevice) -> dict:
response = device.send_command("get_serial")
if has_command_failed(response):
print(f"❌ Failed to get serial/MAC: {response['error']}")
return {"serial": None, "mac": None}
return {
"serial": response["results"][0]["result"]["data"]["serial"],
"mac": response["results"][0]["result"]["data"]["mac"],
}
def get_device_info(device: OpenIrisDevice) -> dict:
response = device.send_command("get_who_am_i")
if has_command_failed(response):
print(f"❌ Failed to get device info: {response['error']}")
return {"who_am_i": None, "version": None}
return {"who_am_i": response["results"][0]["result"]["data"]["who_am_i"], "version": response["results"][0]["result"]["data"]["version"]}
def get_wifi_status(device: OpenIrisDevice) -> dict:
response = device.send_command("get_wifi_status")
if has_command_failed(response):
print(f"❌ Failed to get wifi status: {response['error']}")
return {"wifi_status": {"status": "unknown"}}
return {"wifi_status": response["results"][0]["result"]["data"]}
def get_led_current(device: OpenIrisDevice) -> dict:
response = device.send_command("get_led_current")
if has_command_failed(response):
print(f"❌ Failed to get LED current: {response}")
return {"led_current_ma": "unknown"}
return {"led_current_ma": response["results"][0]["result"]["data"]["led_current_ma"]}
def configure_device_name(device: OpenIrisDevice, *args, **kwargs):
current_name = get_mdns_name(device)
print(f"\n📍 Current device name: {current_name['name']} \n")
print(
"💡 Please enter your preferred device name, your board will be accessible under http://<name>.local/"
)
print("💡 Please avoid spaces and special characters")
print(" To back out, enter `back`")
print("\n Note, this will also modify the name of the UVC device")
while True:
name_choice = input("\nDevice name: ").strip()
if any(space in name_choice for space in string.whitespace):
print("❌ Please avoid spaces and special characters")
else:
break
if is_back(name_choice):
return
response = device.send_command("set_mdns", {"hostname": name_choice})
if "error" in response:
print(f"❌ MDNS name setup failed: {response['error']}")
return
print("✅ MDNS name set successfully")
def start_streaming(device: OpenIrisDevice, *args, **kwargs):
print("🚀 Starting streaming mode...")
response = device.send_command("start_streaming")
if "error" in response:
print(f"❌ Failed to start streaming: {response['error']}")
return
print("✅ Streaming mode started")
def switch_device_mode_command(device: OpenIrisDevice, *args, **kwargs):
modes = ["wifi", "uvc", "auto"]
current_mode = get_device_mode(device)["mode"]
print(f"\n📍 Current device mode: {current_mode}")
print("\n🔄 Select new device mode:")
print("1. WiFi - Stream over WiFi connection")
print("2. UVC - Stream as USB webcam")
print("3. Auto - Automatic mode selection")
print("Back - Return to main menu")
mode_choice = input("\nSelect mode (1-3): ").strip()
if is_back(mode_choice):
return
try:
mode = modes[int(mode_choice) - 1]
except ValueError:
print("❌ Invalid mode selection")
return
command_result = device.send_command("switch_mode", {"mode": mode})
if "error" in command_result:
print(f"❌ Failed to switch mode: {command_result['error']}")
return
print(f"✅ Device mode switched to '{mode}' successfully!")
print("🔄 Please restart the device for changes to take effect")
def set_led_duty_cycle(device: OpenIrisDevice, *args, **kwargs):
current_duty_cycle = get_led_duty_cycle(device)["duty_cycle"]
print(f"💡 Current LED duty cycle: {current_duty_cycle}%")
while True:
desired_pwd = input(
"Enter LED external PWM duty cycle (0-100) or `back` to exit: \n>> "
)
if is_back(desired_pwd):
break
try:
duty_cycle = int(desired_pwd)
except ValueError:
print("❌ Invalid input. Please enter a number between 0 and 100.")
if duty_cycle < 0 or duty_cycle > 100:
print("❌ Duty cycle must be between 0 and 100.")
else:
response = device.send_command(
"set_led_duty_cycle", {"dutyCycle": duty_cycle}
)
if has_command_failed(response):
print(f"❌ Failed to set LED duty cycle: {response['error']}")
return False
print("✅ LED duty cycle set successfully")
updated = get_led_duty_cycle(device)["duty_cycle"]
print(f"💡 Current LED duty cycle: {updated}%")
def get_settings_summary(device: OpenIrisDevice, *args, **kwargs):
print("🧩 Collecting device settings...\n")
probes = [
("Identity", get_serial_info),
("AdvertisedName", get_mdns_name),
("Info", get_device_info),
("LED", get_led_duty_cycle),
("Current", get_led_current),
("Mode", get_device_mode),
("WiFi", get_wifi_status),
]
summary: dict[str, dict] = {}
for label, probe in probes:
summary[label] = probe(device)
print(f"🔑 Serial: {summary['Identity']}")
print(f"💡 LED PWM Duty: {summary['LED']['duty_cycle']}%")
print(f"🎚️ Mode: {summary['Mode']['mode']}")
current_section = summary.get("Current", {})
led_current_ma = current_section.get("led_current_ma")
print(f"🔌 LED Current: {led_current_ma} mA")
advertised_name_data = summary.get("AdvertisedName", {})
advertised_name = advertised_name_data.get("name")
print(f"📛 Name: {advertised_name}")
info = summary.get("Info", {})
who = info.get("who_am_i")
ver = info.get("version")
if who:
print(f"🏷️ Device: {who}")
if ver:
print(f"🧭 Version: {ver}")
wifi = summary.get("WiFi", {}).get("wifi_status", {})
if wifi:
status = wifi.get("status", "unknown")
ip = wifi.get("ip_address") or "-"
configured = wifi.get("networks_configured", 0)
print(f"📶 WiFi: {status} | IP: {ip} | Networks configured: {configured}")
def scan_networks(wifi_scanner: WiFiScanner, *args, **kwargs):
use_custom_timeout = (
input("Should we use a custom scan timeout? (y/n)\n>> ").strip().lower() == "y"
)
if use_custom_timeout:
timeout = input("Enter timeout in seconds (5-120) or back to go back\n>> ")
if is_back(timeout):
return
try:
timeout = int(timeout)
if 5 <= timeout <= 120:
print(
f"🔍 Scanning for WiFi networks (this may take up to {timeout} seconds)..."
)
wifi_scanner.scan_networks(timeout)
else:
print("❌ Timeout must be between 5 and 120 seconds, using default")
wifi_scanner.scan_networks()
except ValueError:
print("❌ Invalid timeout")
else:
wifi_scanner.scan_networks()
def display_networks(wifi_scanner: WiFiScanner, *args, **kwargs):
networks = wifi_scanner.get_networks()
if not networks:
print("❌ No networks found, please scan first")
return
print("\n📡 Available WiFi Networks:")
print("-" * 85)
print(f"{'#':<3} {'SSID':<32} {'Channel':<8} {'Signal':<20} {'Security':<15}")
print("-" * 85)
channels = {}
for idx, network in enumerate(networks, 1):
channels[network.channel] = channels.get(network.channel, 0) + 1
signal_bars = "" * min(5, max(0, (network.rssi + 100) // 10))
signal_str = f"{signal_bars:<5} ({network.rssi} dBm)"
ssid_display = network.ssid if network.ssid else "<hidden>"
print(
f"{idx:<3} {ssid_display:<32} {network.channel:<8} {signal_str:<20} {network.security_type:<15}"
)
print("-" * 85)
print("\n📊 Channel distribution: ", end="")
for channel in sorted(channels.keys()):
print(f"Ch{channel}: {channels[channel]} networks ", end="")
def configure_wifi(device: OpenIrisDevice, wifi_scanner: WiFiScanner, *args, **kwargs):
networks = wifi_scanner.get_networks()
if not networks:
print("❌ No networks available. Please scan first.")
return
display_networks(wifi_scanner, *args, **kwargs)
while True:
net_choice = input("\nEnter network number (or 'back'): ").strip()
if is_back(net_choice):
break
try:
net_idx = int(net_choice) - 1
except ValueError:
print("❌ Please enter a number or 'back'")
continue
sorted_networks = wifi_scanner.get_networks()
if 0 <= net_idx < len(sorted_networks):
selected_network = sorted_networks[net_idx]
ssid = selected_network.ssid
print(f"\n🔐 Selected: {ssid}")
print(f"Security: {selected_network.security_type}")
if selected_network.auth_mode == 0: # Open network
password = ""
print("🔓 Open network - no password required")
else:
password = input("Enter WiFi password (or 'back'): ")
if is_back(password):
break
print(f"🔧 Setting WiFi credentials for '{ssid}'...")
params = {
"name": "main",
"ssid": ssid,
"password": password,
"channel": 0,
"power": 0,
}
response = device.send_command("set_wifi", params)
if has_command_failed(response):
print(f"❌ WiFi setup failed: {response['error']}")
break
print("✅ WiFi configured successfully!")
print("💡 Next steps:")
print(" • Open WiFi menu to connect to WiFi (if needed)")
print(" • Open WiFi menu to check WiFi status")
print(" • Start streaming from the main menu when connected")
break
else:
print("❌ Invalid network number")
def automatic_wifi_configuration(
device: OpenIrisDevice, wifi_scanner: WiFiScanner, *args, **kwargs
):
print("\n⚙️ Automatic WiFi setup starting...")
scan_networks(wifi_scanner, *args, **kwargs)
configure_wifi(device, wifi_scanner, *args, **kwargs)
attempt_wifi_connection(device)
print("⏳ Connecting to WiFi, waiting for IP...")
start = time.time()
timeout_s = 30
ip = None
last_status = None
while time.time() - start < timeout_s:
status = get_wifi_status(device).get("wifi_status", {})
last_status = status
ip = status.get("ip_address")
if ip and ip not in ("0.0.0.0", "", None):
break
time.sleep(0.5)
if ip and ip not in ("0.0.0.0", "", None):
print(f"✅ Connected! IP Address: {ip}")
else:
print("⚠️ Connection not confirmed within timeout")
if last_status:
print(
f" Status: {last_status.get('status', 'unknown')} | IP: {last_status.get('ip_address', '-')}"
)
def attempt_wifi_connection(device: OpenIrisDevice, *args, **kwargs):
print("🔗 Attempting WiFi connection...")
response = device.send_command("connect_wifi")
if has_command_failed(response):
print(f"❌ WiFi connection failed: {response['error']}")
return
print("✅ WiFi connection attempt started")
def check_wifi_status(device: OpenIrisDevice, *args, **kwargs):
status = get_wifi_status(device).get("wifi_status")
print(f"📶 WiFi Status: {status.get('status', 'Unknown')}")
if ip_address := status.get("ip_address"):
print(f"🌐 IP Address: {ip_address}")
def handle_menu(menu_context: dict | None = None) -> str:
menu = Menu("OpenIris Setup", menu_context)
wifi_settings = SubMenu("📶 WiFi settings", menu_context, menu)
wifi_settings.add_action("⚙️ Automatic WiFi setup", automatic_wifi_configuration)
manual_wifi_actions = SubMenu(
"📁 WiFi Manual Actions:",
menu_context,
wifi_settings,
)
manual_wifi_actions.add_action("🔍 Scan for WiFi networks", scan_networks)
manual_wifi_actions.add_action("📡 Show available networks", display_networks)
manual_wifi_actions.add_action("🔐 Configure WiFi", configure_wifi)
manual_wifi_actions.add_action("🔗 Connect to WiFi", attempt_wifi_connection)
manual_wifi_actions.add_action("🛰️ Check WiFi status", check_wifi_status)
menu.add_action("🌐 Configure MDNS", configure_device_name)
menu.add_action("💻 Configure UVC Name", configure_device_name)
menu.add_action("🚀 Start streaming mode", start_streaming)
menu.add_action("🔄 Switch device mode (WiFi/UVC/Auto)", switch_device_mode_command)
menu.add_action("💡 Update PWM Duty Cycle", set_led_duty_cycle)
menu.add_action("🧩 Get settings summary", get_settings_summary)
menu.show()
def valid_port(port: str):
if not port.startswith("COM"):
raise argparse.ArgumentTypeError("Invalid port name. We only support COM ports")
return port
def main():
parser = argparse.ArgumentParser(description="OpenIris CLI Setup Tool")
parser.add_argument(
"--port",
type=valid_port,
help="Serial port to connect to [COM4, COM3, etc]",
required=True,
)
parser.add_argument(
"--debug",
action="store_true",
help="Show debug output including raw serial data",
)
parser.add_argument(
"--show-commands",
action="store_true",
help="Debug mode, but will show only sent commands",
)
args = parser.parse_args()
print("🔧 OpenIris Setup Tool")
print("=" * 50)
with OpenIrisDevice(args.port, args.debug, args.show_commands) as device:
if not device.is_connected():
return 1
wifi_scanner = WiFiScanner(device)
try:
handle_menu({"device": device, "args": args, "wifi_scanner": wifi_scanner})
except KeyboardInterrupt:
print("\n🛑 Setup interrupted, disconnecting")
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -1,177 +0,0 @@
import serial
import time
import json
from typing import List, Dict
class ESPWiFiScanner:
def __init__(self, port: str, baudrate: int = 115200):
self.port = port
self.baudrate = baudrate
self.serial = None
def connect(self) -> bool:
try:
self.serial = serial.Serial(
port=self.port,
baudrate=self.baudrate,
timeout=1
)
return True
except serial.SerialException as e:
print(f"Error connecting to ESP32: {e}")
return False
def scan_networks(self, timeout_seconds: int = 30) -> List[Dict]:
if not self.serial:
print("Not connected to ESP32")
return []
self.serial.reset_input_buffer()
command = '{"commands":[{"command":"scan_networks"}]}\n'
self.serial.write(command.encode())
timeout_start = time.time()
response_buffer = ""
while time.time() - timeout_start < timeout_seconds:
if self.serial.in_waiting:
data = self.serial.read(self.serial.in_waiting).decode('utf-8', errors='ignore')
response_buffer += data
# Look for WiFi networks JSON directly (new format)
# The scan command now outputs JSON directly followed by command result
if '{"networks":[' in response_buffer:
import re
# Look for the networks JSON pattern that appears first
networks_pattern = r'\{"networks":\[.*?\]\}'
matches = re.findall(networks_pattern, response_buffer, re.DOTALL)
for match in matches:
try:
# Parse the networks JSON directly
networks_data = json.loads(match)
if "networks" in networks_data:
return networks_data["networks"]
except json.JSONDecodeError:
continue
# Also check if we have the command result indicating completion
if '{"results":' in response_buffer and '"Networks scanned"' in response_buffer:
# We've received the completion message, parse any networks found
import re
networks_pattern = r'\{"networks":\[.*?\]\}'
matches = re.findall(networks_pattern, response_buffer, re.DOTALL)
for match in matches:
try:
networks_data = json.loads(match)
if "networks" in networks_data:
return networks_data["networks"]
except json.JSONDecodeError:
continue
# If we get here, scan completed but no networks found
return []
else:
time.sleep(0.1)
print("Failed to receive clean JSON response. Raw data:")
print("=" * 50)
print(response_buffer)
print("=" * 50)
return []
def close(self):
if self.serial:
self.serial.close()
def main():
import sys
import argparse
parser = argparse.ArgumentParser(description='ESP32 WiFi Scanner')
parser.add_argument('port', nargs='?', help='Serial port - COM1, /dev/ttyUSB0, etc.')
parser.add_argument('-t', '--timeout', type=int, default=30,
help='Scan timeout in seconds (default: 30)')
args = parser.parse_args()
scanner = ESPWiFiScanner(args.port)
if scanner.connect():
print(f"Connected to ESP32 on {args.port}")
print(f"Scanning for WiFi networks (timeout: {args.timeout} seconds)...")
start_time = time.time()
networks = scanner.scan_networks(args.timeout)
scan_time = time.time() - start_time
if networks:
# Sort by RSSI (strongest first)
networks.sort(key=lambda x: x.get('rssi', -100), reverse=True)
print(f"\n✅ Found {len(networks)} WiFi Networks in {scan_time:.1f} seconds:")
print("{:<32} | {:<7} | {:<15} | {:<17} | {:<9}".format(
"SSID", "Channel", "Signal", "MAC Address", "Security"
))
print("-" * 85)
# Track channels found
channels_found = set()
auth_modes = {
0: "Open",
1: "WEP",
2: "WPA-PSK",
3: "WPA2-PSK",
4: "WPA/WPA2",
5: "WPA2-Enterprise",
6: "WPA3-PSK",
7: "WPA2/WPA3",
8: "WAPI-PSK"
}
for network in networks:
ssid = network.get('ssid', '')
if not ssid:
ssid = "<hidden>"
channel = network.get('channel', 0)
channels_found.add(channel)
# Create signal strength visualization
rssi = network.get('rssi', -100)
signal_bars = "" * min(5, max(0, (rssi + 100) // 10))
signal_str = f"{signal_bars:<5} ({rssi} dBm)"
auth_mode = network.get('auth_mode', 0)
security = auth_modes.get(auth_mode, f"Type {auth_mode}")
print("{:<32} | {:<7} | {:<15} | {:<17} | {:<9}".format(
ssid[:32], # Truncate long SSIDs
channel,
signal_str,
network.get('mac_address', '?'),
security
))
# Show channel distribution
print(f"\n📊 Channels detected: {sorted(channels_found)}")
channel_counts = {}
for net in networks:
ch = net.get('channel', 0)
channel_counts[ch] = channel_counts.get(ch, 0) + 1
print("📊 Channel distribution: ", end="")
for ch in sorted(channel_counts.keys()):
print(f"Ch{ch}: {channel_counts[ch]} networks ", end="")
print()
else:
print("❌ No networks found or scan failed")
scanner.close()
else:
print(f"Failed to connect to ESP32 on {args.port}")
if __name__ == "__main__":
main()