Велосипедные фары с поворотниками и одометром на Arduino

Велосипедные фары с поворотниками и одометром на Arduino

Сегодня я расскажу, как я собрал велосипедные фары с использованием Arduino, которые имеют не только фары, но и поворотники для указания направления, а также одометр, показывающий скорость и пройденное расстояние. Живя в небольшом городке, я часто передвигаюсь на велосипеде, и наличие ярких фар делает это намного безопаснее, особенно ночью. Моя разработка включает фары, которые помимо белого или красного света, имеют желтые индикаторы для указания направления. Для активации поворотников на руле установлены кнопки, которые также можно использовать для переключения информации на дисплее и включения/выключения фар. Рядом с кнопками находится дисплей, который показывает скорость, время, общие километры и другую интересную информацию о нашей поездке. Фары очень яркие и хорошо видны как днем, так и ночью. Вся система управляется Arduino и питается от аккумулятора, который можно заряжать через USB-C порт. Более того, я нашел способ скрыть всю электронику на велосипеде, чтобы предотвратить кражу системы.

Как всегда, я также сделал видео об этом проекте, которое можно найти на моем YouTube-канале (с английскими субтитрами).

Инструменты и материалы

Инструменты и материалы

Инструменты и материалы

Для создания этого проекта мне понадобились:

Инструменты, которые я использовал для этого проекта:

  • 3D-принтер с черной нитью PLA.
  • Паяльник
  • Горячий клей
  • Отвертки, плоскогубцы и другие основные инструменты.

Шаг 1: Обзор

Шаг 1: Обзор

Итак, у нас есть проект, который состоит из нескольких частей. Во-первых, это передние и задние фары, которые, помимо традиционного красного и белого света, также имеют поворотники для указания направления. На руле велосипеда расположены дисплей, показывающий скорость, время, пройденные километры и другие интересные данные о поездке. Рядом с дисплеем находятся три кнопки, с помощью которых можно активировать поворотники, переключать страницы на дисплее и включать или выключать фары. Также имеется магнитный датчик, который определяет обороты колёс велосипеда для расчёта скорости. Все эти компоненты соединены с платой, включающей Arduino, модуль реального времени и четыре MOSFET-транзистора для управления светодиодами в фарах. Система питается от литий-ионного аккумулятора, который заряжается через USB-C порт. Все электроника спрятана в флягодержателе велосипеда, но об этом подробнее позже.

Начало разработки было с задней фары.

Шаг 2: Задняя фара

Шаг 2: Задняя фара

Для задней фары я использовал 15 прозрачных красных 5 мм светодиодов и 28 прозрачных желтых 5 мм светодиодов для поворотников (по 14 на каждый указатель). Важно, чтобы задняя фара была видна даже днем, поэтому я использовал много светодиодов.

Сначала я спроектировал и напечатал на 3D-принтере корпус для фары и плату с отверстиями для монтажа светодиодов. Затем я вставил светодиоды в отверстия платы, аккуратно разместив красные светодиоды в центре и желтые по бокам. Светодиоды соединены попарно с резистором на 47 Ом, а затем все пары соединены параллельно. Для подключения я просто согнул и припаял выводы светодиодов и резисторов. Плата вставлена в корпус, и все соединения защищены горячим клеем, чтобы предотвратить короткие замыкания.

Шаг 3: Дисплей

Шаг 3: Дисплей

После завершения задней фары я занялся дисплеем, который будет установлен на руле велосипеда и позволит видеть информацию, такую как скорость и общий маршрут. Дисплей представляет собой небольшой монохромный OLED-экран с диагональю 0,96″ и разрешением 128×64 пикселей с интерфейсом i2c. Для подключения дисплея к Arduino я использовал тот же четырехпроводный кабель, что и для задней фары. Я спроектировал и напечатал на 3D-принтере корпус для дисплея и крепление для руля. Все соединения выполнены и закреплены, и дисплей готов к использованию.

Шаг 4: Кнопки

Шаг 4: Кнопки

Далее я подумал о кнопках, которые будут использоваться для активации поворотников и управления дисплеем. У нас будет три кнопки: две для левых и правых поворотников и одна для навигации по страницам дисплея. Для крепления кнопок к велосипеду я напечатал на 3D-принтере крепление, очень похожее на крепление дисплея. Все соединения выполнены и проверены.

Шаг 5: Передние фары

Шаг 5: Передние фары

Теперь пришло время сделать передние фары, которые построены по аналогии с задней фарой. Передние фары разделены на две части, каждая из которых будет иметь три белых светодиода для основного света и шесть желтых для указателей поворота. Я спроектировал и напечатал на 3D-принтере корпуса для передних фар и платы с отверстиями для светодиодов. Все соединения выполнены и проверены.

Шаг 6: Датчик скорости

Шаг 6: Датчик скорости

Для измерения скорости я выбрал магнитный датчик, установленный рядом с задним колесом велосипеда, который определяет магнит, прикрепленный к колесу. Этот датчик срабатывает при каждом обороте колеса. Подсчитав количество срабатываний за определенный промежуток времени, Arduino может вычислить скорость в километрах в час, зная окружность колеса. Я использовал датчик эффекта Холла US5881 и подключил его к Arduino.

Шаг 7: Плата управления — Arduino и RTC

Шаг 7: Плата управления - Arduino и RTC

