Slotbaer / 7 Tage im Oktober / Tag 2
 

22.10.2013 7 Tage im Oktober Tag 2.

//
//  CBTBlue.h
//  CBT
//
//  Created by Brumbaer on 21.10.13.
//  Copyright (c) 2013 Brumbaer. All rights reserved.
//

#import <Foundation/Foundation.h>
#import <CoreBluetooth/CoreBluetooth.h>

@class CBTBlue;

@protocol CBTBlueDelegate 

- (void) blueDidUpdateState: (CBTBlue*) bt;
- (void) blue: (CBTBlue*) bt didReceiveData: (NSData*) data;
@end



@interface CBTBlue : NSObject <CBCentralManagerDelegate, CBPeripheralDelegate>

@property (nonatomic, strong, readonly) CBCentralManager *manager;
@property (nonatomic, strong, readonly) CBPeripheral *peripheral;
@property (nonatomic, strong, readonly) CBCharacteristic *characteristic;

@property (nonatomic, readonly, nonatomic) NSString *state;

- (instancetype) initWithDelegate: (id) delegate;

- (void) addDelegate: (id) delegate;
- (void) removeDelegate: (id) delegate;
- (void) removeAllDelegates;

- (void) writeData: (NSData*) data;

@end

Ziel für heute war die BT Verbindung zwischen Programm und Converter.

Ich habe längere Zeit nichts für das iPad programmiert also gibt es ein paar neue Dinge auszuprobieren. Storyboards, Constraints und Modern Objective C, um nur drei zu nennen. Ich bin einer von denen, die kurze Programmstücke schreiben und dann kompilieren und testen. Vor allem, wenn ich mich mit Dingen beschäftige, die ich noch nicht kenne. Um den Kompilier/Test Zyklus möglichst kurz zu halten, wäre es schön Alles im Simulator testen zu können. Mein MacPro hat aber kein BT 4.0, also kann das nicht gehen. Herr Google meint, das geht aber mit einem BT 4.0 Stick. Also los zu dem Elektronik Fach Markt in nur 5 Minuten Entfernung. Das mit dem "Fach" war übertrieben - kein BT 4.0 Dongle. Auf zum nächsten Elektronik Markt, etwa genauso weit entfernt und an einer vierspurigen Ausfallstraße gelegen. Zwischen den zwei Märkten befinden sich 5 Ampeln. Und alle rot bei Tempo 50. Statt 5 Minuten dauert die Fahrt 10 Minuten. Aber wenigstens gibt es Parkplätze und USB-BT 4.0 Dongle.

Rasch nach Hause und weil Gott gerecht ist, sind in der anderen Richtung auch alle Ampeln rot.

Nach einer Stunde bin ich zu Hause und versuche den Dongle zum Laufen zu bringen. Nichts geht, selbst das kext Patchen bringt nichts. Ich geb's nach einer weiteren Stunde auf. Da habe ich ja richtig Zeit gespart, ziemlich genau -2 Stunden :(

//
//  CBTBlue.m
//  CBT
//
//  Created by Brumbaer on 21.10.13.
//  Copyright (c) 2013 Brumbaer. All rights reserved.
//

#import "CBTBlue.h"

static NSString * const kServiceUUID = @"FFE0";
static NSString * const kCharacteristicUUID = @"FFE1";

@implementation CBTBlue {
	
	NSMutableArray* _delegates;
}

- (id) init {
	
	if ((self = [super init]) != nil) {
		
		_manager = [[CBCentralManager alloc] initWithDelegate: self queue: nil]; // GCC verwendet die Main queue, aber queue was macht die Global Queue ?
		
		_delegates = [NSMutableArray new];
		_state = NSLocalizedString (@"Nicht verbunden", nil);
    }
	return self;
}

- (instancetype) initWithDelegate: (id) delegate {
	
	if ((self = [self init]) != nil) {
		[self addDelegate: delegate];
	}
	return self;
}


- (void) addDelegate:(id)delegate {
	
	[_delegates addObject: delegate];
	if ([delegate respondsToSelector: @selector(blueDidUpdateState:)])
		[delegate blueDidUpdateState: self];
}

