Enjoy Eating & Creating

咱们牛逼的新产品——码表,在数据同步上使用蓝牙和Wi-Fi两种同步方式,其中Wi-Fi同步是让码表和手机处在同一局域网内,然后通过一个协议来传输数据。

很容易想到Socket。

Socket,提供了IP层的数据传输业务。让两个节点通信成为了可能。

Socket有TCP和UDP传输两种方式。据说现在最先进的是TCP,它是对UDP的升级。从技术角度说,UDP是一个过时的技术。那么,它真的没用了吗?

简单一搜索,发现现在还是有很多业务是使用UDP的,而且用更先进的TCP反而效果不理想。

TCP

之前水壶架硬件和服务器交互的时候,使用的是TCP来传输。TCP的优点就是,保证数据正确、完整、已送达。它有强大又复杂的重传校验等机制。保证了可靠性。

而且,由于TCP是一个长链接,所以实时性也非常的好。

所以我们二话没说,就开始使用TCP传输。

Android TCP

其实android直接用系统的接口就可以很方便的进行socket操作。

注意,Socket是必须在新线程中操作的

初始化一个socket实例,获取它的input和output的流,对流进行读写,就相当于用socket进行收发了。

不过这里有一个问题,那就是你很难知道自己是否还连着socket,除非用一个写操作,出现异常。

因为他的isConnected或者isClosed方法都是获取本地状态的,并不知道远程服务器是否还连接着。

所以stackoverflow上的解决方法是,发送心跳包…发送一个urgentData来确定服务器是否还可达。

说实在的,这个解决方法,真的垃圾。

Timeout

那么超时呢?

我觉得完全没法做。读取流的时候,当前线程就完全阻塞了。根本没有机会去检查等待时间,而且完全无法中断读取的操作…………

iOS TCP

iOS上用CocoaAsyncSocket这个网络库来实现socket通信。实在是方便。

他的读和写可以增加Tag,方便实用状态基。并且有读取指定长度这种操作,还可以设置超时。

实在是太方便了,所以我都不想多说了……

BUT

写到这里,是比较了Android原生TCP超精简实现和iOS超厉害的框架实现的TCP通信。

当我根据协议把一切的一切写好了之后,联调时候发现了严重的丢帧问题。

从TCP的理论来说,丢帧了,应该会自动重传。但是并没有收到新的帧。上下位机都停在了某一个状态。

经过调试,我们发现问题出在了下位机发送数据和接受数据的地方。下位机发送数据后会收到TCP的响应。而这时候手机也发了协议中的响应数据回来。这就导致了下位机串口收到的数据,出现了粘包情况。导致了状态异常。从上位机的角度来说,自己的发送的响应已经得到了响应,会认为下位机已经收到了响应……

对,状态乱了,也不同步了。

所以我们在上位机这里增加了延迟机制,保护下位机脆弱的心灵。但是这并没有从根本上解决这个问题,而且这会大大增加传输需要的时间。(经过测试,发现稳定的用了延迟方法传输一个文件的速率是蓝牙的3倍)

UDP

这肯定是不能被接受的!

虽然说Wi-Fi的理论速度是用MB做单位的,但是由于我们硬件的各种限制。这个数字会大大大大的缩小。但是也不至于缩小到3kb左右~

我们通过进一步调试,发现主要问题还是出现在下位机TCP响应和数据包的粘连情况。所以,为了解决这个棘手的问题,不如直接把这个响应去掉算了!

Android UDP

UDP在Android中也有很现成的接口。

使用DatagramSocket类来操作udp。

udp可以算是数据包了,它是不基于连接的。但是它还是需要端口来通信。所以对于下位机来说,不能直接将数据通过一个连接写入了。得确定目标节点的地址和端口才能将数据返回回去。

由于之前TCP版本抽象的比较好,所以转换到UDP比较轻松。

Timeout

关键的一点是,android的DatagramSocket类自带了timeout属性!在等待数据进入的时候,可以设置一个timeout。一旦超时,直接抛出异常。之后就可以进入重传流程!

iOS UDP

CocoaAsyncSocket这个库必然也是提供了UDP操作的,不过可气的是,这次他不提供timeout这个操作了!

Timeout

所以iOS的超时就需要自己写了……

其实想想还是比较简单的。只要在发送完毕之后启动一个定时器,在x秒后检查是否有数据即可。

其中的难点大概就是,如何确定是否有数据进入,以及如何确定进入的数据是不是之前开启定时器时候的数据的回复信息……

所以,特地引入了一个变量

NSDate *reading_timestamp;

读取数据的时间。

一个定时检查的方法

-(void)check_receive:(NSTimeInterval)timestamp{
    if (reading_timestamp.timeIntervalSince1970==timestamp) {
    //timeout!!
    NSLog(@"timeout !");
    }
//.......
}

然后在需要回馈的发送中添加了一些赋值操作。