Теперь, когда у нас есть все компоненты системы, нужно собрать плату с Arduino, к которой они будут подключены. Я начал с кусочка перфорированной платы размером 55×90 мм, на которой припаял Arduino Nano с помощью разъемов. В дополнение к Arduino я установил модуль реального времени DS3231, который будет поддерживать время на дисплее даже при выключенном Arduino.

Шаг 8: Плата управления — MOSFET, I2C и делитель напряжения

Шаг 8: Плата управления - MOSFET, I2C и делитель напряжения

Как только RTC был установлен на место, я припаял к плате два четырехконтактных разъема JST для дисплея и кнопок, а также два разъема JST для датчика оборотов колеса и еще один магнитный датчик Холла для определения момента торможения, что в конечном итоге Я не устанавливал. Если вам интересно, этот последний магнитный датчик использовался бы для увеличения яркости заднего фонаря, когда мы нажимаем на тормоз. Для двух магнитных датчиков я установил подтягивающие резисторы сопротивлением 10 кОм между сигнальным контактом и источником питания +5 В. Лучшее место для размещения этих резисторов — рядом с разъемами.

Для управления светодиодами фар от выводов Arduino я использовал мосфеты IRFZ44N , два для левого и правого поворотников и два для передних и задних фонарей. Для каждого мосфета я установил резистор сопротивлением 10 кОм между затвором и землей. Затвор каждого МОП-транзистора подключен к одному выводу Arduino с резистором сопротивлением 220 Ом между ними. Для подключения заднего и переднего фонаря я припаял к плате два четырехконтактных разъема JST, к которым подвел питание +5В и земли для разных лампочек, идущие со стоков четырех МОП-транзисторов .

Дисплей и модуль RTC будут подключены к Arduino через i2c, используя контакты A4 и A5. На обеих линиях i2c я установил подтягивающие резисторы сопротивлением 2,2 кОм, подключенные между каждой линией i2c и питанием +5 В.

Чтобы измерить напряжение батареи и отобразить его на дисплее, я использовал аналоговый вывод на Arduino, к которому подключил делитель напряжения, состоящий из двух резисторов (100 кОм и 20 кОм), чтобы довести напряжение батареи до значение ниже внутреннего опорного напряжения 1,1 В Arduino.

После того, как все компоненты были припаяны к плате, я выполнил соединения под платой, следуя схеме подключения, которую вы можете найти ниже. Для подключения я использовал комбинацию сплошной медной проволоки и проводов с силиконовой оболочкой . Эта работа, очевидно, заняла некоторое время, но, наконец, мне удалось закончить доску. Если хотите, следуя схеме, вы можете спроектировать собственную печатную плату для производства.

Шаг 9: Код Arduino

Шаг 9: Код Arduino

Перед установкой системы нам необходимо загрузить код в Arduino. Написание кода было, пожалуй, самой длинной и сложной частью этого проекта. Конечно код написан не очень хорошо, однако он работает и это главное. Код занимается измерением скорости, считыванием времени с RTC, управлением освещением и поворотниками, а также отображением интерфейса на дисплее… происходит много всего. Хорошая новость в том, что вам не нужно модифицировать код, если вы этого не хотите: все настройки можно выполнить прямо из интерфейса на дисплее. Эти настройки, конечно, сохраняются в EEPROM, поэтому они сохраняются даже при выключении системы.

Прежде чем загружать код, я предлагаю отключить Arduino от платы. Код можно скомпилировать и загрузить из Arduino IDE после установки библиотеки Adafruit_SSD1306 и библиотеки RTClib.

Ниже вы можете найти код для загрузки, что я и предлагаю. Если хотите, вы можете скопировать его прямо отсюда и вставить в Arduino IDE.

#include 
#include 
#include 
#include 
//#include 
#include "RTClib.h"
#include 

#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels

#define OLED_RESET     -1 // Reset pin # (or -1 if sharing Arduino reset pin)
//#define OLED_RESET     12 // Reset pin # (or -1 if sharing Arduino reset pin)
#define SCREEN_ADDRESS 0x3C ///< See datasheet for Address; 0x3D for 128x64, 0x3C for 128x32
//#define i2c_Address 0x3c
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
//Adafruit_SH1106 display = Adafruit_SH1106(OLED_RESET);

RTC_DS3231 rtc;

float wheelCircumference = 2.00; //meters (can be set from the settings page of the device)
const float speedUpdateInterval = 5000; //calculate speed every N milliseconds

const long turnSignalBlink = 500; //speed at which the turn signals blink in milliseconds
const int turnSignalMaxCycles = 12; //number of turn signal blinks before they turn off automatically

const int wheelSensor = 2; //pin to which the wheel hall effect sensor is connected

const int brakeSensor = 7; //pin to which the brake hall effect sensor is connected
const bool invertBrakeSensor = false; //true: brake detected when the magnet is far from the sensor

const int LEFTbutton = 3; //pins to which the three buttons are connected
const int RIGHTbutton = 4;
const int SETbutton = 5;

const int LEFTled = 9; //pins to which the lights are connected
const int RIGHTled = 10;
const int REDled = 11;
const int WHITEled = 6;

float totalKm = 0; //variable that stores total distance
float speed = 0; //variable that stores current speed
float maxSpeed = 0; //variable that stores maximum speed
float avgSpeed = 0; //variable that stores average speed
long activeTime = 0; //variable that stores active time

float speedMillis = 0; //milliseconds from the last time speed was calculated

