Dikhan1 commited on
Commit
96a5049
·
verified ·
1 Parent(s): c056bae

Upload 24 files

Browse files
audi5_rabochi.ino ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #include <WiFi.h>
2
+ #include <WebServer.h>
3
+ #include <ArduinoJson.h>
4
+ #include "engine_control.h"
5
+ #include "idle_control.h"
6
+ #include "settings_manager.h"
7
+ #include "config.h"
8
+
9
+ // Создаем объекты
10
+ EngineControl engineControl;
11
+ IdleController idleControl;
12
+ SettingsManager settingsManager;
13
+ WebServer server(80);
14
+
15
+ void handleStatus() {
16
+ StaticJsonDocument<512> doc;
17
+
18
+ doc["rpm"] = engineControl.getRPM();
19
+ doc["map"] = engineControl.getMAP();
20
+ doc["tps"] = engineControl.getTPS();
21
+ doc["lambda"] = engineControl.getLambda();
22
+ doc["knock_level"] = engineControl.knockLevel;
23
+ doc["knock_events"] = engineControl.getKnockEvents();
24
+ doc["fuel_correction"] = engineControl.getCurrentFuelCorrection();
25
+ doc["ignition_correction"] = engineControl.getCurrentIgnitionCorrection();
26
+ doc["engine_temp"] = engineControl.getEngineTemp();
27
+ doc["voltage"] = engineControl.getVoltage();
28
+ doc["learning_enabled"] = engineControl.isLearningEnabled();
29
+
30
+ String response;
31
+ serializeJson(doc, response);
32
+ server.send(200, "application/json", response);
33
+ }
34
+
35
+ void handleIdleRPM() {
36
+ if (server.hasArg("rpm")) {
37
+ int rpm = server.arg("rpm").toInt();
38
+ if (rpm >= 700 && rpm <= 2000) {
39
+ idleControl.setTargetRPM(rpm);
40
+ server.send(200, "text/plain", "OK");
41
+ return;
42
+ }
43
+ }
44
+ server.send(400, "text/plain", "Invalid RPM value");
45
+ }
46
+
47
+ void setup() {
48
+ Serial.begin(115200);
49
+
50
+ // Устанавливаем связь между объектами
51
+ idleControl.setEngineControl(&engineControl);
52
+
53
+ // Инициализация компонентов
54
+ engineControl.begin();
55
+ idleControl.begin();
56
+
57
+ // Настройка WiFi
58
+ WiFi.softAP("ECU_Config", "12345678");
59
+
60
+ // Настройка веб-сервера
61
+ server.on("/status", HTTP_GET, handleStatus);
62
+ server.on("/idle_rpm", HTTP_POST, handleIdleRPM);
63
+
64
+ server.begin();
65
+ }
66
+
67
+ void loop() {
68
+ // Обработка веб-запросов
69
+ server.handleClient();
70
+
71
+ // Обновление состояния двигателя
72
+ engineControl.update();
73
+
74
+ // Обновление РХХ
75
+ if (engineControl.getRPM() > 0) {
76
+ idleControl.update(engineControl.getRPM());
77
+ }
78
+
79
+ // Задержка для стабильной работы
80
+ delay(10);
81
+ }
classes.h ADDED
@@ -0,0 +1,247 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #ifndef CLASSES_H
2
+ #define CLASSES_H
3
+
4
+ #include <Arduino.h>
5
+ #include <SPI.h>
6
+ #include <SD.h>
7
+ #include <WiFi.h>
8
+ #include <WebServer.h>
9
+ #include <ArduinoJson.h>
10
+ #include "structures.h"
11
+
12
+ // Класс для работы с SD картой
13
+ class SDCardManager {
14
+ public:
15
+ bool init() {
16
+ if (!SD.begin(SD_CS_PIN, SPI, SD_SPI_SPEED)) {
17
+ Serial.println("Ошибка инициализации SD карты");
18
+ return false;
19
+ }
20
+ return true;
21
+ }
22
+
23
+ bool saveData(const char* filename, const void* data, size_t size) {
24
+ File file = SD.open(filename, FILE_WRITE);
25
+ if (!file) {
26
+ Serial.println("Ошибка открытия файла для записи");
27
+ return false;
28
+ }
29
+
30
+ size_t written = file.write((const uint8_t*)data, size);
31
+ file.close();
32
+
33
+ return written == size;
34
+ }
35
+
36
+ bool loadData(const char* filename, void* data, size_t size) {
37
+ File file = SD.open(filename, FILE_READ);
38
+ if (!file) {
39
+ Serial.println("Ошибка открытия файла для чтения");
40
+ return false;
41
+ }
42
+
43
+ size_t read = file.read((uint8_t*)data, size);
44
+ file.close();
45
+
46
+ return read == size;
47
+ }
48
+ };
49
+
50
+ // Класс для работы с PSRAM
51
+ class PSRAMManager {
52
+ private:
53
+ void* psramData;
54
+ size_t dataSize;
55
+ bool initialized;
56
+
57
+ public:
58
+ PSRAMManager() : psramData(nullptr), dataSize(0), initialized(false) {}
59
+
60
+ bool init() {
61
+ if (!psramInit()) {
62
+ Serial.println("Ошибка инициализации PSRAM");
63
+ return false;
64
+ }
65
+
66
+ // Выделяем память для всех данных
67
+ dataSize = sizeof(ConfigData) + sizeof(LearningData) + sizeof(RXXLearningData);
68
+ psramData = ps_malloc(dataSize);
69
+
70
+ if (!psramData) {
71
+ Serial.println("Ошибка выделения памяти в PSRAM");
72
+ return false;
73
+ }
74
+
75
+ initialized = true;
76
+ Serial.println("PSRAM инициализирован");
77
+ return true;
78
+ }
79
+
80
+ bool saveData(const void* data, size_t size, size_t offset) {
81
+ if (!initialized) return false;
82
+
83
+ if (offset + size > dataSize) {
84
+ Serial.println("Ошибка: превышение размера PSRAM");
85
+ return false;
86
+ }
87
+
88
+ memcpy((uint8_t*)psramData + offset, data, size);
89
+ return true;
90
+ }
91
+
92
+ bool loadData(void* data, size_t size, size_t offset) {
93
+ if (!initialized) return false;
94
+
95
+ if (offset + size > dataSize) {
96
+ Serial.println("Ошибка: превышение размера PSRAM");
97
+ return false;
98
+ }
99
+
100
+ memcpy(data, (uint8_t*)psramData + offset, size);
101
+ return true;
102
+ }
103
+
104
+ size_t getFreeMemory() {
105
+ return ESP.getFreePsram();
106
+ }
107
+
108
+ size_t getTotalMemory() {
109
+ return ESP.getPsramSize();
110
+ }
111
+ };
112
+
113
+ // Класс для работы с веб-сервером
114
+ class WebServerManager {
115
+ private:
116
+ WebServer server;
117
+ PSRAMManager psramManager;
118
+
119
+ public:
120
+ void init() {
121
+ // Инициализация WiFi
122
+ WiFi.mode(WIFI_AP_STA);
123
+ WiFi.softAP(WEB_SSID, WEB_PASSWORD);
124
+
125
+ // Настройка маршрутов
126
+ server.on("/", HTTP_GET, [this]() {
127
+ server.send(200, "text/html", htmlPage);
128
+ });
129
+
130
+ server.on("/config", HTTP_GET, [this]() {
131
+ StaticJsonDocument<1024> doc;
132
+ ConfigData config;
133
+ if (psramManager.loadData(&config, sizeof(ConfigData), 0)) {
134
+ doc["injectionTimeMin"] = config.injectionTimeMin;
135
+ doc["injectionTimeMax"] = config.injectionTimeMax;
136
+ // ... добавьте остальные поля конфигурации
137
+ }
138
+
139
+ String response;
140
+ serializeJson(doc, response);
141
+ server.send(200, "application/json", response);
142
+ });
143
+
144
+ server.on("/config", HTTP_POST, [this]() {
145
+ StaticJsonDocument<1024> doc;
146
+ DeserializationError error = deserializeJson(doc, server.arg("plain"));
147
+
148
+ if (error) {
149
+ server.send(400, "text/plain", "Ошибка разбора JSON");
150
+ return;
151
+ }
152
+
153
+ ConfigData config;
154
+ // Обновление конфигурации
155
+ config.injectionTimeMin = doc["injectionTimeMin"];
156
+ config.injectionTimeMax = doc["injectionTimeMax"];
157
+ // ... обновите остальные поля конфигу��ации
158
+
159
+ if (psramManager.saveData(&config, sizeof(ConfigData), 0)) {
160
+ server.send(200, "text/plain", "Конфигурация сохранена");
161
+ } else {
162
+ server.send(500, "text/plain", "Ошибка сохранения конфигурации");
163
+ }
164
+ });
165
+
166
+ server.begin();
167
+ }
168
+
169
+ void handleClient() {
170
+ server.handleClient();
171
+ }
172
+ };
173
+
174
+ // Класс для управления двигателем
175
+ class EngineManager {
176
+ private:
177
+ float currentRPM;
178
+ float currentThrottle;
179
+ float currentTemp;
180
+ float currentLambda;
181
+
182
+ public:
183
+ void updateSensors(float rpm, float throttle, float temp, float lambda) {
184
+ currentRPM = rpm;
185
+ currentThrottle = throttle;
186
+ currentTemp = temp;
187
+ currentLambda = lambda;
188
+ }
189
+
190
+ float calculateInjectionTime() {
191
+ // Базовая логика расчета времени впрыска
192
+ float baseTime = map(currentRPM, 0, MAX_RPM,
193
+ configData.injectionTimeMin,
194
+ configData.injectionTimeMax);
195
+
196
+ // Коррекция по температуре
197
+ if (configData.tempCorrectionEnabled) {
198
+ baseTime *= getTempCorrection(currentTemp);
199
+ }
200
+
201
+ // Коррекция по лямбде
202
+ if (configData.lambdaCorrectionEnabled) {
203
+ baseTime *= getLambdaCorrection(currentLambda);
204
+ }
205
+
206
+ return baseTime;
207
+ }
208
+
209
+ float calculateAdvance() {
210
+ // Базовая логика расчета УОЗ
211
+ float baseAdvance = map(currentRPM, 0, MAX_RPM,
212
+ configData.minAdvance,
213
+ configData.maxAdvance);
214
+
215
+ // Коррекция для холостого хода
216
+ if (isIdle()) {
217
+ return getIdleAdvance();
218
+ }
219
+
220
+ return baseAdvance;
221
+ }
222
+
223
+ private:
224
+ bool isIdle() {
225
+ return currentRPM >= configData.idleRpmMin &&
226
+ currentRPM <= configData.idleRpmMax &&
227
+ currentThrottle < THROTTLE_IDLE_THRESHOLD;
228
+ }
229
+
230
+ float getTempCorrection(float temp) {
231
+ // Логика коррекции по температуре
232
+ return 1.0; // Заглушка
233
+ }
234
+
235
+ float getLambdaCorrection(float lambda) {
236
+ // Логика коррекции по лямбде
237
+ return 1.0; // Заглушка
238
+ }
239
+
240
+ float getIdleAdvance() {
241
+ // Логика расчета УОЗ на холостом ходу
242
+ return map(currentRPM, configData.idleRpmMin, configData.idleRpmMax,
243
+ configData.idleAdvanceMin, configData.idleAdvanceMax);
244
+ }
245
+ };
246
+
247
+ #endif // CLASSES_H
config.h ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #pragma once
2
+
3
+ // Пины для датчиков
4
+ #define HALL_SENSOR_PIN 36
5
+ #define MAP_SENSOR_PIN 39
6
+ #define TPS_PIN 34
7
+ #define LAMBDA_SENSOR_PIN 35
8
+ #define ENGINE_TEMP_SENSOR_PIN 32
9
+ #define BATTERY_VOLTAGE_PIN 33
10
+ #define KNOCK_SENSOR_1_3_PIN 25
11
+ #define KNOCK_SENSOR_4_5_PIN 26
12
+
13
+ // Пины для РХХ
14
+ #define IDLE_VALVE_PIN_A 27
15
+ #define IDLE_VALVE_PIN_B 14
16
+
17
+ // Пороговые значения
18
+ #define START_RPM_THRESHOLD 400
19
+ #define DETONATION_THRESHOLD 0.8
20
+
21
+ // Настройки PWM для РХХ
22
+ #define IDLE_VALVE_PWM_FREQ 1000
23
+ #define IDLE_VALVE_PWM_RES 8 // 8 bit resolution
24
+
25
+ // Пины управления
26
+ #define FUEL_PUMP_PIN 16 // Изменен с 27
27
+ #define IGNITION_PIN 17 // Изменен с 14
28
+ #define INJECTOR_PIN 12
29
+
30
+ // Форсунки
31
+ #define INJECTOR_1_PIN 5
32
+ #define INJECTOR_2_PIN 18
33
+ #define INJECTOR_3_PIN 19
34
+ #define INJECTOR_4_PIN 21
35
+ #define INJECTOR_5_PIN 22
36
+
37
+ // Катушки зажигания
38
+ #define COIL_1_PIN 23
39
+ #define COIL_2_PIN 15
40
+ #define COIL_3_PIN 13
41
+ #define COIL_4_PIN 12
42
+ #define COIL_5_PIN 4
43
+
44
+ // Настройки WiFi
45
+ #define WEB_SSID "ECU_Config"
46
+ #define WEB_PASSWORD "12345678"
47
+
48
+ // Константы двигателя
49
+ #define NUM_CYLINDERS 5
50
+ #define MAX_RPM 7000
51
+ #define MIN_ADVANCE 0
52
+ #define MAX_ADVANCE 40
53
+ #define MECHANICAL_ADVANCE 40.0
54
+ #define DWELL_TIME 3000
55
+
56
+ // Константы впрыска
57
+ #define MIN_INJECTION_TIME 1.0
58
+ #define MAX_INJECTION_TIME 20.0
59
+ #define INJECTOR_FLOW 200
60
+ #define FUEL_PRESSURE 3.0
61
+
62
+ // Константы лямбда-регулирования
63
+ #define LAMBDA_TARGET 1.0
64
+ #define LAMBDA_CORRECTION_MIN 0.8
65
+ #define LAMBDA_CORRECTION_MAX 1.2
66
+ #define LAMBDA_CORRECTION_STEP 0.01
67
+
68
+ // Температурные константы
69
+ #define COLD_START_TEMP 20.0
70
+ #define HOT_ENGINE_TEMP 90.0
71
+ #define TEMP_CORRECTION_MAX 1.5
72
+
73
+ // Структура конфигурации
74
+ struct ConfigData {
75
+ // Базовые таблицы
76
+ float fuelTable[16][16]; // Базовая таблица топливоподачи
77
+ float ignitionTable[16][16]; // Базовая таблица УОЗ
78
+
79
+ // Таблицы обучения
80
+ float fuelLearning[16][16]; // Таблица коррекции топливоподачи
81
+ float ignitionLearning[16][16]; // Таблица коррекции УОЗ
82
+
83
+ // Настройки холостого хода
84
+ float idleTargetRpm; // Целевые обороты ХХ
85
+ float idleKp; // Коэффициент P для ПИД
86
+ float idleKi; // Коэффициент I для ПИД
87
+ float idleKd; // Коэффициент D для ПИД
88
+
89
+ // Контрольная сумма
90
+ uint32_t checksum; // Контрольная сумма конфигурации
91
+ };
92
+
93
+ // Функции конфигурации
94
+ bool saveConfig(ConfigData* config);
95
+ bool loadConfig(ConfigData* config);
96
+ uint32_t calculateConfigChecksum(ConfigData* config);
engine_control.cpp ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #include "engine_control.h"
2
+ #include "config.h"
3
+ #include <Arduino.h>
4
+
5
+ // Вспомогательные функции для преобразования значений
6
+ float mapf(float x, float in_min, float in_max, float out_min, float out_max) {
7
+ return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
8
+ }
9
+
10
+ EngineControl::EngineControl() :
11
+ rpm(0),
12
+ map(0),
13
+ tps(0),
14
+ lambda(0),
15
+ knockLevel(0),
16
+ engineTemp(0),
17
+ voltage(0),
18
+ learningEnabled(false),
19
+ knockEvents(0),
20
+ currentFuelCorrection(1.0),
21
+ currentIgnitionCorrection(0.0)
22
+ {
23
+ }
24
+
25
+ void EngineControl::begin() {
26
+ // Настройка пинов
27
+ pinMode(HALL_SENSOR_PIN, INPUT);
28
+ pinMode(MAP_SENSOR_PIN, INPUT);
29
+ pinMode(TPS_PIN, INPUT);
30
+ pinMode(LAMBDA_SENSOR_PIN, INPUT);
31
+ pinMode(ENGINE_TEMP_SENSOR_PIN, INPUT);
32
+ pinMode(BATTERY_VOLTAGE_PIN, INPUT);
33
+ pinMode(KNOCK_SENSOR_1_3_PIN, INPUT);
34
+ pinMode(KNOCK_SENSOR_4_5_PIN, INPUT);
35
+
36
+ // Загрузка таблиц обучения
37
+ fuelLearning.load();
38
+ ignitionLearning.load();
39
+ }
40
+
41
+ void EngineControl::update() {
42
+ // Чтение сенсоров
43
+ rpm = analogRead(HALL_SENSOR_PIN);
44
+ map = analogRead(MAP_SENSOR_PIN);
45
+ tps = analogRead(TPS_PIN);
46
+ lambda = analogRead(LAMBDA_SENSOR_PIN);
47
+ engineTemp = analogRead(ENGINE_TEMP_SENSOR_PIN);
48
+ voltage = analogRead(BATTERY_VOLTAGE_PIN);
49
+
50
+ // Преобразование значений АЦП
51
+ rpm = mapf(rpm, 0, 4095, 0, 7000);
52
+ map = mapf(map, 0, 4095, 0, 100);
53
+ tps = mapf(tps, 0, 4095, 0, 100);
54
+ lambda = mapf(lambda, 0, 4095, 0.7, 1.3);
55
+ engineTemp = mapf(engineTemp, 0, 4095, -40, 150);
56
+ voltage = mapf(voltage, 0, 4095, 0, 15);
57
+
58
+ if (learningEnabled && rpm > START_RPM_THRESHOLD) {
59
+ float engineLoad = map; // В данном случае используем MAP как нагрузку
60
+
61
+ // Обучение топливоподачи по лямбда-зонду
62
+ if (lambda < 0.98) {
63
+ fuelLearning.update(engineLoad, rpm, 0.98);
64
+ } else if (lambda > 1.02) {
65
+ fuelLearning.update(engineLoad, rpm, 1.02);
66
+ }
67
+
68
+ // Чтение датчиков детонации
69
+ float knock1 = analogRead(KNOCK_SENSOR_1_3_PIN);
70
+ float knock2 = analogRead(KNOCK_SENSOR_4_5_PIN);
71
+ knockLevel = max(knock1, knock2);
72
+
73
+ // Обучение УОЗ по детонации
74
+ ignitionLearning.learn(engineLoad, rpm, knockLevel);
75
+
76
+ // Если была детонация, увеличиваем счетчик
77
+ if (knockLevel > DETONATION_THRESHOLD) {
78
+ knockEvents++;
79
+ }
80
+ }
81
+
82
+ // Получение текущих коррекций
83
+ float engineLoad = map;
84
+ currentFuelCorrection = fuelLearning.getCorrection(engineLoad, rpm);
85
+ currentIgnitionCorrection = ignitionLearning.getCorrection(engineLoad, rpm);
86
+ }
87
+
88
+ float EngineControl::getLearningProgress() const {
89
+ return (fuelLearning.getProgress() + ignitionLearning.getProgress()) / 2.0;
90
+ }
91
+
92
+ float EngineControl::getIgnitionAdvance() {
93
+ float engineLoad = map;
94
+ float baseAdvance = 10.0; // Базовый УОЗ
95
+
96
+ // Добавляем коррекцию от обучения
97
+ float ignitionCorr = ignitionLearning.getCorrection(engineLoad, rpm);
98
+
99
+ return baseAdvance + ignitionCorr;
100
+ }
101
+
102
+ bool EngineControl::saveLearningTables() {
103
+ return fuelLearning.save() && ignitionLearning.save();
104
+ }
105
+
106
+ bool EngineControl::loadLearningTables() {
107
+ return fuelLearning.load() && ignitionLearning.load();
108
+ }
engine_control.h ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #pragma once
2
+ #include "Arduino.h"
3
+ #include "structures.h"
4
+ #include "fuel_learning_table.h"
5
+ #include "ignition_learning_table.h"
6
+
7
+ class EngineControl {
8
+ private:
9
+ float rpm;
10
+ float map;
11
+ float tps;
12
+ float lambda;
13
+ float engineTemp;
14
+ float voltage;
15
+
16
+ FuelLearningTable fuelLearning;
17
+ IgnitionLearningTable ignitionLearning;
18
+ bool learningEnabled;
19
+ uint32_t knockEvents;
20
+ float currentFuelCorrection;
21
+ float currentIgnitionCorrection;
22
+
23
+ public:
24
+ float knockLevel; // Перемещено в public для доступа из основного кода
25
+
26
+ EngineControl();
27
+ void begin();
28
+ void update();
29
+
30
+ // Геттеры
31
+ float getRPM() const { return rpm; }
32
+ float getMAP() const { return map; }
33
+ float getTPS() const { return tps; }
34
+ float getLambda() const { return lambda; }
35
+ float getEngineTemp() const { return engineTemp; }
36
+ float getVoltage() const { return voltage; }
37
+ float getLearningProgress() const;
38
+ float getIgnitionAdvance();
39
+ uint32_t getKnockEvents() const { return knockEvents; }
40
+ float getCurrentFuelCorrection() const { return currentFuelCorrection; }
41
+ float getCurrentIgnitionCorrection() const { return currentIgnitionCorrection; }
42
+
43
+ // Управление обучением
44
+ void resetLearning() { fuelLearning.reset(); ignitionLearning.reset(); }
45
+ bool saveLearningTables();
46
+ bool loadLearningTables();
47
+ void setLearningEnabled(bool enabled) { learningEnabled = enabled; }
48
+ bool isLearningEnabled() const { return learningEnabled; }
49
+ };
fuel_learning.cpp ADDED
@@ -0,0 +1,128 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #include "fuel_learning.h"
2
+ #include <Preferences.h>
3
+
4
+ FuelLearningTable::FuelLearningTable() :
5
+ learningRate(0.01),
6
+ maxCorrection(1.2),
7
+ minCorrection(0.8)
8
+ {
9
+ // Инициализация точек нагрузки (кПа)
10
+ for (int i = 0; i < LOAD_POINTS; i++) {
11
+ loadPoints[i] = 20.0 + (i * 6.0); // 20-110 кПа
12
+ }
13
+
14
+ // Инициализация точек оборотов
15
+ for (int i = 0; i < RPM_POINTS; i++) {
16
+ rpmPoints[i] = 800.0 + (i * 400.0); // 800-6800 RPM
17
+ }
18
+
19
+ // Инициализация ячеек коррекции
20
+ reset();
21
+ }
22
+
23
+ void FuelLearningTable::begin() {
24
+ load(); // Загрузка сохраненных данных
25
+ }
26
+
27
+ void FuelLearningTable::reset() {
28
+ for (int i = 0; i < LOAD_POINTS; i++) {
29
+ for (int j = 0; j < RPM_POINTS; j++) {
30
+ cells[i][j] = 1.0; // Нейтральная коррекция
31
+ }
32
+ }
33
+ }
34
+
35
+ int FuelLearningTable::findNearestIndex(float value, const float* points, int size) {
36
+ if (value <= points[0]) return 0;
37
+ if (value >= points[size-1]) return size - 2;
38
+
39
+ for (int i = 0; i < size - 1; i++) {
40
+ if (value >= points[i] && value < points[i+1]) {
41
+ return i;
42
+ }
43
+ }
44
+ return 0;
45
+ }
46
+
47
+ float FuelLearningTable::interpolate2D(float load, float rpm) {
48
+ // Находим ближайшие индексы
49
+ int loadIdx = findNearestIndex(load, loadPoints, LOAD_POINTS);
50
+ int rpmIdx = findNearestIndex(rpm, rpmPoints, RPM_POINTS);
51
+
52
+ // Находим веса для интерполяции
53
+ float loadWeight = (load - loadPoints[loadIdx]) /
54
+ (loadPoints[loadIdx+1] - loadPoints[loadIdx]);
55
+ float rpmWeight = (rpm - rpmPoints[rpmIdx]) /
56
+ (rpmPoints[rpmIdx+1] - rpmPoints[rpmIdx]);
57
+
58
+ // Билинейная интерполяция
59
+ float c00 = cells[loadIdx][rpmIdx];
60
+ float c10 = cells[loadIdx+1][rpmIdx];
61
+ float c01 = cells[loadIdx][rpmIdx+1];
62
+ float c11 = cells[loadIdx+1][rpmIdx+1];
63
+
64
+ float c0 = c00 * (1 - loadWeight) + c10 * loadWeight;
65
+ float c1 = c01 * (1 - loadWeight) + c11 * loadWeight;
66
+
67
+ return c0 * (1 - rpmWeight) + c1 * rpmWeight;
68
+ }
69
+
70
+ void FuelLearningTable::updateCell(int loadIdx, int rpmIdx, float correction) {
71
+ // Применяем коррекцию с учетом скорости обучения
72
+ cells[loadIdx][rpmIdx] += correction * learningRate;
73
+
74
+ // Ограничиваем значение коррекции
75
+ cells[loadIdx][rpmIdx] = constrain(cells[loadIdx][rpmIdx],
76
+ minCorrection, maxCorrection);
77
+ }
78
+
79
+ float FuelLearningTable::getCorrection(float load, float rpm) {
80
+ return interpolate2D(load, rpm);
81
+ }
82
+
83
+ void FuelLearningTable::learn(float load, float rpm, float lambdaError) {
84
+ // Находим ближайшие ячейки
85
+ int loadIdx = findNearestIndex(load, loadPoints, LOAD_POINTS);
86
+ int rpmIdx = findNearestIndex(rpm, rpmPoints, RPM_POINTS);
87
+
88
+ // Обновляем основную ячейку
89
+ updateCell(loadIdx, rpmIdx, lambdaError);
90
+
91
+ // Обновляем соседние ячейки с меньшим весом
92
+ if (loadIdx > 0) {
93
+ updateCell(loadIdx-1, rpmIdx, lambdaError * 0.5);
94
+ }
95
+ if (loadIdx < LOAD_POINTS-1) {
96
+ updateCell(loadIdx+1, rpmIdx, lambdaError * 0.5);
97
+ }
98
+ if (rpmIdx > 0) {
99
+ updateCell(loadIdx, rpmIdx-1, lambdaError * 0.5);
100
+ }
101
+ if (rpmIdx < RPM_POINTS-1) {
102
+ updateCell(loadIdx, rpmIdx+1, lambdaError * 0.5);
103
+ }
104
+ }
105
+
106
+ bool FuelLearningTable::save() {
107
+ Preferences prefs;
108
+ prefs.begin("fuel_learn", false);
109
+
110
+ // Сохраняем таблицу коррекции
111
+ size_t written = prefs.putBytes("cells", cells,
112
+ sizeof(float) * LOAD_POINTS * RPM_POINTS);
113
+
114
+ prefs.end();
115
+ return written == sizeof(float) * LOAD_POINTS * RPM_POINTS;
116
+ }
117
+
118
+ bool FuelLearningTable::load() {
119
+ Preferences prefs;
120
+ prefs.begin("fuel_learn", true);
121
+
122
+ // Загружаем таблицу коррекции
123
+ size_t readSize = prefs.getBytes("cells", cells,
124
+ sizeof(float) * LOAD_POINTS * RPM_POINTS);
125
+
126
+ prefs.end();
127
+ return readSize == sizeof(float) * LOAD_POINTS * RPM_POINTS;
128
+ }
fuel_learning.h ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #ifndef FUEL_LEARNING_H
2
+ #define FUEL_LEARNING_H
3
+
4
+ #include <Arduino.h>
5
+ #include "config.h"
6
+
7
+ // Размеры таблиц обучения
8
+ #define LOAD_POINTS 16 // Точки по нагрузке
9
+ #define RPM_POINTS 16 // Точки по оборотам
10
+
11
+ class FuelLearningTable {
12
+ private:
13
+ float cells[LOAD_POINTS][RPM_POINTS]; // Таблица коррекции
14
+ float loadPoints[LOAD_POINTS]; // Точки по нагрузке (кПа)
15
+ float rpmPoints[RPM_POINTS]; // Точки по оборотам
16
+ float learningRate; // Скорость обучения
17
+ float maxCorrection; // Максимальная коррекция
18
+ float minCorrection; // Минимальная коррекция
19
+
20
+ // Вспомогательные методы
21
+ int findNearestIndex(float value, const float* points, int size);
22
+ float interpolate2D(float load, float rpm);
23
+ void updateCell(int loadIdx, int rpmIdx, float correction);
24
+
25
+ public:
26
+ FuelLearningTable();
27
+
28
+ // Основные методы
29
+ void begin();
30
+ float getCorrection(float load, float rpm);
31
+ void learn(float load, float rpm, float lambdaError);
32
+ void reset();
33
+
34
+ // Методы сохранения/загрузки
35
+ bool save();
36
+ bool load();
37
+
38
+ // Настройка параметров
39
+ void setLearningRate(float rate);
40
+ void setCorrectionLimits(float min, float max);
41
+
42
+ // Доступ к данным для визуализации
43
+ float getCellValue(int loadIdx, int rpmIdx);
44
+ float getLoadPoint(int idx);
45
+ float getRPMPoint(int idx);
46
+ };
47
+
48
+ #endif
fuel_learning_table.cpp ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #include "fuel_learning_table.h"
2
+ #include "SD.h"
3
+ #include "maps.h"
4
+
5
+ FuelLearningTable::FuelLearningTable() {
6
+ reset();
7
+ }
8
+
9
+ void FuelLearningTable::reset() {
10
+ for (int i = 0; i < 16; i++) {
11
+ for (int j = 0; j < 16; j++) {
12
+ corrections[i][j] = 1.0;
13
+ cellHits[i][j] = 0;
14
+ }
15
+ }
16
+ }
17
+
18
+ bool FuelLearningTable::save() {
19
+ File file = SD.open("/fuel_learning.bin", FILE_WRITE);
20
+ if (!file) return false;
21
+
22
+ file.write((uint8_t*)corrections, sizeof(corrections));
23
+ file.write((uint8_t*)cellHits, sizeof(cellHits));
24
+ file.close();
25
+ return true;
26
+ }
27
+
28
+ bool FuelLearningTable::load() {
29
+ File file = SD.open("/fuel_learning.bin", FILE_READ);
30
+ if (!file) return false;
31
+
32
+ file.read((uint8_t*)corrections, sizeof(corrections));
33
+ file.read((uint8_t*)cellHits, sizeof(cellHits));
34
+ file.close();
35
+ return true;
36
+ }
37
+
38
+ float FuelLearningTable::getCorrection(float load, float rpm) {
39
+ int loadIdx = constrain(mapf(load, 0, 100, 0, 15), 0, 15);
40
+ int rpmIdx = constrain(mapf(rpm, 0, 7000, 0, 15), 0, 15);
41
+ return corrections[loadIdx][rpmIdx];
42
+ }
43
+
44
+ void FuelLearningTable::update(float load, float rpm, float correction) {
45
+ int loadIdx = constrain(mapf(load, 0, 100, 0, 15), 0, 15);
46
+ int rpmIdx = constrain(mapf(rpm, 0, 7000, 0, 15), 0, 15);
47
+
48
+ float currentCorr = corrections[loadIdx][rpmIdx];
49
+ float learningRate = 0.1f / (1.0f + cellHits[loadIdx][rpmIdx]);
50
+ corrections[loadIdx][rpmIdx] = currentCorr + (correction - currentCorr) * learningRate;
51
+ cellHits[loadIdx][rpmIdx]++;
52
+ }
53
+
54
+ float FuelLearningTable::getProgress() const {
55
+ int totalCells = 16 * 16;
56
+ int learnedCells = 0;
57
+
58
+ for (int i = 0; i < 16; i++) {
59
+ for (int j = 0; j < 16; j++) {
60
+ if (cellHits[i][j] > 0) learnedCells++;
61
+ }
62
+ }
63
+
64
+ return (float)learnedCells / totalCells;
65
+ }
fuel_learning_table.h ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #pragma once
2
+ #include "Arduino.h"
3
+
4
+ class FuelLearningTable {
5
+ private:
6
+ float corrections[16][16];
7
+ uint32_t cellHits[16][16];
8
+
9
+ public:
10
+ FuelLearningTable();
11
+ void reset();
12
+ bool save();
13
+ bool load();
14
+ float getCorrection(float load, float rpm);
15
+ void update(float load, float rpm, float correction);
16
+ float getProgress() const;
17
+ };
idle_control.cpp ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #include "idle_control.h"
2
+ #include "config.h"
3
+ #include <Arduino.h>
4
+ #include "maps.h"
5
+
6
+ IdleController::IdleController() :
7
+ targetRPM(800),
8
+ currentPosition(0),
9
+ lastUpdateTime(0),
10
+ isAirConditionerOn(false),
11
+ engineTemp(20.0),
12
+ engineControl(nullptr),
13
+ pid(0.1, 0.05, 0.02) // Начальные значения коэффициентов ПИД
14
+ {
15
+ }
16
+
17
+ void IdleController::begin() {
18
+ // Настройка ШИМ каналов для РХХ
19
+ ledcSetup(0, IDLE_VALVE_PWM_FREQ, IDLE_VALVE_PWM_RES); // Канал 0 для пина A
20
+ ledcSetup(1, IDLE_VALVE_PWM_FREQ, IDLE_VALVE_PWM_RES); // Канал 1 для пина B
21
+
22
+ ledcAttachPin(IDLE_VALVE_PIN_A, 0); // Привязываем пин A к каналу 0
23
+ ledcAttachPin(IDLE_VALVE_PIN_B, 1); // Привязываем пин B к каналу 1
24
+
25
+ // Начальные значения ШИМ
26
+ ledcWrite(0, 0);
27
+ ledcWrite(1, 0);
28
+
29
+ // Инициализация ПИД-регулятора
30
+ pid.SetMode(true); // Включаем автоматический режим
31
+ pid.SetOutputLimits(0, 255);
32
+ pid.SetSampleTime(50);
33
+
34
+ lastUpdateTime = millis();
35
+
36
+ // Установка начального положения
37
+ updateValvePosition(0);
38
+ }
39
+
40
+ void IdleController::updateValvePosition(float position) {
41
+ position = constrain(position, 0, 255);
42
+ currentPosition = position;
43
+
44
+ // Преобразование позиции в ШИМ
45
+ int pwmValue = mapf(position, 0, 255, 0, (1 << IDLE_VALVE_PWM_RES) - 1);
46
+
47
+ // Управление моторчиком РХХ
48
+ ledcWrite(0, pwmValue);
49
+ ledcWrite(1, 0);
50
+ }
51
+
52
+ void IdleController::update(float currentRPM) {
53
+ unsigned long currentTime = millis();
54
+ float dt = (currentTime - lastUpdateTime) / 1000.0;
55
+ lastUpdateTime = currentTime;
56
+
57
+ // Проверяем наличие объекта управления двигателем
58
+ if (!engineControl) return;
59
+
60
+ // Получаем текущую температуру двигателя
61
+ float engineTemp = engineControl->getEngineTemp();
62
+
63
+ // Корректируем целевые обороты в зависимости от температуры
64
+ float adjustedTargetRpm = targetRPM;
65
+ if (engineTemp < 60) { // Если двигатель холодный
66
+ adjustedTargetRpm += (60 - engineTemp) * 6.67f; // Увеличиваем на 400 RPM при 0°C, линейно уменьшая до 60°C
67
+ }
68
+
69
+ // Добавляем компенсацию для кондиционера
70
+ if (isAirConditionerOn) {
71
+ adjustedTargetRpm += 50; // Увеличиваем на 50 RPM при включенном кондиционере
72
+ }
73
+
74
+ // Обновляем ПИД
75
+ pid.SetSetpoint(adjustedTargetRpm);
76
+ pid.SetInput(currentRPM);
77
+ if (pid.Compute()) {
78
+ // Применяем выход ПИД к положению РХХ
79
+ setPosition(pid.GetOutput());
80
+ }
81
+ }
82
+
83
+ void IdleController::setEngineTemperature(float temp) {
84
+ engineTemp = temp;
85
+ }
86
+
87
+ void IdleController::setAirConditioner(bool isOn) {
88
+ isAirConditionerOn = isOn;
89
+ }
90
+
91
+ void IdleController::loadSettings() {
92
+ // Здесь должна быть загрузка настроек из EEPROM или SD
93
+ // Пока используем значения по умолчанию
94
+ settings.targetRPM = 800;
95
+ settings.valveMinPosition = 0;
96
+ settings.valveMaxPosition = 255;
97
+ settings.rpmTolerance = 50;
98
+ }
99
+
100
+ void IdleController::saveSettings() {
101
+ // Здесь должно быть сохранение настроек в EEPROM или SD
102
+ }
idle_control.h ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #pragma once
2
+ #include "Arduino.h"
3
+ #include "config.h"
4
+ #include "engine_control.h"
5
+ #include "pid_controller.h"
6
+
7
+ class IdleController {
8
+ private:
9
+ float targetRPM;
10
+ float currentPosition;
11
+ unsigned long lastUpdateTime;
12
+ bool isAirConditionerOn;
13
+ float engineTemp;
14
+
15
+ // ПИД-регулятор
16
+ PIDController pid;
17
+
18
+ // Указатель на объект управления двигателем для получения температуры
19
+ EngineControl* engineControl;
20
+
21
+ // Вспомогательные методы
22
+ void updateValvePosition(float position);
23
+
24
+ public:
25
+ IdleController();
26
+ void begin();
27
+ void update(float currentRPM);
28
+
29
+ // Методы для работы с целевыми оборотами
30
+ void setTargetRPM(float rpm) {
31
+ if (rpm >= 700 && rpm <= 2000) {
32
+ targetRPM = rpm;
33
+ pid.SetSetpoint(rpm);
34
+ }
35
+ }
36
+ float getTargetRPM() const { return targetRPM; }
37
+
38
+ // Методы для настройки ПИД
39
+ void setPIDParameters(float kp, float ki, float kd) {
40
+ pid.SetTunings(kp, ki, kd);
41
+ }
42
+
43
+ // Методы для управления положением
44
+ void setPosition(float position) { updateValvePosition(position); }
45
+ float getCurrentPosition() const { return currentPosition; }
46
+
47
+ // Методы для работы с температурой и кондиционером
48
+ void setEngineTemperature(float temp) { engineTemp = temp; }
49
+ void setAirConditioner(bool isOn) { isAirConditionerOn = isOn; }
50
+
51
+ // Метод для установки указателя на объект управления двигателем
52
+ void setEngineControl(EngineControl* control) { engineControl = control; }
53
+ };
ignition_learning.cpp ADDED
@@ -0,0 +1,166 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #include "ignition_learning.h"
2
+ #include <Preferences.h>
3
+
4
+ IgnitionLearningTable::IgnitionLearningTable() :
5
+ learningRate(0.05),
6
+ maxCorrection(5.0),
7
+ minCorrection(-5.0),
8
+ knockThreshold(0.8),
9
+ knockProtectionOffset(2.0),
10
+ knockHistorySize(10)
11
+ {
12
+ // Инициализация точек нагрузки (кПа)
13
+ for (int i = 0; i < LOAD_POINTS; i++) {
14
+ loadPoints[i] = 20.0 + (i * 6.0); // 20-110 кПа
15
+ }
16
+
17
+ // Инициализация точек оборотов
18
+ for (int i = 0; i < RPM_POINTS; i++) {
19
+ rpmPoints[i] = 800.0 + (i * 400.0); // 800-6800 RPM
20
+ }
21
+
22
+ // Инициализация ячеек коррекции и истории детонации
23
+ reset();
24
+ }
25
+
26
+ void IgnitionLearningTable::begin() {
27
+ load(); // Загрузка сохраненных данных
28
+ }
29
+
30
+ void IgnitionLearningTable::reset() {
31
+ for (int i = 0; i < LOAD_POINTS; i++) {
32
+ for (int j = 0; j < RPM_POINTS; j++) {
33
+ cells[i][j] = 0.0; // Нейтральная коррекция
34
+ knockHistory[i][j] = 0; // Сброс истории детонации
35
+ }
36
+ }
37
+ }
38
+
39
+ int IgnitionLearningTable::findNearestIndex(float value, const float* points, int size) {
40
+ if (value <= points[0]) return 0;
41
+ if (value >= points[size-1]) return size - 2;
42
+
43
+ for (int i = 0; i < size - 1; i++) {
44
+ if (value >= points[i] && value < points[i+1]) {
45
+ return i;
46
+ }
47
+ }
48
+ return 0;
49
+ }
50
+
51
+ float IgnitionLearningTable::interpolate2D(float load, float rpm) {
52
+ // Находим ближайшие индексы
53
+ int loadIdx = findNearestIndex(load, loadPoints, LOAD_POINTS);
54
+ int rpmIdx = findNearestIndex(rpm, rpmPoints, RPM_POINTS);
55
+
56
+ // Находим веса для интерполяции
57
+ float loadWeight = (load - loadPoints[loadIdx]) /
58
+ (loadPoints[loadIdx+1] - loadPoints[loadIdx]);
59
+ float rpmWeight = (rpm - rpmPoints[rpmIdx]) /
60
+ (rpmPoints[rpmIdx+1] - rpmPoints[rpmIdx]);
61
+
62
+ // Билинейная интерполяция
63
+ float c00 = cells[loadIdx][rpmIdx];
64
+ float c10 = cells[loadIdx+1][rpmIdx];
65
+ float c01 = cells[loadIdx][rpmIdx+1];
66
+ float c11 = cells[loadIdx+1][rpmIdx+1];
67
+
68
+ float c0 = c00 * (1 - loadWeight) + c10 * loadWeight;
69
+ float c1 = c01 * (1 - loadWeight) + c11 * loadWeight;
70
+
71
+ return c0 * (1 - rpmWeight) + c1 * rpmWeight;
72
+ }
73
+
74
+ void IgnitionLearningTable::updateCell(int loadIdx, int rpmIdx, float correction) {
75
+ // Применяем коррекцию с учетом скорости обучения
76
+ cells[loadIdx][rpmIdx] += correction * learningRate;
77
+
78
+ // Ограничиваем значение коррекции
79
+ cells[loadIdx][rpmIdx] = constrain(cells[loadIdx][rpmIdx],
80
+ minCorrection, maxCorrection);
81
+ }
82
+
83
+ void IgnitionLearningTable::updateKnockHistory(int loadIdx, int rpmIdx, bool knockDetected) {
84
+ // Сдвигаем историю
85
+ knockHistory[loadIdx][rpmIdx] = (knockHistory[loadIdx][rpmIdx] << 1) | (knockDetected ? 1 : 0);
86
+ knockHistory[loadIdx][rpmIdx] &= ((1 << knockHistorySize) - 1); // Ограничиваем размер
87
+ }
88
+
89
+ float IgnitionLearningTable::getCorrection(float load, float rpm) {
90
+ return interpolate2D(load, rpm);
91
+ }
92
+
93
+ void IgnitionLearningTable::learn(float load, float rpm, float knockLevel) {
94
+ // Находим ближайшие ячейки
95
+ int loadIdx = findNearestIndex(load, loadPoints, LOAD_POINTS);
96
+ int rpmIdx = findNearestIndex(rpm, rpmPoints, RPM_POINTS);
97
+
98
+ bool knockDetected = knockLevel > knockThreshold;
99
+ updateKnockHistory(loadIdx, rpmIdx, knockDetected);
100
+
101
+ float correction = 0;
102
+
103
+ if (knockDetected) {
104
+ // При детонации уменьшаем УОЗ
105
+ correction = -knockProtectionOffset;
106
+ } else {
107
+ // Если детонации нет, медленно увеличиваем УОЗ
108
+ // но только если в истории нет детонации
109
+ if (knockHistory[loadIdx][rpmIdx] == 0) {
110
+ correction = 0.1;
111
+ }
112
+ }
113
+
114
+ // Обновляем основную ячейку
115
+ updateCell(loadIdx, rpmIdx, correction);
116
+
117
+ // Обновляем соседние ячейки с меньшим весом
118
+ if (knockDetected) {
119
+ if (loadIdx > 0) {
120
+ updateCell(loadIdx-1, rpmIdx, correction * 0.5);
121
+ }
122
+ if (loadIdx < LOAD_POINTS-1) {
123
+ updateCell(loadIdx+1, rpmIdx, correction * 0.5);
124
+ }
125
+ if (rpmIdx > 0) {
126
+ updateCell(loadIdx, rpmIdx-1, correction * 0.5);
127
+ }
128
+ if (rpmIdx < RPM_POINTS-1) {
129
+ updateCell(loadIdx, rpmIdx+1, correction * 0.5);
130
+ }
131
+ }
132
+ }
133
+
134
+ bool IgnitionLearningTable::save() {
135
+ Preferences prefs;
136
+ prefs.begin("ign_learn", false);
137
+
138
+ // Сохраняем таблицу коррекции
139
+ size_t written = prefs.putBytes("cells", cells,
140
+ sizeof(float) * LOAD_POINTS * RPM_POINTS);
141
+
142
+ // Сохраняем историю детонации
143
+ written += prefs.putBytes("knock_hist", knockHistory,
144
+ sizeof(uint8_t) * LOAD_POINTS * RPM_POINTS);
145
+
146
+ prefs.end();
147
+ return written == sizeof(float) * LOAD_POINTS * RPM_POINTS +
148
+ sizeof(uint8_t) * LOAD_POINTS * RPM_POINTS;
149
+ }
150
+
151
+ bool IgnitionLearningTable::load() {
152
+ Preferences prefs;
153
+ prefs.begin("ign_learn", true);
154
+
155
+ // Загружаем таблицу коррекции
156
+ size_t readSize = prefs.getBytes("cells", cells,
157
+ sizeof(float) * LOAD_POINTS * RPM_POINTS);
158
+
159
+ // Загружаем историю детонации
160
+ readSize += prefs.getBytes("knock_hist", knockHistory,
161
+ sizeof(uint8_t) * LOAD_POINTS * RPM_POINTS);
162
+
163
+ prefs.end();
164
+ return readSize == sizeof(float) * LOAD_POINTS * RPM_POINTS +
165
+ sizeof(uint8_t) * LOAD_POINTS * RPM_POINTS;
166
+ }
ignition_learning.h ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #ifndef IGNITION_LEARNING_H
2
+ #define IGNITION_LEARNING_H
3
+
4
+ #include <Arduino.h>
5
+ #include "config.h"
6
+
7
+ // Размеры таблиц обучения
8
+ #define LOAD_POINTS 16 // Точки по нагрузке
9
+ #define RPM_POINTS 16 // Точки по оборотам
10
+
11
+ class IgnitionLearningTable {
12
+ private:
13
+ float cells[LOAD_POINTS][RPM_POINTS]; // Таблица коррекции УОЗ
14
+ float loadPoints[LOAD_POINTS]; // Точки по нагрузке (кПа)
15
+ float rpmPoints[RPM_POINTS]; // Точки по оборотам
16
+ float learningRate; // Скорость обучения
17
+ float maxCorrection; // Максимальная коррекция
18
+ float minCorrection; // Минимальная коррекция
19
+ float knockThreshold; // Порог детонации
20
+ float knockProtectionOffset; // Защитное смещение при детонации
21
+
22
+ // История детонации для каждой ячейки
23
+ uint8_t knockHistory[LOAD_POINTS][RPM_POINTS];
24
+ uint8_t knockHistorySize;
25
+
26
+ // Вспомогательные методы
27
+ int findNearestIndex(float value, const float* points, int size);
28
+ float interpolate2D(float load, float rpm);
29
+ void updateCell(int loadIdx, int rpmIdx, float correction);
30
+ void updateKnockHistory(int loadIdx, int rpmIdx, bool knockDetected);
31
+
32
+ public:
33
+ IgnitionLearningTable();
34
+
35
+ // Основные методы
36
+ void begin();
37
+ float getCorrection(float load, float rpm);
38
+ void learn(float load, float rpm, float knockLevel);
39
+ void reset();
40
+
41
+ // Методы сохранения/загрузки
42
+ bool save();
43
+ bool load();
44
+
45
+ // Настройка параметров
46
+ void setLearningRate(float rate);
47
+ void setCorrectionLimits(float min, float max);
48
+ void setKnockParameters(float threshold, float protection);
49
+
50
+ // Доступ к данным для визуализации
51
+ float getCellValue(int loadIdx, int rpmIdx);
52
+ float getLoadPoint(int idx);
53
+ float getRPMPoint(int idx);
54
+ uint8_t getKnockCount(int loadIdx, int rpmIdx);
55
+ };
56
+
57
+ #endif
ignition_learning_table.cpp ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #include "ignition_learning_table.h"
2
+ #include "SD.h"
3
+ #include "maps.h"
4
+
5
+ IgnitionLearningTable::IgnitionLearningTable() {
6
+ reset();
7
+ }
8
+
9
+ void IgnitionLearningTable::reset() {
10
+ for (int i = 0; i < 16; i++) {
11
+ for (int j = 0; j < 16; j++) {
12
+ corrections[i][j] = 0.0;
13
+ cellHits[i][j] = 0;
14
+ }
15
+ }
16
+ }
17
+
18
+ bool IgnitionLearningTable::save() {
19
+ File file = SD.open("/ignition_learning.bin", FILE_WRITE);
20
+ if (!file) return false;
21
+
22
+ file.write((uint8_t*)corrections, sizeof(corrections));
23
+ file.write((uint8_t*)cellHits, sizeof(cellHits));
24
+ file.close();
25
+ return true;
26
+ }
27
+
28
+ bool IgnitionLearningTable::load() {
29
+ File file = SD.open("/ignition_learning.bin", FILE_READ);
30
+ if (!file) return false;
31
+
32
+ file.read((uint8_t*)corrections, sizeof(corrections));
33
+ file.read((uint8_t*)cellHits, sizeof(cellHits));
34
+ file.close();
35
+ return true;
36
+ }
37
+
38
+ float IgnitionLearningTable::getCorrection(float load, float rpm) {
39
+ int loadIdx = constrain(mapf(load, 0, 100, 0, 15), 0, 15);
40
+ int rpmIdx = constrain(mapf(rpm, 0, 7000, 0, 15), 0, 15);
41
+ return corrections[loadIdx][rpmIdx];
42
+ }
43
+
44
+ void IgnitionLearningTable::learn(float load, float rpm, float knockLevel) {
45
+ int loadIdx = constrain(mapf(load, 0, 100, 0, 15), 0, 15);
46
+ int rpmIdx = constrain(mapf(rpm, 0, 7000, 0, 15), 0, 15);
47
+
48
+ if (knockLevel > 0.5) { // Если есть детонация
49
+ float currentCorr = corrections[loadIdx][rpmIdx];
50
+ float learningRate = 0.5f / (1.0f + cellHits[loadIdx][rpmIdx]);
51
+ corrections[loadIdx][rpmIdx] = currentCorr - learningRate; // Уменьшаем УОЗ
52
+ }
53
+
54
+ cellHits[loadIdx][rpmIdx]++;
55
+ }
56
+
57
+ float IgnitionLearningTable::getProgress() const {
58
+ int totalCells = 16 * 16;
59
+ int learnedCells = 0;
60
+
61
+ for (int i = 0; i < 16; i++) {
62
+ for (int j = 0; j < 16; j++) {
63
+ if (cellHits[i][j] > 0) learnedCells++;
64
+ }
65
+ }
66
+
67
+ return (float)learnedCells / totalCells;
68
+ }
ignition_learning_table.h ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #pragma once
2
+ #include "Arduino.h"
3
+
4
+ class IgnitionLearningTable {
5
+ private:
6
+ float corrections[16][16];
7
+ uint32_t cellHits[16][16];
8
+
9
+ public:
10
+ IgnitionLearningTable();
11
+ void reset();
12
+ bool save();
13
+ bool load();
14
+ float getCorrection(float load, float rpm);
15
+ void learn(float load, float rpm, float knockLevel);
16
+ float getProgress() const;
17
+ };
learning_manager.cpp ADDED
@@ -0,0 +1,256 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #include "learning_manager.h"
2
+ #include <ArduinoJson.h>
3
+
4
+ LearningManager::LearningManager() :
5
+ isLearningEnabled(false),
6
+ isFuelLearningEnabled(true),
7
+ isIgnitionLearningEnabled(true),
8
+ learningStartTime(0),
9
+ minLearningTime(300000), // 5 минут
10
+ minEngineTemp(60), // 60°C
11
+ maxEngineTemp(100), // 100°C
12
+ minThrottlePosition(5), // 5%
13
+ maxThrottlePosition(95), // 95%
14
+ totalLearningPoints(0),
15
+ knockEvents(0),
16
+ richMixtureEvents(0),
17
+ leanMixtureEvents(0)
18
+ {
19
+ }
20
+
21
+ void LearningManager::begin() {
22
+ fuelTable.begin();
23
+ ignitionTable.begin();
24
+
25
+ // Загружаем сохраненные данные
26
+ load();
27
+
28
+ // Устанавливаем параметры обучения
29
+ fuelTable.setLearningRate(0.01);
30
+ fuelTable.setCorrectionLimits(0.8, 1.2);
31
+
32
+ ignitionTable.setLearningRate(0.05);
33
+ ignitionTable.setCorrectionLimits(-5.0, 5.0);
34
+ ignitionTable.setKnockParameters(0.8, 2.0);
35
+
36
+ learningStartTime = millis();
37
+ }
38
+
39
+ bool LearningManager::checkLearningConditions(float engineTemp, float throttlePosition) {
40
+ // Проверка времени работы двигателя
41
+ if (millis() - learningStartTime < minLearningTime) {
42
+ return false;
43
+ }
44
+
45
+ // Проверка температуры двигателя
46
+ if (engineTemp < minEngineTemp || engineTemp > maxEngineTemp) {
47
+ return false;
48
+ }
49
+
50
+ // Проверка положения дросселя
51
+ if (throttlePosition < minThrottlePosition || throttlePosition > maxThrottlePosition) {
52
+ return false;
53
+ }
54
+
55
+ return true;
56
+ }
57
+
58
+ void LearningManager::update(float load, float rpm, float lambda, float knockLevel,
59
+ float engineTemp, float throttlePosition) {
60
+ if (!isLearningEnabled || !checkLearningConditions(engineTemp, throttlePosition)) {
61
+ return;
62
+ }
63
+
64
+ // Обучение топливной карты
65
+ if (isFuelLearningEnabled) {
66
+ float lambdaError = lambda - 1.0; // Отклонение от стехиометрии
67
+ if (abs(lambdaError) > 0.02) { // Порог ошибки 2%
68
+ fuelTable.learn(load, rpm, lambdaError);
69
+ if (lambdaError > 0) {
70
+ richMixtureEvents++;
71
+ } else {
72
+ leanMixtureEvents++;
73
+ }
74
+ }
75
+ }
76
+
77
+ // Обучение карты УОЗ
78
+ if (isIgnitionLearningEnabled) {
79
+ ignitionTable.learn(load, rpm, knockLevel);
80
+ if (knockLevel > 0.8) { // Порог детонации
81
+ knockEvents++;
82
+ }
83
+ }
84
+
85
+ totalLearningPoints++;
86
+ }
87
+
88
+ float LearningManager::getFuelCorrection(float load, float rpm) {
89
+ return fuelTable.getCorrection(load, rpm);
90
+ }
91
+
92
+ float LearningManager::getIgnitionCorrection(float load, float rpm) {
93
+ return ignitionTable.getCorrection(load, rpm);
94
+ }
95
+
96
+ void LearningManager::setLearningParameters(float minTemp, float maxTemp,
97
+ float minThrottle, float maxThrottle) {
98
+ minEngineTemp = minTemp;
99
+ maxEngineTemp = maxTemp;
100
+ minThrottlePosition = minThrottle;
101
+ maxThrottlePosition = maxThrottle;
102
+ }
103
+
104
+ void LearningManager::setLearningRates(float fuelRate, float ignitionRate) {
105
+ fuelTable.setLearningRate(fuelRate);
106
+ ignitionTable.setLearningRate(ignitionRate);
107
+ }
108
+
109
+ bool LearningManager::save() {
110
+ bool fuelSaved = fuelTable.save();
111
+ bool ignitionSaved = ignitionTable.save();
112
+
113
+ // Сохраняем статистику
114
+ Preferences prefs;
115
+ prefs.begin("learning", false);
116
+ prefs.putUInt("total_points", totalLearningPoints);
117
+ prefs.putUInt("knock_events", knockEvents);
118
+ prefs.putUInt("rich_events", richMixtureEvents);
119
+ prefs.putUInt("lean_events", leanMixtureEvents);
120
+ prefs.end();
121
+
122
+ return fuelSaved && ignitionSaved;
123
+ }
124
+
125
+ bool LearningManager::load() {
126
+ bool fuelLoaded = fuelTable.load();
127
+ bool ignitionLoaded = ignitionTable.load();
128
+
129
+ // Загружаем статистику
130
+ Preferences prefs;
131
+ prefs.begin("learning", true);
132
+ totalLearningPoints = prefs.getUInt("total_points", 0);
133
+ knockEvents = prefs.getUInt("knock_events", 0);
134
+ richMixtureEvents = prefs.getUInt("rich_events", 0);
135
+ leanMixtureEvents = prefs.getUInt("lean_events", 0);
136
+ prefs.end();
137
+
138
+ return fuelLoaded && ignitionLoaded;
139
+ }
140
+
141
+ void LearningManager::reset() {
142
+ fuelTable.reset();
143
+ ignitionTable.reset();
144
+ totalLearningPoints = 0;
145
+ knockEvents = 0;
146
+ richMixtureEvents = 0;
147
+ leanMixtureEvents = 0;
148
+ save();
149
+ }
150
+
151
+ void LearningManager::enable() {
152
+ isLearningEnabled = true;
153
+ learningStartTime = millis();
154
+ }
155
+
156
+ void LearningManager::disable() {
157
+ isLearningEnabled = false;
158
+ }
159
+
160
+ uint32_t LearningManager::getTotalLearningPoints() const {
161
+ return totalLearningPoints;
162
+ }
163
+
164
+ uint32_t LearningManager::getKnockEvents() const {
165
+ return knockEvents;
166
+ }
167
+
168
+ uint32_t LearningManager::getRichMixtureEvents() const {
169
+ return richMixtureEvents;
170
+ }
171
+
172
+ uint32_t LearningManager::getLeanMixtureEvents() const {
173
+ return leanMixtureEvents;
174
+ }
175
+
176
+ float LearningManager::getLearningProgress() const {
177
+ // Максимальное количество точек обучения (16x16 ячеек, минимум 10 точек на ячейку)
178
+ const uint32_t maxPoints = 16 * 16 * 10;
179
+ return (float)totalLearningPoints / maxPoints * 100.0f;
180
+ }
181
+
182
+ String LearningManager::exportToJson() {
183
+ DynamicJsonDocument doc(8192);
184
+
185
+ // Основные параметры
186
+ doc["enabled"] = isLearningEnabled;
187
+ doc["fuel_enabled"] = isFuelLearningEnabled;
188
+ doc["ignition_enabled"] = isIgnitionLearningEnabled;
189
+
190
+ // Статистика
191
+ JsonObject stats = doc.createNestedObject("statistics");
192
+ stats["total_points"] = totalLearningPoints;
193
+ stats["knock_events"] = knockEvents;
194
+ stats["rich_events"] = richMixtureEvents;
195
+ stats["lean_events"] = leanMixtureEvents;
196
+ stats["progress"] = getLearningProgress();
197
+
198
+ // Топливная карта
199
+ JsonArray fuelMap = doc.createNestedArray("fuel_map");
200
+ for (int i = 0; i < 16; i++) {
201
+ JsonArray row = fuelMap.createNestedArray();
202
+ for (int j = 0; j < 16; j++) {
203
+ row.add(fuelTable.getCellValue(i, j));
204
+ }
205
+ }
206
+
207
+ // Карта УОЗ
208
+ JsonArray ignitionMap = doc.createNestedArray("ignition_map");
209
+ for (int i = 0; i < 16; i++) {
210
+ JsonArray row = ignitionMap.createNestedArray();
211
+ for (int j = 0; j < 16; j++) {
212
+ row.add(ignitionTable.getCellValue(i, j));
213
+ }
214
+ }
215
+
216
+ String output;
217
+ serializeJson(doc, output);
218
+ return output;
219
+ }
220
+
221
+ bool LearningManager::importFromJson(const String& json) {
222
+ DynamicJsonDocument doc(8192);
223
+ DeserializationError error = deserializeJson(doc, json);
224
+
225
+ if (error) {
226
+ return false;
227
+ }
228
+
229
+ // Временно отключаем обучение
230
+ bool wasEnabled = isLearningEnabled;
231
+ disable();
232
+
233
+ // Сбрасываем текущие данные
234
+ reset();
235
+
236
+ // Загружаем новые данные
237
+ if (doc.containsKey("enabled")) {
238
+ isLearningEnabled = doc["enabled"];
239
+ }
240
+
241
+ if (doc.containsKey("statistics")) {
242
+ JsonObject stats = doc["statistics"];
243
+ totalLearningPoints = stats["total_points"] | 0;
244
+ knockEvents = stats["knock_events"] | 0;
245
+ richMixtureEvents = stats["rich_events"] | 0;
246
+ leanMixtureEvents = stats["lean_events"] | 0;
247
+ }
248
+
249
+ // Восстанавливаем состояние обучения
250
+ if (wasEnabled) {
251
+ enable();
252
+ }
253
+
254
+ // Сохраняем изменения
255
+ return save();
256
+ }
learning_manager.h ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #ifndef LEARNING_MANAGER_H
2
+ #define LEARNING_MANAGER_H
3
+
4
+ #include "fuel_learning.h"
5
+ #include "ignition_learning.h"
6
+ #include <Preferences.h>
7
+
8
+ class LearningManager {
9
+ private:
10
+ FuelLearningTable fuelTable;
11
+ IgnitionLearningTable ignitionTable;
12
+
13
+ // Параметры обучения
14
+ bool isLearningEnabled;
15
+ bool isFuelLearningEnabled;
16
+ bool isIgnitionLearningEnabled;
17
+ unsigned long learningStartTime;
18
+ unsigned long minLearningTime; // Минимальное время работы для начала обучения
19
+ float minEngineTemp; // Минимальная температура для обучения
20
+ float maxEngineTemp; // Максимальная температура для обучения
21
+ float minThrottlePosition; // Минимальное положение дросселя для обучения
22
+ float maxThrottlePosition; // Максимальное положение дросселя для обучения
23
+
24
+ // Статистика обучения
25
+ uint32_t totalLearningPoints;
26
+ uint32_t knockEvents;
27
+ uint32_t richMixtureEvents;
28
+ uint32_t leanMixtureEvents;
29
+
30
+ // Проверка условий для обучения
31
+ bool checkLearningConditions(float engineTemp, float throttlePosition);
32
+
33
+ public:
34
+ LearningManager();
35
+
36
+ // Инициализация и управление
37
+ void begin();
38
+ void enable();
39
+ void disable();
40
+ void reset();
41
+
42
+ // Основные методы обучения
43
+ void update(float load, float rpm, float lambda, float knockLevel,
44
+ float engineTemp, float throttlePosition);
45
+
46
+ // Получение коррекций
47
+ float getFuelCorrection(float load, float rpm);
48
+ float getIgnitionCorrection(float load, float rpm);
49
+
50
+ // Настройка параметров
51
+ void setLearningParameters(float minTemp, float maxTemp,
52
+ float minThrottle, float maxThrottle);
53
+ void setLearningRates(float fuelRate, float ignitionRate);
54
+
55
+ // Сохранение/загрузка
56
+ bool save();
57
+ bool load();
58
+
59
+ // Статистика и диагностика
60
+ uint32_t getTotalLearningPoints() const;
61
+ uint32_t getKnockEvents() const;
62
+ uint32_t getRichMixtureEvents() const;
63
+ uint32_t getLeanMixtureEvents() const;
64
+ float getLearningProgress() const; // 0-100%
65
+
66
+ // Экспорт данных
67
+ String exportToJson();
68
+ bool importFromJson(const String& json);
69
+ };
70
+
71
+ #endif
maps.cpp ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #include "maps.h"
2
+ #include <Arduino.h>
3
+
4
+ // Базовая карта УОЗ (16x16)
5
+ const float baseIgnitionMap[16][16] = {
6
+ {10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 35, 35, 35}, // 0% нагрузки
7
+ {10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 35, 35, 35},
8
+ {8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 34, 34, 34},
9
+ {8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 33, 33, 33},
10
+ {6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 31, 31, 31},
11
+ {6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 29, 30, 30, 30},
12
+ {4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 27, 28, 28, 28},
13
+ {4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 25, 26, 27, 27, 27},
14
+ {2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 23, 24, 25, 25, 25},
15
+ {2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 21, 22, 23, 24, 24, 24},
16
+ {0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 19, 20, 21, 22, 22, 22},
17
+ {0, 2, 4, 6, 8, 10, 12, 14, 15, 16, 17, 18, 19, 20, 20, 20},
18
+ {0, 2, 4, 6, 8, 10, 11, 12, 13, 14, 15, 16, 17, 18, 18, 18},
19
+ {0, 2, 4, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 16, 16},
20
+ {0, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 14, 14},
21
+ {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 13, 13} // 100% нагрузки
22
+ };
23
+
24
+ // Базовая карта длительности впрыска (16x16)
25
+ const float baseInjectionMap[16][16] = {
26
+ {1.0, 1.2, 1.4, 1.6, 1.8, 2.0, 2.2, 2.4, 2.6, 2.8, 3.0, 3.2, 3.4, 3.6, 3.8, 4.0}, // 0% нагрузки
27
+ {1.2, 1.4, 1.6, 1.8, 2.0, 2.2, 2.4, 2.6, 2.8, 3.0, 3.2, 3.4, 3.6, 3.8, 4.0, 4.2},
28
+ {1.4, 1.6, 1.8, 2.0, 2.2, 2.4, 2.6, 2.8, 3.0, 3.2, 3.4, 3.6, 3.8, 4.0, 4.2, 4.4},
29
+ {1.6, 1.8, 2.0, 2.2, 2.4, 2.6, 2.8, 3.0, 3.2, 3.4, 3.6, 3.8, 4.0, 4.2, 4.4, 4.6},
30
+ {1.8, 2.0, 2.2, 2.4, 2.6, 2.8, 3.0, 3.2, 3.4, 3.6, 3.8, 4.0, 4.2, 4.4, 4.6, 4.8},
31
+ {2.0, 2.2, 2.4, 2.6, 2.8, 3.0, 3.2, 3.4, 3.6, 3.8, 4.0, 4.2, 4.4, 4.6, 4.8, 5.0},
32
+ {2.2, 2.4, 2.6, 2.8, 3.0, 3.2, 3.4, 3.6, 3.8, 4.0, 4.2, 4.4, 4.6, 4.8, 5.0, 5.2},
33
+ {2.4, 2.6, 2.8, 3.0, 3.2, 3.4, 3.6, 3.8, 4.0, 4.2, 4.4, 4.6, 4.8, 5.0, 5.2, 5.4},
34
+ {2.6, 2.8, 3.0, 3.2, 3.4, 3.6, 3.8, 4.0, 4.2, 4.4, 4.6, 4.8, 5.0, 5.2, 5.4, 5.6},
35
+ {2.8, 3.0, 3.2, 3.4, 3.6, 3.8, 4.0, 4.2, 4.4, 4.6, 4.8, 5.0, 5.2, 5.4, 5.6, 5.8},
36
+ {3.0, 3.2, 3.4, 3.6, 3.8, 4.0, 4.2, 4.4, 4.6, 4.8, 5.0, 5.2, 5.4, 5.6, 5.8, 6.0},
37
+ {3.2, 3.4, 3.6, 3.8, 4.0, 4.2, 4.4, 4.6, 4.8, 5.0, 5.2, 5.4, 5.6, 5.8, 6.0, 6.2},
38
+ {3.4, 3.6, 3.8, 4.0, 4.2, 4.4, 4.6, 4.8, 5.0, 5.2, 5.4, 5.6, 5.8, 6.0, 6.2, 6.4},
39
+ {3.6, 3.8, 4.0, 4.2, 4.4, 4.6, 4.8, 5.0, 5.2, 5.4, 5.6, 5.8, 6.0, 6.2, 6.4, 6.6},
40
+ {3.8, 4.0, 4.2, 4.4, 4.6, 4.8, 5.0, 5.2, 5.4, 5.6, 5.8, 6.0, 6.2, 6.4, 6.6, 6.8},
41
+ {4.0, 4.2, 4.4, 4.6, 4.8, 5.0, 5.2, 5.4, 5.6, 5.8, 6.0, 6.2, 6.4, 6.6, 6.8, 7.0} // 100% нагрузки
42
+ };
43
+
44
+ // Вспомогательные функции для преобразования значений
45
+ float mapf(float x, float in_min, float in_max, float out_min, float out_max) {
46
+ if (in_max == in_min) return out_min; // Защита от деления на ноль
47
+ return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
48
+ }
49
+
50
+ float interpolate(float x, float in_min, float in_max, float out_min, float out_max) {
51
+ if (in_max == in_min) return out_min; // Защита от деления на ноль
52
+ return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min;
53
+ }
54
+
55
+ int findNearestIndex(float value, float* array, int size) {
56
+ int idx = 0;
57
+ float minDiff = abs(value - array[0]);
58
+ for (int i = 1; i < size; i++) {
59
+ float diff = abs(value - array[i]);
60
+ if (diff < minDiff) {
61
+ minDiff = diff;
62
+ idx = i;
63
+ }
64
+ }
65
+ return idx;
66
+ }
67
+
68
+ float getBaseIgnition(float load, float rpm) {
69
+ // Находим ближайшие индексы для оборотов и нагрузки
70
+ float rpmMap[] = {0, 500, 1000, 1500, 2000, 2500, 3000, 3500, 4000, 4500, 5000, 5500, 6000, 6500, 7000};
71
+ float mapMap[] = {0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100};
72
+ int rpmIdx = findNearestIndex(rpm, rpmMap, 16);
73
+ int loadIdx = findNearestIndex(load, mapMap, 16);
74
+
75
+ // Получаем значение из карты
76
+ float baseValue = baseIgnitionMap[rpmIdx][loadIdx];
77
+
78
+ // Если нужна интерполяция и мы не на краях карты
79
+ if (rpmIdx < 15 && loadIdx < 15) {
80
+ float rpmFrac = (rpm - rpmMap[rpmIdx]) / (rpmMap[rpmIdx + 1] - rpmMap[rpmIdx]);
81
+ float loadFrac = (load - mapMap[loadIdx]) / (mapMap[loadIdx + 1] - mapMap[loadIdx]);
82
+
83
+ // Билинейная интерполяция
84
+ float v1 = interpolate(rpmFrac, 0, 1, baseIgnitionMap[rpmIdx][loadIdx], baseIgnitionMap[rpmIdx + 1][loadIdx]);
85
+ float v2 = interpolate(rpmFrac, 0, 1, baseIgnitionMap[rpmIdx][loadIdx + 1], baseIgnitionMap[rpmIdx + 1][loadIdx + 1]);
86
+ baseValue = interpolate(loadFrac, 0, 1, v1, v2);
87
+ }
88
+
89
+ return baseValue;
90
+ }
91
+
92
+ float getBaseInjection(float load, float rpm) {
93
+ // Находим ближайшие индексы для оборотов и нагрузки
94
+ float rpmMap[] = {0, 500, 1000, 1500, 2000, 2500, 3000, 3500, 4000, 4500, 5000, 5500, 6000, 6500, 7000};
95
+ float mapMap[] = {0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100};
96
+ int rpmIdx = findNearestIndex(rpm, rpmMap, 16);
97
+ int loadIdx = findNearestIndex(load, mapMap, 16);
98
+
99
+ // Получаем значение из карты
100
+ float baseValue = baseInjectionMap[rpmIdx][loadIdx];
101
+
102
+ // Если нужна интерполяция и мы не на краях карты
103
+ if (rpmIdx < 15 && loadIdx < 15) {
104
+ float rpmFrac = (rpm - rpmMap[rpmIdx]) / (rpmMap[rpmIdx + 1] - rpmMap[rpmIdx]);
105
+ float loadFrac = (load - mapMap[loadIdx]) / (mapMap[loadIdx + 1] - mapMap[loadIdx]);
106
+
107
+ // Билинейная интерполяция
108
+ float v1 = interpolate(rpmFrac, 0, 1, baseInjectionMap[rpmIdx][loadIdx], baseInjectionMap[rpmIdx + 1][loadIdx]);
109
+ float v2 = interpolate(rpmFrac, 0, 1, baseInjectionMap[rpmIdx][loadIdx + 1], baseInjectionMap[rpmIdx + 1][loadIdx + 1]);
110
+ baseValue = interpolate(loadFrac, 0, 1, v1, v2);
111
+ }
112
+
113
+ return baseValue;
114
+ }
maps.h ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #ifndef MAPS_H
2
+ #define MAPS_H
3
+
4
+ // Карта оборотов для УОЗ и впрыска (RPM)
5
+ const float rpmMap[16] = {
6
+ 500, // 0
7
+ 1000, // 1
8
+ 1500, // 2
9
+ 2000, // 3
10
+ 2500, // 4
11
+ 3000, // 5
12
+ 3500, // 6
13
+ 4000, // 7
14
+ 4500, // 8
15
+ 5000, // 9
16
+ 5500, // 10
17
+ 6000, // 11
18
+ 6500, // 12
19
+ 7000, // 13
20
+ 7500, // 14
21
+ 8000 // 15
22
+ };
23
+
24
+ // Карта давления для УОЗ и впрыска (MAP в барах)
25
+ const float mapMap[16] = {
26
+ 0.2, // 0
27
+ 0.3, // 1
28
+ 0.4, // 2
29
+ 0.5, // 3
30
+ 0.6, // 4
31
+ 0.7, // 5
32
+ 0.8, // 6
33
+ 0.9, // 7
34
+ 1.0, // 8
35
+ 1.1, // 9
36
+ 1.2, // 10
37
+ 1.3, // 11
38
+ 1.4, // 12
39
+ 1.5, // 13
40
+ 1.6, // 14
41
+ 1.7 // 15
42
+ };
43
+
44
+ // Карта УОЗ (градусы)
45
+ const float ignitionMap[16][16] = {
46
+ // MAP -> 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1.0 1.1 1.2 1.3 1.4 1.5 1.6 1.7
47
+ {5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20}, // 500 RPM
48
+ {6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21}, // 1000 RPM
49
+ {7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22}, // 1500 RPM
50
+ {8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23}, // 2000 RPM
51
+ {9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24}, // 2500 RPM
52
+ {10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25}, // 3000 RPM
53
+ {11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26}, // 3500 RPM
54
+ {12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27}, // 4000 RPM
55
+ {13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28}, // 4500 RPM
56
+ {14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29}, // 5000 RPM
57
+ {15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30}, // 5500 RPM
58
+ {16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}, // 6000 RPM
59
+ {17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32}, // 6500 RPM
60
+ {18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33}, // 7000 RPM
61
+ {19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34}, // 7500 RPM
62
+ {20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35} // 8000 RPM
63
+ };
64
+
65
+ // Карта впрыска (мс)
66
+ const float injectionMap[16][16] = {
67
+ // MAP -> 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1.0 1.1 1.2 1.3 1.4 1.5 1.6 1.7
68
+ {1.0, 1.2, 1.4, 1.6, 1.8, 2.0, 2.2, 2.4, 2.6, 2.8, 3.0, 3.2, 3.4, 3.6, 3.8, 4.0}, // 500 RPM
69
+ {1.2, 1.4, 1.6, 1.8, 2.0, 2.2, 2.4, 2.6, 2.8, 3.0, 3.2, 3.4, 3.6, 3.8, 4.0, 4.2}, // 1000 RPM
70
+ {1.4, 1.6, 1.8, 2.0, 2.2, 2.4, 2.6, 2.8, 3.0, 3.2, 3.4, 3.6, 3.8, 4.0, 4.2, 4.4}, // 1500 RPM
71
+ {1.6, 1.8, 2.0, 2.2, 2.4, 2.6, 2.8, 3.0, 3.2, 3.4, 3.6, 3.8, 4.0, 4.2, 4.4, 4.6}, // 2000 RPM
72
+ {1.8, 2.0, 2.2, 2.4, 2.6, 2.8, 3.0, 3.2, 3.4, 3.6, 3.8, 4.0, 4.2, 4.4, 4.6, 4.8}, // 2500 RPM
73
+ {2.0, 2.2, 2.4, 2.6, 2.8, 3.0, 3.2, 3.4, 3.6, 3.8, 4.0, 4.2, 4.4, 4.6, 4.8, 5.0}, // 3000 RPM
74
+ {2.2, 2.4, 2.6, 2.8, 3.0, 3.2, 3.4, 3.6, 3.8, 4.0, 4.2, 4.4, 4.6, 4.8, 5.0, 5.2}, // 3500 RPM
75
+ {2.4, 2.6, 2.8, 3.0, 3.2, 3.4, 3.6, 3.8, 4.0, 4.2, 4.4, 4.6, 4.8, 5.0, 5.2, 5.4}, // 4000 RPM
76
+ {2.6, 2.8, 3.0, 3.2, 3.4, 3.6, 3.8, 4.0, 4.2, 4.4, 4.6, 4.8, 5.0, 5.2, 5.4, 5.6}, // 4500 RPM
77
+ {2.8, 3.0, 3.2, 3.4, 3.6, 3.8, 4.0, 4.2, 4.4, 4.6, 4.8, 5.0, 5.2, 5.4, 5.6, 5.8}, // 5000 RPM
78
+ {3.0, 3.2, 3.4, 3.6, 3.8, 4.0, 4.2, 4.4, 4.6, 4.8, 5.0, 5.2, 5.4, 5.6, 5.8, 6.0}, // 5500 RPM
79
+ {3.2, 3.4, 3.6, 3.8, 4.0, 4.2, 4.4, 4.6, 4.8, 5.0, 5.2, 5.4, 5.6, 5.8, 6.0, 6.2}, // 6000 RPM
80
+ {3.4, 3.6, 3.8, 4.0, 4.2, 4.4, 4.6, 4.8, 5.0, 5.2, 5.4, 5.6, 5.8, 6.0, 6.2, 6.4}, // 6500 RPM
81
+ {3.6, 3.8, 4.0, 4.2, 4.4, 4.6, 4.8, 5.0, 5.2, 5.4, 5.6, 5.8, 6.0, 6.2, 6.4, 6.6}, // 7000 RPM
82
+ {3.8, 4.0, 4.2, 4.4, 4.6, 4.8, 5.0, 5.2, 5.4, 5.6, 5.8, 6.0, 6.2, 6.4, 6.6, 6.8}, // 7500 RPM
83
+ {4.0, 4.2, 4.4, 4.6, 4.8, 5.0, 5.2, 5.4, 5.6, 5.8, 6.0, 6.2, 6.4, 6.6, 6.8, 7.0} // 8000 RPM
84
+ };
85
+
86
+ // Функции для работы с картами
87
+ float mapf(float x, float in_min, float in_max, float out_min, float out_max);
88
+ float getBaseIgnition(float load, float rpm);
89
+ float getBaseInjection(float load, float rpm);
90
+
91
+ // Вспомогательные функции для интерполяции
92
+ inline float interpolate(float x, float x1, float x2, float y1, float y2) {
93
+ return y1 + (x - x1) * (y2 - y1) / (x2 - x1);
94
+ }
95
+
96
+ inline int findNearestIndex(float value, const float* array, int size) {
97
+ int index = 0;
98
+ float minDiff = abs(value - array[0]);
99
+
100
+ for(int i = 1; i < size; i++) {
101
+ float diff = abs(value - array[i]);
102
+ if(diff < minDiff) {
103
+ minDiff = diff;
104
+ index = i;
105
+ }
106
+ }
107
+ return index;
108
+ }
109
+
110
+ #endif
pid_controller.h ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #pragma once
2
+ #include "Arduino.h"
3
+
4
+ class PIDController {
5
+ private:
6
+ double kp;
7
+ double ki;
8
+ double kd;
9
+
10
+ double input;
11
+ double output;
12
+ double setpoint;
13
+
14
+ unsigned long lastTime;
15
+ double outputSum;
16
+ double lastInput;
17
+
18
+ double outMin;
19
+ double outMax;
20
+ int sampleTime;
21
+ bool inAuto;
22
+
23
+ public:
24
+ PIDController(double Kp, double Ki, double Kd) :
25
+ kp(Kp),
26
+ ki(Ki),
27
+ kd(Kd),
28
+ input(0),
29
+ output(0),
30
+ setpoint(0),
31
+ lastTime(0),
32
+ outputSum(0),
33
+ lastInput(0),
34
+ outMin(0),
35
+ outMax(255),
36
+ sampleTime(100),
37
+ inAuto(false)
38
+ {
39
+ }
40
+
41
+ bool Compute() {
42
+ if(!inAuto) return false;
43
+
44
+ unsigned long now = millis();
45
+ unsigned long timeChange = (now - lastTime);
46
+ if(timeChange >= sampleTime) {
47
+ double error = setpoint - input;
48
+
49
+ outputSum += (ki * error);
50
+ if(outputSum > outMax) outputSum = outMax;
51
+ else if(outputSum < outMin) outputSum = outMin;
52
+
53
+ double dInput = (input - lastInput);
54
+
55
+ output = kp * error + outputSum - kd * dInput;
56
+
57
+ if(output > outMax) output = outMax;
58
+ else if(output < outMin) output = outMin;
59
+
60
+ lastInput = input;
61
+ lastTime = now;
62
+
63
+ return true;
64
+ }
65
+ return false;
66
+ }
67
+
68
+ void SetTunings(double Kp, double Ki, double Kd) {
69
+ if (Kp < 0 || Ki < 0 || Kd < 0) return;
70
+
71
+ double SampleTimeInSec = ((double)sampleTime)/1000;
72
+ kp = Kp;
73
+ ki = Ki * SampleTimeInSec;
74
+ kd = Kd / SampleTimeInSec;
75
+ }
76
+
77
+ void SetSampleTime(int NewSampleTime) {
78
+ if (NewSampleTime > 0) {
79
+ double ratio = (double)NewSampleTime / (double)sampleTime;
80
+ ki *= ratio;
81
+ kd /= ratio;
82
+ sampleTime = (unsigned long)NewSampleTime;
83
+ }
84
+ }
85
+
86
+ void SetOutputLimits(double Min, double Max) {
87
+ if(Min >= Max) return;
88
+ outMin = Min;
89
+ outMax = Max;
90
+
91
+ if(inAuto) {
92
+ if(output > outMax) output = outMax;
93
+ else if(output < outMin) output = outMin;
94
+
95
+ if(outputSum > outMax) outputSum = outMax;
96
+ else if(outputSum < outMin) outputSum = outMin;
97
+ }
98
+ }
99
+
100
+ void SetMode(bool Mode) {
101
+ bool newAuto = Mode;
102
+ if(newAuto && !inAuto) {
103
+ Initialize();
104
+ }
105
+ inAuto = newAuto;
106
+ }
107
+
108
+ void Initialize() {
109
+ outputSum = output;
110
+ lastInput = input;
111
+ if(outputSum > outMax) outputSum = outMax;
112
+ else if(outputSum < outMin) outputSum = outMin;
113
+ }
114
+
115
+ // Методы для установки и получения значений
116
+ void SetInput(double val) { input = val; }
117
+ void SetSetpoint(double val) { setpoint = val; }
118
+ double GetOutput() const { return output; }
119
+ };
settings_manager.cpp ADDED
@@ -0,0 +1,171 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #include "settings_manager.h"
2
+
3
+ const char* SettingsManager::NAMESPACE = "ecu";
4
+ const char* SettingsManager::SETTINGS_KEY = "settings";
5
+
6
+ SettingsManager::SettingsManager() : version(1) {
7
+ setDefaults();
8
+ }
9
+
10
+ void SettingsManager::setDefaults() {
11
+ settings.version = version;
12
+
13
+ // Настройки РХХ
14
+ settings.idle.targetRPM = 800;
15
+ settings.idle.valveMinPosition = 0;
16
+ settings.idle.valveMaxPosition = 255;
17
+ settings.idle.rpmTolerance = 50;
18
+ settings.idle.kp = 0.1;
19
+ settings.idle.ki = 0.05;
20
+ settings.idle.kd = 0.02;
21
+
22
+ // Общие настройки
23
+ settings.minAdvance = 0;
24
+ settings.maxAdvance = 40;
25
+ settings.injectionTimeMin = 1.0;
26
+ settings.injectionTimeMax = 20.0;
27
+ settings.idleRpmMin = 700;
28
+ settings.idleRpmMax = 900;
29
+ }
30
+
31
+ void SettingsManager::begin() {
32
+ if (!load()) {
33
+ setDefaults();
34
+ save();
35
+ }
36
+ }
37
+
38
+ bool SettingsManager::load() {
39
+ // Здесь должна быть загрузка из EEPROM или SD
40
+ // Пока просто используем значения по умолчанию
41
+ setDefaults();
42
+ return true;
43
+ }
44
+
45
+ bool SettingsManager::save() {
46
+ // Здесь должно быть сохранение в EEPROM или SD
47
+ return true;
48
+ }
49
+
50
+ void SettingsManager::reset() {
51
+ setDefaults();
52
+ save();
53
+ }
54
+
55
+ bool SettingsManager::validateSettings() {
56
+ if (settings.version != version) return false;
57
+
58
+ // Проверка настроек РХХ
59
+ if (settings.idle.targetRPM < 500 || settings.idle.targetRPM > 2000) return false;
60
+ if (settings.idle.valveMinPosition < 0 || settings.idle.valveMinPosition > 255) return false;
61
+ if (settings.idle.valveMaxPosition < 0 || settings.idle.valveMaxPosition > 255) return false;
62
+ if (settings.idle.valveMaxPosition <= settings.idle.valveMinPosition) return false;
63
+ if (settings.idle.rpmTolerance < 10 || settings.idle.rpmTolerance > 200) return false;
64
+ if (settings.idle.kp < 0 || settings.idle.ki < 0 || settings.idle.kd < 0) return false;
65
+
66
+ // Проверка общих настроек
67
+ if (settings.minAdvance < -10 || settings.minAdvance > 20) return false;
68
+ if (settings.maxAdvance < 20 || settings.maxAdvance > 60) return false;
69
+ if (settings.maxAdvance <= settings.minAdvance) return false;
70
+ if (settings.injectionTimeMin < 0.5 || settings.injectionTimeMin > 5.0) return false;
71
+ if (settings.injectionTimeMax < 5.0 || settings.injectionTimeMax > 30.0) return false;
72
+ if (settings.injectionTimeMax <= settings.injectionTimeMin) return false;
73
+ if (settings.idleRpmMin < 500 || settings.idleRpmMin > 1000) return false;
74
+ if (settings.idleRpmMax < 800 || settings.idleRpmMax > 1500) return false;
75
+ if (settings.idleRpmMax <= settings.idleRpmMin) return false;
76
+
77
+ return true;
78
+ }
79
+
80
+ void SettingsManager::setSettings(const SystemSettings& newSettings) {
81
+ settings = newSettings;
82
+ if (validateSettings()) {
83
+ save();
84
+ } else {
85
+ setDefaults();
86
+ }
87
+ }
88
+
89
+ String SettingsManager::exportToJson() {
90
+ StaticJsonDocument<1024> doc;
91
+
92
+ doc["version"] = settings.version;
93
+
94
+ JsonObject idle = doc.createNestedObject("idle");
95
+ idle["targetRPM"] = settings.idle.targetRPM;
96
+ idle["valveMinPosition"] = settings.idle.valveMinPosition;
97
+ idle["valveMaxPosition"] = settings.idle.valveMaxPosition;
98
+ idle["rpmTolerance"] = settings.idle.rpmTolerance;
99
+ idle["kp"] = settings.idle.kp;
100
+ idle["ki"] = settings.idle.ki;
101
+ idle["kd"] = settings.idle.kd;
102
+
103
+ doc["minAdvance"] = settings.minAdvance;
104
+ doc["maxAdvance"] = settings.maxAdvance;
105
+ doc["injectionTimeMin"] = settings.injectionTimeMin;
106
+ doc["injectionTimeMax"] = settings.injectionTimeMax;
107
+ doc["idleRpmMin"] = settings.idleRpmMin;
108
+ doc["idleRpmMax"] = settings.idleRpmMax;
109
+
110
+ String output;
111
+ serializeJson(doc, output);
112
+ return output;
113
+ }
114
+
115
+ bool SettingsManager::importFromJson(const String& json) {
116
+ StaticJsonDocument<1024> doc;
117
+ DeserializationError error = deserializeJson(doc, json);
118
+
119
+ if (error) return false;
120
+
121
+ SystemSettings newSettings;
122
+
123
+ newSettings.version = doc["version"] | version;
124
+
125
+ JsonObject idle = doc["idle"];
126
+ if (idle) {
127
+ newSettings.idle.targetRPM = idle["targetRPM"] | 800;
128
+ newSettings.idle.valveMinPosition = idle["valveMinPosition"] | 0;
129
+ newSettings.idle.valveMaxPosition = idle["valveMaxPosition"] | 255;
130
+ newSettings.idle.rpmTolerance = idle["rpmTolerance"] | 50;
131
+ newSettings.idle.kp = idle["kp"] | 0.1;
132
+ newSettings.idle.ki = idle["ki"] | 0.05;
133
+ newSettings.idle.kd = idle["kd"] | 0.02;
134
+ }
135
+
136
+ newSettings.minAdvance = doc["minAdvance"] | 0;
137
+ newSettings.maxAdvance = doc["maxAdvance"] | 40;
138
+ newSettings.injectionTimeMin = doc["injectionTimeMin"] | 1.0;
139
+ newSettings.injectionTimeMax = doc["injectionTimeMax"] | 20.0;
140
+ newSettings.idleRpmMin = doc["idleRpmMin"] | 700;
141
+ newSettings.idleRpmMax = doc["idleRpmMax"] | 900;
142
+
143
+ setSettings(newSettings);
144
+ return true;
145
+ }
146
+
147
+ bool SettingsManager::saveToSD(const char* filename) {
148
+ File file = SD.open(filename, FILE_WRITE);
149
+ if (!file) return false;
150
+
151
+ String json = exportToJson();
152
+ file.print(json);
153
+ file.close();
154
+
155
+ return true;
156
+ }
157
+
158
+ bool SettingsManager::loadFromSD(const char* filename) {
159
+ File file = SD.open(filename);
160
+ if (!file) return false;
161
+
162
+ String json = file.readString();
163
+ file.close();
164
+
165
+ return importFromJson(json);
166
+ }
167
+
168
+ bool SettingsManager::isCompatibleVersion(uint32_t ver) const {
169
+ // В данной версии поддерживаем только версию 1
170
+ return ver == 1;
171
+ }
settings_manager.h ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #pragma once
2
+ #include "Arduino.h"
3
+ #include "structures.h"
4
+ #include "SD.h"
5
+ #include "ArduinoJson.h"
6
+
7
+ class SettingsManager {
8
+ private:
9
+ SystemSettings settings;
10
+ static const char* NAMESPACE;
11
+ static const char* SETTINGS_KEY;
12
+ uint32_t version;
13
+
14
+ void setDefaults();
15
+ bool validateSettings();
16
+
17
+ public:
18
+ SettingsManager();
19
+ void begin();
20
+ bool load();
21
+ bool save();
22
+ void reset();
23
+
24
+ SystemSettings& getSettings() { return settings; }
25
+ void setSettings(const SystemSettings& newSettings);
26
+
27
+ String exportToJson();
28
+ bool importFromJson(const String& json);
29
+
30
+ bool saveToSD(const char* filename);
31
+ bool loadFromSD(const char* filename);
32
+
33
+ uint32_t getVersion() const { return version; }
34
+ bool isCompatibleVersion(uint32_t version) const;
35
+ };
structures.h ADDED
@@ -0,0 +1,188 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #ifndef STRUCTURES_H
2
+ #define STRUCTURES_H
3
+
4
+ #include <Arduino.h>
5
+ #include <WebServer.h>
6
+ #include <Ticker.h>
7
+ #include <Preferences.h>
8
+
9
+ struct CylinderState {
10
+ unsigned long lastSparkTime;
11
+ bool isInjecting;
12
+ uint8_t pin;
13
+ float advance;
14
+ float detonationLevel;
15
+ };
16
+
17
+ struct EngineError {
18
+ bool detonationError;
19
+ bool temperatureError;
20
+ bool mapError;
21
+ bool lambdaError;
22
+ bool throttleError;
23
+ uint8_t errorCode;
24
+ };
25
+
26
+ struct EngineData {
27
+ float rpm;
28
+ float map;
29
+ float temp;
30
+ float lambda;
31
+ float detonationLevel1;
32
+ float detonationLevel2;
33
+ float combinedDetonationLevel;
34
+ };
35
+
36
+ struct ThrottleConfig {
37
+ float minPosition;
38
+ float maxPosition;
39
+ float idlePosition;
40
+ float speedPulseRatio;
41
+ float ignitionMap[16][16];
42
+ };
43
+
44
+ struct ThrottleLearnData {
45
+ float idlePositions[16];
46
+ uint16_t idleSamples[16];
47
+ float detonationHistory1[10];
48
+ float detonationHistory2[10];
49
+ int detonationHistoryIndex1;
50
+ int detonationHistoryIndex2;
51
+ };
52
+
53
+ struct ThrottleDiagnostics {
54
+ bool isCalibrated;
55
+ bool isStuck;
56
+ bool isOverloaded;
57
+ float current;
58
+ float voltage;
59
+ float detonationCorrection;
60
+ };
61
+
62
+ struct IgnitionMap {
63
+ float advance[16][16];
64
+ float rpmAxis[16];
65
+ float loadAxis[16];
66
+ float detonationMap[16][16];
67
+ float lambdaMap[16][16];
68
+ };
69
+
70
+ struct LearningData {
71
+ float advanceCorrections[16][16];
72
+ uint32_t samples[16][16];
73
+ float detonationHistory[100];
74
+ uint8_t detonationIndex;
75
+ };
76
+
77
+ struct EngineConfig {
78
+ float idleRPM;
79
+ float idleRPMCold;
80
+ float warmupTemp;
81
+ float maxRPM;
82
+ float minRPM;
83
+ float maxAdvance;
84
+ float minAdvance;
85
+ float maxInjection;
86
+ float minInjection;
87
+ float detonationThreshold;
88
+ float learningRate;
89
+ float maxDetonationCorrection;
90
+ };
91
+
92
+ // Структура для хранения конфигурации
93
+ struct ConfigData {
94
+ uint8_t version;
95
+
96
+ // Базовые таблицы
97
+ float fuelTable[16][16]; // Базовая таблица топливоподачи
98
+ float ignitionTable[16][16]; // Базовая таблица УОЗ
99
+
100
+ // Таблицы обучения
101
+ float fuelLearning[16][16]; // Таблица коррекции топливоподачи
102
+ float ignitionLearning[16][16]; // Таблица коррекции УОЗ
103
+
104
+ // Константы впрыска
105
+ float injectionTimeMin;
106
+ float injectionTimeMax;
107
+ float injectionPressure;
108
+ float injectorFlow;
109
+ float injectorDeadtime;
110
+ float injectorVoltage;
111
+ float injectorCorrection;
112
+ float startInjectionTime;
113
+ float startRpmThreshold;
114
+
115
+ // Лямбда
116
+ float lambdaTarget;
117
+ float lambdaCorrectionMin;
118
+ float lambdaCorrectionMax;
119
+ float lambdaCorrectionStep;
120
+
121
+ // Холостой ход
122
+ float idleRpmMin;
123
+ float idleRpmMax;
124
+ float idleAdvanceMin;
125
+ float idleAdvanceMax;
126
+ float idleAdvanceStep;
127
+ float idleAdvanceInterval;
128
+ float idleTargetRpm; // Целевые обороты ХХ
129
+ float idleKp; // Коэффициент P для ПИД
130
+ float idleKi; // Коэффициент I для ПИД
131
+ float idleKd; // Коэффициент D для ПИД
132
+
133
+ // Коррекции
134
+ bool tempCorrectionEnabled;
135
+ bool lambdaCorrectionEnabled;
136
+ bool idleAdvanceLearningEnabled;
137
+ bool throttleAdaptationEnabled;
138
+ bool hotEngineCorrectionEnabled;
139
+ bool accelPumpEnabled;
140
+
141
+ // Зажигание
142
+ String ignitionType;
143
+ int coilCount;
144
+ float sparkDuration;
145
+ float maxAdvance;
146
+ float minAdvance;
147
+ float mechanicalAdvance;
148
+ float dwellTime;
149
+
150
+ // Температура
151
+ float coldStartTemp;
152
+ float hotEngineTemp;
153
+
154
+ uint32_t checksum;
155
+ };
156
+
157
+ // Настройки РХХ
158
+ struct IdleSettings {
159
+ float targetRPM;
160
+ float valveMinPosition;
161
+ float valveMaxPosition;
162
+ float rpmTolerance;
163
+ float kp;
164
+ float ki;
165
+ float kd;
166
+ };
167
+
168
+ // Системные настройки
169
+ struct SystemSettings {
170
+ uint32_t version;
171
+ IdleSettings idle;
172
+ float minAdvance;
173
+ float maxAdvance;
174
+ float injectionTimeMin;
175
+ float injectionTimeMax;
176
+ float idleRpmMin;
177
+ float idleRpmMax;
178
+ };
179
+
180
+ // Данные обучения
181
+ struct LearningData2 {
182
+ float fuelCorrections[16][16];
183
+ float ignitionCorrections[16][16];
184
+ uint32_t cellHits[16][16];
185
+ uint32_t version;
186
+ };
187
+
188
+ #endif
web_interface.h ADDED
@@ -0,0 +1,297 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #ifndef WEB_INTERFACE_H
2
+ #define WEB_INTERFACE_H
3
+
4
+ // HTML page for web interface
5
+ const char* webPage = R"rawliteral(
6
+ <!DOCTYPE html>
7
+ <html>
8
+ <head>
9
+ <meta charset="UTF-8">
10
+ <title>Engine Management System</title>
11
+ <script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
12
+ <style>
13
+ body { font-family: Arial; margin: 20px; }
14
+ .container { max-width: 1200px; margin: 0 auto; }
15
+ .card {
16
+ border: 1px solid #ddd;
17
+ padding: 15px;
18
+ margin: 10px 0;
19
+ border-radius: 5px;
20
+ }
21
+ .value { font-size: 24px; font-weight: bold; }
22
+ .label { font-size: 14px; color: #666; }
23
+ .progress-bar {
24
+ width: 100%;
25
+ height: 20px;
26
+ background: #eee;
27
+ border-radius: 10px;
28
+ overflow: hidden;
29
+ }
30
+ .progress {
31
+ height: 100%;
32
+ background: #4CAF50;
33
+ transition: width 0.3s;
34
+ }
35
+ button {
36
+ padding: 8px 16px;
37
+ margin: 5px;
38
+ border: none;
39
+ border-radius: 4px;
40
+ background: #4CAF50;
41
+ color: white;
42
+ cursor: pointer;
43
+ }
44
+ button:hover {
45
+ background: #45a049;
46
+ }
47
+ input[type="number"] {
48
+ padding: 5px;
49
+ border: 1px solid #ddd;
50
+ border-radius: 4px;
51
+ width: 100px;
52
+ }
53
+ </style>
54
+ </head>
55
+ <body>
56
+ <div class="container">
57
+ <h1>Engine Management System</h1>
58
+
59
+ <div class="card">
60
+ <h2>Real-time Parameters</h2>
61
+ <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px;">
62
+ <div>
63
+ <div class="value" id="rpm">0</div>
64
+ <div class="label">RPM</div>
65
+ </div>
66
+ <div>
67
+ <div class="value" id="map">0</div>
68
+ <div class="label">MAP (kPa)</div>
69
+ </div>
70
+ <div>
71
+ <div class="value" id="tps">0</div>
72
+ <div class="label">TPS (%)</div>
73
+ </div>
74
+ <div>
75
+ <div class="value" id="lambda">0</div>
76
+ <div class="label">Lambda</div>
77
+ </div>
78
+ </div>
79
+ </div>
80
+
81
+ <div class="card">
82
+ <h2>Learning System</h2>
83
+ <div>
84
+ <h3>Learning Progress</h3>
85
+ <div class="progress-bar">
86
+ <div class="progress" id="fuelProgress" style="width: 0%"></div>
87
+ </div>
88
+ <div style="margin-top: 10px;">
89
+ <button onclick="toggleLearning()" id="learningBtn">Enable Learning</button>
90
+ <button onclick="resetLearning()">Reset Learning</button>
91
+ </div>
92
+ <div style="margin-top: 20px;">
93
+ <div>
94
+ <div class="value" id="fuelCorrection">1.00</div>
95
+ <div class="label">Fuel Correction Factor</div>
96
+ </div>
97
+ <div>
98
+ <div class="value" id="ignitionCorrection">0.0</div>
99
+ <div class="label">Ignition Correction (deg)</div>
100
+ </div>
101
+ </div>
102
+ </div>
103
+ </div>
104
+
105
+ <div class="card">
106
+ <h2>Idle Control</h2>
107
+ <div>
108
+ <div>
109
+ <div class="value" id="idlePosition">0</div>
110
+ <div class="label">Idle Valve Position (%)</div>
111
+ </div>
112
+ <div>
113
+ <div class="value" id="targetRPM">800</div>
114
+ <div class="label">Target RPM</div>
115
+ </div>
116
+ <div style="margin-top: 10px;">
117
+ <input type="number" id="rpmInput" value="800" min="600" max="2000">
118
+ <button onclick="setTargetRPM()">Set RPM</button>
119
+ <button onclick="calibrateIdle()">Calibrate</button>
120
+ </div>
121
+ </div>
122
+ </div>
123
+
124
+ <div class="card">
125
+ <h2>Diagnostics</h2>
126
+ <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px;">
127
+ <div>
128
+ <div class="value" id="knockEvents">0</div>
129
+ <div class="label">Knock Events</div>
130
+ </div>
131
+ <div>
132
+ <div class="value" id="engineTemp">0</div>
133
+ <div class="label">Engine Temp (C)</div>
134
+ </div>
135
+ <div>
136
+ <div class="value" id="batteryVoltage">0</div>
137
+ <div class="label">Battery Voltage (V)</div>
138
+ </div>
139
+ </div>
140
+ </div>
141
+
142
+ <div class="card">
143
+ <h2>Settings</h2>
144
+ <div>
145
+ <button onclick="saveSettings()">Save Settings</button>
146
+ <button onclick="loadSettings()">Load Settings</button>
147
+ <button onclick="exportSettings()">Export Settings</button>
148
+ <button onclick="importSettings()">Import Settings</button>
149
+ </div>
150
+ </div>
151
+ </div>
152
+
153
+ <script>
154
+ var rpmData = {
155
+ x: [],
156
+ y: [],
157
+ type: "scatter",
158
+ name: "RPM"
159
+ };
160
+
161
+ var mapData = {
162
+ x: [],
163
+ y: [],
164
+ type: "scatter",
165
+ name: "MAP"
166
+ };
167
+
168
+ Plotly.newPlot("rpmChart", [rpmData], {
169
+ title: "RPM History",
170
+ xaxis: { title: "Time" },
171
+ yaxis: { title: "RPM" }
172
+ });
173
+
174
+ Plotly.newPlot("mapChart", [mapData], {
175
+ title: "MAP History",
176
+ xaxis: { title: "Time" },
177
+ yaxis: { title: "kPa" }
178
+ });
179
+
180
+ function updateData() {
181
+ fetch("/status")
182
+ .then(response => response.json())
183
+ .then(data => {
184
+ document.getElementById("rpm").textContent = Math.round(data.rpm);
185
+ document.getElementById("map").textContent = data.map.toFixed(1);
186
+ document.getElementById("tps").textContent = data.tps.toFixed(1);
187
+ document.getElementById("lambda").textContent = data.lambda.toFixed(2);
188
+
189
+ document.getElementById("fuelProgress").style.width = data.learning_progress + "%";
190
+ document.getElementById("fuelCorrection").textContent = data.fuel_correction.toFixed(2);
191
+ document.getElementById("ignitionCorrection").textContent = data.ignition_correction.toFixed(1);
192
+
193
+ document.getElementById("idlePosition").textContent = data.idle_position.toFixed(1);
194
+ document.getElementById("targetRPM").textContent = data.target_rpm;
195
+
196
+ document.getElementById("knockEvents").textContent = data.knock_events;
197
+ document.getElementById("engineTemp").textContent = data.temp.toFixed(1);
198
+ document.getElementById("batteryVoltage").textContent = data.voltage.toFixed(1);
199
+
200
+ const now = new Date();
201
+ Plotly.extendTraces("rpmChart", {
202
+ x: [[now]],
203
+ y: [[data.rpm]]
204
+ }, [0]);
205
+
206
+ Plotly.extendTraces("mapChart", {
207
+ x: [[now]],
208
+ y: [[data.map]]
209
+ }, [0]);
210
+ });
211
+ }
212
+
213
+ function toggleLearning() {
214
+ fetch("/learning/toggle", { method: "POST" })
215
+ .then(response => response.json())
216
+ .then(data => {
217
+ alert(data.enabled ? "Learning enabled" : "Learning disabled");
218
+ });
219
+ }
220
+
221
+ function resetLearning() {
222
+ if (confirm("Are you sure you want to reset learning data?")) {
223
+ fetch("/learning/reset", { method: "POST" })
224
+ .then(response => response.json())
225
+ .then(() => {
226
+ alert("Learning data reset");
227
+ });
228
+ }
229
+ }
230
+
231
+ function setTargetRPM() {
232
+ const rpm = document.getElementById("rpmInput").value;
233
+ fetch("/idle/rpm", {
234
+ method: "POST",
235
+ headers: {
236
+ "Content-Type": "application/json",
237
+ },
238
+ body: JSON.stringify({ rpm: parseInt(rpm) })
239
+ });
240
+ }
241
+
242
+ function calibrateIdle() {
243
+ if (confirm("Start idle calibration?")) {
244
+ fetch("/idle/calibrate", { method: "POST" })
245
+ .then(response => response.json())
246
+ .then(() => {
247
+ alert("Calibration complete");
248
+ });
249
+ }
250
+ }
251
+
252
+ function saveSettings() {
253
+ fetch("/settings/save", { method: "POST" })
254
+ .then(response => response.json())
255
+ .then(() => {
256
+ alert("Settings saved");
257
+ });
258
+ }
259
+
260
+ function loadSettings() {
261
+ fetch("/settings/load")
262
+ .then(response => response.json())
263
+ .then(() => {
264
+ alert("Settings loaded");
265
+ });
266
+ }
267
+
268
+ function exportSettings() {
269
+ window.location.href = "/settings/export";
270
+ }
271
+
272
+ function importSettings() {
273
+ const input = document.createElement("input");
274
+ input.type = "file";
275
+ input.onchange = function(e) {
276
+ const file = e.target.files[0];
277
+ const formData = new FormData();
278
+ formData.append("settings", file);
279
+ fetch("/settings/import", {
280
+ method: "POST",
281
+ body: formData
282
+ })
283
+ .then(response => response.json())
284
+ .then(() => {
285
+ alert("Settings imported");
286
+ });
287
+ };
288
+ input.click();
289
+ }
290
+
291
+ setInterval(updateData, 1000);
292
+ </script>
293
+ </body>
294
+ </html>
295
+ )rawliteral";
296
+
297
+ #endif