- (void) removeDelegate:(id)delegate {	[_delegates removeObject: delegate]; }
- (void) removeAllDelegates { [_delegates removeAllObjects];}

- (void)centralManagerDidUpdateState:(CBCentralManager *)central {
    switch (central.state) {
        case CBCentralManagerStatePoweredOn:
            // Scans for any peripheral
            [central scanForPeripheralsWithServices:@[ [CBUUID UUIDWithString: kServiceUUID] ] options:@{}];
			_peripheral = nil;
			_characteristic = nil;
			_state = NSLocalizedString (@"Scanne", nil);
            break;
        case CBCentralManagerStatePoweredOff:
			_state = NSLocalizedString (@"Aus geschaltet", nil);
            break;
        case CBCentralManagerStateUnauthorized:
			_state = NSLocalizedString (@"Keine Freigabe", nil);
            break;
        case CBCentralManagerStateResetting:
			_state = NSLocalizedString (@"Reset", nil);
            break;
        case CBCentralManagerStateUnsupported:
			_state = NSLocalizedString (@"Kein Bluetooth", nil);
            break;
        default:
            NSLog(@"Central Manager did change state");
            break;
    }
	[self didUpdateState];
}

- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI {
	
	NSString* name = [advertisementData objectForKey: CBAdvertisementDataLocalNameKey];
    
	if (self.peripheral != peripheral && [name isEqualToString: @"Slotbaer"]) {
		[central stopScan];
		
        _peripheral = peripheral;
		_state = [NSString stringWithFormat: NSLocalizedString (@"Verbinde mit: %@", nil), peripheral.name];
        [central connectPeripheral: peripheral options:nil];
		[self didUpdateState];
    }
}

- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral {
	_state = [NSString stringWithFormat: NSLocalizedString (@"Verbunden mit: %@", nil), peripheral.name];
	[peripheral discoverServices: nil];
	[peripheral setDelegate: self];
	[self didUpdateState];
}

- (void)centralManager:(CBCentralManager *)central didDisconnectPeripheral:(CBPeripheral *)peripheral error:(NSError *)error {
	_state = [NSString stringWithFormat: NSLocalizedString (@"Verbindung zu %@ beendet", nil), peripheral.name];
	_peripheral = nil;
	_characteristic = nil;
	[self didUpdateState];
	
	[self centralManagerDidUpdateState: central];
}

- (void) peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error {
	
	for (CBService* service in peripheral.services)
		[peripheral discoverCharacteristics: @[[CBUUID UUIDWithString: kCharacteristicUUID]] forService: service];
	
	[self didUpdateState];
}

- (void) peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error {
	
	for (CBCharacteristic* charcteristic in service.characteristics) {
		[peripheral setNotifyValue: YES forCharacteristic: _characteristic = charcteristic];
	}
	
	_state = NSLocalizedString (@"Characteristiken gefunden", nil);
	
	[self didUpdateState];
}

- (void) peripheral:(CBPeripheral *)peripheral didWriteValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error {
}

- (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error {
	
	if (characteristic.isNotifying || characteristic.isBroadcasted) {
        [peripheral readValueForCharacteristic:characteristic];
		if (characteristic.value.length) {
			[_delegates enumerateObjectsUsingBlock:^(id delegate, NSUInteger idx, BOOL *stop) {
				if ([delegate respondsToSelector: @selector(blue:didReceiveData:)])
					[delegate blue: self didReceiveData: characteristic.value];
			}];
		}
	}
}

- (void) didUpdateState {
	
	[_delegates enumerateObjectsUsingBlock:^(id delegate, NSUInteger idx, BOOL *stop) {
		if ([delegate respondsToSelector: @selector(blueDidUpdateState:)])
			[delegate blueDidUpdateState: self];
	}];
}

- (void) writeData: (NSData*) data {

	if (self.peripheral && self.peripheral.state == CBPeripheralStateConnected && self.characteristic) {
		[self.peripheral writeValue: data forCharacteristic: _characteristic type: CBCharacteristicWriteWithResponse];
	}
}

@end

Also "dann man Butter bei die Fische" und XCode gestartet. Ein neues iOS Project angelegt CBT - Carrera BlueTooth) und zwar das "Single View" Projekt, dass auch gleich ein Storyboard mit anlegt und initialisiert. Storyboards sollen u.A. die Übersichtlichkeit erhöhen und Beziehungen zwischen den Views verdeutlichen. Bisher hieß es immer "ein Resource File für jeden ViewController sind gut für Dich und Deine Applikation" und machen auch noch ein seidiges Fell. Storyboards sind das genaue Gegenteil - alles in ein Resource File gepackt - und ob sie ein seidiges Fell machen oder sich einen die Haare raufen lassen wird sich noch zeigen müssen.

