Сегодня я расскажу, как я собрал велосипедные фары с использованием Arduino, которые имеют не только фары, но и поворотники для указания направления, а также одометр, показывающий скорость и пройденное расстояние. Живя в небольшом городке, я часто передвигаюсь на велосипеде, и наличие ярких фар делает это намного безопаснее, особенно ночью. Моя разработка включает фары, которые помимо белого или красного света, имеют желтые индикаторы для указания направления. Для активации поворотников на руле установлены кнопки, которые также можно использовать для переключения информации на дисплее и включения/выключения фар. Рядом с кнопками находится дисплей, который показывает скорость, время, общие километры и другую интересную информацию о нашей поездке. Фары очень яркие и хорошо видны как днем, так и ночью. Вся система управляется Arduino и питается от аккумулятора, который можно заряжать через USB-C порт. Более того, я нашел способ скрыть всю электронику на велосипеде, чтобы предотвратить кражу системы.
Как всегда, я также сделал видео об этом проекте, которое можно найти на моем YouTube-канале (с английскими субтитрами).
Инструменты и материалы
Для создания этого проекта мне понадобились:
- Плата Arduino Nano
- 0.96″ OLED-дисплей 128×64 с i2c коммуникацией
- 3 тактильных переключателя 12×12 мм
- 2 датчика Холла US5881 (один опциональный)
- Модуль реального времени DS3231
- 4 MOSFET IRFZ44N
- Литий-ионный аккумулятор 18650
- Зарядное устройство TP4056 с USB-C
- Повышающий преобразователь 5V 500 мА (или больше)
- Переключатель
- Приблизительно 5 метров 26 AWG 4-жильного кабеля
- Печатная плата
- 4 резистора 220 Ом
- 6 резисторов 10K Ом
- 2 резистора 2.2K Ом
- 1 резистор 100K Ом
- 1 резистор 20K Ом
- 15 красных светодиодов 5 мм
- 40 желтых светодиодов 5 мм
- 6 белых светодиодов 5 мм
- 27 резисторов 47 Ом
- 6 резисторов 130 Ом
- 1 резистор 330 Ом
- 5 4-контактных разъемов JST XH 2.54 мм (мужские и женские)
- 2 3-контактных разъема JST XH 2.54 мм (мужские и женские)
- 1 красный светодиод 3 мм и 1 синий светодиод 3 мм
- 2 латунных резьбовых вставки M3
- Болты M3 длиной 12 мм
- Болты M3 длиной 20 мм
- Самоблокирующиеся гайки M3
- Термоусадочные трубки разного размера
- Велосипедная фляга
Инструменты, которые я использовал для этого проекта:
- 3D-принтер с черной нитью PLA.
- Паяльник
- Горячий клей
- Отвертки, плоскогубцы и другие основные инструменты.
Шаг 1: Обзор
Итак, у нас есть проект, который состоит из нескольких частей. Во-первых, это передние и задние фары, которые, помимо традиционного красного и белого света, также имеют поворотники для указания направления. На руле велосипеда расположены дисплей, показывающий скорость, время, пройденные километры и другие интересные данные о поездке. Рядом с дисплеем находятся три кнопки, с помощью которых можно активировать поворотники, переключать страницы на дисплее и включать или выключать фары. Также имеется магнитный датчик, который определяет обороты колёс велосипеда для расчёта скорости. Все эти компоненты соединены с платой, включающей Arduino, модуль реального времени и четыре MOSFET-транзистора для управления светодиодами в фарах. Система питается от литий-ионного аккумулятора, который заряжается через USB-C порт. Все электроника спрятана в флягодержателе велосипеда, но об этом подробнее позже.
Начало разработки было с задней фары.
Шаг 2: Задняя фара
Для задней фары я использовал 15 прозрачных красных 5 мм светодиодов и 28 прозрачных желтых 5 мм светодиодов для поворотников (по 14 на каждый указатель). Важно, чтобы задняя фара была видна даже днем, поэтому я использовал много светодиодов.
Сначала я спроектировал и напечатал на 3D-принтере корпус для фары и плату с отверстиями для монтажа светодиодов. Затем я вставил светодиоды в отверстия платы, аккуратно разместив красные светодиоды в центре и желтые по бокам. Светодиоды соединены попарно с резистором на 47 Ом, а затем все пары соединены параллельно. Для подключения я просто согнул и припаял выводы светодиодов и резисторов. Плата вставлена в корпус, и все соединения защищены горячим клеем, чтобы предотвратить короткие замыкания.
Шаг 3: Дисплей
После завершения задней фары я занялся дисплеем, который будет установлен на руле велосипеда и позволит видеть информацию, такую как скорость и общий маршрут. Дисплей представляет собой небольшой монохромный OLED-экран с диагональю 0,96″ и разрешением 128×64 пикселей с интерфейсом i2c. Для подключения дисплея к Arduino я использовал тот же четырехпроводный кабель, что и для задней фары. Я спроектировал и напечатал на 3D-принтере корпус для дисплея и крепление для руля. Все соединения выполнены и закреплены, и дисплей готов к использованию.
Шаг 4: Кнопки
Далее я подумал о кнопках, которые будут использоваться для активации поворотников и управления дисплеем. У нас будет три кнопки: две для левых и правых поворотников и одна для навигации по страницам дисплея. Для крепления кнопок к велосипеду я напечатал на 3D-принтере крепление, очень похожее на крепление дисплея. Все соединения выполнены и проверены.
Шаг 5: Передние фары
Теперь пришло время сделать передние фары, которые построены по аналогии с задней фарой. Передние фары разделены на две части, каждая из которых будет иметь три белых светодиода для основного света и шесть желтых для указателей поворота. Я спроектировал и напечатал на 3D-принтере корпуса для передних фар и платы с отверстиями для светодиодов. Все соединения выполнены и проверены.
Шаг 6: Датчик скорости
Для измерения скорости я выбрал магнитный датчик, установленный рядом с задним колесом велосипеда, который определяет магнит, прикрепленный к колесу. Этот датчик срабатывает при каждом обороте колеса. Подсчитав количество срабатываний за определенный промежуток времени, Arduino может вычислить скорость в километрах в час, зная окружность колеса. Я использовал датчик эффекта Холла US5881 и подключил его к Arduino.
Шаг 7: Плата управления — Arduino и RTC
Теперь, когда у нас есть все компоненты системы, нужно собрать плату с Arduino, к которой они будут подключены. Я начал с кусочка перфорированной платы размером 55×90 мм, на которой припаял Arduino Nano с помощью разъемов. В дополнение к Arduino я установил модуль реального времени DS3231, который будет поддерживать время на дисплее даже при выключенном Arduino.
Шаг 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
Перед установкой системы нам необходимо загрузить код в 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: Схема батареи
Как я уже говорил, вся система будет питаться от литиевой батареи 18650. Чтобы установить его, я напечатал в 3D простой держатель для батарейки. Чтобы подключить аккумулятор, я восстановил пружинный и плоский контакт из держателя батарейки от старой рождественской гирлянды. Я припаял два провода к двум контактам и приклеил их к двум концам напечатанного на 3D-принтере держателя батареи.
Для зарядки и защиты аккумулятора я решил использовать модуль TP4056 с портом USB-C для зарядки аккумулятора. Я подключил провода, идущие от держателя батареи, к контактам B+ и B- на печатной плате. Модуль имеет два светодиода для индикации состояния зарядки, которые будут скрыты при установке модуля. Поэтому я отпаял их и подключил к маленьким площадкам два прозрачных светодиода диаметром 3 мм с помощью коротких проводов. Эти светодиоды будут размещены рядом с портом USB.
К двум выходным клеммам схемы зарядки я подключил повышающий модуль для доведения напряжения аккумулятора до 5В. В повышающем модуле у меня был порт USB, который я отпаял и заменил двумя проводами, подключенными к положительному и отрицательному выходным контактам. Вы можете использовать любой повышающий модуль с достаточным выходным током, имейте в виду, что минус входа должен быть напрямую связан с минусом выхода. В последнюю очередь поставил кулисный переключатель, прерывающий плюсовой провод, идущий от модуля зарядки к повышающему модулю. Этот переключатель будет использоваться для включения или выключения всей системы. Ниже вы можете найти схему этого участка схемы.
Шаг 11. Как спрятать электронику на велосипеде
Теперь мне нужно было найти место для крепления платы с Ардуино вместе с аккумулятором на велосипеде. За последние несколько месяцев мои велосипедные фары украли трижды, поэтому я хотел, чтобы эта система не закончилась таким же образом. Поэтому я решил спрятать все в бутылке с водой, которая много лет была у меня на велосипеде, но никогда не использовалась. У бутылки есть несколько характеристик, которые делают ее идеальным выбором: она достаточно велика, чтобы вместить плату с Arduino и батарею, защищает их от дождя и, самое главное, никто не расскажет, что находится внутри.
Сначала я вырезал дно бутылки с водой с помощью пилы и сделал отверстие на одной стороне, чтобы пропустить кабели. Затем я напечатал в 3D держатель для крепления бутылки с водой на велосипед и еще одну деталь, на которую можно установить держатель аккумулятора, схему зарядки, переключатель и плату с Arduino. На той стороне, где есть место для порта зарядки, я закрепил модуль зарядки аккумулятора горячим клеем. Затем я установил держатель батареи, используя четыре болта M3x12 мм и самоконтрящиеся гайки. С другой стороны я установил плату с Arduino, используя опять же болты M3x12 мм и самоконтрящиеся гайки, а также несколько напечатанных на 3D-принтере прокладок. Отверстия для крепления платы не включены в 3D-файл, поэтому вы можете просверлить их в нужном положении, чтобы они соответствовали отверстиям на вашей плате. Наконец, я подключил питание +5 В и GND от повышающего преобразователя к плате, а также один провод, идущий непосредственно от положительного полюса батареи для питания RTC, и один провод, идущий от входа повышающего преобразователя, чтобы сделать это возможным. чтобы Arduino считывал напряжение батареи через делитель напряжения.
Шаг 12: Установка системы на велосипед
Спустя 2 месяца работы я наконец смог смонтировать всю систему на своем велосипеде. Я начал с установки подсветки, используя два болта М5, которые мы вставили в корпус, в сочетании с двумя самоконтрящимися гайками. Затем я прикрепил кнопки и дисплей к рулю велосипеда с помощью напечатанного на 3D-принтере кронштейна и винтов M3x20 мм. Два передних фонаря можно установить рядом с центром руля так же, как дисплей и кнопки. Я закрепил кабели стяжками, подведя их к месту установки Arduino и аккумулятора. Затем я установил магнитный датчик колеса с его 3D-печатным креплением рядом с задним колесом, чтобы магнит, прикрепленный к колесу, мог его активировать. И наконец, я прикрепил держатель бутылки к двум резьбам, которые есть в большинстве велосипедов, пропустив кабели через отверстие внизу. Затем я подключил к плате все разъемы JST различных компонентов. Внутри бутылкодержателя я закрепил крепление со всей электроникой и аккумулятором с помощью двух винтов М3 и самоконтрящихся гаек. В последнюю очередь я вставил бутылку, которая просто скользит по креплению с электроникой, в ее держатель и закрепил ее винтами, предварительно просверлив в ней два отверстия. Конечно, в конце концов, может быть, вы увидите, что это не обычная бутылка с водой, но в наши дни у кого нет бутылки с водой с выключателем питания?
Шаг 13: Настройки
Когда мы впервые запускаем всю систему, нам нужно ввести некоторую информацию на дисплей. Для этого включаем выключатель питания, удерживая кнопку SET. Таким образом мы входим в настройки системы. Клавишами «влево» и «вправо» мы можем перемещаться по пунктам меню, а нажатием SET — расширять настройку, в которой находимся. Сначала нам нужно установить время. После входа на соответствующую страницу мы можем нажать SET, чтобы выбрать часы, и использовать кнопки влево и вправо, чтобы увеличить или уменьшить значение. Повторное нажатие SET выбирает минуты, которые можно регулировать, как и часы. Повторным нажатием SET и затем левой или правой клавиши устанавливаем время и выходим из настроек времени. Важно отметить, что время, которое мы вводим, считается текущим временем на момент выхода из настроек времени. Время, конечно, будет поддерживаться модулем RTC. В настройках Колеса нам нужно установить окружность колеса ( а не диаметр ) в метрах. Порядок действий такой же, как и при настройке времени, и с помощью кнопок «влево» и «вправо» изменяем значение в 1 сантиметр. Как только мы выйдем со страницы настроек, значение будет сохранено непосредственно в памяти EEPROM Arduino и не будет потеряно при выключении системы. Последняя страница настроек под названием «Датчики» позволяет нам увидеть, обнаруживает ли магнитный датчик колеса магнит, который мы установили на колесо; это полезно для правильного совмещения магнита с датчиком. Если вы заметили, что когда магнит проходит над датчиком, его состояние последовательно меняется с выключенного на включенное несколько раз , это потому, что ваш магнит на самом деле включает в себя несколько магнитов. В этом случае просто разделите окружность колеса на количество магнитов и установите новое значение; в противном случае значения скорости и общего количества километров будут увеличены вдвое или втрое. Выбрав «Выход» в меню настроек, вы можете, как вы уже догадались, выйти из настроек.
Шаг 14: Использование системы
Теперь, когда система настроена, во время езды мы видим на дисплее скорость, время и общее количество километров. Нажатие SET переводит нас на вторую страницу с другими данными, такими как средняя и максимальная скорость, общее время и время, которое мы фактически проехали, исключая, например, время, когда мы остановились на светофоре. Повторное нажатие SET переводит нас на страницу, с которой мы можем включить или выключить передние или задние фонари или оба. Находясь на этой странице, мы можем выбрать работу со всеми огнями (передними и задними) или только с передними или задними огнями, нажав кнопку со стрелкой влево. Чтобы включить или выключить выбранный нами свет, мы можем использовать кнопку со стрелкой вправо. Дисплей автоматически возвращается на главную страницу через 10 секунд.
Для активации левого и правого указателей поворота мы можем использовать две соответствующие кнопки, а для их выключения можно нажать любую кнопку. Обратите внимание, что поворотники можно активировать только тогда, когда мы находимся на главной странице дисплея. При включении поворотников мы видим на дисплее индикатор, что является очень полезной функцией.
Свет очень яркий, его хорошо видно как днем, так и ночью. Аккумулятора хватает на несколько часов, а когда он разряжается, мы можем зарядить его через порт USB-C.
Шаг 15: Готово!
В целом, я очень доволен тем, как получился этот проект. Я использовал его в течение последних нескольких недель и нашел данные, отображаемые на дисплее, очень интересными. Фары очень важны для безопасной езды ночью. Как всегда, я надеюсь, что это руководство показалось вам интересным и, возможно, полезным.
Часто задаваемые вопросы
1. Сколько времени занимает сборка этой системы?
- В среднем, сборка может занять около 10-15 часов, в зависимости от вашего опыта и наличия всех необходимых материалов.
2. Можно ли использовать другие типы светодиодов?
- Да, вы можете использовать любые светодиоды, которые у вас есть, но учтите, что яркость и потребление энергии могут различаться.
3. Как долго держит заряд аккумулятор?
- В среднем, аккумулятор может держать заряд около 5-6 часов при непрерывной работе фар.
4. Можно ли использовать эту систему на электровелосипеде?
- Да, эта система может быть установлена на любой велосипед, включая электровелосипеды.
5. Что делать, если дисплей не показывает информацию?
- Проверьте соединения и убедитесь, что все провода правильно подключены. Также убедитесь, что дисплей исправен и получает питание.
Джованни Аджустатутто, Италия