Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions include/network/http_utils.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
#include <esp_http_server.h>

#include <ebus/detail/json_reader.hpp>
#include <algorithm>
#include <cstring>
#include <string>
#include <string_view>

Expand All @@ -11,6 +13,48 @@ namespace HttpUtils {
constexpr size_t max_request_body_size = 8192;
constexpr size_t streaming_buffer_size = 4096;

// Coalesce small JSON fragments and stop socket I/O after the first failure.
// Call finish() after all writers/scopes have emitted their closing bytes.
class ChunkedResponse {
public:
explicit ChunkedResponse(httpd_req_t* req) : req_(req) {}
ChunkedResponse(const ChunkedResponse&) = delete;
ChunkedResponse& operator=(const ChunkedResponse&) = delete;

void write(std::string_view chunk) {
while (!finished_ && result_ == ESP_OK && !chunk.empty()) {
const size_t count = std::min(chunk.size(), sizeof(buffer_) - size_);
std::memcpy(buffer_ + size_, chunk.data(), count);
size_ += count;
chunk.remove_prefix(count);
if (size_ == sizeof(buffer_)) flush();
}
}

esp_err_t finish() {
if (!finished_) {
flush();
if (result_ == ESP_OK) result_ = httpd_resp_send_chunk(req_, nullptr, 0);
finished_ = true;
}
return result_;
}

private:
void flush() {
if (result_ == ESP_OK && size_ > 0) {
result_ = httpd_resp_send_chunk(req_, buffer_, size_);
size_ = 0;
}
}

httpd_req_t* req_;
char buffer_[1024];
size_t size_ = 0;
esp_err_t result_ = ESP_OK;
bool finished_ = false;
};

class StreamingReader {
public:
explicit StreamingReader(httpd_req_t* req);
Expand Down
38 changes: 38 additions & 0 deletions include/system/detail/monitor_schedule.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once

#include <cstdint>

namespace app::detail {

// Monotonic deadlines: bus notifications must not accelerate health sampling.
class MonitorSchedule {
public:
uint32_t waitMs(uint64_t now_us) const {
if (now_us >= next_status_us_) return 0;
return static_cast<uint32_t>((next_status_us_ - now_us + 999) / 1000);
}

bool statusDue(uint64_t now_us) {
return advance(now_us, next_status_us_, 30ULL * 1000000);
}
bool summaryDue(uint64_t now_us) {
return advance(now_us, next_summary_us_, 300ULL * 1000000);
}
bool trendDue(uint64_t now_us) {
return advance(now_us, next_trend_us_, 3600ULL * 1000000);
}

private:
static bool advance(uint64_t now_us, uint64_t& next_us, uint64_t period_us) {
if (now_us < next_us) return false;
next_us = now_us + period_us;
return true;
}

uint64_t next_status_us_ = 0;
uint64_t next_summary_us_ = 0;
uint64_t next_trend_us_ = 0;
};

} // namespace app::detail
16 changes: 12 additions & 4 deletions include/system/string_pool.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,16 @@

#include <cstdint>
#include <cstring>
#include <mutex>
#include <string_view>

class StringPool {
public:
static StringPool& instance();

uint8_t intern(std::string_view str) {
if (str.empty() || count_ >= max_strings_) return 0;
if (str.empty() || str.size() > 255) return 0;
std::lock_guard<std::mutex> lock(mutex_);

for (uint8_t i = 0; i < count_; i++) {
if (lengths_[i] == str.size() &&
Expand All @@ -20,7 +22,8 @@ class StringPool {
}
}

if (buffer_offset_ + str.size() + 1 > buffer_size_) return 0;
if (count_ >= max_strings_ ||
buffer_offset_ + str.size() + 1 > buffer_size_) return 0;

std::memcpy(buffer_ + buffer_offset_, str.data(), str.size());
buffer_[buffer_offset_ + str.size()] = '\0';
Expand All @@ -31,17 +34,22 @@ class StringPool {
}

std::string_view lookup(uint8_t id) const {
std::lock_guard<std::mutex> lock(mutex_);
if (id == 0 || id > count_) return {};
const uint8_t idx = id - 1;
return std::string_view(buffer_ + offsets_[idx], lengths_[idx]);
}

uint8_t count() const { return count_; }
uint8_t count() const {
std::lock_guard<std::mutex> lock(mutex_);
return count_;
}

private:
static constexpr size_t max_strings_ = 256;
static constexpr size_t max_strings_ = 255;
static constexpr size_t buffer_size_ = 2048;

mutable std::mutex mutex_;
char buffer_[buffer_size_]{};
uint16_t offsets_[max_strings_]{};
uint8_t lengths_[max_strings_]{};
Expand Down
4 changes: 3 additions & 1 deletion include/system/system_monitor.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
#include <freertos/task.h>

#include <atomic>

#include "system/detail/monitor_schedule.hpp"
#include <cstdint>
#include <string>
#include <string_view>
Expand Down Expand Up @@ -100,7 +102,7 @@ class SystemMonitor {
HeapSample heap_trend_[heap_trend_capacity] = {};
size_t heap_trend_index_ = 0;
size_t heap_trend_count_ = 0;
uint32_t heap_trend_tick_ = 0;
app::detail::MonitorSchedule schedule_;
};

#endif
16 changes: 8 additions & 8 deletions src/api/adc_api.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ esp_err_t AdcApi::handleAdcPage(httpd_req_t* req) {
}

esp_err_t AdcApi::handleAdcRaw(httpd_req_t* req) {
HttpUtils::ChunkedResponse response(req);
if (!instance_->adc_.isRunning() && !instance_->adc_.begin()) {
HttpUtils::sendErrorResponse(req, "500 Internal Server Error", "adc_raw",
"ADC not running or failed to start");
Expand Down Expand Up @@ -132,27 +133,26 @@ esp_err_t AdcApi::handleAdcRaw(httpd_req_t* req) {
httpd_resp_set_hdr(req, "X-ADC-Controller-Sample-Rate", tmp6);

if (!instance_->adc_.streamRaw(
[req](std::string_view chunk) {
httpd_resp_send_chunk(req, chunk.data(), chunk.size());
[&response](std::string_view chunk) {
response.write(chunk);
},
sample_rate, samples_per_channel, channel_mask))
return ESP_FAIL;
httpd_resp_send_chunk(req, nullptr, 0);
return ESP_OK;
return response.finish();
}

esp_err_t AdcApi::handleAdcState(httpd_req_t* req) {
HttpUtils::ChunkedResponse response(req);
httpd_resp_set_type(req, "application/json;charset=utf-8");
HttpUtils::applyCustomHeaders(req);
{
ebus::detail::JsonWriter writer([req](std::string_view chunk) {
httpd_resp_send_chunk(req, chunk.data(), chunk.size());
ebus::detail::JsonWriter writer([&response](std::string_view chunk) {
response.write(chunk);
});
auto scope = writer.objectScope();
writer.writeField("running", instance_->adc_.isRunning());
}
httpd_resp_send_chunk(req, nullptr, 0);
return ESP_OK;
return response.finish();
}

esp_err_t AdcApi::handleAdcEnable(httpd_req_t* req) {
Expand Down
8 changes: 4 additions & 4 deletions src/api/commands_api.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,13 @@ esp_err_t CommandsApi::handleCommandsPage(httpd_req_t* req) {
}

esp_err_t CommandsApi::handleCommands(httpd_req_t* req) {
HttpUtils::ChunkedResponse response(req);
httpd_resp_set_type(req, "application/json;charset=utf-8");
HttpUtils::applyCustomHeaders(req);
instance_->command_manager_.fetchCommands([req](std::string_view chunk) {
httpd_resp_send_chunk(req, chunk.data(), chunk.size());
instance_->command_manager_.fetchCommands([&response](std::string_view chunk) {
response.write(chunk);
});
httpd_resp_send_chunk(req, nullptr, 0);
return ESP_OK;
return response.finish();
}

esp_err_t CommandsApi::handleCommandsEvaluate(httpd_req_t* req) {
Expand Down
8 changes: 4 additions & 4 deletions src/api/cron_api.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,13 @@ esp_err_t CronApi::handleCronPage(httpd_req_t* req) {
}

esp_err_t CronApi::handleCron(httpd_req_t* req) {
HttpUtils::ChunkedResponse response(req);
httpd_resp_set_type(req, "application/json;charset=utf-8");
HttpUtils::applyCustomHeaders(req);
instance_->cron_.fetchRules([req](std::string_view chunk) {
httpd_resp_send_chunk(req, chunk.data(), chunk.size());
instance_->cron_.fetchRules([&response](std::string_view chunk) {
response.write(chunk);
});
httpd_resp_send_chunk(req, nullptr, 0);
return ESP_OK;
return response.finish();
}

esp_err_t CronApi::handleCronEvaluate(httpd_req_t* req) {
Expand Down
8 changes: 4 additions & 4 deletions src/api/devices_api.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,13 @@ esp_err_t DevicesApi::handleDevicesPage(httpd_req_t* req) {
}

esp_err_t DevicesApi::handleDevices(httpd_req_t* req) {
HttpUtils::ChunkedResponse response(req);
httpd_resp_set_type(req, "application/json;charset=utf-8");
HttpUtils::applyCustomHeaders(req);
getEbusController().fetchDevices([req](std::string_view chunk) {
httpd_resp_send_chunk(req, chunk.data(), chunk.size());
getEbusController().fetchDevices([&response](std::string_view chunk) {
response.write(chunk);
});
httpd_resp_send_chunk(req, nullptr, 0);
return ESP_OK;
return response.finish();
}

esp_err_t DevicesApi::handleDevicesScan(httpd_req_t* req) {
Expand Down
24 changes: 12 additions & 12 deletions src/api/logs_api.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ esp_err_t LogsApi::handleLogsPage(httpd_req_t* req) {
}

esp_err_t LogsApi::handleLogs(httpd_req_t* req) {
HttpUtils::ChunkedResponse response(req);
uint64_t since_millis = 0;
const size_t query_len = httpd_req_get_url_query_len(req);
if (query_len > 0) {
Expand All @@ -68,25 +69,25 @@ esp_err_t LogsApi::handleLogs(httpd_req_t* req) {
httpd_resp_set_type(req, "application/json;charset=utf-8");
HttpUtils::applyCustomHeaders(req);
instance_->logger_.fetchLogs(
[req](std::string_view chunk) {
httpd_resp_send_chunk(req, chunk.data(), chunk.size());
[&response](std::string_view chunk) {
response.write(chunk);
},
since_millis);
httpd_resp_send_chunk(req, nullptr, 0);
return ESP_OK;
return response.finish();
}

esp_err_t LogsApi::handleLogsTimeRelation(httpd_req_t* req) {
HttpUtils::ChunkedResponse response(req);
httpd_resp_set_type(req, "application/json;charset=utf-8");
HttpUtils::applyCustomHeaders(req);
instance_->logger_.fetchTimeRelation([req](std::string_view chunk) {
httpd_resp_send_chunk(req, chunk.data(), chunk.size());
instance_->logger_.fetchTimeRelation([&response](std::string_view chunk) {
response.write(chunk);
});
httpd_resp_send_chunk(req, nullptr, 0);
return ESP_OK;
return response.finish();
}

esp_err_t LogsApi::handleTap(httpd_req_t* req) {
HttpUtils::ChunkedResponse response(req);
uint64_t since_millis = 0;
const size_t query_len = httpd_req_get_url_query_len(req);
if (query_len > 0) {
Expand All @@ -107,12 +108,11 @@ esp_err_t LogsApi::handleTap(httpd_req_t* req) {
httpd_resp_set_type(req, "application/json;charset=utf-8");
HttpUtils::applyCustomHeaders(req);
DeviceStatus::monitor().fetchTap(
[req](std::string_view chunk) {
httpd_resp_send_chunk(req, chunk.data(), chunk.size());
[&response](std::string_view chunk) {
response.write(chunk);
},
since_millis);
httpd_resp_send_chunk(req, nullptr, 0);
return ESP_OK;
return response.finish();
}

#endif
8 changes: 4 additions & 4 deletions src/api/metrics_api.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,13 @@ esp_err_t MetricsApi::handleMetricsPage(httpd_req_t* req) {
}

esp_err_t MetricsApi::handleMetrics(httpd_req_t* req) {
HttpUtils::ChunkedResponse response(req);
httpd_resp_set_type(req, "application/json;charset=utf-8");
HttpUtils::applyCustomHeaders(req);
getEbusController().fetchMetrics([req](std::string_view chunk) {
httpd_resp_send_chunk(req, chunk.data(), chunk.size());
getEbusController().fetchMetrics([&response](std::string_view chunk) {
response.write(chunk);
});
httpd_resp_send_chunk(req, nullptr, 0);
return ESP_OK;
return response.finish();
}

esp_err_t MetricsApi::handleMetricsReset(httpd_req_t* req) {
Expand Down
16 changes: 8 additions & 8 deletions src/api/network_api.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,11 @@ bool NetworkApi::registerHandlers(httpd_handle_t server) {
}

esp_err_t NetworkApi::handleNetwork(httpd_req_t* req) {
HttpUtils::ChunkedResponse response(req);
httpd_resp_set_type(req, "application/json;charset=utf-8");
HttpUtils::applyCustomHeaders(req);
ebus::detail::JsonWriter writer([req](std::string_view chunk) {
httpd_resp_send_chunk(req, chunk.data(), chunk.size());
ebus::detail::JsonWriter writer([&response](std::string_view chunk) {
response.write(chunk);
});
{
auto scope = writer.objectScope();
Expand Down Expand Up @@ -66,11 +67,11 @@ esp_err_t NetworkApi::handleNetwork(httpd_req_t* req) {
appendSntpStatus(writer, DeviceStatus::config().sntp);
}
}
httpd_resp_send_chunk(req, nullptr, 0);
return ESP_OK;
return response.finish();
}

esp_err_t NetworkApi::handleWifiScan(httpd_req_t* req) {
HttpUtils::ChunkedResponse response(req);
wifi_scan_config_t scan_config = {};
scan_config.show_hidden = true;
scan_config.scan_type = WIFI_SCAN_TYPE_ACTIVE;
Expand All @@ -96,8 +97,8 @@ esp_err_t NetworkApi::handleWifiScan(httpd_req_t* req) {

httpd_resp_set_type(req, "application/json;charset=utf-8");
HttpUtils::applyCustomHeaders(req);
ebus::detail::JsonWriter writer([req](std::string_view chunk) {
httpd_resp_send_chunk(req, chunk.data(), chunk.size());
ebus::detail::JsonWriter writer([&response](std::string_view chunk) {
response.write(chunk);
});

{
Expand Down Expand Up @@ -150,9 +151,8 @@ esp_err_t NetworkApi::handleWifiScan(httpd_req_t* req) {
writer.writeField("authMode", auth_mode);
}
}
httpd_resp_send_chunk(req, nullptr, 0);
esp_wifi_clear_ap_list();
return ESP_OK;
return response.finish();
}

#endif
Loading
Loading