Ein Viewcontroller ist schon angelegt und offensichtlich kann man UI Elemente genauso platzieren wie in den xibs.

So weit so Gehöft. Nun zu den Constraints. Ein Blick in die Dokumentation, zeigt Constraints sind Klasse. Mit einem Constraint stellt man eine Beziehung zwischen Positions- oder Größeneigenschaften von UI Elementen her. Z.B. der linke Rand von dem Element soll 5 Pixel vom rechten Rand des anderen Elements entfernt sein, und die Mitte des Elements soll bei 20% der Breite des übergeordneten Elementes sein. Und dann werden die Elemente automatisch entsprechend platziert under Berücksichtigung der Constraints und der "Gegebenen Objektrgröße". Manche Objekte habe nämlich keine feste Größe, sonder deren Größe richtet sich nach ihrem Inhalt. Die Breite eines Label ändert sich z.B. mit der Länge des Textes. Dank des Contstraints bleibt die Mitte des Labels aber immer noch bei 20% der Breite des Superviews und das andere Element wird automatisch so verschoben, dass linker und rechter Rand der Elemente 5 Pixel von einander entfernt sind. Und wird das iPad gedreht, wird alles automatisch angepasst.

Boah ey, ist das krass. Das Dumme ist nur, dass man alle Möglichkeiten nur nutzen kann, wenn man die Constraints im Programmtext definiert. Verwendet man den IB für die Constraints und will man mehr als die einfachsten Arten von Constraints verwenden, besteht keine Chance mehr für ein seidiges Fell. Die wichtigste Einschränkung ist, dass man über den IB nur Offset verwenden kann obwohl Constraints auch Faktoren enthalten können. Definiert man die Constraints im Programmtext kann man z.B. jedes von 9 Elementen so setzen, dass ihre Mitten gleichmäßig im übergeordneten Element verteilt sind. man setzt die Mitten einfach auf das 0.1, 0.2 .. 0.9 fache der Breite des übergeordneten Elements. Im IB geht das nicht - was dazu führt, dass beim Drehen des iPads die Elemente nicht mehr gleichmäßig über die Breite verteilt sind. 

Constraints zweier Elemente werden dem nächsten Element zugeordnet, dem beide Elemente untergeordnet sind. Das kann auch eins der beiden Elemente sein. Dadurch findet man eine Menge Constraints unter vielen Views. Allerdings ist nicht leicht erkennbar zu welchen Elementen sie gehören. Man kann sie auch nicht anordnen um das Chaos zu bändigen.

Gott sei dank, werden im Element Inspector die zum Element gehörigen Constraints unter den "Dimensions" Einstellungen angezeigt. Aber um eine Constraint zu editieren, muss man es öffnen. Nicht mit einem Doppel Click, was zu einfach wäre, sonder über ein PullDown Menü - dann öffnet sich der Constraint Editor, aber unter dem Eigenschaften Tab. Will man den nächsten Constraint des selben oder eines anderen Elementes ändern, muss man das Element anwählen, zum Dimension-Tab, wechseln,das PopUp aufrufen, zum Eigenschaften-Tab wechseln, in das Textfeld Klicken - danach erscheint die Ernährung eines Eichhörnchens nicht mehr so mühsam.

Es gibt noch eine dritte Art Constraints zu editieren. Wählt man ein Element an, erscheinen Constraint Linien in der Hauptansicht. Diese kann man Doppelclicken und dann erscheint ein Constraint Editor. Manchmal sind sie aber so kurz, dass man sie nicht sieht oder sie sind überdeckt von anderen Elementen und man schaltet zwischen den Elementen um, statt den Constrainteditor zu öffnen.

