M5Stackとswiftでスマホからエアコンを操作する

最近流行っている IoT の世界にも手を出してみたいと思い、swift でアプリを作成、M5Stack と赤外線送受信ユニットでスマホからエアコンを操作するというものを作ってみました。

構成

構成

  • Swift で iPhone から M5Stack に UDP 通信で送信するアプリを作成
  • M5Stack で受信した UDP 通信を処理して赤外線送受信ユニットでエアコンに送信する
  • エアコンの赤外線を受信して、エアコンを操作する

環境

  • iOS アプリ

    • Swift 5.7
    • iPhone 13 Pro max : iOS 16.0.2
  • M5Stack

    • M5Stack Basic
    • M5Stack 赤外線送受信ユニット(U002)
  • 開発環境

    • macOS Monterey 12.6
    • Xcode 14.0.1
    • PlatformIO Core 6.1.4

Swift でのアプリ作成

今回 Swift でのアプリ作成は初だったので Storyboard での作成を行いました。

StoryBoard

まずはボタンの配置をサクッと行い、ボタンを押した時に UDP 通信を送信するように設定しました。

UDP 通信の送信は以下のような関数を実装しました。

1import UIKit
2import Foundation
3import Network
4
5let host = "***.***.***.***"
6let port = "****"
7
8/* コネクション開始 */
9let connection = connect(host: host, port: port)
10
11class ViewController: UIViewController {
12    @IBOutlet weak var status_label: UILabel!
13
14    override func viewDidLoad() {
15        super.viewDidLoad()
16        // Do any additional setup after loading the view.
17    }
18}
19
20func send(connection: NWConnection,message: String) {
21    /* 送信データ生成 */
22    let data = message.data(using: .utf8)!
23    let semaphore = DispatchSemaphore(value: 0)
24
25    /* データ送信 */
26    connection.send(content: data, completion: .contentProcessed { error in
27        if let error = error {
28            NSLog("\(#function), \(error)")
29        } else {
30            semaphore.signal()
31        }
32    })
33    /* 送信完了待ち */
34    semaphore.wait()
35}
36
37func recv(connection: NWConnection){
38    let semaphore = DispatchSemaphore(value: 0)
39    var result : String?
40    /* データ受信 */
41    connection.receive(minimumIncompleteLength: 0,
42                       maximumLength: 65535,
43                       completion:{(data, context, flag, error) in
44        if let error = error {
45            NSLog("\(#function), \(error)")
46        } else {
47            if let data = data ,let message = String(data: data, encoding: .utf8){
48                print(message)
49                /* 受信データのデシリアライズ */
50                semaphore.signal()
51            }
52            else {
53                NSLog("receiveMessage data nil")
54            }
55        }
56    })
57    /* 受信完了待ち */
58    semaphore.wait()
59}
60
61func disconnect(connection: NWConnection)
62{
63    /* コネクション切断 */
64    connection.cancel()
65}
66
67func connect(host: String, port: String) -> NWConnection
68{
69    let t_host = NWEndpoint.Host(host)
70    let t_port = NWEndpoint.Port(port)
71    let connection : NWConnection
72    let semaphore = DispatchSemaphore(value: 0)
73
74    /* コネクションの初期化 */
75    connection = NWConnection(host: t_host, port: t_port!, using: .udp)
76
77    /* コネクションのStateハンドラ設定 */
78    connection.stateUpdateHandler = { (newState) in
79        switch newState {
80            case .ready:
81                NSLog("Ready to send")
82                semaphore.signal()
83            case .waiting(let error):
84                NSLog("\(#function), \(error)")
85            case .failed(let error):
86                NSLog("\(#function), \(error)")
87            case .setup: break
88            case .cancelled: break
89            case .preparing: break
90            @unknown default:
91                fatalError("Illegal state")
92        }
93    }
94
95    /* コネクション開始 */
96    let queue = DispatchQueue(label: "example")
97    connection.start(queue:queue)
98
99    /* コネクション完了待ち */
100    semaphore.wait()
101    return connection
102}

遷移後の画面では、赤外線送受信ユニットに送信するボタンに対し送信する文字列の割り当てや現在の温度、風量などを表示するようにしました。

