Skip to content

Commit 6499e3d

Browse files
committed
feat(core): Initial Monolithic Firmware (v0.2)
- Implemented SystemIO dispatcher (Clean CLI + Binary Mux) - Implemented Console with Command Registry (Click-like) - Implemented NVS Config & Network Core (ESP-NOW) - Added GatewayMgr for Coordinator role - Added FactoryMgr for initial provisioning
1 parent fc679ed commit 6499e3d

16 files changed

Lines changed: 714 additions & 91 deletions

.clang-format

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
BasedOnStyle: Google
2+
IndentWidth: 4
3+
TabWidth: 4
4+
UseTab: Never
5+
ColumnLimit: 120
6+
AccessModifierOffset: -4
7+
8+
# Скобки
9+
BreakBeforeBraces: Attach
10+
AllowShortFunctionsOnASingleLine: Empty
11+
12+
# Выравнивание
13+
AlignAfterOpenBracket: Align
14+
AlignOperands: true
15+
AlignTrailingComments: true
16+
17+
# Указатели и ссылки (BaseManager* mgr, а не BaseManager *mgr)
18+
PointerAlignment: Left
19+
20+
# Сортировка инклудов (SystemIO.h после CD_Defs.h, но до системных)
21+
SortIncludes: false

docs/architecture.md

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,17 @@
11
# Архитектура прошивки (Universal Firmware)
22

3+
**Версия:** 0.2 (Initial Core)
4+
**Тип:** Monolithic Firmware w/ Role Switching
5+
36
**Концепция:** One Binary to Rule Them All.
4-
Мы используем единый бинарный файл для всех типов устройств. Роль устройства определяется конфигурацией в NVS.
7+
Мы используем **Единую Прошивку** для всех устройств в сети (Координатор, Помпа, Бойлер, Весы).
8+
Роль устройства определяется при старте на основе конфигурации в **NVS** (`HU_TYPE_*`).
9+
10+
## Ключевые преимущества
11+
12+
- Одна точка сборки и тестирования.
13+
- Единый механизм OTA-обновлений (A/B Partitioning).
14+
- Возможность смены роли устройства "на лету" без программатора (через Console).
515

616
---
717