bool frontLights = false; //state of front lights
bool backLights = true; //state of back lights
bool brakeLights = false; //state of brake lights
bool left = false; //state of left turn signal
bool right = false; //state of right turn signal
bool turnSignalState = true;
bool lastLeftState = true;
bool lastRightState = true;

bool leftReleased = true;
bool rightReleased = true;
bool setReleased = true;

int lightsCycle = 0;
int turnSignalCycle = 0;
long previousMillis = 0;
long releaseMillis = 0;
long lightsMillis = 0;

int displayPage = 0; //0: home screen - 1: info screen - 2: control screen
int menuSelection = 0;
bool pageUpdated = false;

int batteryState = 0;
const int batteryReadingDeadband = 10;

int pulses = 0;

void setup() {
  pinMode(wheelSensor, INPUT);
  pinMode(brakeSensor, INPUT);
  pinMode(LEFTbutton, INPUT_PULLUP);
  pinMode(RIGHTbutton, INPUT_PULLUP);
  pinMode(SETbutton, INPUT_PULLUP);
  pinMode(LEFTled,  OUTPUT);
  pinMode(RIGHTled, OUTPUT);
  pinMode(REDled, OUTPUT);
  pinMode(WHITEled, OUTPUT);

  analogReference(INTERNAL);

  //Serial.begin(9600);

  display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS);
  //display.begin(SH1106_SWITCHCAPVCC, 0x3C);

  rtc.begin();

  //rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));

  display.clearDisplay();
  display.setTextSize(2);
  display.setTextColor(WHITE);

  for(int i = -20; i <= 24; i++) { display.fillRect(0, 0, 128, 64, BLACK); display.setCursor(28, i); display.print("Hello!"); display.display(); delay(2); } delay(500); display.clearDisplay(); delay(200); bool settings = false; bool settingsChanged = false; int settingsPage = 0; int selection = 0; DateTime now = rtc.now(); int h = now.hour(); int m = now.minute(); EEPROM.get(0, wheelCircumference); //enter settings when the SET button is pressed during startup if(digitalRead(SETbutton) == 0 || isnan(wheelCircumference)) { setReleased = false; delay(100); display.clearDisplay(); settings = true; //remain into the settings while(settings == true) { //detect if the SET button has been released after being pressed if(digitalRead(SETbutton) == 1 && setReleased == false) { setReleased = true; delay(100); } //main settings menu if(settingsPage == 0) { display.fillRect(0, 0, 128, 64, BLACK); //move between the settings using the LEFT and RIGHT buttons if(digitalRead(RIGHTbutton) == 0) { selection++; if(selection > 3) selection = 0;
          delay(200);
        }

        if(digitalRead(LEFTbutton) == 0) {
          selection--;
          if(selection < 0) selection = 3;
          delay(200);
        }

        display.setTextColor(WHITE);
        display.setTextSize(2);
        display.setCursor(15, 0);
        display.print("Settings");

        display.drawLine(0, 28, 127, 28, WHITE);
        display.drawLine(0, 52, 127, 52, WHITE);
        display.setCursor(0, 33);
        display.print("<"); display.setCursor(116, 33); display.print(">");

        if(selection == 0) {
          display.setCursor(18, 33);
          display.print("Exit");

          //exit from the settings if "Exit" is selected
          if(digitalRead(SETbutton) == 0 && setReleased == true) {
            settings = false;
            delay(200);
          }
        }

        if(selection == 1) {
          display.setCursor(18, 33);
          display.print("Time");
        }

        if(selection == 2) {
          display.setCursor(18, 33);
          display.print("Wheel");
        }

        if(selection == 3) {
          display.setCursor(18, 33);
          display.print("Sensors");

        }

        //if SET is pressed, go into the menu that has been chosen
        if(digitalRead(SETbutton) == 0 && setReleased == true && selection != 0) {
          settingsPage = selection;
          selection = 0;
          setReleased = false;
          display.clearDisplay();
          delay(200);
        }
        //display.display();
      }

      display.display();
    
      //settings page for entering the current time
      if(settingsPage == 1) {
        display.fillRect(0, 0, 128, 64, BLACK);

        //move between the settings using the SET button
        if(digitalRead(SETbutton) == 0 && setReleased == true) {
          selection++;
          if(selection > 2) selection = 0;
          setReleased = false;

          delay(200);

          //Serial.println(selection);
        }
        
        if(selection == 0) {
          display.fillRect(0, 0, 128, 18, WHITE);
          display.setTextColor(BLACK);

          if(digitalRead(RIGHTbutton) == 0 || digitalRead(LEFTbutton) == 0) {
            settingsPage = 0;
            if(settingsChanged == true) {
              rtc.adjust(DateTime(2024, 1, 1, h, m, 0)); //set date-time manually: yr, mo, dy, hr, mn, sec
              settingsChanged = false;
              settingsConfirmation();
              delay(1200);
              display.clearDisplay();
            }
            delay(200);
          }
        }

        else {
          display.fillRect(0, 0, 128, 18, BLACK);
          display.setTextColor(WHITE);
        }
        
        display.setTextSize(2);
        display.setCursor(0, 2);
        display.print("<"); display.setCursor(18, 2); display.print("Set time"); display.setTextSize(3); if(selection == 1) { display.fillRect(14, 29, 41, 25, WHITE); display.setTextColor(BLACK); if(digitalRead(RIGHTbutton) == 0) { h++; if(h > 23) h = 0;
            settingsChanged = true;
            delay(200);
          }

          if(digitalRead(LEFTbutton) == 0) {
            h--;
            if(h < 0) h = 23;
            settingsChanged = true;
            delay(200);
          }
        }

        else {
          display.fillRect(14, 29, 41, 25, BLACK);
          display.setTextColor(WHITE);
        }

        display.setCursor(18, 31);
        if(h < 10) display.print(0); display.print(h); display.setTextColor(WHITE); display.print(":"); if(selection == 2) { display.fillRect(68, 29, 41, 25, WHITE); display.setTextColor(BLACK); if(digitalRead(RIGHTbutton) == 0) { m++; if(m > 59) m = 0;
            settingsChanged = true;
            delay(200);
          }

          if(digitalRead(LEFTbutton) == 0) {
            m--;
            if(m < 0) m = 59;
            settingsChanged = true;
            delay(200);
          }
        }

        else {
          display.fillRect(68, 29, 41, 25, BLACK);
          display.setTextColor(WHITE);
        }

        if(m < 10) display.print(0); display.print(m); //display.display(); } if(settingsPage == 2) { display.fillRect(0, 0, 128, 64, BLACK); if(isnan(wheelCircumference)) wheelCircumference = 2.0; if(digitalRead(SETbutton) == 0 && setReleased == true) { selection++; if(selection > 1) selection = 0;
          setReleased = false;

          delay(200);
        }
        
        if(selection == 0) {
          display.fillRect(0, 0, 128, 18, WHITE);
          display.setTextColor(BLACK);

          if(digitalRead(RIGHTbutton) == 0 || digitalRead(LEFTbutton) == 0) {
            settingsPage = 0;
            if(settingsChanged == true) {
              EEPROM.put(0, wheelCircumference);
              settingsChanged = false;
              settingsConfirmation();
              delay(1200);
              display.clearDisplay();
            }
            delay(200);
          }
        }

        else {
          display.fillRect(0, 0, 128, 18, BLACK);
          display.setTextColor(WHITE);
        }
        
        display.setTextSize(2);
        display.setCursor(0, 2);
        display.print("<"); display.setCursor(18, 2); display.print("Set wheel"); display.setTextSize(3); if(selection == 1) { display.fillRect(6, 27, 113, 29, WHITE); display.setTextColor(BLACK); if(digitalRead(RIGHTbutton) == 0) { wheelCircumference += 0.01; if(wheelCircumference > 5.00) wheelCircumference = 5.00;
            settingsChanged = true;
            delay(150);
          }

          if(digitalRead(LEFTbutton) == 0) {
            wheelCircumference -= 0.01;
            if(wheelCircumference < 0.10) wheelCircumference = 0.10;
            settingsChanged = true;
            delay(150);
          }
        }

        else {
          display.fillRect(6, 27, 113, 29, BLACK);
          display.setTextColor(WHITE);
        }

        display.setCursor(10, 31);
        display.print(wheelCircumference);
        display.setTextSize(2);
        display.setCursor(93, 38);
        display.print("mt");

       // display.display();
      }

      if(settingsPage == 3) {
        display.fillRect(0, 0, 128, 64, BLACK);
        
        if(digitalRead(RIGHTbutton) == 0 || digitalRead(LEFTbutton) == 0) {
          settingsPage = 0;
          delay(200);
        }

        display.fillRect(0, 0, 128, 18, WHITE);
        display.setTextColor(BLACK);
        display.setTextSize(2);
        display.setCursor(0, 2);
        display.print("<"); display.setCursor(18, 2); display.print("Sensors"); display.setTextColor(WHITE); display.setCursor(0, 26); display.print("Wheel: "); if(digitalRead(wheelSensor) == 0) { display.print("ON"); } else { display.print("OFF"); } display.setCursor(0, 48); display.print("Brake: "); if(digitalRead(brakeSensor) == 0) { display.print("ON"); } else { display.print("OFF"); } } } } EEPROM.get(0, wheelCircumference); //Serial.println(wheelCircumference); attachInterrupt(digitalPinToInterrupt(wheelSensor), interruptFunction, RISING); } void loop() { //update the display every 250ms if(millis() % 250 == 0) { update(); } if(millis() - lightsMillis >= 180) {

    lightsMillis = millis();

    lightsCycle++; //0 to 1: red bright - 2 to 5: red dim
    if(lightsCycle > 5) lightsCycle = 0;
  }

  if(millis() - speedMillis >= speedUpdateInterval) {

    calculateSpeed();

    speedMillis = millis();
  }

  if(frontLights == true) {    
    if(left == true || right == true) {
      analogWrite(WHITEled, 20);
    }
    else {
      analogWrite(WHITEled, 180);
    }
  }

  if(frontLights == false) {
    analogWrite(WHITEled, 0);
  }

  //blinking red headlight
  if(backLights == true) {
    if(lightsCycle < 1 && left == false && right == false && brakeLights == false) { analogWrite(REDled, 200); } if((lightsCycle >= 1 || (lightsCycle < 1 && (left == true || right == true))) && brakeLights == false) { analogWrite(REDled, 20); } if(brakeLights == true) { analogWrite(REDled, 255); } } if(backLights == false) { analogWrite(REDled, 0); } if(digitalRead(brakeSensor) == invertBrakeSensor) { brakeLights = true; } else { brakeLights = false; } //turn LEFT turn signal ON if(digitalRead(LEFTbutton) == 0 && left == false && right == false && displayPage == 0 && leftReleased == true) { left = true; turnSignalState = true; turnSignalCycle = 0; leftReleased = false; releaseMillis = millis(); } //turn RIGHT turn signal ON if(digitalRead(RIGHTbutton) == 0 && right == false && left == false && displayPage == 0 && rightReleased == true) { right = true; turnSignalState = true; turnSignalCycle = 0; rightReleased = false; releaseMillis = millis(); } //turn signals OFF when LEFT button is pressed if(digitalRead(LEFTbutton) == 0 && leftReleased == true && displayPage == 0) { if(left == true) { left = false; digitalWrite(LEFTled, LOW); } if(right == true) { right = false; digitalWrite(RIGHTled, LOW); } leftReleased = false; releaseMillis = millis(); } //turn signals OFF when RIGHT button is pressed if(digitalRead(RIGHTbutton) == 0 && rightReleased == true && displayPage == 0) { if(left == true) { left = false; digitalWrite(LEFTled, LOW); } if(right == true) { right = false; digitalWrite(RIGHTled, LOW); } rightReleased = false; releaseMillis = millis(); } //turn signals OFF when SET button is pressed if(digitalRead(SETbutton) == 0 && (left == true || right == true) && setReleased == true) { if(left == true) { left = false; digitalWrite(LEFTled, LOW); } if(right == true) { right = false; digitalWrite(RIGHTled, LOW); } setReleased = false; releaseMillis = millis(); } if(digitalRead(SETbutton) == 0 && left == false && right == false && setReleased == true) { displayPage++; if(displayPage > 2) displayPage = 0;

    menuSelection = 0;

    pageUpdated = false;

    setReleased = false;
    releaseMillis = millis();
  }

  if(digitalRead(LEFTbutton) == 0 && displayPage == 2 && leftReleased == true) {
    menuSelection++;
    
    if(menuSelection > 2) menuSelection = 0;

    leftReleased = false;
    releaseMillis = millis();
  }

  if(digitalRead(RIGHTbutton) == 0 && displayPage == 2 && rightReleased == true) {
    bool clicked = false;

    if(menuSelection == 0) {
      if(frontLights == false && backLights == false && clicked == false) {
        frontLights = true;
        backLights = true;

        clicked = true;
      }
      
      if((frontLights == true || backLights == true) && clicked == false) {
        frontLights = false;
        backLights = false;

        clicked = true;
      }
    }

    if(menuSelection == 1) {
      frontLights = !frontLights;
    }

    if(menuSelection == 2) {
      backLights = !backLights;
    }

    rightReleased = false;
    releaseMillis = millis();
  }

  if(millis() - releaseMillis > 10000 && displayPage != 0) {
    displayPage = 0;
  }

  //detect if the buttons have been released
  if(digitalRead(SETbutton) == 1 && millis() - releaseMillis >= 200) {
    setReleased = true;
  }

  if(digitalRead(RIGHTbutton) == 1 && millis() - releaseMillis >= 200) {
    rightReleased = true;
  }

  if(digitalRead(LEFTbutton) == 1 && millis() - releaseMillis >= 200) {
    leftReleased = true;
  }

  //LEFT turn signal
  if(left == true) {

    if (millis() - previousMillis >= turnSignalBlink) {
      // save the last time you blinked the LED
      previousMillis = millis();

      digitalWrite(LEFTled, turnSignalState);

      turnSignalState = !turnSignalState;
      turnSignalCycle++;
    }

    if(turnSignalCycle > (turnSignalMaxCycles*2 - 1)) {
      left = false;
      digitalWrite(LEFTled, LOW);
    }
  }

  //RIGHT turn signal
  if(right == true) {

    if (millis() - previousMillis >= turnSignalBlink) {
      // save the last time you blinked the LED
      previousMillis = millis();

      digitalWrite(RIGHTled, turnSignalState);

      turnSignalState = !turnSignalState;
      turnSignalCycle++;
    }

    if(turnSignalCycle > (turnSignalMaxCycles*2 - 1)) {
      right = false;
      digitalWrite(RIGHTled, LOW);
    }
  }

  //delay(1);
}

void interruptFunction() {
  pulses++;
}

void calculateSpeed() {
  float staticPulses = pulses;
  pulses = 0;
  
  totalKm = totalKm + (staticPulses * wheelCircumference) / 1000.0;
  
  speed = (staticPulses * wheelCircumference * 3.6) / (speedUpdateInterval / 1000.0);

  if(speed > 2) activeTime += speedUpdateInterval;

  if(speed > maxSpeed) maxSpeed = speed;

  //Serial.println(speed);
}

void update() {
  //detachInterrupt(2);

  DateTime now = rtc.now();

  display.clearDisplay();
  
  if(displayPage == 0) {
    //display speed
    display.setTextSize(3);
    display.setCursor(1, 0);
    if(speed < 10) display.print(0);
    display.print(speed, 0);
    display.setTextSize(2);
    display.print(" Km/h");

    //display time
    display.setTextSize(2);
    display.setCursor(1, 29);
    if(now.hour() < 10) display.print(0);
    display.print(now.hour());
    display.print(":");
    if(now.minute() < 10) display.print(0);
    display.print(now.minute());
    
    //display total space
    display.setCursor(1, 50);
    if(totalKm < 10) display.print(0);
    display.print(totalKm);
    display.print(" Km");

    //display turn signals icons
    if(left == true && turnSignalState == true) {
      display.fillRect(110, 27, 18, 11, WHITE);
      display.fillTriangle(110, 20, 110, 44, 98, 32, WHITE);
    }
    if(right == true && turnSignalState == true) {
      display.fillRect(98, 27, 18, 11, WHITE);
      display.fillTriangle(115, 20, 115, 44, 127, 32, WHITE);
    }

    checkBattery();

    display.display();
  }

  if(displayPage == 1 && pageUpdated == false) {
    pageUpdated = true;

    display.drawLine(0, 31, 127, 31, WHITE);
    display.drawLine(63, 0, 63, 63, WHITE);
    
    //display total time
    display.setTextSize(1);
    display.setCursor(1, 0);
    display.print("TOTAL");
    display.setCursor(0, 12);
    display.setTextSize(2);
    if((millis()/60000)/60 < 10) display.print(0);
    display.print((millis()/60000)/60);
    display.print(":");
    if((millis()/60000)%60 < 10) display.print(0);
    display.print((millis()/60000)%60);

    //display active time
    display.setTextSize(1);
    display.setCursor(69, 0);
    display.print("ACTIVE");
    display.setCursor(68, 12);
    display.setTextSize(2);
    if((activeTime/60000)/60 < 10) display.print(0);
    display.print((activeTime/60000)/60);
    display.print(":");
    if((activeTime/60000)%60 < 10) display.print(0);
    display.print((activeTime/60000)%60);

    //display max speed
    display.setTextSize(1);
    display.setCursor(1, 35);
    display.print("MAX");
    display.setCursor(30, 52);
    display.print("Km/h");
    display.setTextSize(2);
    display.setCursor(0, 46);    
    if(maxSpeed < 10) display.print(0);
    display.print(maxSpeed, 0);

    //display average speed
    display.setTextSize(1);
    display.setCursor(69, 35);
    display.print("AVERAGE");
    display.setCursor(98, 52);
    display.print("Km/h");
    display.setTextSize(2);    
    display.setCursor(68, 46);
    if(totalKm == 0.0 || activeTime == 0) {
      display.print(0); display.print(0);
    }
    else {
      if(totalKm/(activeTime/3600000.0) < 10) display.print(0); display.print(totalKm/(activeTime/3600000.0), 0); } display.display(); } if(displayPage == 2) { display.setTextSize(2); if(menuSelection == 0) { display.setCursor(0, 2); display.print(">");
    }

    if(menuSelection == 1) {
      display.setCursor(0, 24);
      display.print(">");
    }

    if(menuSelection == 2) {
      display.setCursor(0, 46);
      display.print(">");
    }
    
    display.setCursor(16, 2);
    display.print("All");
    display.setCursor(16, 24);
    display.print("Front");
    display.setCursor(16, 46);
    display.print("Back");
    
    if(frontLights == true || backLights == true) {
      display.fillRect(87, 0, 39, 18, WHITE);
      display.setTextColor(BLACK);
      display.setCursor(95, 2);
      display.print("ON");
    }

    if(frontLights == false && backLights == false) {
      display.setTextColor(WHITE);
      display.setCursor(89, 2);
      display.print("OFF");
    }

    if(frontLights == true) {
      display.fillRect(87, 22, 39, 18, WHITE);
      display.setTextColor(BLACK);
      display.setCursor(95, 24);
      display.print("ON");
    }

    if(frontLights == false) {
      display.setTextColor(WHITE);
      display.setCursor(89, 24);
      display.print("OFF");
    }

    if(backLights == true) {
      display.fillRect(87, 44, 39, 18, WHITE);
      display.setTextColor(BLACK);
      display.setCursor(95, 46);
      display.print("ON");
    }

    if(backLights == false) {
      display.setTextColor(WHITE);
      display.setCursor(89, 46);
      display.print("OFF");
    }
    
    display.setTextColor(WHITE);
    display.display();
  }

  //attachInterrupt(digitalPinToInterrupt(wheelSensor), calculateSpeed, RISING);
}

