從頭開始做一個(gè)智能家居設(shè)備: iOS終端


前言


由于百度云有網(wǎng)頁版的MQTT測試終端,所以前期我們可以用那個(gè)網(wǎng)頁終端作為測試端即可.但是我們畢竟是處在移動互聯(lián)網(wǎng)時(shí)代啊,我們總是用網(wǎng)頁是什么鬼~,所以我們要使用手機(jī)終端來接入MQTT,從而實(shí)現(xiàn)手機(jī)終端控制物聯(lián)網(wǎng)設(shè)備.網(wǎng)上的關(guān)于這方面的資料還是比較多的,我用到的三方是MQTTClient,個(gè)人感覺還是十分的簡單.API方法也就那么多,所以上手很快.

在一切開始之前,我們先來回顧一下整個(gè)項(xiàng)目的代碼邏輯.示意圖如下所示.

首先是上線部分,這里主要是分為兩種情況,一,用戶終端已經(jīng)不管在線與否,硬件上線都必須發(fā)布設(shè)備上線消息,里面包含設(shè)備的ID,設(shè)備的名稱以及設(shè)備的功能管理等;二,當(dāng)前用戶上線,會發(fā)布一個(gè)終端設(shè)備上線信息,這時(shí)候所有的在線硬件設(shè)備都需要發(fā)送一遍設(shè)備信息,用戶終端根據(jù)反饋回來的設(shè)備信息更新UI.

然后就是下線邏輯,用戶終端下線不需要通知硬件,但是當(dāng)硬件設(shè)備下線的時(shí)候,需要去通過遺囑消息通知用戶終端,用戶終端根據(jù)其信息更新UI界面.

硬件發(fā)送溫濕度消息邏輯比較簡單只需要發(fā)送溫濕度消息給終端即可.用戶終端根據(jù)發(fā)送過來的消息更新UI界面.

用戶終端發(fā)送控制硬件消息則和上面的有所不同.因?yàn)楫?dāng)硬件接受到控制消息做出對應(yīng)改變的時(shí)候,終端需要根據(jù)設(shè)備當(dāng)前的狀態(tài)來顯示UI,所以硬件設(shè)備還需要發(fā)送一個(gè)反饋消息給用戶終端,告訴用戶終端當(dāng)前設(shè)備的狀態(tài).用戶終端再根據(jù)這個(gè)狀態(tài)修改對應(yīng)的UI界面即可.

又一次分析了代碼邏輯,接下來我們一起看一下MQTTClient提供的API方法以及常用屬性.


MQTTClient的API


  • 初始化連接MQTT服務(wù)器操作.
- (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;
  • 主動斷開與服務(wù)器的連接.
- (void)disconnectWithDisconnectHandler:(MQTTDisconnectHandler)disconnectHandler;
  • 發(fā)送消息,包括消息主體(data),主題(topic),消息質(zhì)量等級(qos),是否是需要回執(zhí)(retainFlag)等參數(shù).
- (UInt16)sendData:(NSData *)data topic:(NSString *)topic qos:(MQTTQosLevel)qos retain:(BOOL)retainFlag;
  • 訂閱主題字典屬性,以訂閱主題名為key值,以消息質(zhì)量等級(Qos)為value值進(jìn)行存儲.
@property (strong, nonatomic) NSDictionary<NSString *, NSNumber *> *subscriptions;
  • MQTT當(dāng)前狀態(tài)屬性,只讀屬性.
@property (nonatomic, readonly) MQTTSessionManagerState state;
  • MQTT代理屬性
@property (weak, nonatomic) id<MQTTSessionManagerDelegate> delegate;
  • MQTT狀態(tài)改變回調(diào)方法,MQTT代理方法之一.通過這個(gè)方法,我們可以監(jiān)聽MQTT的狀態(tài)從而做出對應(yīng)的UI改變.
- (void)sessionManager:(MQTTSessionManager *)sessionManager didChangeState:(MQTTSessionManagerState)newState;
  • MQTT接受消息方法,MQTT代理方法之一.通過這個(gè)方法,我們可以監(jiān)聽所有訂閱的主題所接受到的消息.
- (void)handleMessage:(NSData *)data onTopic:(NSString *)topic retained:(BOOL)retained;

上面說了一堆API,接下來,我們就看一下我們?nèi)绾问褂?strong>MQTTClient的API來實(shí)現(xiàn)我們想要的功能吧.