Für das Erstellen von Constraint gibt es auch mehrere Methoden. Einige sind brauchbar, andere inexakt, was dazu führt, dass man den falschen Constraint erstellt. Aber das Schlimmste ist, dass man nur eine Handvoll von Eigenschaften in Relation setzen kann. Mann kann z.B. nicht die Mitte eines Elements mit dem linken Rand eines anderen Elements in Übereinstimmung bringen. Wenn man die Constraints im Programm erstellt schon. 

Zu allem Überfluss überschreibt die "graphische" Methode der Constrainterstellung den Mechanismus der für die Verbindung eines Elements mit einem IBOutlet verwendet wurde. D.h. man muss diese Verbindung nun über den Inspector machen.

Die IB Anbindung der Constraints ist so seidig, wie das Fell eines Rauhaardackels gegen den Strich gebürstet.

Trotzdem um das Label 20 Pixel vom linken Rand beginnen und 20 Pixel vom rechten Rand enden zu lassen und 20 Pixel unter der Top Layout Guide beginnen zu lassen. langt es. Die Unterkante wird nicht definiert, dadurch berechnet iOS sie aus der Oberkante und der Höhe des Labels. Die Top Layout Guide ist eine imaginäre Linie, die die Oberkante des verfügbaren Platzes darstellt. Sie verschiebt sich automatisch, je nachdem ob ein Status und/oder, NavigationBar vorhanden ist. Praktisch.

Wieder eineinhalb Stunden verbracht mit Lesen und Ausprobieren und Harre raufen. Zugegebenerweise zum Teil über Dinge, die im Moment gar nicht relevant sind.

Aber jetzt geht's richtig los.

Core Bluetooth bietet keine Einzelklasse, die das macht, was eigentlich fast immer haben will. Also werde ich eine Schreiben. Die Aufgabe dieser Klasse wird es sein, den Converter zu finden und Daten von ihm zu empfangen und an ihn zu senden. Die Klasse bekommt den Namen CBTBlue und wird im ViewController als readonly Property gespeichert, Sie wird in der viewDidLoad: Methode initialisiert.

Sie bekommt erstmal nur eine init Routine und als readonly property einen CBCentralManager. Dieser wird in der init Methode initialisiert. Die init Methode des Managers erwarten einen Delegaten und eine DispatchQueue. Letztere kann man nil setzen, ersterer wird CBTBlue.

Da CBTBlue, der Delgate wird, werden erst einmal alle Delegat ´Methoden hinzugefügt und mit NSLogs versehen, damit ich sehe ob und wann sie aufgerufen werden.

Programm läuft und das iPad verlangt das Einschalten von Bluetooth und der Manager meldet PowerOn.

Ok. CBTBlue etwas aufbohren. Zum einen gibt es  jetzt ein state property. Einfach nur ein String, der den gegenwärtigen Zustand der Bluetoothverbindung widerspiegelt. Dient zum einen der Anzeige und zum anderen der Dokumentation, so dass ich weiß was hier passiert ist.

centralManagerDidUpdateState wird so erweitert, dass je nach Status ein entsprechender String in state gespeichert wird. Und wenn PowerOn erkannt wird, wird ein Scan nach geeigneten Geräten gestartet. Die Geräte können mit Hilfe einer ServiceID vorselektiert werden. Die ServiceId kann man mit LightBlue ermitteln.

Kompilieren und Starten und sie da didDiscoverPeripheral wird aufgerufen. Die Methode bekommt eine Dictionary mit advertismentData übergeben. Eines der Objete darin ist der Name des Geräts. Den habe ich ja auf Slotbaer gesetzt, also werden alle Geräte mit anderem Namen ignoriert. Wird ein Slotbaer erkannt, so beende ich das Scannen und versuche ein Verbindung mit dem Gerät herzustellen.