void checkBattery() {
  int batValue = analogRead(A0);

  //value above maximum threshold
  if ((605 + batteryReadingDeadband) < batValue) {  // more than 3.9V (605)
    batteryState = 3;
  }

  //value between two thresholds (middle region)
  if (((559 + batteryReadingDeadband) < batValue) && (batValue <= (605 - batteryReadingDeadband))) {  // 3.6V (559) to 3.9V (605)
    batteryState = 2;
  }

  //value between two thresholds (middle region)
  if (((513 + batteryReadingDeadband) < batValue) && (batValue <= (559 - batteryReadingDeadband))) {  // 3.3V (513) to 3.6V (559)       
    batteryState = 1;
  }

  //value below minimum threshold
  if (batValue < (513 - batteryReadingDeadband)) {  // less than 3.3V (513)
    batteryState = 0;    
  }

  display.drawLine(112, 55, 126, 55, WHITE);
  display.drawLine(112, 63, 126, 63, WHITE);
  display.drawLine(112, 56, 112, 62, WHITE);
  display.drawLine(126, 56, 126, 57, WHITE);
  display.drawLine(126, 61, 126, 62, WHITE);
  display.drawLine(127, 57, 127, 61, WHITE);
  
  if(batteryState == 0) {  // less than 3.3V
    display.fillRect(114, 57, 3, 5, BLACK);
    display.fillRect(118, 57, 3, 5, BLACK);
    display.fillRect(122, 57, 3, 5, BLACK);
  } 
  if(batteryState == 1) { // 3.3V to 3.6V
    display.fillRect(114, 57, 3, 5, WHITE);
    display.fillRect(118, 57, 3, 5, BLACK);
    display.fillRect(122, 57, 3, 5, BLACK);
  }
  if(batteryState == 2) { // 3.6V to 3.9V
    display.fillRect(114, 57, 3, 5, WHITE);
    display.fillRect(118, 57, 3, 5, WHITE);
    display.fillRect(122, 57, 3, 5, BLACK);
  }
  if(batteryState == 3) { // more than 3.9V
    display.fillRect(114, 57, 3, 5, WHITE);
    display.fillRect(118, 57, 3, 5, WHITE);
    display.fillRect(122, 57, 3, 5, WHITE);
  }

  //Serial.println(batValue);
}

