Кручу, кручу, кручу, кручу педали, кручу

от автора

Дети подросли и оборвали провода на тренажере. Вело-табло перестало работать и крутить педали стало совсем не интересно. Я решил починить табло по-нашенски, по ios-овски.

И проделал следующие шаги

  • примотал простейший BLE датчик к корпусу тренажера
  • прилепил магнит к шатуну
  • написал программу под iPad

Далее чуть-чуть подробнее, со схемой, текстом, фото и видео.

Cadence sensor

image
Рис. 1 Фотография датчика

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

image
Рис. 2 Схема счетчика обормотов

Для создания датчика необходимо купить следующие детали

  • BLE112 — блутуз-контроллер компании BlueGiga
  • литиевую батарейку 3 вольта
  • геркон (на схеме S1)
  • сопротивление и два конденсатора
  • черную коробочку

и собрать согласно схеме.
Общая стоимость устройства — менее $20.

Размер датчика смотрите на рисунке 3, вес — 50 граммов.
image
Рис. 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 магнит в виде шайбы прилеплен к шаго-тренажеру.

image
Рис. 4 Крепление магнита к шатуну

image
Рис. 5 При приближении магнита к датчику, датчик срабатывает и посылает сигнал на iPad

При приближении к магниту геркон издает характерный щелчок — это полезно при отладке программы и проверки работоспособности устройства.

Приложение под iOS

Приложение состоит из трех замечательных частей

  • часть первая — прием события от BLE
  • часть вторая — расчет и отображение данных полета
  • часть третья — 3D анимация

Прием события от BLE

Сканируем сигнал от 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)

image
Рисунок 6. Мост
Анимация соперника.
Соперник взят с Тур Де Франс. Ян Ульрих. Достаточно 4-ех кадров для анимации Яна. 4 кадра на 1 оборот педалей. Качество не ахти, программа была сделана за день, поэтому без изысков.
image
Рисунок 7. Ян Ульрих

Анимация себя — это святое.
Основное время ушло на себя. Я прислонил велосипед в угол офиса и взгромоздился на него, изображая движение.
image
Рисунок 8. Я в офисе на велосипеде.

16 раз равномерно крутанул педали — сделал 16 кадров, почистил в фотошопе, склеил анимацию. После редактирования осталось 12 кадров на 1 оборот педалей.

Для интереса пришлось размножить Яна Ульриха до 50 копий и программа завершена.
Замечу, пока отлаживался — накачал ляхи.

Полезное приложение, скажу Вам, только начинаешь гонку и уже не остановиться.

В заключении 45-секундное видео, как это работат

Извиняюсь за вертикальное видео, зато видно, что снималось на 5-ый iPhone).

Всем спасибо. Крутите педали.

ссылка на оригинал статьи http://habrahabr.ru/post/230101/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *