Enjoy Eating & Creating
post @ 2018-04-21

Context

最近做了三个涉及蓝牙的小应用,都有Android版本。说实话,写了一年多Android(完完整整的写)。我对Android的蓝牙链接状态还是存在一些不明确的地方。

毕竟最早接触的是iOS的蓝牙开发,接触新领域的时候总是会根据以前的认知来加以理解。

iOS

iOS上面对蓝牙外设的连接状态是通过BluetoothCentralManager来告知的。

@protocol CBCentralManagerDelegate <NSObject>

//外设链接成功
- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral;
//外设连接失败
- (void)centralManager:(CBCentralManager *)central didFailToConnectPeripheral:(CBPeripheral *)peripheral error:(nullable NSError *)error;
//外设已断开连接
- (void)centralManager:(CBCentralManager *)central didDisconnectPeripheral:(CBPeripheral *)peripheral error:(nullable NSError *)error;

所以蓝牙是否连接成功是通过中心来通知的,但是在Android上,连接状态是由外设来通知的

Android

public final BluetoothGattCallback gattCallback = new BluetoothGattCallback() {
    @Override
    public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
        super.onConnectionStateChange(gatt, status, newState);
    }
}

其中status代表了GATT操作的成功与失败,newState代表了GATT的新状态

status有GATT_SUCCESS,GATT_FAIL

newState有STATE_CONNECTED,STATE_CONNECTING,STATE_DISCONNECTED,STATE_DISCONNECTING

所以这个回调函数可以出现8种状态。

State

那么,对比上面iOS的三个协议,列一张对比表

状态 iOS Android
已连接 didConnectPeripheral GATT_SUCCESS+STATE_CONNECTED
已断开 didDisconnectPeripheral GATT_SUCCESS+STATE_DISCONNECTED,GATT_FAIL+STATE_DISCONNECTED
连接失败 didFailToConnectPeripheral GATT_FAIL+STATE_CONNECTED

可以看到已断开这里,android有两种可能性,第一种是系统底层发现连接断开了GATT_FAIL+STATE_DISCONNECTED,另外一种是用户控制断开GATT_SUCCESS+STATE_DISCONNECTED。
这并不是说明iOS的接口设计的不够周到,而只是iOS把断开的原因放在了error里面而已。

Disconnect

在iOS里断开外设的连接实在是简单。只要让centralManager cancel Connection就可以了。但是在android中,会发现有两个方法~disconnect(),close()

感觉好像两个方法都是对的样子,但实际上他们是有区别的。

BLE通信从底层来说,是在每个connection中传递数据的。所以disconnect是指停止connection通信,但是并没有断开连接。当gatt disconnect了之后,再使用close()便可以真正的断开连接。

假设直接使用close()会怎样呢?

其实不会怎样,蓝牙照样断开了,只不过不会在BluetoothGattCallback中通知。这或许对于开发者来说,会比较麻烦。因为像我就是在BluetoothGattCallback中来定义外设的连接状态,并使用event来通知相关的组件和界面做操作的。
直接close()对于蓝牙来说就会出现一个延迟,因为没有disconnect的蓝牙外设完全断开需要花点时间。

End

这一篇blog是写了一年多Android蓝牙后,从一次次的项目中慢慢总结的。现在回过去看最早最早做的蓝牙类,那是真的漏洞百出啊。

现在感觉自己封装的蓝牙库,和iOS越来越接近了。这样以后写完iOS,再写android会更方便~有点儿爽。

阅读此文

咱们牛逼的新产品——码表,在数据同步上使用蓝牙和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……

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

阅读此文
⬆︎TOP