Skip to content
Merged
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
3 changes: 3 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,9 @@ jobs:
target: esp32s3
- path: 'components/sx126x/example'
target: esp32s3
- path: 'components/system/example'
target: esp32s3
command: 'IDF_COMPONENT_MANAGER=0 idf.py build'
- path: 'components/t-deck/example'
target: esp32s3
- path: 'components/t-dongle-s3/example'
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/upload_components.yml
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ jobs:
components/state_machine
components/switch_pro
components/sx126x
components/system
components/t_keyboard
components/t-deck
components/t-dongle-s3
Expand Down
2 changes: 2 additions & 0 deletions components/dispatcher/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ protocols and examples use these ids by default:
| 4 | crash dump | `espp.coredump` v1 |
| 5 | CAN bridge | `espp.can-bridge` v1 |
| 6 | MCP266 | `espp.mcp266` v1 |
| 7 | System info / reboot (`espp::SystemService`) | `espp.system` v1 |
| 8 | Heap / task monitor (`espp::MonitorService`) | `espp.monitor` v1 |
| 0xF0–0xFE | reserved (meta) | — |
| 0xFF | capability discovery | — |

Expand Down
1 change: 1 addition & 0 deletions components/dispatcher/web/test/resolve_module_id_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const consoles = [
"components/mcp266/web/mcp266_console.html",
"components/bldc_haptics/web/haptics_console.html",
"components/telemetry/web/telemetry.html",
"components/system/web/system_console.html",
];
const hub = "components/dispatcher/web/dispatcher_hub.html";

Expand Down
4 changes: 3 additions & 1 deletion components/monitor/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# stream_frame + dispatcher: the header-only MonitorService (monitor_service.hpp)
# that serves the heap / task statistics over a framed byte stream.
idf_component_register(
INCLUDE_DIRS "include"
SRC_DIRS "src"
REQUIRES base_component task)
REQUIRES base_component task stream_frame dispatcher)
21 changes: 21 additions & 0 deletions components/monitor/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ system.

