#!/usr/bin/python3
import os
import sys
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.widgets import TextBox, Button, RadioButtons
print(sys.version)
# Получаем путь к директории скрипта
if getattr(sys, 'frozen', False):
script_dir = os.path.dirname(sys.executable)
else:
script_dir = os.path.dirname(os.path.abspath(__file__))
# Глобальные переменные
max_rpm_limit = 10000 # По умолчанию
max_deg_limit = 40
skip = 5 # Сколько циклов пропустить перед установкой угла опережения
in_pin = 4 # Пин датчика по умолчанию
out_pin = 5 # Пин катушки по умолчанию
led_pin = 2 # Пин LED по умолчанию
data_points = [(0, 10), (max_rpm_limit, 10)] # Базовая линия на 10 градусов
selected_point = None
protection = True # Помехозащита
# Глобальные переменные для режимов
modes = {
'inverse': True,
'forward': False,
'plate': False,
'cutout': False
}
# Создаем функцию для вывода справки в консоль
def show_help(event):
from textwrap import dedent
print(dedent("""
======================== СПРАВКА ========================
| |
| Управление графиком: |
| - ЛКМ: перетаскивание точек |
| - ПКМ: добавление новых точек |
| |
| Настройки: |
| - Max RPM: настраиваемый диапазон оборотов |
| - Max Degree: настраиваемый диапазон углов |
| - SKIP: количество первых оборотов без опережения, |
| нужно для стабилизации алгоритма |
| - IN/OUT/LED: цифровые пины Arduino/ESP/Attiny, |
| если LED отсутствует на плате или неизвестен |
| выставить тот же пин что и OUT |
| - Режимы: Инверсный/Прямой/Пластина/С вырезом |
| - Помехозащита: добавление защитных задержек |
| |
| Генерация кода: |
| - Нажмите 'Создать скетч' для генерации |
| - Файл сохраняется рядом со скриптом |
| |
=========================================================
"""))
# Интерполяция значений с шагом 50 RPM
def interpolate_data():
if len(data_points) < 2:
return []
data_points.sort()
interpolated = []
for i in range(len(data_points) - 1):
x1, y1 = data_points[i]
x2, y2 = data_points[i + 1]
rpm_range = np.arange(x1, x2 + 1, 50) # Каждые 50 RPM
degree_range = np.interp(rpm_range, [x1, x2], [y1, y2])
interpolated.extend(list(zip(rpm_range, degree_range)))
return interpolated
# Группировка диапазонов
def group_intervals(data):
if not data:
return [(0, 999999, 10)]
grouped = []
start_rpm, prev_degree = data[0]
for i in range(1, len(data)):
rpm, degree = data[i]
if int(degree) != int(prev_degree): # Если угол изменился, фиксируем диапазон
grouped.append((start_rpm, rpm, int(prev_degree)))
start_rpm = rpm
prev_degree = degree
grouped.append((start_rpm, 999999, int(prev_degree))) # Последний диапазон
return grouped
# Обновление графика
def update_plot():
ax.clear()
ax.set_title("График углов опережения зажигания")
ax.set_xlabel("Обороты двигателя (RPM)")
ax.set_ylabel("Угол опережения (градусы)")
ax.set_ylim(0, max_deg_limit)
ax.set_xlim(0, max_rpm_limit + 100)
ax.set_xticks(range(0, max_rpm_limit + 1, 1000))
ax.grid()
# Получаем интерполированные точки
interpolated_data = interpolate_data()
if interpolated_data:
rpm_values = [p[0] for p in interpolated_data]
degree_values = [p[1] for p in interpolated_data]
ax.plot(rpm_values, degree_values, linestyle='-', color='blue', alpha=0.7, label="Редактируемый график")
# Отображаем только реальные точки
rpm_values = [p[0] for p in data_points]
degree_values = [p[1] for p in data_points]
ax.scatter(rpm_values, degree_values, color='b')
ax.legend()
plt.draw()
# Обработчик клика мыши
def on_click(event):
global selected_point
if event.inaxes is not None:
x, y = int(event.xdata), int(event.ydata)
if event.button == 1: # Левая кнопка - редактирование точки
distances = [((x - p[0])**2 + (y - p[1])**2, i) for i, p in enumerate(data_points)]
if distances:
min_distance, index = min(distances)
if min_distance < 10000:
selected_point = index
return
if event.button == 3: # Правая кнопка - добавление точки
if 0 < x < max_rpm_limit and 0 <= y <= max_deg_limit:
data_points.append((x, y))
data_points.sort()
update_plot()
# Обработчик перемещения мыши
def on_motion(event):
global selected_point
if selected_point is not None and event.inaxes is not None:
y = max(0, min(max_deg_limit, int(event.ydata)))
data_points[selected_point] = (data_points[selected_point][0], y)
update_plot()
# Обработчик отпускания кнопки мыши
def on_release(event):
global selected_point
selected_point = None
# Обновление max_rpm_limit
def update_rpm_limit(text):
global max_rpm_limit
try:
new_limit = int(text)
if 1000 <= new_limit <= 25000:
max_rpm_limit = new_limit
data_points[-1] = (max_rpm_limit, data_points[-1][1]) # Перемещение правой границы
update_plot()
except ValueError:
pass
# Обновление max_deg_limit
def update_deg_limit(text):
global max_deg_limit
try:
new_limit = int(text)
if 10 <= new_limit <= 120:
max_deg_limit = new_limit
update_plot()
except ValueError:
pass
# Обновление skip
def update_skip(text):
global skip
try:
skip = int(text)
except ValueError:
pass
# Обновление pin значений
def update_in_pin(text):
global in_pin
try:
in_pin = int(text)
except ValueError:
pass
def update_out_pin(text):
global out_pin
try:
out_pin = int(text)
except ValueError:
pass
def update_led_pin(text):
global led_pin
try:
led_pin = int(text)
except ValueError:
pass
# Обработчик изменения радиокнопки
def mode_changed(label):
global modes
# Сбрасываем все режимы
for mode in modes:
modes[mode] = False
# Устанавливаем текущий режим
if label == 'Инверсный':
modes['inverse'] = True
elif label == 'Прямой':
modes['forward'] = True
elif label == 'Пластина':
modes['plate'] = True
elif label == 'С вырезом':
modes['cutout'] = True
print(f"Активный режим: {label}")
# Формируем строку с названием режима
mode_names = {
'inverse': 'Инверсный',
'forward': 'Прямой',
'plate': 'Пластина',
'cutout': 'С вырезом'
}
# Обработчик изменения радиокнопки
def protection_changed(label):
global protection
protection = (label == 'Помехозащита') # Обратите внимание на "щ" в слове
print(f"Режим изменен на: {'Помехозащита' if protection else 'Нет'}")
# Генерация полного скетча Arduino
def generate_sketch():
grouped = group_intervals(interpolate_data())
# Определяем текущий активный режим при каждом вызове функции
current_mode = next((mode for mode, active in modes.items() if active), None)
mode_display = f"{mode_names.get(current_mode, 'Неизвестно')}"
# Условия для разных режимов
if modes['inverse']:
rise_condition = """
/**
* Проверяет, если датчик переключился с низкого уровня на высокий.
* Это может означать, что сигнал от датчика положения поступил.
*/
if (detector == HIGH && oldDetector == LOW) {"""
fall_condition = """
/**
* Проверяет, если датчик переключился с высокого уровня на низкий.
* Это может означать, что сигнал от датчика завершился.
*/
if (detector == LOW && oldDetector == HIGH) {"""
out_pin_0 = """controlCoil(false); // Выключаем выходной пин"""
out_pin_1 = """controlCoil(true); // Включаем выходной пин"""
out_pin_2 = """controlCoil(false); // Выключаем выходной пин"""
out_pin_3 = """controlCoil(false); // Выключаем выходной пин"""
elif modes['forward']:
rise_condition = """
/**
* Проверяет, если датчик переключился с высокого уровня на низкий.
* Это может означать, что сигнал от датчика положения поступил.
*/
if (detector == LOW && oldDetector == HIGH) {"""
fall_condition = """
/**
* Проверяет, если датчик переключился с низкого уровня на высокий.
* Это может означать, что сигнал от датчика завершился.
*/
if (detector == HIGH && oldDetector == LOW) {"""
out_pin_0 = """controlCoil(false); // Выключаем выходной пин"""
out_pin_1 = """controlCoil(true); // Включаем выходной пин"""
out_pin_2 = """controlCoil(false); // Выключаем выходной пин"""
out_pin_3 = """controlCoil(false); // Выключаем выходной пин"""
elif modes['plate']:
rise_condition = """
/**
* Проверяет, если датчик переключился с низкого уровня на высокий.
* Это может означать, что сигнал от датчика положения поступил.
*/
if (detector == HIGH && oldDetector == LOW) {"""
fall_condition = """
/**
* Проверяет, если датчик переключился с высокого уровня на низкий.
* Это может означать, что сигнал от датчика завершился.
*/
if (detector == LOW && oldDetector == HIGH) {"""
out_pin_0 = """controlCoil(true); // Включаем выходной пин"""
out_pin_1 = """controlCoil(false); // Выключаем выходной пин"""
out_pin_2 = """controlCoil(true); // Включаем выходной пин"""
out_pin_3 = """controlCoil(true); // Включаем выходной пин"""
elif modes['cutout']:
rise_condition = """
/**
* Проверяет, если датчик переключился с высокого уровня на низкий.
* Это может означать, что сигнал от датчика положения поступил.
*/
if (detector == LOW && oldDetector == HIGH) {"""
fall_condition = """
/**
* Проверяет, если датчик переключился с низкого уровня на высокий.
* Это может означать, что сигнал от датчика завершился.
*/
if (detector == HIGH && oldDetector == LOW) {"""
out_pin_0 = """controlCoil(true); // Включаем выходной пин"""
out_pin_1 = """controlCoil(false); // Выключаем выходной пин"""
out_pin_2 = """controlCoil(true); // Включаем выходной пин"""
out_pin_3 = """controlCoil(true); // Включаем выходной пин"""
if protection:
protect = """ delayMicroseconds(periodTime/5); // Для помехоустойчивости"""
else:
protect = """ //delayMicroseconds(periodTime/5); // Для помехоустойчивости"""
sketch = f"""/*
* Скетч управления зажиганием с опережением
* Режим работы: {mode_display}
* Сгенерировано автоматически на основе кривой зажигания
*/
#define IN {in_pin} // Входной пин для подключения датчика
#define OUT {out_pin} // Выходной пин для управления катушкой зажигания
#define LED {led_pin} // Пин встроенного светодиода если есть.
const int SKIP = {skip}; // Количество первых циклов, в которых нужно пропустить установку угла опережения
const long FUSE_TIMER = 1000000; // Длительность зарадки катушки зажигания не более 1 секунды в микросекундах
// Переменные для логики зажигания
bool detector = false, // Текущее состояние датчика положения (шторка открыта/закрыта)
oldDetector = false, // Предыдущее состояние датчика положения
advanceGranted = false; // Флаг, разрешающий выполнение опережения зажигания
// Переменные для расчёта времени в микросекундах
unsigned long oldTime = 0, // Время предыдущего цикла (для расчёта периода)
midTime = 0, // Время начала зарядки катушки
periodTime = 0, // Период одного полного оборота вала двигателя
chargeTime = 0, // Время, в течение которого катушка заряжается
ignitionAdvance = 0, // Время опережения зажигания (вычисляется на основе оборотов rpm)
rpm = 0; // Текущие обороты двигателя (об/мин)
int ignitionDegree = 0, // Угол опережения зажигания в градусах
skipImpulses = 0;
// Таблица углов опережения зажигания
const struct {{
long minRPM, maxRPM;
int ignitionDegree;
}} rpmTable[] = {{
"""
# Добавляем таблицу
for min_rpm, max_rpm, degree in grouped:
sketch += f" {{{min_rpm}, {max_rpm}, {degree}}},\n"
sketch += """};
void setup() {
Serial.begin(115200);
pinMode(IN, INPUT);
pinMode(OUT, OUTPUT);
pinMode(LED, OUTPUT);
oldTime = micros();
}
/**
* Считывает параметры цикла: период, обороты и время зарядки катушки.
* Используется для анализа времени полного оборота вала двигателя.
*/
void readAll() {
unsigned long newTime = micros(); // Текущее время в микросекундах
periodTime = newTime - oldTime; // Вычисляем период вращения (время одного полного оборота)
chargeTime = newTime - midTime; // Вычисляем время зарядки катушки
rpm = 60000000 / periodTime; // Расчёт оборотов двигателя (об/мин)
oldTime = newTime; // Обновляем старое время для следующего цикла
}
/**
* Возвращает угол опережения зажигания на основе текущих оборотов двигателя.
* Использует карту зажигания (таблицу соответствия оборотов и углов).
*/
int getIgnitionDegree(long rpm) {
// Поиск угла опережения на основе оборотов из таблицы
for (int i = 0; i < sizeof(rpmTable) / sizeof(rpmTable[0]); ++i) {
if (rpm >= rpmTable[i].minRPM && rpm < rpmTable[i].maxRPM) {
return rpmTable[i].ignitionDegree; // Возвращаем угол для данного диапазона оборотов
}
}
return 0; // Если обороты не входят в диапазон таблицы, возвращаем 0
}
/**
* Вычисляет время опережения зажигания, основываясь на угле опережения и текущем периоде вращения.
* Используется для управления моментом искрообразования.
*/
void setIgnition() {
ignitionDegree = getIgnitionDegree(rpm); // Получаем угол опережения из таблицы
ignitionAdvance = chargeTime - ((periodTime / 360) * ignitionDegree); // Расчёт времени опережения
}
/**
* Управляет состоянием выходного пина (включение или выключение).
*/
void controlCoil(bool state) {
if (state) {
digitalWrite(OUT, HIGH);
digitalWrite(LED, HIGH);
} else {
digitalWrite(OUT, LOW);
digitalWrite(LED, LOW);
}
}
void loop() {
detector = digitalRead(IN); // Считываем текущее состояние датчика положения
"""
sketch += f" {rise_condition}\n"
sketch += f" {out_pin_0}"
sketch += """
oldDetector = detector; // Обновляем предыдущее состояние датчика
readAll(); // Считываем параметры цикла
setIgnition(); // Установка угла опережения
"""
sketch += f" {protect}"
sketch += """
return; //
}
"""
sketch += f" {fall_condition}\n"
sketch += f" {out_pin_1}"
sketch += """
oldDetector = detector; // Обновляем предыдущее состояние датчика
midTime = micros(); // Фиксируем время начала зарядки
if (skipImpulses < SKIP) {
skipImpulses++;
return; // Пропускаем обработку первых импульсов
}
advanceGranted = true; // Разрешаем выполнять опережение зажигания
"""
sketch += f" {protect}"
sketch += """
return; //
}
/**
* Проверяет, прошло ли достаточно времени для отключения катушки зажигания
* с учётом установленного угла опережения зажигания.
* Если время прошло и блок опережения разрешён, катушка выключается.
*/
if (micros() - midTime >= ignitionAdvance && advanceGranted) {
"""
sketch += f" {out_pin_2}"
sketch += """
advanceGranted = false; // Сбрасываем флаг опережения
}
/**
* Когда двигатель простаивает но катушка находится в состоянии зарядки более 1 сек.
* Данный блок реализует отключение катушки после секунды простоя,
* А так же переводит состояние алгоритма опережения в предстартовое
*/
if (micros() - midTime >= FUSE_TIMER) { // Контроль длительности зарядки катушки (не более 1 секунды)
"""
sketch += f" {out_pin_3}"
sketch += """
skipImpulses = 0; // переход в режим предстарта
}
}
"""
return sketch
# Построение UI
fig, ax = plt.subplots()
fig.set_size_inches(10, 6) # Ширина 12 дюймов, высота 7 дюймов
# Настройки макета
plt.subplots_adjust(
left=0.1,
right=0.95,
bottom=0.34, # Увеличили отступ снизу для элементов управления
top=0.9
)
update_plot()
# Создаем область для текстовых полей
axbox_rpm = plt.axes([0.14, 0.2, 0.06, 0.05])
axbox_deg = plt.axes([0.14, 0.135, 0.06, 0.05])
axbox_skip = plt.axes([0.25, 0.2, 0.04, 0.05])
axbox_in = plt.axes([0.325, 0.2, 0.03, 0.05])
axbox_out = plt.axes([0.405, 0.2, 0.03, 0.05])
axbox_led = plt.axes([0.48, 0.2, 0.03, 0.05])
axradio_mode = plt.axes([0.53, 0.15, 0.125, 0.11]) # x,y,width,height
axradio_protection = plt.axes([0.665, 0.2, 0.15, 0.05]) # Для помехозащиты
axbutton = plt.axes([0.83, 0.2, 0.13, 0.05])
ax_help = plt.axes([0.92, 0.02, 0.06, 0.06]) # Правая нижняя часть
# Варианты режимов
mode_options = ['Инверсный', 'Прямой', 'Пластина', 'С вырезом']
# Создаем текстовые поля
text_box_rpm = TextBox(axbox_rpm, 'Max RPM: ', initial=str(max_rpm_limit))
text_box_deg = TextBox(axbox_deg, 'Max Degree: ', initial=str(max_deg_limit))
text_box_skip = TextBox(axbox_skip, 'SKIP: ', initial=str(skip))
text_box_in = TextBox(axbox_in, 'IN: ', initial=str(in_pin))
text_box_out = TextBox(axbox_out, 'OUT: ', initial=str(out_pin))
text_box_led = TextBox(axbox_led, 'LED: ', initial=str(led_pin))
generate_button = Button(axbutton, 'Создать скетч')
# кнопка help
help_button = Button(ax_help, '?',
color='white')
#ax_help.set_title('Help', fontsize=8, pad=2)
# Создаем радиокнопку с двумя вариантами
radio_mode = RadioButtons(axradio_mode, mode_options, active=0)
for label in radio_mode.labels:
label.set_fontsize(9) # Чуть меньший шрифт
# Создаем радиокнопку с двумя вариантами
radio_protection = RadioButtons(axradio_protection, ('Нет', 'Помехозащита'), active=1 if protection else 0)
radio_protection.labels[0].set_fontsize(9) # Размер шрифта для первого варианта
radio_protection.labels[1].set_fontsize(9) # Размер шрифта для второго варианта
radio_protection.set_active(1 if protection else 0)
# Центрирование текста (как в предыдущем решении)
for text_box in [text_box_rpm, text_box_deg, text_box_skip, text_box_in, text_box_out, text_box_led]:
text_box.text_disp.set_horizontalalignment('center')
text_box.text_disp.set_position((0.5, 0.5))
text_box.text_disp.set_verticalalignment('center')
# Подключаем обработчики
text_box_rpm.on_submit(update_rpm_limit)
text_box_deg.on_submit(update_deg_limit)
text_box_skip.on_submit(update_skip)
text_box_in.on_submit(update_in_pin)
text_box_out.on_submit(update_out_pin)
text_box_led.on_submit(update_led_pin)
radio_mode.on_clicked(mode_changed)
radio_protection.on_clicked(protection_changed)
help_button.on_clicked(show_help)
def on_generate(event):
sketch = generate_sketch()
try:
# Формируем полный путь к файлу
save_path = os.path.join(script_dir, "ignition_controller.ino")
with open(save_path, "w", encoding='utf-8') as f:
f.write(sketch)
print(f"Скетч успешно сохранён в: {save_path}")
except Exception as e:
print(f"Ошибка сохранения: {str(e)}")
# Попробуем альтернативный путь на рабочем столе
try:
desktop = os.path.join(os.path.expanduser("~"), "Desktop")
save_path = os.path.join(desktop, "ignition_controller.ino")
with open(save_path, "w", encoding='utf-8') as f:
f.write(sketch)
print(f"Сохранено на рабочий стол: {save_path}")
except Exception as e2:
print(f"Не удалось сохранить даже на рабочий стол: {str(e2)}")
generate_button.on_clicked(on_generate)
# Подключение событий мыши
fig.canvas.mpl_connect('button_press_event', on_click)
fig.canvas.mpl_connect('motion_notify_event', on_motion)
fig.canvas.mpl_connect('button_release_event', on_release)
plt.show()