Thanks to Chatgpt for writing my docs once again
The goal of this design is to support multiple communication transports (e.g., BLE, TCP, RF, etc.) under a single central management system. Each transport type may have different connection and configuration requirements (e.g., BLE GATT characteristics, TCP IP/port). To avoid hard-coding transport-specific logic in the central controller, we introduce a session-based abstraction.
In this design, the system is composed of:
- CentralTransferManager — a registry and factory for different transport managers.
- ITransferManager — an interface implemented by each transport type (BLE, TCP, etc.) that knows how to create new connection sessions.
- ITransferSession — represents a single, active connection with send/receive capability.
- ITransferConfig — a polymorphic configuration base for per-transport connection parameters.
This separation allows the system to manage multiple concurrent connections, mix different transport types, and extend easily with new ones.
+---------------------------+
| CentralTransferManager |
+---------------------------+
| registerManager()
| createSession(type, cfg)
v
+---------------------+
| ITransferManager | <-- implemented by BLEManager, TCPManager, etc.
+---------------------+
| createSession(cfg)
v
+--------------------+
| ITransferSession | <-- represents one connection
+--------------------+
| send(), receive()
| close()
Each TransferManager handles the knowledge of its own transport type
and can spawn multiple TransferSession instances, each with unique configuration.
A lightweight, empty polymorphic base class used to represent configuration data. Each transport defines its own derived configuration type.
Example:
struct BLEConfig : public ITransferConfig {
std::string deviceAddress;
std::string gattWriteUUID;
std::string gattReadUUID;
};
struct TCPConfig : public ITransferConfig {
std::string ip;
uint16_t port;
};Represents a live connection.
It exposes send(), receive(), and close() operations.
Each session instance owns its connection state and uses configuration provided during creation.
Example methods:
bool send(const std::vector<uint8_t>& data);
std::vector<uint8_t> receive();
void close();A session is usually short-lived and can be safely owned via std::shared_ptr.
Responsible for creating sessions of a particular transport type. Each concrete manager implements how to interpret its configuration and establish a connection.
Interface concept:
class ITransferManager {
public:
virtual std::string getType() const = 0;
virtual std::shared_ptr<ITransferSession> createSession(
const ITransferConfig& cfg) = 0;
};Each subclass (e.g., BLEManager, TCPManager) knows how to construct its corresponding session type.
The central coordinator that holds registered managers and creates sessions on request. It knows what managers are available, but not how they connect.
High-level behavior:
- Managers are registered once at startup.
- A session is created by specifying a type (e.g., "BLE") and configuration.
- The caller receives a session object used for communication.
Conceptual usage:
CentralTransferManager central;
central.registerManager(std::make_shared<BLEManager>());
central.registerManager(std::make_shared<TCPManager>());
BLEConfig bleCfg{...};
TCPConfig tcpCfg{...};
auto bleSession = central.createSession("BLE", bleCfg);
auto tcpSession = central.createSession("TCP", tcpCfg);
bleSession->send(data1);
tcpSession->send(data2);-
Registration phase — the central manager is initialized with available transport managers.
central.registerManager(std::make_shared<BLEManager>()); central.registerManager(std::make_shared<TCPManager>());
-
Session creation — when a connection is needed, the caller supplies a config:
BLEConfig cfg{ "AA:BB:CC:DD:EE:FF", "writeUUID", "readUUID" }; auto session = central.createSession("BLE", cfg);The central manager locates the correct manager by type ("BLE") and forwards the configuration. The BLE manager interprets the config, opens the connection, and returns a
BLESessionobject. -
Data transfer — all further operations (send, receive, close) are handled by the session itself.
-
Closure — when finished, the session cleans up transport resources.
| Benefit | Description |
|---|---|
| Extensibility | Adding a new transport (e.g., CAN, UART) requires only implementing a new Manager and Session pair. |
| Encapsulation | Transport-specific logic is isolated inside its own manager and session. |
| Scalability | Multiple concurrent connections (sessions) per transport are supported. |
| Simplicity at top-level | The central manager remains generic and type-agnostic. |
| Reusability | Sessions can be reused or pooled depending on connection lifetimes. |
-
Startup: Register all available
ITransferManagerimplementations. -
Configuration / Connection: User or system provides a specific
ITransferConfig. A manager interprets it and spawns aITransferSession. -
Operation:
ITransferSessionhandles all communication independently. -
Shutdown: Sessions are closed gracefully via
close()or destroyed automatically.
To add a new transport:
- Define a new configuration struct derived from
ITransferConfig. - Implement a new
Sessionclass that inherits fromITransferSession. - Implement a new
Managerthat inherits fromITransferManager, returning instances of the new session. - Register the new manager in the central manager at startup.
No existing code needs modification — only extension.
- Serialization Layer: wrap sessions with a Protobuf message encoder/decoder to allow structured data transfer.
- Async / Event Loop Integration: extend sessions with asynchronous send/receive to support non-blocking I/O.
- Connection Pooling: central manager could manage active session pools for high-frequency reconnects.
- Config Loading: automatically generate configurations from JSON/YAML descriptors.
The session-based architecture cleanly separates connection setup from message transfer. Each transport remains self-contained, the central manager stays generic, and the system can scale across multiple transport types and concurrent sessions.
This design strikes a balance between extensibility, maintainability, and clarity, while staying suitable for embedded, desktop, or gateway environments.
This project provides a flexible and extensible architecture for managing multiple data transfer protocols (BLE, TCP, RF, etc.) with a centralized manager and session-based transport handling. It supports polymorphic transport configurations, unified callbacks, and protobuf integration.
- Support multiple transports through a common interface.
- Centralize session management via CentralTransferManager.
- Allow polymorphic transport configurations (
ITransferConfig) for context-aware callbacks. - Enable single unified callbacks for send and receive.
- Keep sessions isolated from each other and from the central manager.
+------------------------+
| CentralTransferMgr |
| (manages sessions) |
+------------------------+
│
│ attachReceiveHandler(cb)
▼
+----------------+ +----------------+ +----------------+
| BLESession | | TCPSession | | RFSession |
| (ITransferSess)| | (ITransferSess)| | (ITransferSess)|
+----------------+ +----------------+ +----------------+
│ │ │
│ notifyData() │ notifyData() │ notifyData()
▼ ▼ ▼
+----------------+ +----------------+ +----------------+
| TransferContext| | TransferContext| | TransferContext|
| - sessionID | | - sessionID | | - sessionID |
| - transportType| | - transportType| | - transportType|
| - sourceID | | - sourceID | | - sourceID |
| - config | | - config | | - config |
+----------------+ +----------------+ +----------------+
│ │ │
└─────────► Unified Receive Callback ◄───────┘
│
▼
Application / Protobuf Layer
Explanation:
- Each transport session is independent.
TransferContextencapsulates session metadata and polymorphic config.- Central manager attaches one callback for all sessions.
- Callback can downcast
configto access transport-specific info.
ITransferSession
+--------------------+
| send(data) |
| onReceive(cb) |
| getConfig() |
+--------------------+
▲
│
----------------------------
| | |
BLESession TCPSession RFSession
| | |
▼ ▼ ▼
BLEConfig TCPConfig RFConfig
- deviceAddr - ip/port - freq/channel
- characteristics - etc.
- mtu
- All sessions implement a common interface (
ITransferSession). - Each session stores its own transport-specific config, derived from
ITransferConfig. - Central manager and handlers see only the base interface, promoting extensibility.
struct ITransferConfig {
virtual ~ITransferConfig() = default;
};
struct TransferContext {
std::string sessionID; // Unique session ID
std::string transportType; // e.g., "BLE", "TCP", "RF"
std::string sourceID; // e.g., characteristic UUID, port
std::shared_ptr<ITransferConfig> config; // Polymorphic transport config
};- Provides full metadata for received data.
configis polymorphic, allowing callbacks to access transport-specific fields.- TransferConfig and TransferContext should be separate since they represents different things
using ReceiveCallback = std::function<void(
TransferContext context,
const std::vector<uint8_t>& data
)>;- Called by any session when data arrives.
- Receives
TransferContextfor transport/session-specific metadata. - Safe downcasting enables access to transport-specific fields.
void BLESession::notifyData(const std::vector<uint8_t>& data, const std::string& charUUID) {
if (recvCb_) {
TransferContext ctx{
getID(),
getTransportType(),
charUUID,
bleConfig_ // shared_ptr<BLEConfig> stored as ITransferConfig
};
recvCb_(ctx, data);
}
}void TCPSession::notifyData(const std::vector<uint8_t>& data) {
if (recvCb_) {
TransferContext ctx{
getID(),
getTransportType(),
std::to_string(tcpConfig_->port),
tcpConfig_ // shared_ptr<TCPConfig> stored as ITransferConfig
};
recvCb_(ctx, data);
}
}auto handler = [](TransferContext ctx, const std::vector<uint8_t>& data) {
std::cout << "[" << ctx.transportType << "][" << ctx.sessionID
<< "][" << ctx.sourceID << "] Received "
<< data.size() << " bytes\n";
if (ctx.transportType == "BLE") {
auto bleCfg = std::dynamic_pointer_cast<BLEConfig>(ctx.config);
std::cout << "BLE Device: " << bleCfg->deviceAddress << "\n";
} else if (ctx.transportType == "TCP") {
auto tcpCfg = std::dynamic_pointer_cast<TCPConfig>(ctx.config);
std::cout << "TCP IP: " << tcpCfg->ip << ":" << tcpCfg->port << "\n";
}
};- The same handler can be attached to all sessions.
- Downcasting gives access to transport-specific metadata.
- Supports multiple sessions without duplicating logic.
CentralTransferManager central;
central.attachReceiveHandler(handler);
auto bleSession = central.createSession("BLE", bleConfig);
auto tcpSession = central.createSession("TCP", tcpConfig);- Central manager manages all sessions.
- Sessions notify the manager via callbacks.
- The handler sees all incoming data with its context.
BLE / TCP / RF Session
│
▼
+--------------------+
| TransferContext |
| + ITransferConfig |
+--------------------+
│
▼
Unified Receive Callback
│
▼
+--------------------+
| Protobuf Deserialize|
| / Application Logic |
+--------------------+
- Raw bytes are decoded in the application layer.
configprovides transport-specific metadata.- Supports multi-transport handling with a single handler.
| Benefit | Description |
|---|---|
| Unified handler | Single callback receives data from all transports. |
| Context-rich | Session ID, transport type, source ID, and polymorphic config. |
| Safe polymorphism | Downcast to access transport-specific fields. |
| Extensible | Add new transport types without modifying central manager. |
| Session isolation | Transport logic stays inside session. |
| Metadata storage | Callbacks can store extra info like timestamps, RSSI, etc. |