-(void)udpSocket:(GCDAsyncUdpSocket *)sock didSendDataWithTag:(long)tag{
    isSending=NO;
    if(tag>0&&tag<FitFileTransmissionStatusDone){
        transmissionStatus=(FitFileTransmissionStatus)tag;
        reading_timestamp=[NSDate date];
        NSTimeInterval interval=reading_timestamp.timeIntervalSince1970;
        [sock receiveOnce:nil];
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), queue, ^{
            [self check_receive:interval];
        });
    }
}

这里要注意,interval这个中间变量是一定要写的,不写会死。具体原因,就和block的机制有关了,想通过我的笔记了解的话,可以看看之前的一片关于危险的block的记录。

在延迟的block中比较全局的开始读取的时间和执行block前的时间局部的时间,来确定全局的读取时间是否有改变。用这个思路成功的解决了这个问题!!

End

通过UDP,传输稳得很,而且速度是蓝牙的19倍左右!这才叫爽。

全文只有iOS的Timeout贴了代码,因为。别的代码实在是太简单了,而且网上一查一大片。只有iOS这一段是我原创的~哈哈

阅读此文
post @ 2018-01-16

前几天通过比较了iOS和Android端BLE蓝牙开发时,订阅Characteristic的不同,得出了Android订阅特征相当复杂的结论。但是在结尾我也替Android开发洗了一次白,因为今天说的这个东西,在iOS上非常难搞,而在Android上相当容易!

Context

新的码表产品,是一款为广大骑行爱好者省钱的GPS码表!

这个硬件会记录骑行时候的速度,位置,海拔,心率,踏频,等等等等信息,并在表盘显示实时的状态。所以,手机的用处就是,将骑行记录导出,然后对数据进行备份,为骑行者分析骑行过程。

咱们不仅仅是价格很厉害,在同步方式上也很厉害。因为我们可以通过Wi-Fi同步,而不仅仅是蓝牙同步。Wi-Fi的速度可比蓝牙高得多。不过这些都不是今天的主题,今天是要说app中,展示某一次骑行详情时候的图表展示。

废话不多说,先上图

iOS
Android

这里的图表的表项,是不确定数量的。比如说,这个用户没有踏频器的话,那么数据分析中就会没有踏频的图表。没有心率计的话,就会没有心率的图表。

Android

Android实在是,太方便了吧!

用了LinearLayout之后,我们把所有有可能出线的图表,都写在LinearLayout中,外面再套一个ScrollView。

不需要的图表我们直接把它setVisibility(View.GONE)就完事儿了,后面的图表会紧紧的挨上来,不会留出一个大空白来。

而且ScrollView会随着LinearLayout的大小来确定是否可以滑动,最多可以滑动多少距离。

iOS

没有LinearLayout,只有frame或者auto layout,你说怎么办?

用框架!

是的,有MyLinearLayout,很方便,就是对标Android的LinearLayout做的,其原理也是逃不掉frame和auto layout的。

如果我用框架做,那还写这篇干什么呢~

因为如果不手动写,是完全不会遇到UIScrollView和AutoLayout的一个大坑的!

Step 1

想法很简单,我只要给一个变量叫做lastChart,其实就好了~

@interface ChartViewController () {

    UIView *lastChart; //用了表示上一个图表的指针

    UIScrollView *scrollV;
    UIView *contentView;
    __weak ActivityDetailPresenter *presenterReference;

    ChartBlockView *distanceView;
    ChartBlockView *timeView;
    ChartBlockView *cadenceView;
    ChartBlockView *heartView;

}
@end

这里ChartBlockView表示图表,他是UIView的子类,所以lastChart完完全全可以引用他们。

Step 2

每次从Presenter中reload图表数据的时候,我会一个图表一个图表的去渲染。

比如,我先去检查,有没有速度信息,有的话就搞一个速度的表,没有的话就跳过。

再获取踏频的数据,有数据搞一个踏频的表,没有的话就跳过。

每次在渲染一个表的时候,会去检查lastChart是否为nil,如果为nil,那top就以顶端为参考;如果不为nil,那top就以lastChart的bottom做为参考。

根据这个准则,便很容易实现。

Step 3

当我兴高采烈的run了之后,发现可以显示,但是scrollview完全不能滑动!

查看了view Hierarchy,发现UIScrollView的ContentSize竟然是(0,0)!

因为……autolayout并不能填充scrollview……

所以,后来我又花了些时间,解决了这个问题……

End

所以,iOS没有LinearLayout还是蛮讨厌的……

虽然iOS原生推出了一个新的控件,就是LinearLayout,但是需要iOS 11……

预知后事如何,请听下回分解……

阅读此文
post @ 2018-01-11

之前记录过很多Android和iOS的BLE的开发。不过他们都有一个共性,就是上位机都只订阅了一个特征值。

Context

这次,我们需要订阅两个特征了。

iOS

iOS是真的方便,直接订阅了就好了。

