Как стать автором
Обновить

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

Время на прочтение 8 мин
Количество просмотров 33K
image

Дети подросли и оборвали провода на тренажере. Вело-табло приказало долго жить и крутить педали стало не интересно. Я решил починить табло по-нашенски, по 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
//

#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 км. Для трассы сделал 256 асфальтовых полигонов длиной 2 метра и шириной 8 метров. Добавил столько же травяных полигонов по правой и левой обочинам (рисунок 6)

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

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

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

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

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

В заключении 45-секундное видео, как это работает на шагоходе. Кроме того, прибор отлично работает на велосипеде и велотренажере.



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

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

UPD. Добавил картинку с девушкой. Куда катимся, парни?
Теги:
Хабы:
+62
Комментарии 26
Комментарии Комментарии 26

Публикации

Истории

Работа

iOS разработчик
23 вакансии
Swift разработчик
38 вакансий

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн
PG Bootcamp 2024
Дата 16 апреля
Время 09:30 – 21:00
Место
Минск Онлайн
EvaConf 2024
Дата 16 апреля
Время 11:00 – 16:00
Место
Москва Онлайн