ARDUINO Блок опережения зажигания ДВС

user314

★★✩✩✩✩✩
26 Апр 2022
51
53
@ukr100, у меня на самом деле стоит IGBTв корпусе to-220, марку не помню, поставил тот который был. По характеристикам они все высокотоковые и высоковольтные.
Если бы были mosfet, я бы остановился на каком-то высковольтном, например irf540, irf640 или на худой конец irf840, так, чтобы встроенный стабилитрон не гасил всплеск на катушке зажигания, а он там приличный должен быть, чтобы была искра.
 
Изменено:
  • Лойс +1
Реакции: ukr100

user314

★★✩✩✩✩✩
26 Апр 2022
51
53

Оставлю это здесь, чтобы не потерялось. В видео по ссылке рассказ о том, как настраивать УОЗ в двухтактном двигателе.
В свою очередь я наваял python скрипт, который генерирует таблицу УОЗ для скетча, пока скрипт генерит только таблицу а не весь скетч!


Скрипт генерации таблицы ФУОЗ:
#!/usr/bin/python3

import matplotlib.pyplot as plt
import numpy as np
from matplotlib.widgets import TextBox

# Глобальные переменные
max_rpm_limit = 10000  # По умолчанию
data_points = [(0, 10), (max_rpm_limit, 10)]  # Базовая линия на 10 градусов
selected_point = None

# Интерполяция значений с шагом 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, 40)
    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 <= 40:
                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(40, 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

# Генерация таблицы
def generate_table():
    grouped = group_intervals(interpolate_data())
    table = "// Таблица углов опережения зажигания\nconst struct {\n  int minRPM, maxRPM, ignitionDegree;\n} rpmTable[] = {\n"

    for min_rpm, max_rpm, degree in grouped:
        table += f"  {{{min_rpm}, {max_rpm}, {degree}}},\n"

    table += "};\n"
    return table

# Построение графика
fig, ax = plt.subplots()
plt.subplots_adjust(bottom=0.2)
update_plot()

# Поле для max_rpm_limit
axbox = plt.axes([0.15, 0.05, 0.2, 0.05])
text_box = TextBox(axbox, "Max RPM: ", initial=str(max_rpm_limit))
text_box.on_submit(update_rpm_limit)

# Подключение событий мыши
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()

# Вывод таблицы
print(generate_table())
И пример результата работы этого скрипта:
2025-03-03_12_15_20.png

C++:
// Таблица углов опережения зажигания
const struct {
  int minRPM, maxRPM, ignitionDegree;
} rpmTable[] = {
  {0, 150, 0},
  {150, 250, 1},
  {250, 400, 2},
  {400, 483, 3},
  {483, 583, 4},
  {583, 683, 5},
  {683, 783, 6},
  {783, 833, 7},
  {833, 933, 8},
  {933, 1001, 9},
  {1001, 2110, 10},
  {2110, 2160, 11},
  {2160, 2260, 12},
  {2260, 2310, 13},
  {2310, 2410, 14},
  {2410, 2460, 15},
  {2460, 2560, 16},
  {2560, 2610, 17},
  {2610, 2660, 18},
  {2660, 2760, 19},
  {2760, 2810, 20},
  {2810, 2910, 21},
  {2910, 2960, 22},
  {2960, 3019, 23},
  {3019, 3069, 24},
  {3069, 3269, 23},
  {3269, 3469, 22},
  {3469, 3669, 21},
  {3669, 3869, 20},
  {3869, 4119, 19},
  {4119, 4319, 18},
  {4319, 4519, 17},
  {4519, 4719, 16},
  {4719, 4969, 15},
  {4969, 5169, 14},
  {5169, 5369, 13},
  {5369, 5569, 12},
  {5569, 5819, 11},
  {5819, 999999, 10},
};
Это предварительная версия скрипта, запускал только в среде Linux, как запускать в винде пока-что понятия не имею.

Так же у меня есть доработанный скетч от customcult, но с применением этой таблицы УОЗ вместо динамической формулы. Но публиковать буду, когда начнётся сезон и появится возможность тестировать, иначе нет смысла.
 
Изменено:
  • Лойс +1
Реакции: UJV 5901

BOT_Zilla

★✩✩✩✩✩✩
1 Апр 2022
21
13
В Windows скрипты python можно запускать через встроенный терминал Power Shell. Пробовал Ваш скрипт - работает, только нужно предварительно подтянуть нужные библиотеки. И установить сам Python, конечно же, если его нет (через этот же терминал).
 

nikolau777

✩✩✩✩✩✩✩
26 Апр 2025
1
1
Всем привет я начинающий во всех этих делах. Подскажите пожалуста а можно сделать трех канальную систему . То есть вход с трех датчиков хола выход на три камутатора. Хочу сделать на трех цилиндровый гидроцикл двух такник . Пробывал на дигиспарк делать как на сайте кастом но почему-то не работает делал на каждый цилиндор по дигиспарк. При запуске они завесают и не каких сигналов не даю почетал ваши посты понял что можно на ардуинки нано сделать но интересно можно сделать сразу на три канала на одной или придется ставить три ардуинки. И если можно как скейч переделать под три канала график уоз подойдет как на фото выше только вместо 24 градусов 28 и с трех 3000 до 4000 идет 28 а потом также плавно спускается до 6000 спасибо всем кто ответит.

Сколько искал все делают один или два канала а три не нашел.
 

Вложения

  • Лойс +1
Реакции: UJV 5901

user314

★★✩✩✩✩✩
26 Апр 2022
51
53
Итак, представляю python3 скрипт, который умеет генерировать скетч с разными параметрами:
2025-05-01_15_24_18.png


Python:
#!/usr/bin/python3

import os
import sys
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.widgets import TextBox, Button, RadioButtons

# Получаем путь к директории скрипта
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  # По умолчанию
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: настраиваемый диапазон оборотов           |
    |  - 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, 50)
    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 <= 50:
                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(50, 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

# Обновление 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()
#plt.subplots_adjust(bottom=0.34)
#update_plot()

# Построение 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.1, 0.2, 0.10, 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_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_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_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()
Помощь с описанием что и зачем выводится в терминал.

Как запустить в Windows 10:
1. сохраняем код в файл fuoz.py в любое удобное место.
2. клавиша win+r, вводим cmd и жмём выполнить
3. в открывшемся терминале вводим команду python, после чего если он не установлен, то винда предлагает установить python3 через microsoft store, соглашаемся.
4. после установки python вводим в терминалpip install matplotlib
5. после успешной установки терминал можно закрыть и запустить файл fuoz.py

По более старым версиям Windows - вероятно проще поставить пакет anaconda.

Что я могу сказать по режимам, а точнее показать:
inverse1.pngforward1.pngplastina1.pngs_virezom1.png

Мы видим осциллограммы, которые помогут оценить подходяший вам режим и логику вашей схемы.
Синий луч это входной сигнал (IN),
Жёлтый луч это выходной сигнал (OUT),
Красными линиями схематично изображено где и как происходит опережение относительно входного сигнала.
Левая красная вертикальная чёрточка это момент, когда по идее должна происходить искра.
Правая красная вертикальная чёрточка показывает момент, когда по идее должна происходить искра если бы опережения не было (то есть 0 градусов)
Осциллограммы были сняты в виртуальной среде, на частоте 76 Гц (примерно 4500 оборотов в минуту) и ~50 градусов опережения (для наглядности)
В указанных примерах скважность входного сигнала составляет 50%, в реальности всё зависит от конструкции вашего концевика на коленчатом валу (шторка, модулятор, называйте как удобно).
Ни для чего, кроме как для оценки нужного вам режима, эти осциллограммы не предназначены!

Какой способ выбирать - решать вам, я изначально создавал свой скетч для режима "инверсный", его противоположностью является "прямой", остальные режимы "пластина" и "с вырезом" я повторил логику из скетчей customcult. Я хотел бы дать двум последним режимам более "научное" название, но решил, раз так привыкли, то пусть так и будет для взаимопонимания.

Методики как определить логику вашего зажигания - я сказать не могу, потому что у меня только один вариант - "инверсный", хотите - подарите мне зажигание для мотора Т200, я расскажу вам как работает конкретный случай.
 

Вложения

Изменено:
  • Лойс +1
Реакции: ukr100 и UJV 5901

user314

★★✩✩✩✩✩
26 Апр 2022
51
53
@nikolau777, Для создания скетча используйте мой последний скрипт на python
Зависает скорее всего по причине плохого экрана от датчика, нужен очень хороший экран на землю.
Чтобы определиться какой скетч заливать, нужно проследить логику работы хотя бы одного цилиндра.
 
  • Лойс +1
Реакции: ukr100 и UJV 5901

user314

★★✩✩✩✩✩
26 Апр 2022
51
53
Кому надо собрал EXE файл. Должно работать на Windows 7, но это не точно, не проверял. Virustotal показывает два срабатывания на вирусы, но это немного.


Внутри файла статический питон 3.8 и все необходимые библиотеки, путь до файла сгенерированного скетча выводится в консоль.

Это чистый прототип и я терпеть не могу питон, но это самый быстрый способ донести мысль, поэтому прошу не судить строго.
 
  • Лойс +1
Реакции: ukr100 и UJV 5901

user314

★★✩✩✩✩✩
26 Апр 2022
51
53
Добавлен код для регулировки y-оси на графике.

2025-05-07_17_48_39.png

Python:
#!/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()
 
Изменено:
  • Лойс +1
Реакции: UJV 5901 и ukr100