- (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(nullable NSError *)error {
NSArray *chs = service.characteristics;
for (CBCharacteristic *ch in chs) {
    if ([ch.UUID.UUIDString isEqualToString:kCharacteristicControlPoint]) {
        ControlPointCharacteristic = ch;
        // 订阅它!
        [peripheral setNotifyValue:YES forCharacteristic:ControlPointCharacteristic];
    } else if ([ch.UUID.UUIDString isEqualToString:kCharacteristicDataReceiver]) {
        DataReceiverCharacteristic = ch;
    } else if ([ch.UUID.UUIDString isEqualToString:kCharacteristicDataSender]) {
        DataSenderCharacteristic = ch;
        // 订阅它!
        [peripheral setNotifyValue:YES forCharacteristic:DataSenderCharacteristic];
    }
    if (ControlPointCharacteristic != nil && DataSenderCharacteristic != nil && DataReceiverCharacteristic != nil) {
        //All set up
    }
}

我查到一个需要订阅的characteristic,直接执行订阅就可以了。非常的简单,没什么好说的~

Android

我以为,Android大概也是这么简单的吧~

@Override
    public void onServicesDiscovered(BluetoothGatt gatt, int status) {
        super.onServicesDiscovered(gatt, status);
BluetoothGattService service = gatt.getService(UUID.fromString(ServiceUUID));
    if (service != null) {
        fit_control = service.getCharacteristic(UUID.fromString(ControlPointCharacteristic));
        fit_receiver = service.getCharacteristic(UUID.fromString(DataReceiverCharacteristic));
        fit_sender = service.getCharacteristic(UUID.fromString(DataSenderCharacteristic));
        if (fit_control != null && fit_receiver != null && fit_sender != null) {
            // 订阅它!
            gatt.setCharacteristicNotification(fit_control, true);
            // 订阅它!
            gatt.setCharacteristicNotification(fit_sender, true);
        }
    }
}

结果,死都收不到fit_sender的数据,我甚至以为是硬件的问题了~

后来我想到了,N久以前看到过一个帖子,说是Android BLE开发的一些坑,里面提到了android的蓝牙底层是串行的,没有队列。一次只能做一件事情,多的事情,会被忽略。

所以我得等到一次订阅成功之后,再订阅下一个特征。然后我看到

gatt.setCharacteristicNotification()

这个方法是有返回值的,而且还是个boolean,说不定是成功或者失败的意思。官方的解释是true, if the requested notification status was set successfully,貌似正合我意。

看了一下他的实现源码,在这里有关registerForNotification()没有办法继续反编译了

try {
    mService.registerForNotification(mClientIf, device.getAddress(),
        characteristic.getInstanceId(), enable);
} catch (RemoteException e) {
    Log.e(TAG,"",e);
    return false;
}

所以我也不知道这个返回值有没有卵用~

可是仔细想了一下,这个方法既然不是同步的(执行在这里并不会卡住一段时间去订阅这个Characteristic)那就说明这个返回值,可能并没有乱用,说不定他只是去判断一下这个characteristic是否拥有notify的权限而已。

而且即便获取到了这个返回的boolean值,我又能怎么做呢?写个while(1)?

How does it subscribe

或许,我需要的,是一个回调方法吧~

那么,蓝牙到底是怎么做订阅这个动作的呢?

肯定不是本地订阅一下就完事儿了的,肯定会让remote知道,有个client订阅了自己!

所以,一定是要写入数据的!

于是,在Google很偏僻的文档里面,找到了如下的代码段:

private BluetoothGatt mBluetoothGatt;
BluetoothGattCharacteristic characteristic;
boolean enabled;
...
mBluetoothGatt.setCharacteristicNotification(characteristic, enabled);
...
BluetoothGattDescriptor descriptor = characteristic.getDescriptor(UUID.fromString(SampleGattAttributes.CLIENT_CHARACTERISTIC_CONFIG));
descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
mBluetoothGatt.writeDescriptor(descriptor);

是啊,除了本地要开始监听之外,还要写一个描述,告诉BLE设备,我开始订阅了,请开启Notification。

而,writeDescriptor()是由回调的!

public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status);

只要在回调函数里面再去订阅下一个characteristic,再在回调函数里面再再再去订阅下下个characteristic……就可以实现订阅很多很多很多的characteristic了!

Key

@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
    super.onServicesDiscovered(gatt, status);        ...
    gatt.setCharacteristicNotification(fit_control, true);
    List<BluetoothGattDescriptor> descriptorList = fit_control.getDescriptors();
    if (descriptorList != null && descriptorList.size() > 0) {
        for (BluetoothGattDescriptor descriptor : descriptorList) {
            descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
            gatt.writeDescriptor(descriptor);
        }
    }    
 }

 @Override
 public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
        super.onDescriptorWrite(gatt, descriptor, status);
        // 订阅下一个
 }

End

其实,还是蛮折腾的~以前没遇到过这个问题,而且android官方例程里面也没有说这个问题,所以还是花了很多时间。赶紧记录一下,免得以后再次遇到这样的坑儿。

不要以为这个例子就说明了Android比iOS开发难。明天我就写一篇,iOS上花了我差不多一天时间,而Android上半个小时就搞定的东西!

阅读此文
⬆︎TOP