1import UIKit
2
3class ModalViewController: UIViewController {
4    @IBOutlet weak var label_temp: UILabel!
5    @IBOutlet weak var label_level: UILabel!
6    @IBOutlet weak var label_mode: UILabel!
7    var mode = ""
8    var temp = 0
9    var level = 0
10
11    override func viewDidLoad() {
12        super.viewDidLoad()
13        label_temp.text = ""
14        label_level.text = ""
15        label_mode.text = "OFF"
16
17        // Do any additional setup after loading the view.
18    }
19    @IBAction func heat_button(_ sender: Any) {
20        send(connection: connection, message: "heat")
21        temp = 27
22        level = 2
23        label_temp.text = String(temp) + "℃"
24        label_level.text = String(level)
25        label_mode.text = "暖房"
26    }
27
28    @IBAction func cool_button(_ sender: Any) {
29        send(connection: connection, message: "cool")
30        temp = 27
31        level = 2
32        label_temp.text = String(temp) + "℃"
33        label_level.text = String(level)
34        label_mode.text = "冷房"
35    }
36
37    @IBAction func dehumi_button(_ sender: Any) {
38        send(connection: connection, message: "dehumi")
39        temp = 27
40        level = 2
41        label_temp.text = String(temp) + "℃"
42        label_level.text = String(level)
43        label_mode.text = "除湿"
44    }
45
46    @IBAction func tempup_button(_ sender: Any) {
47        send(connection: connection, message: "tempup")
48        temp = temp + 1
49        label_temp.text = String(temp) + "℃"
50    }
51
52    @IBAction func tempdown_button(_ sender: Any) {
53        send(connection: connection, message: "tempdown")
54        temp = temp - 1
55        label_temp.text = String(temp) + "℃"
56    }
57
58    @IBAction func levelup_button(_ sender: Any) {
59        send(connection: connection, message: "levelup")
60        level = level + 1
61        label_level.text = String(level)
62    }
63
64    @IBAction func leveldown_button(_ sender: Any) {
65        send(connection: connection, message: "leveldown")
66        level = level - 1
67        label_level.text = String(level)
68    }
69
70    @IBAction func off_button(_ sender: Any) {
71        send(connection: connection, message: "off")
72        label_temp.text = ""
73        label_level.text = ""
74        label_mode.text = "OFF"
75    }
76}

M5Stack での処理

M5Stack ではスマホからの通信で送られてきた文字列によって適切な赤外線データを送信するという処理を実装しました。

今回うちのエアコンは赤外線ユニットのライブラリにある機種ではなかったため予め読み取った RawData を送信しています。

赤外線の RawData の読み取りは、こちらの記事を参考にしました。 こちらの記事

