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

    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. Добавил картинку с девушкой. Куда катимся, парни?
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 26
    • +4
      Когда ждать гаджет в продаже, а программу в аппсторе?
      • +11
        И мультиплеер :)
        • +1
          Особо хитрые будут жульничать с магнитом, не крутя педали.
        • +1
          По-моему, таких приборов полно, фирменное устройство стоит $200. Мое устройство совместимо с существующими программами Аппстора. В этих приложениях нет анимации — просто числа, время и RPM.
        • +3
          Прекрасный пост!!! Больше всего мне нравится название и теги. А что будет когда вы проедете 6.2 км? Трасса закончится?
          • +2
            На финише — девушки, если Вы доехали в тройке, подведение итогов, лучшее время — в сеть.
          • +4
            Плагин Road Rash планируется?
            • +8
              Наверное, самые суровые отладка и тестирование за всю карьеру.
              • +1
                Кааааак же этого не хватает на беговой дорожке! И вентилятора еще :)
                • +5
                  Ещё окулус рифт и пару сценариев вроде бега от полиции или зомби. Сразу эффективность повысится.
                  • НЛО прилетело и опубликовало эту надпись здесь
                • +3
                  Анимацию, пожалуйста, вон с той девушкой на первой фотографии :)
                  • +3
                    Вот, кстати, да. Наблюдать за… хм… спиной девушки-велосипедистки гораздо интереснее)
                    • 0
                      Так Вам девушку или первое место на финише?)
                      • 0
                        both!
                        • +2
                          А зачем финиш? Если перестаешь крутить педали то девушка начинает удаляться от тебя и придется её догонять.
                      • +9
                        Той не было, сделал с другой

                        image
                      • +1
                        А вот это например, подойдет m.aliexpress.com/item/1935894000.html?
                        • +1
                          Хочу такое приложение. Передатчик-то собрать не вопрос, тренажер тоже есть, а вот приложение я не нашел такое, чтобы был и интерактив и зависимость от скорости. Можно его запустить отдельно от айпада, которого у меня нет? На маке напрмиер?
                          • 0
                            BLE API есть для OS X. Надо заменить UI* на NS*.
                            Однако, я не проверял.
                            Для эмулятора точно не работает.
                            • 0
                              дешево и сердито, респект!

                              Но для тренировок (на велике точно), полагаю, не очень подходит — сложно прописать взаимосвязь между нагрузкой и реальной скоростью.
                              станки для велика с VR стоят весьма бодро.
                              сейчас погуглил и нашел: tourdegiro.com — нужно будет попробовать (нужны как минимум датчики скорости и каденса ANT+)
                              • 0
                                Для тренажера легко адаптируется — один датчик на маховик, который будет снимать скорость его вращения, второй на ручку, которая регулирует усилие. Скорость маховика домножаем на усилие и получаем скорость передвижения, чем труднее крутить педали — тем быстрее едем.
                            • 0
                              Вадим, как и чем и прошивали BLE112?
                            • 0
                              без моста обойдёмся…

                              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.