Велосипедні фари з поворотниками та одометром на 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: Готово!

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

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

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

  • На одній дошці з вітрилом
    Безумовно, парусну дошку дехто може купити в магазині. Однак більшості це не по кишені. Такі напевно захочуть зробити її самостійно. Тим більше, що тут не пропонується конструкція...
  • ПАР НА ЗАМОВЛЕННЯ!
    Не даремно кажуть, що "баня парить, баня жарить, баня все підправить!". На жаль, для багатьох сенс цього народного прислів'я дійде занадто пізно, коли перелік захворювань починає...