美文网首页iOS 功能类
从头开始做一个智能家居设备: iOS终端

从头开始做一个智能家居设备: iOS终端

作者: 神经骚栋 | 来源:发表于2019-08-20 15:32 被阅读0次

    前言


    由于百度云有网页版的MQTT测试终端,所以前期我们可以用那个网页终端作为测试端即可.但是我们毕竟是处在移动互联网时代啊,我们总是用网页是什么鬼~,所以我们要使用手机终端来接入MQTT,从而实现手机终端控制物联网设备.网上的关于这方面的资料还是比较多的,我用到的三方是MQTTClient,个人感觉还是十分的简单.API方法也就那么多,所以上手很快.

    在一切开始之前,我们先来回顾一下整个项目的代码逻辑.示意图如下所示.

    首先是上线部分,这里主要是分为两种情况,一,用户终端已经不管在线与否,硬件上线都必须发布设备上线消息,里面包含设备的ID,设备的名称以及设备的功能管理等;二,当前用户上线,会发布一个终端设备上线信息,这时候所有的在线硬件设备都需要发送一遍设备信息,用户终端根据反馈回来的设备信息更新UI.

    然后就是下线逻辑,用户终端下线不需要通知硬件,但是当硬件设备下线的时候,需要去通过遗嘱消息通知用户终端,用户终端根据其信息更新UI界面.

    硬件发送温湿度消息逻辑比较简单只需要发送温湿度消息给终端即可.用户终端根据发送过来的消息更新UI界面.

    用户终端发送控制硬件消息则和上面的有所不同.因为当硬件接受到控制消息做出对应改变的时候,终端需要根据设备当前的状态来显示UI,所以硬件设备还需要发送一个反馈消息给用户终端,告诉用户终端当前设备的状态.用户终端再根据这个状态修改对应的UI界面即可.

    又一次分析了代码逻辑,接下来我们一起看一下MQTTClient提供的API方法以及常用属性.

    MQTTClient的API


    • 初始化连接MQTT服务器操作.
    - (void)connectTo:(NSString *)host
                 port:(NSInteger)port
                  tls:(BOOL)tls
            keepalive:(NSInteger)keepalive
                clean:(BOOL)clean
                 auth:(BOOL)auth
                 user:(NSString *)user
                 pass:(NSString *)pass
                 will:(BOOL)will
            willTopic:(NSString *)willTopic
              willMsg:(NSData *)willMsg
              willQos:(MQTTQosLevel)willQos
       willRetainFlag:(BOOL)willRetainFlag
         withClientId:(NSString *)clientId
       securityPolicy:(MQTTSSLSecurityPolicy *)securityPolicy
         certificates:(NSArray *)certificates
        protocolLevel:(MQTTProtocolVersion)protocolLevel
       connectHandler:(MQTTConnectHandler)connectHandler;
    
    • 主动断开与服务器的连接.
    - (void)disconnectWithDisconnectHandler:(MQTTDisconnectHandler)disconnectHandler;
    
    • 发送消息,包括消息主体(data),主题(topic),消息质量等级(qos),是否是需要回执(retainFlag)等参数.
    - (UInt16)sendData:(NSData *)data topic:(NSString *)topic qos:(MQTTQosLevel)qos retain:(BOOL)retainFlag;
    
    • 订阅主题字典属性,以订阅主题名为key值,以消息质量等级(Qos)为value值进行存储.
    @property (strong, nonatomic) NSDictionary<NSString *, NSNumber *> *subscriptions;
    
    • MQTT当前状态属性,只读属性.
    @property (nonatomic, readonly) MQTTSessionManagerState state;
    
    • MQTT代理属性
    @property (weak, nonatomic) id<MQTTSessionManagerDelegate> delegate;
    
    • MQTT状态改变回调方法,MQTT代理方法之一.通过这个方法,我们可以监听MQTT的状态从而做出对应的UI改变.
    - (void)sessionManager:(MQTTSessionManager *)sessionManager didChangeState:(MQTTSessionManagerState)newState;
    
    • MQTT接受消息方法,MQTT代理方法之一.通过这个方法,我们可以监听所有订阅的主题所接受到的消息.
    - (void)handleMessage:(NSData *)data onTopic:(NSString *)topic retained:(BOOL)retained;
    

    上面说了一堆API,接下来,我们就看一下我们如何使用MQTTClient的API来实现我们想要的功能吧.

    代码解读


    在讲解说明这个代码工程之前.我们先来聊聊我都定义了什么主题,所有主题如下所示.

    主题名称 Qos 功能 硬件权限 终端权限
    Client Qos0 设备信息主题 发布,订阅 发布,订阅
    Will Qos0 遗嘱主题 发布,订阅 发布,订阅
    Data Qos0 温湿度数据主题 发布 订阅
    Order Qos0 用户指令主题 订阅 发布

    其中硬件设备反馈是通过 Client 来进行发布的.还有个问题就是如果你的身份对某个主题没有权限,你去对该主题进行订阅和发布消息会导致MQTT的重新连接.

    上面看完了主题设计部分,我们接下来一起来看一下iOS代码部分,对于UI部分,我们没有什么好说的.我们主要看一下MQTT实现部分.

    首先,我们先用cocoapod导入 MQTTClient,如下所示.

      pod 'MQTTClient'
    

    整体的代码部分在Helps目录下的MQTTManager单例类中.其实主要MQTT方法有连接方法,断开方法,重新连接方法,订阅和取消订阅主题方法,发送消息等方法.如下所示.

    
    /**
     绑定连接MQTT服务器
    
     @param username 账号
     @param password 密码
     @param topicArray 主题名称数组
     @param isSSL 是否是SSL连接
     */
    - (void)bindWithUserName:(NSString *)username password:(NSString *)password topicArray:(NSArray <NSString *>*)topicArray isSSL:(BOOL)isSSL;
    
    
    /**
     主动断开MQTT服务器
     */
    - (void)disconnectService;
    
    
    /**
     重新连接MQTT服务器
     */
    - (void)reloadConectService;
    
    
    /**
     订阅某个主题
    
     @param topic 主题名称
     */
    - (void)subscribeTopic:(NSString *)topic;
    
    
    /**
     取消订阅
     
     @param topic 主题名称
     */
    - (void)unsubscribeTopic:(NSString *)topic;
    
    
    /**
     发送字符串类型的消息
    
     @param stringMessage 字符串消息
     @param topic 主题
     */
    - (void)sendMQTTStringMessage:(NSString *)stringMessage topic:(NSString *)topic;
    
    
    /**
     发送字典类型的消息
    
     @param mapMessage 字典类型消息
     @param topic 主题
     */
    - (void)sendMQTTMapMessage:(NSDictionary *)mapMessage topic:(NSString *)topic;
    
    

    然后,我定义了几个通知消息名称,为什么使用通知,主要是可能会有多个界面都需要MQTT的相关数据,所以定了通知来进行数据的传递.通知名称如下所示.

    //收到消息的通知,object携带类型为MQTTMessageModel
    #define ReceiveMessageNotificationName @"ReceiveMessageNotificationName"
    
    //MQTT状态发生改变的通知,不携带object.也可以使用KVO监听单例中mqttState的变化
    #define MQTTChangeStateNotificationName @"MQTTChangeStateNotificationName"
    
    //MQTT的可操作指令发送改变的通知
    #define MQTTOrderChangeStateNotificationName @"MQTTOrderChangeStateNotificationName"
    
    //MQTT的f设备返回指令信息的通知 带有@{@"clientID":xxx, @"switchID":xxx}信息
    #define MQTTOrderResponseStateNotificationName @"MQTTOrderResponseStateNotificationName"
    

    MQTTManager.m 实现逻辑部分主要说明两个方法,一个是状态回调方法,一个是数据接受回调方法.

    我们一一来看,先来看状态回调方法.状态回调方法中不管是哪种状态都会发出通知消息,进行对应的UI界面更新操作. 有一种特殊情况需要注意,那就是当设备连接成功之后,我们需要在MQTTClientTopic发送设备的信息.包括设备的类型(0:用户终端 1:硬件),设备名称,设备ID等消息.这样当在线硬件接受到消息之后就同样会发布硬件信息,这样在用户终端就会知道那个设备是处于在线状态,便于我们去操作和查看.

    //状态监听代理方法
    - (void)sessionManager:(MQTTSessionManager *)sessionManager didChangeState:(MQTTSessionManagerState)newState {
        
        switch (newState) {
            case MQTTSessionManagerStateConnected:{
                NSLog(@"eventCode -- 连接成功");
                NSDictionary *message = @{
                                          @"type":@(3),
                                          @"data":@{
                                                  @"clientType":@(0),
                                                  @"clientName":@"骚栋的手机",
                                                  @"clientID":self.cliendId
                                                  },
                                          };
                
                self.mqttState = MQTTStateDidConnect;
                [self sendMQTTMapMessage:message topic:MQTTClientTopic];
                break;
            }
            case MQTTSessionManagerStateConnecting:
                NSLog(@"eventCode -- 连接中");
                self.mqttState = MQTTStateConnecting;
                break;
            case MQTTSessionManagerStateClosed:
                NSLog(@"eventCode -- 连接被关闭");
                self.mqttState = MQTTStateDisConnect;
                break;
            case MQTTSessionManagerStateError:
                NSLog(@"eventCode -- 连接错误");
                self.mqttState = MQTTStateDisConnect;
                break;
            case MQTTSessionManagerStateClosing:
                NSLog(@"eventCode -- 关闭中");
                self.mqttState = MQTTStateDisConnect;
                break;
            case MQTTSessionManagerStateStarting:
                NSLog(@"eventCode -- 连接开始");
                break;
            default:
                break;
        }
        [[NSNotificationCenter defaultCenter] postNotificationName:MQTTChangeStateNotificationName object:nil];
    
    }
    

    数据接受回调方法中主要做的操作是根据接受到的数据去更新UI.其中有温湿度数据,反馈数据,遗嘱消息,设备消息等,然后根据不同的消息发布不同的通知消息,从而进行UI的修改操作.整体代码如下所示.

    //接受到消息的回调代理方法
    - (void)handleMessage:(NSData *)data onTopic:(NSString *)topic retained:(BOOL)retained {
        
        NSDictionary *message = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableLeaves error:nil];
        int type = [message[@"type"] intValue];
        MQTTMessageModel *messageModel = [[MQTTMessageModel alloc] init];
        [messageModel setValuesForKeysWithDictionary:message[@"data"]];
        switch (type) {
            case 0:
                //温湿度数据
                messageModel.messageType = MQTTMessageTypeData;
                break;
            case 1:{
                //反馈数据
                messageModel.messageType = MQTTMessageTypeResponse;
                
                for (ClientModel *clientModel in self.clientArray) {
                    if ([clientModel.clientID isEqualToString:messageModel.clientID]) {
                        for (SwitchModel *switchModel in clientModel.switchArray) {
                            if ([switchModel.switchID isEqualToString:messageModel.switchID]) {
                                switchModel.switchState = messageModel.isOn;
                                break;
                            }
                        }
                        break;
                    }
                }
                
                [[NSNotificationCenter defaultCenter] postNotificationName:MQTTOrderResponseStateNotificationName object:@{@"clientID":messageModel.clientID,
                                                                                                                           @"switchID":messageModel.switchID,
                                                                                                                           @"isOn":messageModel.isOn}];
                break;
            }
            case 2:{
                //遗嘱离线数据
                messageModel.messageType = MQTTMessageTypeWill;
                
                //移除所有相关的指令信息
                for (NSInteger i = self.clientArray.count - 1; i >= 0; i--) {
                    ClientModel *clientModel = self.clientArray[i];
                    if ([clientModel.clientID isEqualToString:messageModel.clientID]) {
                        [self.clientArray removeObject:clientModel];
                    }
                }
                [[NSNotificationCenter defaultCenter] postNotificationName:MQTTOrderChangeStateNotificationName object:messageModel];
                break;
            }
            case 3:{
                //设备信息
                messageModel.messageType = MQTTMessageTypeClient;
                
                ClientModel *clientModel = [[ClientModel alloc] init];
                [clientModel setValuesForKeysWithDictionary:message[@"data"]];
                NSArray *switchs = message[@"data"][@"switchs"];
                for (NSDictionary * switchDic in switchs) {
                    SwitchModel *switchModel = [[SwitchModel alloc] init];
                    switchModel.clientID = clientModel.clientID;
                    [switchModel setValuesForKeysWithDictionary:switchDic];
                    [clientModel.switchArray addObject:switchModel];
                }
    
                if (clientModel.clientEunmType == ClientTypeESP8266) {
                    BOOL isHaveClient = NO;
                    //查看数组中是否有该设备的信息
                    for (ClientModel *nowClientModel in self.clientArray) {
                        if ([clientModel.clientID isEqualToString:nowClientModel.clientID]) {
                            isHaveClient = YES;
                            break;
                        }
                    }
                    if (!isHaveClient) {
                        [self.clientArray addObject:clientModel];
                        [[NSNotificationCenter defaultCenter] postNotificationName:MQTTOrderChangeStateNotificationName object:messageModel];
                    }
                }
                
                break;
            }
        }
        
        [[NSNotificationCenter defaultCenter] postNotificationName:ReceiveMessageNotificationName object:messageModel];
    }
    
    

    其他的代码都比较简单,连接过程也不过多的叙述了,大家自行修改测试即可.

    结语


    好了,用户iOS终端代码已经完成了,整体来说还是比较简单.因为MQTT协议主要就是订阅和发布.不像其他的即时通讯协议有很多的规定和规则.故造成API也很简单.这个就不过多叙述了.其他的安卓方面的MQTT以及微信小程序的MQTT都比较好实现,这里我不会😂,我就不多说了,各位看官自行百度吧.最后放上Demo的传送门,大家自行修改Macros目录下的SDPrefixHeader.pch宏定义参数即可.如果有任何问题,欢迎在评论区批评指导,谢谢大家了.

    iOS 终端Github传送门

    相关文章

      网友评论

        本文标题:从头开始做一个智能家居设备: iOS终端

        本文链接:https://www.haomeiwen.com/subject/drojsctx.html