1#include <M5Stack.h>
2#include <IRremoteESP8266.h>
3#include <IRsend.h>
4#include <WiFi.h>
5#include <WiFiUdp.h>
6
7//WiFiの設定--------------------------------------
8const char* ssid = "SSID";
9const char* pass = "PASS";
10
11const int udpPort = ****;
12const int phoneport = ****;
13
14WiFiUDP udp;
15
16IPAddress ip(***.***.***.***);
17IPAddress gateway(***.***.***.***);
18IPAddress subnet(***.***.***.***);
19
20IPAddress phoneip(***.***.***.***);
21
22//-----------------------------------------------
23
24
25//IR送信の設定-------------------------------------
26const int IR_SEND_PIN = 21;
27const int TRANSMIT_CAPTURE_SIZE = 38;
28const int IR_RAW_DATA_SIZE = 981;
29
30uint16_t heatRawData[IR_RAW_DATA_SIZE] = {/*省略*/};
31uint16_t coolRawData[IR_RAW_DATA_SIZE] = {/*省略*/};
32uint16_t dehumiRawData[IR_RAW_DATA_SIZE] = {/*省略*/};
33
34uint16_t offRawData[IR_RAW_DATA_SIZE] = {/*省略*/};
35
36String nowmode = "stop";
37int nowtemp = 0;
38int nowlevel = 0;
39
40//温度ごとのデータ
41uint16_t tempRawData[15][IR_RAW_DATA_SIZE] = {/*省略*/};
42
43//風量ごとのデータ
44uint16_t levelRawData[6][IR_RAW_DATA_SIZE] = {/*省略*/};
45
46IRsend irsend(IR_SEND_PIN);
47//----------------------------------------------
48
49void setup()
50{
51  M5.begin();
52  irsend.begin();
53  Serial.begin(115200);
54  M5.Lcd.setTextSize(2);
55
56  WiFi.config(ip,gateway,subnet);
57  //Wi-Fi接続
58  WiFi.begin(ssid,pass);
59  M5.Lcd.printf("Waiting connect to WiFi: %s ", ssid);
60  while(WiFi.status() != WL_CONNECTED) {
61    //接続完了まで待つ
62    delay(1000);
63    M5.Lcd.print(".");
64  }
65  //udp待受開始
66  udp.begin(udpPort);
67  M5.Lcd.println("Waiting udp packet...");
68}
69
70void loop()
71{
72  M5.Lcd.setCursor(0,120);
73  M5.Lcd.printf("mode:%s\n",nowmode);
74  M5.Lcd.printf("temp:%d  ",nowtemp);
75  M5.Lcd.printf("level:%d",nowlevel);
76  if (int len = udp.parsePacket()) {
77    //udpパケットを読み込む
78    char buff[len + 1];
79    memset(buff, '\0', sizeof(buff));
80    udp.read((uint8_t*)buff, len);
81    String str = buff;
82    if(buff == "setup")
83    {
84      uint8_t message = 1111;
85      udp.beginPacket(phoneip,phoneport);
86      udp.write(message);
87      udp.endPacket();
88      delay(500);
89    }
90    else if(str.compareTo("heat") == 0)
91    {
92      //暖房のデータと初期値:温度27℃、風量2を送信
93      irsend.sendRaw(heatRawData, IR_RAW_DATA_SIZE, TRANSMIT_CAPTURE_SIZE);
94      delay(500);
95      irsend.sendRaw(tempRawData[9], IR_RAW_DATA_SIZE, TRANSMIT_CAPTURE_SIZE);
96      delay(500);
97      irsend.sendRaw(levelRawData[1], IR_RAW_DATA_SIZE, TRANSMIT_CAPTURE_SIZE);
98      delay(500);
99      nowmode = "heater";
100      nowtemp = 27;
101      nowlevel = 2;
102    }
103    else if(str.compareTo("cool") == 0)
104    {
105      //冷房のデータと初期値:温度27℃、風量2を送信
106      irsend.sendRaw(coolRawData, IR_RAW_DATA_SIZE, TRANSMIT_CAPTURE_SIZE);
107      delay(500);
108      irsend.sendRaw(tempRawData[9], IR_RAW_DATA_SIZE, TRANSMIT_CAPTURE_SIZE);
109      delay(500);
110      irsend.sendRaw(levelRawData[1], IR_RAW_DATA_SIZE, TRANSMIT_CAPTURE_SIZE);
111      delay(500);
112      nowmode = "cooler";
113      nowtemp = 27;
114      nowlevel = 2;
115    }
116    else if(str.compareTo("dehumi") == 0)
117    {
118      //除湿のデータと初期値:温度27℃、風量2を送信
119      irsend.sendRaw(dehumiRawData, IR_RAW_DATA_SIZE, TRANSMIT_CAPTURE_SIZE);
120      delay(500);
121      irsend.sendRaw(tempRawData[9], IR_RAW_DATA_SIZE, TRANSMIT_CAPTURE_SIZE);
122      delay(500);
123      irsend.sendRaw(levelRawData[1], IR_RAW_DATA_SIZE, TRANSMIT_CAPTURE_SIZE);
124      delay(500);
125      nowmode = "dehumidifier";
126      nowtemp = 27;
127      nowlevel = 2;
128    }
129    else if(str.compareTo("tempup") == 0)
130    {
131      //1℃あげた温度データを送信
132      nowtemp += 1;
133      irsend.sendRaw(tempRawData[nowtemp - 18], IR_RAW_DATA_SIZE, TRANSMIT_CAPTURE_SIZE);
134      delay(500);
135    }
136    else if(str.compareTo("tempdown") == 0)
137    {
138      //1℃下げた温度データを送信
139      nowtemp -= 1;
140      irsend.sendRaw(tempRawData[nowtemp - 18], IR_RAW_DATA_SIZE, TRANSMIT_CAPTURE_SIZE);
141      delay(500);
142    }
143    else if(str.compareTo("levelup") == 0)
144    {
145      //1あげた風量データを送信
146      nowlevel += 1;
147      irsend.sendRaw(tempRawData[nowlevel - 1], IR_RAW_DATA_SIZE, TRANSMIT_CAPTURE_SIZE);
148      delay(500);
149    }
150    else if(str.compareTo("leveldown") == 0)
151    {
152      //1下げた風量データを送信
153      nowlevel -= 1;
154      irsend.sendRaw(tempRawData[nowlevel - 1], IR_RAW_DATA_SIZE, TRANSMIT_CAPTURE_SIZE);
155      delay(500);
156    }
157    else if(str.compareTo("off") == 0)
158    {
159      //停止データを送信
160      irsend.sendRaw(offRawData, IR_RAW_DATA_SIZE, TRANSMIT_CAPTURE_SIZE);
161      nowmode = "stop";
162      nowtemp = 0;
163      nowlevel = 0;
164    }
165  }
166}

実際の動作

実際に動作させたときの動画はこちらです。

まとめ

今回初めて Swift でのアプリ開発をしてみた割に意外と思っていた動作を実現できてよかったです。

現段階ではローカルな環境でしか動作しないため、今後は外部からのアクセスができるようにしたいと考えています。