最近流行っている 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 での作成を行いました。
まずはボタンの配置をサクッと行い、ボタンを押した時に 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 でのアプリ開発をしてみた割に意外と思っていた動作を実現できてよかったです。
現段階ではローカルな環境でしか動作しないため、今後は外部からのアクセスができるようにしたいと考えています。