Дети подросли и оборвали провода на тренажере. Вело-табло перестало работать и крутить педали стало совсем не интересно. Я решил починить табло по-нашенски, по ios-овски.
И проделал следующие шаги
- примотал простейший BLE датчик к корпусу тренажера
- прилепил магнит к шатуну
- написал программу под iPad
Далее чуть-чуть подробнее, со схемой, текстом, фото и видео.
Cadence sensor
Рис. 1 Фотография датчика
Общая схема работы устройства простая — геркон реагирует на приближение магнита, замыкает цепь, BLE-датчик посылает сигнал о событии.
Рис. 2 Схема счетчика обормотов
Для создания датчика необходимо купить следующие детали
- BLE112 — блутуз-контроллер компании BlueGiga
- литиевую батарейку 3 вольта
- геркон (на схеме S1)
- сопротивление и два конденсатора
- черную коробочку
и собрать согласно схеме.
Общая стоимость устройства — менее $20.
Размер датчика смотрите на рисунке 3, вес — 50 граммов.
Рис. 3 Размеры датчика
BLE112 необходимо запрограммировать следующим образом
# Cadence sensor prototype
dim tmp(12)
dim counter
dim result
dim last
dim sleep_counter
dim awake
dim connected
event system_boot(major,minor,patch,build,ll_version,protocol,hw)
# call gap_set_mode(gap_general_discoverable,gap_undirected_connectable)
# call sm_set_bondable_mode(1)
# call hardware_set_soft_timer(32000 * 30, 0, 0)
# Set pins P1_0, P1_1 as output to prevent current leak (BLE112_Datasheet.pdf section 2.1)
call hardware_io_port_config_direction(1, 3)(result)
call hardware_io_port_write(1, 3, 3)(result)
# # Pull P0 up and enable interrupts on P0_0 (on falling edge)
#call hardware_io_port_config_pull(0, 0, 1)(result)
call hardware_io_port_config_irq(0, 1, 0)(result)
end
event hardware_soft_timer(handle)
if connected = 0 then
sleep_counter = sleep_counter + 1
if sleep_counter >= 2 then
# go to sleep
# disable timer
call hardware_set_soft_timer(0, 0, 0)
awake = 0
# disable BT broadcast
call gap_set_mode(gap_non_discoverable, gap_non_connectable)
end if
else
# read battery level
call hardware_adc_read(15,3,0)
end if
end
event hardware_io_port_status(timestamp, port, irq, state)
# Debounce filter: ignore events with rates > ~180 RPM
if timestamp > (last + 10000) then
if awake = 0 then
call gap_set_mode(gap_general_discoverable, gap_undirected_connectable)
#call sm_set_bondable_mode(1)
call hardware_set_soft_timer(32000 * 60, 0, 0) # single shot sleep timer
awake = 1
end if
sleep_counter = 0
counter = counter + 1
result = timestamp >> 5
# S+C
tmp(0:1) = $3
tmp(1:4) = counter
tmp(5:2) = result
tmp(7:2) = counter
tmp(9:2) = result
call attributes_write(xgatt_cadence, 0, 11, tmp(0:11))
end if
last = timestamp
end
event hardware_adc_result(input,value)
#battery level reading received, store to gatt
if input = 15 then
call attributes_write(xgatt_battery, 0, 2, value)
end if
end
event connection_status(connection, flags, address, address_type, conn_interval, timeout, latency, bonding)
connected = 1
end
event connection_disconnected(handle,result)
call gap_set_mode(gap_general_discoverable, gap_undirected_connectable)
connected = 0
end
Магнит
Магнит крепится к любой двигающейся части Вашего велосипеда, тренажера, шагожора и т.д. На рисунке 4 магнит в виде шайбы прилеплен к шаго-тренажеру.
Рис. 4 Крепление магнита к шатуну
Рис. 5 При приближении магнита к датчику, датчик срабатывает и посылает сигнал на iPad
При приближении к магниту геркон издает характерный щелчок — это полезно при отладке программы и проверки работоспособности устройства.
Приложение под iOS
Приложение состоит из трех замечательных частей
- часть первая — прием события от BLE
- часть вторая — расчет и отображение данных полета
- часть третья — 3D анимация
Прием события от BLE
//
// BTLE.m
// doraPhone
//
// Created by Kirill Novichikhin on 2/5/13.
//
//
#import "BTLE.h"
#import "AppDelegate.h"
static CBUUID
*kServiceCbuuidCadence,
*kServiceDeviceInfo,
*kCharacteristicDeviceModel,
*kCharacteristicDeviceSerial,
*kCharacteristicCadence
;
static const char* cbCentralStateNames[] = {
"CBCentralManagerStateUnknown",
"CBCentralManagerStateResetting",
"CBCentralManagerState",
"CBCentralManagerStateUnauthorized",
"CBCentralManagerStatePoweredOff",
"CBCentralManagerStatePoweredOn"
};
static const char* btleStateName(int state)
{
const char* stateName = "INVALID";
if (state >= 0 && state < sizeof(cbCentralStateNames)/sizeof(const char*)) {
stateName = cbCentralStateNames[state];
}
return stateName;
}
@implementation BTLE
+ (void)initialize
{
kServiceCbuuidCadence = [CBUUID UUIDWithString:@"1816"];
kServiceDeviceInfo = [CBUUID UUIDWithString:@"180A"];
kCharacteristicDeviceModel = [CBUUID UUIDWithString:@"2A24"];
kCharacteristicDeviceSerial = [CBUUID UUIDWithString:@"2A25"];
kCharacteristicCadence = [CBUUID UUIDWithString:@"2A5B"];
}
- (void)startScan
{
if (![self isLECapableHardware]) {
return;
}
[_manager scanForPeripheralsWithServices:@[kServiceCbuuidCadence]
options:@{CBCentralManagerScanOptionAllowDuplicatesKey: @YES}];
NSLog(@"Started BLE scan");
}
- (void)stopScan
{
[_manager stopScan];
}
- (void)centralManagerDidUpdateState:(CBCentralManager *)central
{
NSLog(@"New Bluetooth state: %s", btleStateName(central.state));
switch (central.state) {
case CBCentralManagerStatePoweredOn:
[self startScan];
break;
case CBCentralManagerStateResetting:
case CBCentralManagerStateUnauthorized:
case CBCentralManagerStateUnknown:
case CBCentralManagerStateUnsupported:
case CBCentralManagerStatePoweredOff:
break;
}
}
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error
{
NSLog(@"Discovered services for peripheral");
for (CBService* s in peripheral.services) {
NSLog(@"Service: %@", s.UUID);
}
for (CBService* s in peripheral.services) {
if ([s.UUID isEqual:kServiceDeviceInfo]) {
NSLog(@"Device info service found");
[peripheral discoverCharacteristics:[NSArray arrayWithObjects:kCharacteristicDeviceModel, kCharacteristicDeviceSerial, nil] forService:s];
} else if ([s.UUID isEqual:kServiceCbuuidCadence]) {
NSLog(@"Cadence service found");
[peripheral discoverCharacteristics:@[kCharacteristicCadence] forService:s];
}
}
}
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error
{
if ([service.UUID isEqual:kServiceCbuuidCadence]) {
for (CBCharacteristic* c in service.characteristics) {
if ([c.UUID isEqual:kCharacteristicCadence]) {
NSLog(@"Found characteristic: Cadence");
[peripheral setNotifyValue:YES forCharacteristic:c];
} else {
NSLog(@"Discovered unsupported characteristic %@", c.UUID);
}
}
} else if ([service.UUID isEqual:kServiceDeviceInfo]) {
for (CBCharacteristic* c in service.characteristics) {
NSLog(@"Discovered characteristic %@", c.UUID);
if ([c.UUID isEqual:kCharacteristicDeviceModel] || [c.UUID isEqual:kCharacteristicDeviceSerial]) {
[peripheral readValueForCharacteristic:c];
}
}
} else {
NSLog(@"ERROR: got characteristics for service %@ - was not requesting those", service.UUID);
return;
}
}
- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral
{
NSLog(@"Connected peripheral %@", peripheral);
AppDelegate *appRoot = (AppDelegate *)[[UIApplication sharedApplication] delegate];
// TODO
appRoot.isConnected = true;
// FIXME: delegate needs to be set to blePeripheral
peripheral.delegate = self;
[peripheral discoverServices:@[kServiceCbuuidCadence, kServiceDeviceInfo]];
}
-(void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error
{
if ([characteristic.UUID isEqual:kCharacteristicCadence]) {
NSData* data = characteristic.value;
AppDelegate *appRoot = (AppDelegate *)[[UIApplication sharedApplication] delegate];
// TODO
appRoot.serial = _serial;
appRoot.model = _model;
[appRoot performSelectorOnMainThread:@selector(newCadenceMeasurement:)
withObject:data
waitUntilDone:NO];
} else if ([characteristic.UUID isEqual:kCharacteristicDeviceModel]) {
NSString* model = [[NSString alloc] initWithData:characteristic.value encoding:NSUTF8StringEncoding];
NSLog(@"Device model: %@", _model);
_model = model;
} else if ([characteristic.UUID isEqual:kCharacteristicDeviceSerial]) {
// // Convert to a hex string
_serial = [[NSString alloc] initWithData:characteristic.value encoding:NSUTF8StringEncoding];
NSLog(@"Device serial: %@", _serial);
} else {
NSLog(@"ERROR: unexpected BLE Notify: %@ %@=%@", peripheral, characteristic.UUID, characteristic.value);
}
}
- (void)centralManager:(CBCentralManager *)central didDisconnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error
{
AppDelegate *appRoot = (AppDelegate *)[[UIApplication sharedApplication] delegate];
appRoot.isConnected = false;
self.peripheral = nil;
// BLEPeripheral* blePeripheral = [_peripherals ensurePeripheral:peripheral];
NSLog(@"Disconnected from %@ (%@)", peripheral.name, error.description);
}
- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI
{
if (self.peripheral == nil) {
[central connectPeripheral:peripheral options:nil];
NSLog(@"Connecting to "%@"", peripheral.name);
self.peripheral = peripheral;
}
}
/*
Uses CBCentralManager to check whether the current platform/hardware supports Bluetooth LE. An alert is raised if Bluetooth LE is not enabled or is not supported.
*/
- (BOOL)isLECapableHardware
{
BOOL result = FALSE;
BOOL unknownState = NO;
NSString * errorString = nil;
int state = [_manager state];
switch (state)
{
case CBCentralManagerStateUnsupported:
errorString = @"The platform/hardware doesn't support Bluetooth Low Energy.";
break;
case CBCentralManagerStateUnauthorized:
errorString = @"The app is not authorized to use Bluetooth Low Energy.";
break;
case CBCentralManagerStatePoweredOff:
errorString = @"Bluetooth is currently powered off.";
break;
case CBCentralManagerStatePoweredOn:
result = TRUE;
case CBCentralManagerStateUnknown:
default:
unknownState = YES;
errorString = @"Unknown state";
;
//result = FALSE;
}
const char* stateName = btleStateName(state);
NSLog(@"Central manager state: %s (%u)", stateName, state);
if (!result && !unknownState) {
UIAlertView *alert = [[UIAlertView alloc] init];
alert.message = errorString;
[alert addButtonWithTitle:@"OK"];
[alert show];
}
return result;
}
- (id)init
{
_queue = dispatch_queue_create("ru.intersofteurasia.do-ra.ble", NULL);
_manager = [[CBCentralManager alloc] initWithDelegate:self queue:_queue];
return self;
}
- (void)dealloc
{
[self stopScan];
}
@end
Анимация
Быстро делаем трассу — мост в Крым. Кто-бы не владел Крымом — мост нужен. Длина 6.2 км. Ширина 10 метров. Я сделал 256 асфальтовых полигонов длиной 2 метра и столько же травы по обочинам (рисунок 6)
Рисунок 6. Мост
Анимация соперника.
Соперник взят с Тур Де Франс. Ян Ульрих. Достаточно 4-ех кадров для анимации Яна. 4 кадра на 1 оборот педалей. Качество не ахти, программа была сделана за день, поэтому без изысков.
Рисунок 7. Ян Ульрих
Анимация себя — это святое.
Основное время ушло на себя. Я прислонил велосипед в угол офиса и взгромоздился на него, изображая движение.
Рисунок 8. Я в офисе на велосипеде.
16 раз равномерно крутанул педали — сделал 16 кадров, почистил в фотошопе, склеил анимацию. После редактирования осталось 12 кадров на 1 оборот педалей.
Для интереса пришлось размножить Яна Ульриха до 50 копий и программа завершена.
Замечу, пока отлаживался — накачал ляхи.
Полезное приложение, скажу Вам, только начинаешь гонку и уже не остановиться.
В заключении 45-секундное видео, как это работат
Извиняюсь за вертикальное видео, зато видно, что снималось на 5-ый iPhone).
Всем спасибо. Крутите педали.
Автор: PapaBubaDiop