代碼解讀


在講解說明這個(gè)代碼工程之前.我們先來聊聊我都定義了什么主題,所有主題如下所示.

主題名稱 Qos 功能 硬件權(quán)限 終端權(quán)限
Client Qos0 設(shè)備信息主題 發(fā)布,訂閱 發(fā)布,訂閱
Will Qos0 遺囑主題 發(fā)布,訂閱 發(fā)布,訂閱
Data Qos0 溫濕度數(shù)據(jù)主題 發(fā)布 訂閱
Order Qos0 用戶指令主題 訂閱 發(fā)布

其中硬件設(shè)備反饋是通過 Client 來進(jìn)行發(fā)布的.還有個(gè)問題就是如果你的身份對某個(gè)主題沒有權(quán)限,你去對該主題進(jìn)行訂閱和發(fā)布消息會導(dǎo)致MQTT的重新連接.

上面看完了主題設(shè)計(jì)部分,我們接下來一起來看一下iOS代碼部分,對于UI部分,我們沒有什么好說的.我們主要看一下MQTT實(shí)現(xiàn)部分.

首先,我們先用cocoapod導(dǎo)入 MQTTClient,如下所示.

  pod 'MQTTClient'

整體的代碼部分在Helps目錄下的MQTTManager單例類中.其實(shí)主要MQTT方法有連接方法,斷開方法,重新連接方法,訂閱和取消訂閱主題方法,發(fā)送消息等方法.如下所示.


/**
 綁定連接MQTT服務(wù)器

 @param username 賬號
 @param password 密碼
 @param topicArray 主題名稱數(shù)組
 @param isSSL 是否是SSL連接
 */
- (void)bindWithUserName:(NSString *)username password:(NSString *)password topicArray:(NSArray <NSString *>*)topicArray isSSL:(BOOL)isSSL;


/**
 主動斷開MQTT服務(wù)器
 */
- (void)disconnectService;


/**
 重新連接MQTT服務(wù)器
 */
- (void)reloadConectService;


/**
 訂閱某個(gè)主題

 @param topic 主題名稱
 */
- (void)subscribeTopic:(NSString *)topic;


/**
 取消訂閱
 
 @param topic 主題名稱
 */
- (void)unsubscribeTopic:(NSString *)topic;


/**
 發(fā)送字符串類型的消息

 @param stringMessage 字符串消息
 @param topic 主題
 */
- (void)sendMQTTStringMessage:(NSString *)stringMessage topic:(NSString *)topic;


/**
 發(fā)送字典類型的消息

 @param mapMessage 字典類型消息
 @param topic 主題
 */
- (void)sendMQTTMapMessage:(NSDictionary *)mapMessage topic:(NSString *)topic;

然后,我定義了幾個(gè)通知消息名稱,為什么使用通知,主要是可能會有多個(gè)界面都需要MQTT的相關(guān)數(shù)據(jù),所以定了通知來進(jìn)行數(shù)據(jù)的傳遞.通知名稱如下所示.

//收到消息的通知,object攜帶類型為MQTTMessageModel
#define ReceiveMessageNotificationName @"ReceiveMessageNotificationName"

//MQTT狀態(tài)發(fā)生改變的通知,不攜帶object.也可以使用KVO監(jiān)聽單例中mqttState的變化
#define MQTTChangeStateNotificationName @"MQTTChangeStateNotificationName"

//MQTT的可操作指令發(fā)送改變的通知
#define MQTTOrderChangeStateNotificationName @"MQTTOrderChangeStateNotificationName"

//MQTT的f設(shè)備返回指令信息的通知 帶有@{@"clientID":xxx, @"switchID":xxx}信息
#define MQTTOrderResponseStateNotificationName @"MQTTOrderResponseStateNotificationName"

MQTTManager.m 實(shí)現(xiàn)邏輯部分主要說明兩個(gè)方法,一個(gè)是狀態(tài)回調(diào)方法,一個(gè)是數(shù)據(jù)接受回調(diào)方法.

