📜 ⬆️ ⬇️

I twist, I twist, I twist, I twist pedals

image

The children grew up and broke the wires on the simulator. Bicycle-board ordered to live long and pedaling was not interesting. I decided to repair the scoreboard in the Nashen way, according to the ios-features.

And did the following steps

Then a little more detail, with the scheme, text, photos and video.

')

Cadence sensor



image
Fig. 1 Photo sensor

The general scheme of the device is simple - the reed switch responds to the approach of a magnet, closes the circuit, the BLE sensor sends a signal about the event.

image
Fig. 2 Circuit Counter Circuit

To create a sensor, you need to buy the following parts

and assemble according to the scheme.
The total cost of the device is less than $ 20.

The size of the sensor, see figure 3, weight - 50 grams.
image
Fig. 3 sensor dimensions

BLE112 must be programmed as follows.

Firmware text
# 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 




Magnet



The magnet is attached to any moving part of your bike, trainer, walker, etc. In Figure 4, the magnet in the form of a washer is stuck to the stepper simulator.

image
Fig. 4 Mount magnet to the connecting rod

image
Fig. 5 When the magnet approaches the sensor, the sensor triggers and sends a signal to the iPad

When the magnet is approached, the reed switch emits a characteristic click - this is useful when debugging a program and verifying the device’s performance.

IOS application


The app consists of three great parts.


Receive event from BLE



Scan the signal from 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 



Animation


First made the road - a bridge to the Crimea. Whoever owned the Crimea - the bridge is needed.
Length 6.2 km. For the track made 256 asphalt landfills with a length of 2 meters and a width of 8 meters. Added the same grass polygons on the right and left shoulders (Figure 6)

image
Figure 6. Bridge
Animation opponent.
The rival will be pulled off the Tour De France. Jan Ulrich. Enough 4 frames to animate Yana. 4 frames for 1 pedal turn. The quality is not so hot, the program was made for the day, so no frills.
image
Figure 7. Jan Ulrich

The animation of self is sacred.
The main time was spent on himself as the main character of the race. I leaned my bike into the corner of the office and climbed onto it, depicting movement.
image
Figure 8. I'm in the office on the bike.

I shifted the pedals evenly 16 times - as a result I made 16 frames, cleaned them in Photoshop, stuck the animation together. After editing in the cartoon, 12 frames remain per pedal turn.

For interest I had to propagate Jan Ulrich to 50 copies. 50 rivals start in the race. This programming is completed.
I note, while debugging - pumped up the trees.

Useful application, I will tell you, just start the race and no longer stop.

In conclusion, 45-second video, as it works on the walker. In addition, the device works fine on a bicycle and exercise bike.



I apologize for the vertical video, but it is clear that the movie was filmed on the 5th iPhone).

Thanks to all. Spin the pedals.

UPD. Added a picture with a girl. Where are you guys going?

Source: https://habr.com/ru/post/230101/


All Articles