void settingsConfirmation() {
  //display.fillTriangle(58, 40, 48, 30, 127, 0, WHITE);
  display.clearDisplay();
  display.drawRect(11, 5, 107, 53, WHITE);
  display.setTextColor(WHITE);
  display.setTextSize(2);
  display.setCursor(23, 14);
  display.print("Changes");
  display.setCursor(31, 35);
  display.print("saved!");
  display.display();
}

Шаг 10: Схема батареи

Шаг 10: Схема батареи

Как я уже говорил, вся система будет питаться от литиевой батареи 18650. Чтобы установить его, я напечатал в 3D простой держатель для батарейки. Чтобы подключить аккумулятор, я восстановил пружинный и плоский контакт из держателя батарейки от старой рождественской гирлянды. Я припаял два провода к двум контактам и приклеил их к двум концам напечатанного на 3D-принтере держателя батареи.

Для зарядки и защиты аккумулятора я решил использовать модуль TP4056 с портом USB-C для зарядки аккумулятора. Я подключил провода, идущие от держателя батареи, к контактам B+ и B- на печатной плате. Модуль имеет два светодиода для индикации состояния зарядки, которые будут скрыты при установке модуля. Поэтому я отпаял их и подключил к маленьким площадкам два прозрачных светодиода диаметром 3 мм с помощью коротких проводов. Эти светодиоды будут размещены рядом с портом USB.