- [Monitor Component](#monitor-component)
- [Task Monitor](#task-monitor)
- [Monitor Service](#monitor-service)
- [Example](#example)

<!-- markdown-toc end -->
Expand All @@ -24,6 +25,26 @@ There is an associated [task-monitor](https://github.com/esp-cpp/task-monitor)
python gui which can parse the output of this component and render it as a chart
or into a table for visualization.

## Monitor Service

`espp::MonitorService` (`monitor_service.hpp`) serves the heap-region and task
statistics over any framed byte stream as an `espp::Dispatcher` module
(`espp.monitor` v1, module 8 by default): `GET_HEAP` (one record per configured
`MALLOC_CAP_*` region), `GET_TASKS` (the `TaskMonitor` table; needs
`CONFIG_FREERTOS_USE_TRACE_FACILITY` + `CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS`;
capped, like `GET_HEAP`, so the whole frame fits `Config::max_frame_bytes`, 4096 by default)
and `SET_STREAM` (periodic HEAP / TASKS events). Replies echo the request
frame's correlation id, so a host can pair them and drop stale ones; streamed
events carry none. The wire codec lives in
`include/detail/monitor_protocol.hpp` (host-buildable, tested by
`test/monitor_host_test.cpp`). The hosted
[system console](https://esp-cpp.github.io/espp/apps/system_console.html) web
app renders heap gauges and a live task table from it; the
[system](../system) component's example exposes it over USB together with
`espp::SystemService`:

<img width="946" alt="espp System Console streaming the task table from MonitorService (name, CPU %, stack high-water mark, priority, core)" src="https://github.com/user-attachments/assets/c2c962bf-debb-44c7-96a7-95527218d953" />

## Example

This example shows how to use the `monitor` component to monitor the executing
Expand Down
6 changes: 5 additions & 1 deletion components/monitor/idf_component.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
## IDF Component Manager Manifest File
license: "MIT"
description: "System Monitor component for ESP-IDF"
description: "System Monitor component for ESP-IDF: heap and task statistics, plus a framed stream service (MonitorService) that serves them over USB / WebUSB / Web Serial"
url: "https://github.com/esp-cpp/espp/tree/main/components/monitor"
repository: "git://github.com/esp-cpp/espp.git"
maintainers:
Expand All @@ -14,8 +14,12 @@ tags:
- Monitor
- Memory
- Tasks
- WebUSB
- WebSerial
dependencies:
idf:
version: '>=5.0'
espp/base_component: '>=1.0'
espp/task: '>=1.0'
espp/stream_frame: '>=1.0'
espp/dispatcher: '>=1.0'
259 changes: 259 additions & 0 deletions components/monitor/include/detail/monitor_protocol.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
#pragma once

// Wire protocol of espp::MonitorService: heap-region and task statistics over
// the espp stream_frame codec, routed by an espp::Dispatcher on module 8 by
// default (`espp.monitor` v1 through discovery).
//
// This header is deliberately host-buildable (stream_frame.hpp + the standard
// library only) so the codec is unit-tested on the host
// (components/monitor/test/monitor_host_test.cpp) and so host tools can reuse
// it. All multi-byte fields are little-endian; a `str` is [len u8][bytes].
//
// Requests (host -> device, reply flag clear):
// 0x01 GET_HEAP (no payload)
// 0x02 GET_TASKS (no payload)
// 0x03 SET_STREAM [enable u8][period_ms u16][what u8: bit0 heap, bit1 tasks]
// enable = 0 stops the periodic HEAP / TASKS events; the
// device clamps the period to its Config::min_stream_period.
// Replies / events (device -> host, high bit set = frame reply flag):
// 0x81 HEAP [count u8]{[flags u32][free u32][min_free u32]
// [largest_free_block u32][allocated u32][total u32]}
// 0x82 TASKS [count u8]{[name str][cpu_percent u8][high_water_mark u32]
// [priority u8][core i8]}
// 0x83 OK [request_type u8]
// 0x84 ERROR [request_type u8][code u32][utf8 message]
// code = the POSIX errno value of the std::errc the service
// chose (informational; the message is authoritative)
// HEAP / TASKS answer the matching GET_* request and are also sent
// unsolicited while streaming is enabled (same encoding, so a host decodes
// both the same way). Replies echo the request frame's correlation id (if it
// carried one); streamed events carry none. HEAP and TASKS payloads are capped
// (MonitorService: to fit Config::max_frame_bytes with the frame overhead; at
// most the payload limit):
// regions / tasks that would not fit are dropped from the END of the list.

#include <algorithm>
#include <cstdint>
#include <optional>
#include <span>
#include <string>
#include <string_view>
#include <vector>

#include "stream_frame.hpp"

namespace espp::detail::monitor_protocol {

/// Default dispatcher module id (a routing key only; see MonitorService::Config::module).
inline constexpr uint8_t kModule = 8;
/// Stable protocol identifier + version advertised through discovery.
inline constexpr const char *kProtocol = "espp.monitor";
inline constexpr uint16_t kProtocolVersion = 1;

/// Frame `type` values within the monitor module.
enum class Type : uint8_t {
// host -> device
GetHeap = 0x01,
GetTasks = 0x02,
SetStream = 0x03,
// device -> host (high bit set)
Heap = 0x81,
Tasks = 0x82,
Ok = 0x83,
Error = 0x84,
};

/// SET_STREAM `what` bits.
inline constexpr uint8_t kStreamHeap = 0x01;
inline constexpr uint8_t kStreamTasks = 0x02;

/// One heap region as carried in a HEAP payload.
struct HeapRegion {
uint32_t flags{0}; ///< MALLOC_CAP_* bitmask the region was queried with.
uint32_t free_bytes{0};
uint32_t min_free_bytes{0};
uint32_t largest_free_block{0};
uint32_t allocated_bytes{0};
uint32_t total_size{0};
};

/// One task as carried in a TASKS payload.
struct TaskEntry {
std::string name;
uint8_t cpu_percent{0};
uint32_t high_water_mark{0};
uint8_t priority{0};
int8_t core_id{-2}; ///< 0 / 1, -1 = unpinned, -2 = unknown (core ids not compiled in).
};

/// Decoded SET_STREAM request.
struct StreamRequest {
bool enable{false};
uint16_t period_ms{0};
uint8_t what{0}; ///< kStreamHeap | kStreamTasks
};

/// Bytes one TaskEntry occupies on the wire (name capped at 255).
inline size_t task_entry_size(std::string_view name) {
return 1 + (name.size() > 255 ? 255 : name.size()) + 1 + 4 + 1 + 1;
}

/// Append a [len u8][bytes] string (truncated to 255 bytes).
inline void put_str(std::vector<uint8_t> &out, std::string_view s) {
const size_t n = s.size() > 255 ? 255 : s.size();
out.push_back(static_cast<uint8_t>(n));
out.insert(out.end(), s.begin(), s.begin() + static_cast<std::ptrdiff_t>(n));
}

/// Whether a type value is a device->host reply / event.
inline constexpr bool is_reply(Type type) { return (static_cast<uint8_t>(type) & 0x80) != 0; }

/// Build an encoded frame for a monitor message (device->host types map to the
/// frame reply flag).
/// @param correlation The stream_frame correlation id to carry: a reply echoes
/// the request's (so a host can pair them), a streamed event carries none.
inline std::vector<uint8_t> build_frame(Type type, std::span<const uint8_t> payload = {},
uint8_t module = kModule,
std::optional<uint16_t> correlation = std::nullopt) {
return espp::stream_frame::build_frame(is_reply(type), module, static_cast<uint8_t>(type),
payload, correlation);
}

// ---- encoders ---------------------------------------------------------------

/// Bytes one HeapRegion occupies on the wire.
inline constexpr size_t kHeapRegionSize = 24;

/// Encode a HEAP payload, keeping it within @p max_bytes (the frame payload
/// limit by default): regions that would not fit are dropped from the end.
/// At most 255 regions are encoded.
/// @param[out] encoded_count Set to the number of regions encoded, if non-null.
inline std::vector<uint8_t> encode_heap(std::span<const HeapRegion> regions,
size_t max_bytes = espp::stream_frame::kMaxPayloadSize,
size_t *encoded_count = nullptr) {
std::vector<uint8_t> p;
const size_t fit = max_bytes > 1 ? (max_bytes - 1) / kHeapRegionSize : 0;
const size_t n = std::min({regions.size(), size_t{255}, fit});
if (encoded_count)
*encoded_count = n;
p.reserve(1 + kHeapRegionSize * n);
p.push_back(static_cast<uint8_t>(n));
for (const auto &r : regions.first(n)) {
espp::stream_frame::put_u32(p, r.flags);
espp::stream_frame::put_u32(p, r.free_bytes);
espp::stream_frame::put_u32(p, r.min_free_bytes);
espp::stream_frame::put_u32(p, r.largest_free_block);
espp::stream_frame::put_u32(p, r.allocated_bytes);
espp::stream_frame::put_u32(p, r.total_size);
}
return p;
}

/// Encode a TASKS payload, keeping it within @p max_bytes (the frame payload
/// limit by default): tasks that would not fit are dropped from the end.
/// @param[out] encoded_count Set to the number of tasks encoded, if non-null.
inline std::vector<uint8_t> encode_tasks(std::span<const TaskEntry> tasks,
size_t max_bytes = espp::stream_frame::kMaxPayloadSize,
size_t *encoded_count = nullptr) {
std::vector<uint8_t> p;
p.push_back(0); // count, patched below
size_t n = 0;
for (const auto &t : tasks) {
if (n == 255 || p.size() + task_entry_size(t.name) > max_bytes)
break;
put_str(p, t.name);
p.push_back(t.cpu_percent);
espp::stream_frame::put_u32(p, t.high_water_mark);
p.push_back(t.priority);
p.push_back(static_cast<uint8_t>(t.core_id));
++n;
}
p[0] = static_cast<uint8_t>(n);
if (encoded_count)
*encoded_count = n;
return p;
}

/// Encode a SET_STREAM request payload.
inline std::vector<uint8_t> encode_set_stream(bool enable, uint16_t period_ms, uint8_t what) {
std::vector<uint8_t> p;
p.push_back(enable ? 1 : 0);
espp::stream_frame::put_u16(p, period_ms);
p.push_back(what);
return p;
}

/// Encode an OK payload.
inline std::vector<uint8_t> encode_ok(uint8_t request_type) { return {request_type}; }

/// Encode an ERROR payload.
inline std::vector<uint8_t> encode_error(uint8_t request_type, uint32_t code,
std::string_view message) {
std::vector<uint8_t> p;
p.push_back(request_type);
espp::stream_frame::put_u32(p, code);
p.insert(p.end(), message.begin(), message.end());
return p;
}

// ---- decoders (nullopt on a malformed payload) ---------------------------------

inline std::optional<std::vector<HeapRegion>> decode_heap(std::span<const uint8_t> p) {
if (p.empty())
return std::nullopt;
const size_t n = p[0];
if (p.size() < 1 + 24 * n)
return std::nullopt;
std::vector<HeapRegion> out;
out.reserve(n);
size_t i = 1;
for (size_t k = 0; k < n; ++k, i += 24) {
HeapRegion r;
r.flags = espp::stream_frame::get_u32(p.subspan(i));
r.free_bytes = espp::stream_frame::get_u32(p.subspan(i + 4));
r.min_free_bytes = espp::stream_frame::get_u32(p.subspan(i + 8));
r.largest_free_block = espp::stream_frame::get_u32(p.subspan(i + 12));
r.allocated_bytes = espp::stream_frame::get_u32(p.subspan(i + 16));
r.total_size = espp::stream_frame::get_u32(p.subspan(i + 20));
out.push_back(r);
}
return out;
}

inline std::optional<std::vector<TaskEntry>> decode_tasks(std::span<const uint8_t> p) {
if (p.empty())
return std::nullopt;
const size_t n = p[0];
std::vector<TaskEntry> out;
out.reserve(n);
size_t i = 1;
for (size_t k = 0; k < n; ++k) {
if (i >= p.size())
return std::nullopt;
const size_t len = p[i++];
if (i + len + 7 > p.size())
return std::nullopt;
TaskEntry t;
t.name.assign(reinterpret_cast<const char *>(p.data() + i), len);
i += len;
t.cpu_percent = p[i++];
t.high_water_mark = espp::stream_frame::get_u32(p.subspan(i));
i += 4;
t.priority = p[i++];
t.core_id = static_cast<int8_t>(p[i++]);
out.push_back(std::move(t));
}
return out;
}

inline std::optional<StreamRequest> decode_set_stream(std::span<const uint8_t> p) {
if (p.size() < 4)
return std::nullopt;
StreamRequest r;
r.enable = p[0] != 0;
r.period_ms = espp::stream_frame::get_u16(p.subspan(1));
r.what = p[3];
return r;
}
Comment on lines +249 to +257

} // namespace espp::detail::monitor_protocol
Loading
Loading