Und dann geht es immer so weiter, wird die Verbindung hergestellt, so wird nach den Services gesucht. Hat man die wird nach einer bestimmten Characteristic gesucht. Deren Id kann ebenfalls mit LightBlue ermittelt werden. Wird sie gefunden, wo wird sie in einem neuen property gespeichert und wichtig ihr NotifyValue wird auf YES gesetzt. Das bedeutet, wenn Daten ankommen, wird der Manager darüber informiert.

Ist man erstmal soweit ist es Zeit für BLTBlue einen eigenen Delegate zu bekommen. Da es möglicherweise mehr am BT Interessierte gibt, habe ich nicht einen Delegaten vorgesehen, sondern beliebig viele.

Damit der Delegate auch wirklich alle Statusänderungen mitbekommen kann, bekommt die init Methode noch einen delegate Parameter, so dass dieser so früh wie möglich gesetzt werden kann.

Was fehlt ist noch eine WriteMethode. Bevor sie versucht Daten zu übertragen, überprüft sie ob eine Verbindung besteht und die Characteristic gefunden wurde. Die Daten werden an die Characteristic gesendet. Der Converter wird eine Response senden, es ist also der entsprechende WriteMode zu wählen.

Jetzt das Ganze als Testprogramm verpackt.

 Der View bekommt noch ein Daten Label. Die Constraints für die linken und rechten Ränder, setzen diese auf die selben Werte, wie die des Statuslabels. Wenn ich nun die Werte für das Statuslabel ändere werden die des Datenlabels ebenfalls automatisch geändert. Das letzte Constraint ist der vertikale Abstand zwischen beiden Labels, so dass das Daten Label weiß, wo es hingehört.

Der ViewController, wird der Delegate des CBTBlue Objects.

In der Status Methode, wird der Status Text einfach in das Status Label kopiert.

In der Daten  Methode, werden die Daten in das Datenlabel kopiert und sofort wieder an den Konverter zurückgeschickt. 

Tippt man nun in MinTerm ein Zeichen, so sollte es auf dem iPad erscheinen und sofort in MinTerm als Antwort angezeigt werden - und was soll ich sagen so ist es.

Als letzter Test, erfolgt der Anschluss an die CU, Ein Adapter von MinDin6 auf FTDI ist schnell gelötet. Zuerst nur der Stecker test ohne Converter. Stecker in die CU und Spannungen gemessen. Liegt die Versorgungsspannung an den richtigen Pins an und sind nirgendwo 18V zu finden ? - Jau.

Bahntester

Converter angesteckt und - kein Rauch, kein Lärm - Alles gut.

Die Software des ViewCcontrollers so geändert, dass er bei Erhalt einer Statusnachricht, dass die Characteristika erkannt wurden, anfängt im Sekundentakt "0 zu senden.

Aber nichts geht. Stecker kontrolliert, Kabel kontrolliert, kein Fehler. Also das Oszi herausgeholt und nachgemessen. Der Pegel des Signals zur CU, wird nicht niedrig genug. Nicht gut. Also R12 von 20k auf 4k7 reduziert - und siehe da es geht. Der Spannungsabfall an R12 beträgt 200mV, das heißt es fließt ein Strom von 0,0425mA durch die Clamp Dioden im Konverter. das ist keine Gefahr, aber auch kein gutes Design. 

Ich habe das nicht im Schaltplan geändert, wer den Text nicht liest ist selber schuld. Am Ende werde ich die Ausgangsstufe ändern, aber im Moment tut es sie es.

Das Signal kommt nun bei der CU an, die CU antwortet und das iPad empfängt die Antwort. Alles ist gut.

Das war's für heute. 2 Stunden mit dem BT Dongle verschwendet, 1.5 Stunden mit Storyboard und Constraints und weitere 2 Stunden bis die Software soweit stand. Es sind zwar nur wenige Zeilen Code, aber das Nachlesen, wie der CBCentralManager und der ganze Schnickschnack drumherum funktionieren,  braucht halt auch Zeit. Dann noch ne halbe Stunde bis das Ganze an der CU lief macht zusammen wieder etwa 6 Stunden plus 2 Stunden um es niederzuschreiben und für die Webseite aufzubereiten - Schlechtes Verhältnis.