К двум выходным клеммам схемы зарядки я подключил повышающий модуль для доведения напряжения аккумулятора до 5В. В повышающем модуле у меня был порт USB, который я отпаял и заменил двумя проводами, подключенными к положительному и отрицательному выходным контактам. Вы можете использовать любой повышающий модуль с достаточным выходным током, имейте в виду, что минус входа должен быть напрямую связан с минусом выхода. В последнюю очередь поставил кулисный переключатель, прерывающий плюсовой провод, идущий от модуля зарядки к повышающему модулю. Этот переключатель будет использоваться для включения или выключения всей системы. Ниже вы можете найти схему этого участка схемы.

Шаг 11. Как спрятать электронику на велосипеде

Шаг 11. Как спрятать электронику на велосипеде

Теперь мне нужно было найти место для крепления платы с Ардуино вместе с аккумулятором на велосипеде. За последние несколько месяцев мои велосипедные фары украли трижды, поэтому я хотел, чтобы эта система не закончилась таким же образом. Поэтому я решил спрятать все в бутылке с водой, которая много лет была у меня на велосипеде, но никогда не использовалась. У бутылки есть несколько характеристик, которые делают ее идеальным выбором: она достаточно велика, чтобы вместить плату с Arduino и батарею, защищает их от дождя и, самое главное, никто не расскажет, что находится внутри.

Сначала я вырезал дно бутылки с водой с помощью пилы и сделал отверстие на одной стороне, чтобы пропустить кабели. Затем я напечатал в 3D держатель для крепления бутылки с водой на велосипед и еще одну деталь, на которую можно установить держатель аккумулятора, схему зарядки, переключатель и плату с Arduino. На той стороне, где есть место для порта зарядки, я закрепил модуль зарядки аккумулятора горячим клеем. Затем я установил держатель батареи, используя четыре болта M3x12 мм и самоконтрящиеся гайки. С другой стороны я установил плату с Arduino, используя опять же болты M3x12 мм и самоконтрящиеся гайки, а также несколько напечатанных на 3D-принтере прокладок. Отверстия для крепления платы не включены в 3D-файл, поэтому вы можете просверлить их в нужном положении, чтобы они соответствовали отверстиям на вашей плате. Наконец, я подключил питание +5 В и GND от повышающего преобразователя к плате, а также один провод, идущий непосредственно от положительного полюса батареи для питания RTC, и один провод, идущий от входа повышающего преобразователя, чтобы сделать это возможным. чтобы Arduino считывал напряжение батареи через делитель напряжения.

Шаг 12: Установка системы на велосипед

Шаг 12: Установка системы на велосипед

Спустя 2 месяца работы я наконец смог смонтировать всю систему на своем велосипеде. Я начал с установки подсветки, используя два болта М5, которые мы вставили в корпус, в сочетании с двумя самоконтрящимися гайками. Затем я прикрепил кнопки и дисплей к рулю велосипеда с помощью напечатанного на 3D-принтере кронштейна и винтов M3x20 мм. Два передних фонаря можно установить рядом с центром руля так же, как дисплей и кнопки. Я закрепил кабели стяжками, подведя их к месту установки Arduino и аккумулятора. Затем я установил магнитный датчик колеса с его 3D-печатным креплением рядом с задним колесом, чтобы магнит, прикрепленный к колесу, мог его активировать. И наконец, я прикрепил держатель бутылки к двум резьбам, которые есть в большинстве велосипедов, пропустив кабели через отверстие внизу. Затем я подключил к плате все разъемы JST различных компонентов. Внутри бутылкодержателя я закрепил крепление со всей электроникой и аккумулятором с помощью двух винтов М3 и самоконтрящихся гаек. В последнюю очередь я вставил бутылку, которая просто скользит по креплению с электроникой, в ее держатель и закрепил ее винтами, предварительно просверлив в ней два отверстия. Конечно, в конце концов, может быть, вы увидите, что это не обычная бутылка с водой, но в наши дни у кого нет бутылки с водой с выключателем питания?