我們一一來看,先來看狀態(tài)回調(diào)方法.狀態(tài)回調(diào)方法中不管是哪種狀態(tài)都會發(fā)出通知消息,進(jìn)行對應(yīng)的UI界面更新操作. 有一種特殊情況需要注意,那就是當(dāng)設(shè)備連接成功之后,我們需要在MQTTClientTopic發(fā)送設(shè)備的信息.包括設(shè)備的類型(0:用戶終端 1:硬件),設(shè)備名稱,設(shè)備ID等消息.這樣當(dāng)在線硬件接受到消息之后就同樣會發(fā)布硬件信息,這樣在用戶終端就會知道那個(gè)設(shè)備是處于在線狀態(tài),便于我們?nèi)ゲ僮骱筒榭?

//狀態(tài)監(jiān)聽代理方法
- (void)sessionManager:(MQTTSessionManager *)sessionManager didChangeState:(MQTTSessionManagerState)newState {
    
    switch (newState) {
        case MQTTSessionManagerStateConnected:{
            NSLog(@"eventCode -- 連接成功");
            NSDictionary *message = @{
                                      @"type":@(3),
                                      @"data":@{
                                              @"clientType":@(0),
                                              @"clientName":@"騷棟的手機(jī)",
                                              @"clientID":self.cliendId
                                              },
                                      };
            
            self.mqttState = MQTTStateDidConnect;
            [self sendMQTTMapMessage:message topic:MQTTClientTopic];
            break;
        }
        case MQTTSessionManagerStateConnecting:
            NSLog(@"eventCode -- 連接中");
            self.mqttState = MQTTStateConnecting;
            break;
        case MQTTSessionManagerStateClosed:
            NSLog(@"eventCode -- 連接被關(guān)閉");
            self.mqttState = MQTTStateDisConnect;
            break;
        case MQTTSessionManagerStateError:
            NSLog(@"eventCode -- 連接錯(cuò)誤");
            self.mqttState = MQTTStateDisConnect;
            break;
        case MQTTSessionManagerStateClosing:
            NSLog(@"eventCode -- 關(guān)閉中");
            self.mqttState = MQTTStateDisConnect;
            break;
        case MQTTSessionManagerStateStarting:
            NSLog(@"eventCode -- 連接開始");
            break;
        default:
            break;
    }
    [[NSNotificationCenter defaultCenter] postNotificationName:MQTTChangeStateNotificationName object:nil];

}

數(shù)據(jù)接受回調(diào)方法中主要做的操作是根據(jù)接受到的數(shù)據(jù)去更新UI.其中有溫濕度數(shù)據(jù),反饋數(shù)據(jù),遺囑消息,設(shè)備消息等,然后根據(jù)不同的消息發(fā)布不同的通知消息,從而進(jìn)行UI的修改操作.整體代碼如下所示.

//接受到消息的回調(diào)代理方法
- (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:
            //溫濕度數(shù)據(jù)
            messageModel.messageType = MQTTMessageTypeData;
            break;
        case 1:{
            //反饋數(shù)據(jù)
            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:{
            //遺囑離線數(shù)據(jù)
            messageModel.messageType = MQTTMessageTypeWill;
            
            //移除所有相關(guān)的指令信息
            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:{
            //設(shè)備信息
            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;
                //查看數(shù)組中是否有該設(shè)備的信息
                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];
}

其他的代碼都比較簡單,連接過程也不過多的敘述了,大家自行修改測試即可.


結(jié)語


好了,用戶iOS終端代碼已經(jīng)完成了,整體來說還是比較簡單.因?yàn)镸QTT協(xié)議主要就是訂閱和發(fā)布.不像其他的即時(shí)通訊協(xié)議有很多的規(guī)定和規(guī)則.故造成API也很簡單.這個(gè)就不過多敘述了.其他的安卓方面的MQTT以及微信小程序的MQTT都比較好實(shí)現(xiàn),這里我不會??,我就不多說了,各位看官自行百度吧.最后放上Demo的傳送門,大家自行修改Macros目錄下的SDPrefixHeader.pch宏定義參數(shù)即可.如果有任何問題,歡迎在評論區(qū)批評指導(dǎo),謝謝大家了.

iOS 終端Github傳送門


?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容