Never Ride Alone: умная система безопасности для велосипедистов
Компоненты и материалы
| Компонент | Количество |
|---|---|
| Красный светодиод 5 мм | 2 |
| GPS-модуль u-blox NEO-6M | 1 |
| Arduino MKR Fox 1200 | 1 |
| Литий-ионный аккумулятор 3,7 В | 1 |
| Пьезо-зуммер | 1 |
| Тактильная кнопка 12 мм | 1 |
| Тумблер (slide switch) | 1 |
| Резистор 1 кОм | 1 |
| Вибрационный датчик SW-420 (Grove) | 1 |
Инструменты
- 3D-принтер
О проекте
Автор — Жесус Сориано, студент-электроник, предприниматель и велосипедист. Ездит на велосипеде с 12 лет, часто тренируется в одиночку. Много падений и аварий. Однажды осознал: если произойдёт тяжёлая авария и он потеряет сознание в горах — может погибнуть.
Представьте: вы едете, наслаждаетесь пейзажем — внезапно машина сбивает вас. Вы скатываетесь в овраг, водитель скрывается. Вы без сознания, невидимы для мира. Через 2–3 часа семья начнёт волноваться. Спасатели найдут вас через 4–5 часов. Скорее всего, будет поздно.
Существующие решения (Strava Summit, ANGi от Specialized) требуют сотовой связи, GPS и заряженного телефона. В горах связи часто нет.
Так родился Never Ride Alone (NRA). Автор нашёл Sigfox — сеть LPWAN для передачи данных в горах. Обнаружив Arduino с Sigfox-модулем (MKR FOX 1200) — понял, что это идеальное решение.
Принцип работы
NRA работает как инвертированная сигнализация:
- Выезжаете на тренировку — нажимаете кнопку активации.
- Светодиоды мигают (повышают видимость).
- Если нет движения 30 секунд — включается аварийный режим.
- Зуммер пищит. Если никто не останавливает его (забыли выключить, но вы в порядке) — значит, вы травмированы.
- Автоматически активируется GPS. Координаты отправляются через сеть Sigfox друзьям/семье.
Опрос велосипедистов показал: нужен минималистичный гаджет, простой в использовании, с хорошим аккумулятором. NRA — нажал кнопку и педалируешь. После тренировки — выключил и зарядил.
Корпус из ABS-пласта, напечатан на 3D-принтере (Политехнический университет Валенсии).
Файлы для редактирования/скачивания: Tinkercad.
Компоненты: Arduino MKR FOX 1200, NEO-6M GPS, кнопка, 2 светодиода, резистор 1 кОм, зуммер, вибрационный датчик, аккумулятор 3,7 В 150 мАч.
Настройка Sigfox Backend
На скриншоте — конфигурация callback'а. При получении данных сервер преобразует «сырые» байты в координаты (долгота, широта, высота). На их основе формируется email со ссылкой на Google Maps для всех контактов в настройках.
Сборка
GPS: питание, земля, данные. Антенна клеится на корпус. Arduino устанавливается поверх.
Добавляется аккумулятор с тумблером для вкл/выкл.
Разъём зарядки (должен быть выключен при зарядке).
Зарядка от зарядного устройства для дрона — работает отлично.
После монтажа антенна укладывается над Arduino. Внутрь корпуса всё входит плотно, но аккуратно.
В этой версии вибрационный датчик не использовался. При включении NRA отправляет GPS-координаты каждые 10 минут. Результаты хорошие.
Тесты на дорогах — отличный опыт.
Устройство простое в использовании, даёт чувство уверенности.
Экологическая инициатива
Команда NRA также чистит горы во время тестов. Согласно ЦУР, собирают мусор. Основной мусор в горах — от велосипедистов: обёртки батончиков, гели, покрышки. Создают «чистые точки» на гонках — можно выбросить мусор без остановки. После финиша зона убирается.
Спасибо за внимание, ездьте безопасно! :)
Код (Arduino)
///// NEVER RIDE ALONE V.0.1 /////
//.............................//
// ////AUTOR: JESUS SORIANO ADAM//// ////////FECHA: 05/05/2019 /////// /////////////////////////////////
#include <SigFox.h>
#include <ArduinoLowPower.h>
#include <TinyGPS.h>
#define WAITING_TIME 10
#define GPS_PIN 2
#define GPS_INFO_BUFFER_SIZE 128
bool debug = true;
const int backlights = 4;
const int pulsador = 5;
const int audio = 3;
TinyGPS gps;
int year;
byte month, day, hour, minute, second, hundredths;
unsigned long chars;
unsigned short sentences, failed_checksum;
char GPS_info_char;
char GPS_info_buffer[GPS_INFO_BUFFER_SIZE];
unsigned int received_char;
bool message_started = false;
int i = 0;
struct gpscoord {
float a_latitude;
float a_longitude;
float a_altitude;
};
float latitude = 0.0f;
float longitude = 0.0f;
float altitud = 0;
void Wait(int m, bool s) {
if (debug) {
Serial.print("Waiting: ");
Serial.print(m);
Serial.println(" min.");
}
digitalWrite(LED_BUILTIN, LOW);
if (s) {
int seg = m * 30;
for (int i = 0; i < seg; i++) {
digitalWrite(backlights, HIGH); delay(500);
digitalWrite(backlights, LOW); delay(500);
}
} else {
int seg = m * 15;
for (int i = 0; i < seg; i++) {
digitalWrite(backlights, HIGH); delay(500);
digitalWrite(backlights, LOW); delay(500);
}
}
}
void SendSigfox(String data) {
if (debug) {
Serial.print("Sending: ");
Serial.println(data);
if (data.length() > 12) Serial.println("Message too long, only first 12 bytes will be sent");
}
SigFox.begin();
delay(100);
SigFox.status(); delay(1);
if (debug) SigFox.debug(); delay(100);
SigFox.beginPacket();
SigFox.print(data);
if (debug) {
int ret = SigFox.endPacket(true);
if (ret > 0) Serial.println("No transmission");
else Serial.println("Transmission ok");
Serial.println(SigFox.status(SIGFOX));
Serial.println(SigFox.status(ATMEL));
if (SigFox.parsePacket()) {
Serial.println("Response from server:");
while (SigFox.available()) {
Serial.print("0x");
Serial.println(SigFox.read(), HEX);
}
} else {
Serial.println("Could not get any response from the server");
Serial.println("Check the SigFox coverage in your area");
Serial.println("If you are indoor, check the 20dB coverage or move near a window");
}
Serial.println();
} else {
SigFox.endPacket();
}
SigFox.end();
}
String ConvertGPSdata(const void* data, uint8_t len) {
uint8_t* bytes = (uint8_t*)data;
String cadena;
if (debug) {
Serial.print("Length: ");
Serial.println(len);
}
for (uint8_t i = len - 1; i < len; --i) {
if (bytes[i] < 12) cadena.concat(byte(0));
cadena.concat(char(bytes[i]));
if (debug) Serial.print(bytes[i], HEX);
}
if (debug) {
Serial.println();
Serial.print("String to send: ");
Serial.println(cadena);
}
return cadena;
}
String GetGPSpositon() {
int messages_count = 0;
String pos;
if (debug) Serial.println("GPS ON");
digitalWrite(GPS_PIN, HIGH);
Wait(1, false);
while (messages_count < 5000) {
while (Serial1.available()) {
int GPS_info_char = Serial1.read();
if (GPS_info_char == '$') messages_count++;
if (debug) {
if (GPS_info_char == '$') {
message_started = true;
received_char = 0;
} else if (GPS_info_char == '*') {
for (i = 0; i < received_char; i++) Serial.write(GPS_info_buffer[i]);
Serial.println();
message_started = false;
} else if (message_started) {
if (received_char <= GPS_INFO_BUFFER_SIZE) {
GPS_info_buffer[received_char] = GPS_info_char;
received_char++;
} else {
message_started = false;
received_char = 0;
}
}
}
if (gps.encode(GPS_info_char)) {
gps.f_get_position(&latitude, &longitude);
altitud = gps.altitude() / 100;
gpscoord coords = {altitud, longitude, latitude};
gps.crack_datetime(&year, &month, &day, &hour, &minute, &second, &hundredths);
if (debug) {
Serial.println();
Serial.print("Latitud/Longitud: ");
Serial.print(latitude, 5);
Serial.print(", ");
Serial.println(longitude, 5);
Serial.print("Fecha: ");
Serial.print(day, DEC); Serial.print("/");
Serial.print(month, DEC); Serial.print("/");
Serial.print(year);
Serial.print(" Hora: ");
Serial.print(hour, DEC); Serial.print(":");
Serial.print(minute, DEC); Serial.print(":");
Serial.print(second, DEC); Serial.print(".");
Serial.println(hundredths, DEC);
Serial.print("Altitud (metros): ");
Serial.println(gps.f_altitude());
Serial.print("Rumbo (grados): ");
Serial.println(gps.f_course());
Serial.print("Velocidad(kmph): ");
Serial.println(gps.f_speed_kmph());
Serial.print("Satelites: ");
Serial.println(gps.satellites());
Serial.println();
digitalWrite(backlights, HIGH); delay(500);
digitalWrite(backlights, LOW); delay(500);
}
gps.stats(&chars, &sentences, &failed_checksum);
if (debug) Serial.println("GPS turned off");
digitalWrite(GPS_PIN, LOW);
pos = ConvertGPSdata(&coords, sizeof(gpscoord));
return pos;
}
}
}
pos = "No Signal";
return pos;
}
void setup() {
pinMode(4, OUTPUT);
pinMode(backlights, OUTPUT);
pinMode(pulsador, INPUT);
pinMode(audio, OUTPUT);
if (debug) {
Serial.begin(9600);
while (!Serial) {}
Serial.println("Serial Connected");
}
Serial1.begin(9600);
while (!Serial1) {}
if (debug) Serial.println("GPS Connected");
pinMode(GPS_PIN, OUTPUT);
if (!SigFox.begin()) {
Serial.println("Shield error or not present!");
return;
}
if (debug) SigFox.debug();
else SigFox.end();
}
void loop() {
String position_data = GetGPSpositon();
SendSigfox(position_data);
Wait(WAITING_TIME, false);
}
Ресурсы
- 3D-модель корпуса (под MKR1200, батарею, кнопку, светодиоды, GPS, зуммер): nra_v_0_1_qCsc7I6QvO.zip
- Схема подключения (на фото не MKR1200, но принципиальная схема верна): схема
Производственный процесс
- Интегральные схемы (ИС): компактные мощные чипы, питающие современную электронику
- Освоение оператора switch в C#: синтаксис, примеры и лучшие практики
- Цифровые кости на Arduino: генератор случайных чисел с LCD-дисплеем и кнопкой
- Многопозиционная система датчиков температуры для интеграции в умный дом
- DIY USB-геймпад на Arduino: собственный высокопроизводительный контроллер
- Tetris на Arduino Nano: самостоятельная сборка 16×8 LED-матрицы на 74HC595
- MotionSense: умная система обнаружения проникновения на Arduino и ESP8266
- Набор MOSMusic: MOSFET, Arduino и управление светом по музыке
- Основные компоненты сверлильного станка: подробный обзор
- Ключевые компоненты двигателя внутреннего сгорания