@@ -29,6 +39,29 @@ src/
2939
└── console/ # Обработчик текстовых команд (CD-ADB)
3040
```
3141

42+
Проект разделен на изолированные слои.
43+
44+
### Level 0: HAL (Hardware Abstraction)
45+
46+
- **SystemIO (`src/hal/SystemIO`):** - Единый диспетчер UART.
47+
- Разделяет поток на **Текстовые команды** (CLI) и **Бинарные фреймы** (Mesh).
48+
- Обеспечивает "чистый" вывод логов без разрыва строки ввода консоли.
49+
50+
### Level 1: Core (Ядро)
51+
52+
- **NVSConfig (`src/core/NVSConfig`):** Хранение `DeviceType`, `LogicalID` и калибровок.
53+
- **Network (`src/core/Network`):** Обертка над ESP-NOW. Управляет очередями пакетов.
54+
- **Console (`src/console/Console`):** - Реализация CD-ADB (Command Line Interface).
55+
- Паттерн **Command Registry**: модули регистрируют свои команды (`ping`, `status`) динамически.
56+
57+
### Level 2: Managers (Бизнес-логика)
58+
59+
Каждый тип устройства реализуется как класс-наследник `BaseManager`.
60+
61+
- **FactoryMgr:** Режим по умолчанию. Мигает светодиодом, ждет конфигурации.
62+
- **GatewayMgr:** Работает на Координаторе. Мост `Serial <-> ESP-NOW`.
63+
- **ActuatorMgr (TODO):** Управление моторами и PID.
64+
3265
## 2. Концепция "Менеджеров" (Hub Architecture)
3366

3467
Устройство (ESP32) выступает хостом для одного или нескольких функциональных блоков (**Managers**).
@@ -47,7 +80,23 @@ src/
4780

4881
---
4982

50-
## 3. Производственный процесс
83+
## 3. Взаимодействие (Data Flow)
84+
85+
### Сценарий: CLI Команда
86+
87+
1. Пользователь вводит `ping` в Serial Monitor.
88+
2. `SystemIO` накапливает байты в буфер. По `\n` вызывает коллбек `Console`.
89+
3. `Console` ищет `ping` в реестре команд.
90+
4. Вызывается лямбда, зарегистрированная в `GatewayMgr`.
91+
5. Лямбда отправляет пакет в `Network`.
92+
93+
### Сценарий: Логирование
94+
95+
1. Любой модуль вызывает макрос `LOG_INF("Data: %d", val)`.
96+
2. Макрос вызывает `SystemIO::log()`.
97+
3. `SystemIO` стирает текущую строку ввода (`\r\033[K`), печатает лог, затем восстанавливает приглашение `>`.
98+
99+
## 4. Производственный процесс
51100

52101
### Этап 1: Flash (Чистая плата)
53102

platformio.ini

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,14 @@ lib_deps =
3232
cd-protocol
3333
; knolleary/PubSubClient @ ^2.8
3434
; marvinroger/AsyncMqttClient @ ^0.9.0
35+
36+
; Фильтры монитора
37+
; direct: вывод как есть
38+
; send_on_enter: отправляет текст только по нажатию Enter (решает проблему "разрыва" ввода логами)
39+
monitor_filters = direct, send_on_enter
40+
41+
; Включить локальное эхо (ты видишь, что пишешь)
42+
monitor_echo = yes
43+
44+
; Обработка конца строки (CRLF)
45+
monitor_eol = LF

src/CD_Defs.h

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
#pragma once
22
#include <Arduino.h>
3-
#include <headunit_protocol.h> // Из lib/cd-protocol
3+
#include <headunit_protocol.h> // Из lib/cd-protocol
44

55
// Глобальные пины (по умолчанию для DevKit V1)
66
#define PIN_LED_BUILTIN 2
77

8+
// Forward declaration, чтобы не инклудить хедер сюда
9+
class SystemIO;
10+
811
// Логирование (обертка для удобства)
9-
#define LOG_INF(fmt, ...) Serial.printf("[INF] " fmt "\n", ##__VA_ARGS__)
10-
#define LOG_ERR(fmt, ...) Serial.printf("[ERR] " fmt "\n", ##__VA_ARGS__)
11-
#define LOG_WRN(fmt, ...) Serial.printf("[WRN] " fmt "\n", ##__VA_ARGS__)
12+
#define LOG_INF(fmt, ...) SystemIO::log("INF", fmt, ##__VA_ARGS__)
13+
#define LOG_ERR(fmt, ...) SystemIO::log("ERR", fmt, ##__VA_ARGS__)
14+
#define LOG_WRN(fmt, ...) SystemIO::log("WRN", fmt, ##__VA_ARGS__)

src/console/Console.cpp

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
#include "Console.h"
2+
#include "../hal/SystemIO.h"
3+
#include "../core/NVSConfig.h"
4+
5+
std::vector<ConsoleCommand> Console::_commands;
6+
7+
void Console::begin() {
8+
LOG_INF("CD-ADB Shell Ready. Type 'help'.");
9+
10+
SystemIO::onTextCommand([](String cmd) {
11+
// 1. Сбрасываем флаг перед запуском команды
12+
SystemIO::resetOutputFlag();
13+
// 2. Выполняем команду
14+
Console::processCommand(cmd);
15+
// 3. Проверяем: если команда промолчала, нужно восстановить промпт вручную
16+
if (!SystemIO::wasOutputProduced()) {
17+
SystemIO::showPrompt();
18+
}
19+
});
20+
21+
// Регистрация системных команд (Bootstrap)
22+
registerCommand("help", printHelp, "Show help");
23+
registerCommand("reboot", cmd_reboot, "System restart");
24+
registerCommand("nvs", cmd_nvs, "Config tool: nvs <info|set_type|set_id>");
25+
26+
SystemIO::showPrompt();
27+
}
28+
29+
void Console::registerCommand(String name, CmdHandler handler, String help) {
30+
_commands.push_back({name, handler, help});
31+
}
32+
33+
void Console::processCommand(String input) {
34+
if (input.length() == 0) return;
35+
36+
// Разделяем "cmd" и "args"
37+
int spaceIdx = input.indexOf(' ');
38+
String cmdName = (spaceIdx == -1) ? input : input.substring(0, spaceIdx);
39+
String args = (spaceIdx == -1) ? "" : input.substring(spaceIdx + 1);
40+
41+
cmdName.toLowerCase();
42+
43+
// Поиск в реестре
44+
bool found = false;
45+
for (const auto& cmd : _commands) {
46+
if (cmd.name == cmdName) {
47+
cmd.handler(args);
48+
found = true;
49+
break;
50+
}
51+
}
52+
53+
if (!found) {
54+
SystemIO::println("Unknown command: '%s'. Type 'help'.", cmdName.c_str());
55+
}
56+
}
57+
58+
void Console::printHelp(String args) {
59+
SystemIO::println("--- Available Commands ---");
60+
// Выравнивание для красоты
61+
for (const auto& cmd : _commands) {
62+
// Простой форматированный вывод
63+
// Можно заморочиться с паддингом, но пока так
64+
char buf[64];
65+
snprintf(buf, sizeof(buf), " %-10s : %s", cmd.name.c_str(), cmd.help.c_str());
66+
SystemIO::println(buf);
67+
}
68+
SystemIO::println("--------------------------");
69+
}
70+
71+
void Console::cmd_reboot(String args) {
72+
LOG_WRN("Rebooting...");
73+
delay(500);
74+
ESP.restart();
75+
}
76+
77+
void Console::cmd_nvs(String args) {
78+
// Логика NVS без изменений, только аргументы приходят готовые
79+
if (args == "info") {
80+
SystemIO::println("Type: 0x%02X, ID: 0x%02X, Rev: %d", NVSConfig::getDeviceType(), NVSConfig::getLogicalID(),
81+
NVSConfig::getHardwareRevision());
82+
} else if (args.startsWith("set_type ")) {
83+
int val = args.substring(9).toInt();
84+
NVSConfig::setDeviceType((hu_device_type_t)val);
85+
LOG_INF("Type set to %d. Rebooting...", val);
86+
delay(1000);
87+
ESP.restart();
88+
} else if (args.startsWith("set_id ")) {
89+
int val = args.substring(7).toInt();
90+
NVSConfig::setLogicalID(val);
91+
LOG_INF("ID set to %d.", val);
92+
} else {
93+
SystemIO::println("Usage: nvs <info|set_type|set_id>");
94+
}
95+
}
96+
97+
void Console::update() {} // Заглушка

src/console/Console.h

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
#pragma once
2+
#include "../CD_Defs.h"
3+
#include <vector>
4+
#include <functional>
5+
6+
// Тип обработчика: получает строку аргументов (все, что после команды)
7+
using CmdHandler = std::function<void(String args)>;
8+
9+
struct ConsoleCommand {
10+
String name;
11+
CmdHandler handler;
12+
String help;
13+
};
14+
15+
class Console {
16+
public:
17+
static void begin();
18+
static void update(); // Не нужен в loop, но пока оставим для совместимости API
19+
20+
// API для модулей (аналог @click.command)
21+
static void registerCommand(String name, CmdHandler handler, String help);
22+
23+
private:
24+
static void processCommand(String cmd);
25+
static void printHelp(String args);
26+
27+
// Обработчики команд
28+
static void cmd_nvs(String args);
29+
static void cmd_reboot(String args);
30+
31+
static std::vector<ConsoleCommand> _commands;
32+
};

src/core/NVSConfig.cpp

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,42 @@
11
#include "NVSConfig.h"
2+
#include "../hal/SystemIO.h"
23

34
Preferences NVSConfig::prefs;
45

56
void NVSConfig::begin() {
6-
// Открываем namespace "cd-config".
7-
// false = read/write режим (нам нужно уметь писать ID)
8-
prefs.begin("cd-config", false);
9-
LOG_INF("NVS Initialized");
7+
// Открываем namespace "cd-config".
8+
// false = read/write режим (нам нужно уметь писать ID)
9+
prefs.begin("cd-config", false);
10+
LOG_INF("NVS Initialized");
1011
}
1112

1213
void NVSConfig::resetToFactory() {
13-
prefs.clear();
14-
LOG_WRN("NVS Cleared (Factory Reset)");
14+
prefs.clear();
15+
LOG_WRN("NVS Cleared (Factory Reset)");
1516
}
1617

1718
hu_device_type_t NVSConfig::getDeviceType() {
18-
// Читаем байт, приводим к enum.
19-
// Если ключа нет - возвращаем HU_TYPE_UNKNOWN (0)
20-
return (hu_device_type_t)prefs.getUChar("dev_type", HU_TYPE_UNKNOWN);
19+
// Читаем байт, приводим к enum.
20+
// Если ключа нет - возвращаем HU_TYPE_UNKNOWN (0)
21+
return (hu_device_type_t)prefs.getUChar("dev_type", HU_TYPE_UNKNOWN);
2122
}
2223

2324
uint8_t NVSConfig::getLogicalID() {
24-
// Если ID не назначен, возвращаем UNASSIGNED (0xFE)
25-
return prefs.getUChar("log_id", HU_ADDR_UNASSIGNED);
25+
// Если ID не назначен, возвращаем UNASSIGNED (0xFE)
26+
return prefs.getUChar("log_id", HU_ADDR_UNASSIGNED);
2627
}
2728

2829
uint8_t NVSConfig::getHardwareRevision() {
29-
// Версия железа, по умолчанию 1
30-
return prefs.getUChar("hw_rev", 1);
30+
// Версия железа, по умолчанию 1
31+
return prefs.getUChar("hw_rev", 1);
3132
}
3233

3334
void NVSConfig::setDeviceType(hu_device_type_t type) {
34-
prefs.putUChar("dev_type", (uint8_t)type);
35-
LOG_INF("Device Type set to: 0x%02X", type);
35+
prefs.putUChar("dev_type", (uint8_t)type);
36+
LOG_INF("Device Type set to: 0x%02X", type);
3637
}
3738

3839
void NVSConfig::setLogicalID(uint8_t id) {
39-
prefs.putUChar("log_id", id);
40-
LOG_INF("Logical ID set to: 0x%02X", id);
40+
prefs.putUChar("log_id", id);
41+
LOG_INF("Logical ID set to: 0x%02X", id);
4142
}

src/core/Network.cpp

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
#include "Network.h"
2+
#include "../hal/SystemIO.h"
3+
4+
QueueHandle_t Network::_rxQueue;
5+
uint8_t Network::_myMac[6];
6+
7+
void Network::begin(uint8_t logicalID) {
8+
// 1. Инициализация WiFi в режиме Station
9+
WiFi.mode(WIFI_STA);
10+
WiFi.disconnect();
11+
12+
// Получаем MAC
13+
WiFi.macAddress(_myMac);
14+
LOG_INF("Network Init. MAC: %02X:%02X:%02X:%02X:%02X:%02X", _myMac[0], _myMac[1], _myMac[2], _myMac[3], _myMac[4],
15+
_myMac[5]);
16+
17+
// 2. Инициализация ESP-NOW
18+
if (esp_now_init() != ESP_OK) {
19+
LOG_ERR("Error initializing ESP-NOW");
20+
return;
21+
}
22+
23+
// 3. Создаем очередь (FreeRTOS)
24+
_rxQueue = xQueueCreate(NETWORK_RX_QUEUE_SIZE, sizeof(Packet));
25+
26+
// 4. Регистрируем коллбеки
27+
esp_now_register_recv_cb(OnDataRecv);
28+
esp_now_register_send_cb(OnDataSent);
29+
30+
// 5. Добавляем Broadcast пира (чтобы можно было слать всем)
31+
esp_now_peer_info_t peerInfo = {};
32+
memset(peerInfo.peer_addr, 0xFF, 6); // Broadcast addr
33+
peerInfo.channel = 0;
34+
peerInfo.encrypt = false;
35+
36+
if (esp_now_add_peer(&peerInfo) != ESP_OK) {
37+
LOG_ERR("Failed to add broadcast peer");
38+
}
39+
}
40+
41+
void Network::update() {
42+
// Тут можно добавить логику перепосылки, если нужно
43+
}
44+
45+
bool Network::sendBroadcast(const uint8_t *data, size_t len) {
46+
uint8_t broadcastAddr[] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
47+
esp_err_t result = esp_now_send(broadcastAddr, (uint8_t *)data, len);
48+
return result == ESP_OK;
49+
}
50+
51+
bool Network::readPacket(Packet &pkt) {
52+
if (!_rxQueue) return false;
53+
return xQueueReceive(_rxQueue, &pkt, 0) == pdTRUE;
54+
}
55+
56+
// ISR Context! Не использовать тяжелые операции
57+
void Network::OnDataRecv(const uint8_t *mac, const uint8_t *incomingData, int len) {
58+
if (len > sizeof(Packet::data)) return; // Overflow protection
59+
60+
Packet pkt;
61+
memcpy(pkt.mac, mac, 6);
62+
memcpy(pkt.data, incomingData, len);
63+
pkt.len = len;
64+
65+
// Пушим в очередь (из ISR)
66+
xQueueSendFromISR(_rxQueue, &pkt, NULL);
67+
}
68+
69+
void Network::OnDataSent(const uint8_t *mac_addr, esp_now_send_status_t status) {
70+
// Можно добавить дебаг, но будет спамить
71+
// LOG_INF("Last Packet Send Status: %s", status == ESP_NOW_SEND_SUCCESS ? "OK" : "FAIL");
72+
}
73+
74+
uint8_t *Network::getMacAddress() {
75+
return _myMac;
76+
}

0 commit comments

Comments
 (0)