Шаг 13: Настройки

Шаг 13: Настройки

Когда мы впервые запускаем всю систему, нам нужно ввести некоторую информацию на дисплей. Для этого включаем выключатель питания, удерживая кнопку SET. Таким образом мы входим в настройки системы. Клавишами «влево» и «вправо» мы можем перемещаться по пунктам меню, а нажатием SET — расширять настройку, в которой находимся. Сначала нам нужно установить время. После входа на соответствующую страницу мы можем нажать SET, чтобы выбрать часы, и использовать кнопки влево и вправо, чтобы увеличить или уменьшить значение. Повторное нажатие SET выбирает минуты, которые можно регулировать, как и часы. Повторным нажатием SET и затем левой или правой клавиши устанавливаем время и выходим из настроек времени. Важно отметить, что время, которое мы вводим, считается текущим временем на момент выхода из настроек времени. Время, конечно, будет поддерживаться модулем RTC. В настройках Колеса нам нужно установить окружность колеса ( а не диаметр ) в метрах. Порядок действий такой же, как и при настройке времени, и с помощью кнопок «влево» и «вправо» изменяем значение в 1 сантиметр. Как только мы выйдем со страницы настроек, значение будет сохранено непосредственно в памяти EEPROM Arduino и не будет потеряно при выключении системы. Последняя страница настроек под названием «Датчики» позволяет нам увидеть, обнаруживает ли магнитный датчик колеса магнит, который мы установили на колесо; это полезно для правильного совмещения магнита с датчиком. Если вы заметили, что когда магнит проходит над датчиком, его состояние последовательно меняется с выключенного на включенное несколько раз , это потому, что ваш магнит на самом деле включает в себя несколько магнитов. В этом случае просто разделите окружность колеса на количество магнитов и установите новое значение; в противном случае значения скорости и общего количества километров будут увеличены вдвое или втрое. Выбрав «Выход» в меню настроек, вы можете, как вы уже догадались, выйти из настроек.

Шаг 14: Использование системы

Шаг 14: Использование системы

Теперь, когда система настроена, во время езды мы видим на дисплее скорость, время и общее количество километров. Нажатие SET переводит нас на вторую страницу с другими данными, такими как средняя и максимальная скорость, общее время и время, которое мы фактически проехали, исключая, например, время, когда мы остановились на светофоре. Повторное нажатие SET переводит нас на страницу, с которой мы можем включить или выключить передние или задние фонари или оба. Находясь на этой странице, мы можем выбрать работу со всеми огнями (передними и задними) или только с передними или задними огнями, нажав кнопку со стрелкой влево. Чтобы включить или выключить выбранный нами свет, мы можем использовать кнопку со стрелкой вправо. Дисплей автоматически возвращается на главную страницу через 10 секунд.

Для активации левого и правого указателей поворота мы можем использовать две соответствующие кнопки, а для их выключения можно нажать любую кнопку. Обратите внимание, что поворотники можно активировать только тогда, когда мы находимся на главной странице дисплея. При включении поворотников мы видим на дисплее индикатор, что является очень полезной функцией.

Свет очень яркий, его хорошо видно как днем, так и ночью. Аккумулятора хватает на несколько часов, а когда он разряжается, мы можем зарядить его через порт USB-C.

Шаг 15: Готово!

Шаг 15: Готово!

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

Часто задаваемые вопросы

1. Сколько времени занимает сборка этой системы?

  • В среднем, сборка может занять около 10-15 часов, в зависимости от вашего опыта и наличия всех необходимых материалов.

2. Можно ли использовать другие типы светодиодов?

  • Да, вы можете использовать любые светодиоды, которые у вас есть, но учтите, что яркость и потребление энергии могут различаться.

3. Как долго держит заряд аккумулятор?

  • В среднем, аккумулятор может держать заряд около 5-6 часов при непрерывной работе фар.

4. Можно ли использовать эту систему на электровелосипеде?

  • Да, эта система может быть установлена на любой велосипед, включая электровелосипеды.

5. Что делать, если дисплей не показывает информацию?

  • Проверьте соединения и убедитесь, что все провода правильно подключены. Также убедитесь, что дисплей исправен и получает питание.

Джованни Аджустатутто, Италия

Рекомендуем почитать

  • ИЗОБРЕТАЕМ… КОЛЕСОИЗОБРЕТАЕМ… КОЛЕСО
    На этом этапе работы наметим конструкцию исполнительного механизма, обеспечивающего поступательное движение модели. Начнем с анализа уже известных механизмов, которые применяются в...
  • РУЛИ НА НИТКАХРУЛИ НА НИТКАХ
    Надежность системы управления кордовой авиамодели — один из важнейших факторов успеха на соревнованиях. Немаловажное значение имеет то, как подвешены рули высоты и закрылки. Отсутствие...
Тут можете оценить работу автора: