TCP接続でRasPiをリモート化するiPhoneアプリのプログラム例
●Raspberry Piをリモート化する方法
リモート化する方法でよく使われているのが、HTMLとJavaScriptを使ってウエブからWebIOPiでGPIOを制御する方法。HTMLを使った操作パネルの作成はそれほど面倒ではないが、JavaScriptを介してWebIOPiに繋げるワンクッションが必要で、場合によっては工夫を要するところがあり、やや面倒である。
昨年(2019年)後半に、iPhoneアプリの開発ツールとしてApple社からswiftUIが提供され、Xcodeを使って非常に簡単にGUIを含んだアプリの開発ができるようになった。またシミュレーションのひとつの方法として実機のiPhoneにインストールすることもでき、自分のアプリとして動かすだけなら無料で行える。(アプリの有効期間は7日間なので再インストールが必要。Apple Developer Program(年間11,800円)に参加すれば、制限なく利用できる)
●プログラム例の構成

●プログラム事例
<HomeView.swift>
Client側としてServer側にTCP接続して、送受信を確認する画面。ここでは、接続先のIPアドレス、Port番号を入力し、接続ボタンで接続。送受信の確認のための、送信データ入力と送信ボタンがあり、その送受信データが表示される。
// HomeView.swift import SwiftUI struct HomeView: View { @EnvironmentObject var netCl: networksClient @State var IPadr: String = "192.168.1.13" @State var portNoS: String = "5000" @State var sendMsg: String = "" var body: some View { VStack { Spacer() VStack { Text("サーバーのアドレス設定") HStack { Text("IP アドレス:") TextField("IP アドレス", text: $IPadr) .padding(3) .border(Color.blue) } HStack { Text("ポート番号:") TextField("Port番号", text: $portNoS) .padding(3) .border(Color.blue) Button(action: { self.netCl.open(ip: self.IPadr, port: UInt16(self.portNoS)!) }) { Text("接続") .padding(3) .foregroundColor(Color.white) .background(Color.blue) } } HStack { Text("ステータス:") Text("\(self.netCl.statText)") Spacer() Button(action: { self.netCl.close() }) { Text("切断") .padding(3) .foregroundColor(Color.white) .background(Color.red) } } } .frame(alignment: .center) .padding(10) .border(Color.black, width: 1) Spacer() VStack { HStack { Spacer() Text("送信内容:") TextField("送信内容を入力して下さい", text: $sendMsg) .autocapitalization(.none) .padding(3) .border(Color.blue) Button(action: { self.netCl.sendRcv(sText: self.sendMsg) self.sendMsg = "" }) { Text("送信") .padding(3) .foregroundColor(Color.white) .background(Color.blue) } } List { Text("送信 : \(self.netCl.sentText)") Text("受信 : \(self.netCl.receivedText)") } } .frame(alignment: .center) .padding(10) .border(Color.black, width: 1) Spacer() }.padding(10) } } struct HomeView_Previews: PreviewProvider { static let netCl = networksClient() static var previews: some View { HomeView() .environmentObject(netCl) } }
<RemoteView.swift>
方向ボタン、値を増減させるスライダー、カウント増減などいくつかの操作機能があり、操作した結果を文字列でServer側に送る。例えば、スライダーで「Power」を0~100の間で変更して、その値が「50」の時、”PWR, 50”の文字列をServer側に送る。受信側Server(Raspberry Pi)では、”PWR”、”50”を使って、制御プログラムを実行する。このGUIは一例であって、swiftUIで用意されているものは何でも使え、この送る文字列はプログラム内で自由に変えることができる。
// RemoreView.swift import SwiftUI struct RemoteView: View { @EnvironmentObject var netCl: networksClient @State var powerVal: Double = 0 @State var stepVal: Int = 0 @State var selectVal: Int = 0 @State var sizeVal: Int = 0 private var sizeLbl: [String] = [ "Small","Middle","Large"] var body: some View { VStack { VStack { List { Text("送信: \(self.netCl.sentText)") Text("受信: \(self.netCl.receivedText)") Text("ステータス: \(self.netCl.statText)") } Stepper(value: $sizeVal, in: 0...2, step: 1, onEditingChanged: {_ in self.netCl.sendRcv(sText: "SIZE,\(self.sizeVal)") }) { Text("Size: \(self.sizeLbl[self.sizeVal])") } .padding() Stepper(value: $stepVal, in: 0...10, step: 1, onEditingChanged: {_ in self.netCl.sendRcv(sText: "STEP,\(self.stepVal)") }) { Text("Step : \(stepVal)") } .padding() }.padding() VStack { Button(action: { self.netCl.sendRcv(sText: "F") }) { Image(systemName: "arrowtriangle.up.fill") .foregroundColor(.orange) .scaleEffect(4) } }.frame(width: 300, height: 100, alignment: .center) HStack { Button(action: { self.netCl.sendRcv(sText: "L") }) { Image(systemName: "arrowtriangle.left.fill") .foregroundColor(.orange) .scaleEffect(4) } Spacer() Button(action: { self.netCl.sendRcv(sText: "S") }) { Image(systemName: "square.fill") .foregroundColor(.orange) .scaleEffect(4) } Spacer() Button(action: { self.netCl.sendRcv(sText: "R") }) { Image(systemName: "arrowtriangle.right.fill") .foregroundColor(.orange) .scaleEffect(4) } }.frame(width: 250, height: 100, alignment: .center) VStack { Button(action: { self.netCl.sendRcv(sText: "B") }) { Image(systemName: "arrowtriangle.down.fill") .foregroundColor(.orange) .scaleEffect(4) } }.frame(width: 300, height: 100, alignment: .center) VStack { Slider(value: $powerVal, in: 0...100, step:1, onEditingChanged: {changed in self.netCl.sendRcv(sText: "PWR,\(Int(self.powerVal))") }, minimumValueLabel: Text("0"), maximumValueLabel: Text("100"), label: {Text("POWER")} ) Text("Power : \(Int(powerVal))") }.padding() } } } struct RemoteView_Previews: PreviewProvider { static let netCl = networksClient() static var previews: some View { RemoteView() .environmentObject(netCl) } }
<ContentView.swift>
上記のHome画面とRemote画面のタブを切り替えるプログラム
// ContentView.swift import SwiftUI struct ContentView: View { @EnvironmentObject var netCl: networksClient var body: some View { TabView { HomeView() .tabItem { Image(systemName: "house.fill") Text("HOME") } RemoteView() .tabItem { Image(systemName: "gamecontroller.fill") Text("REMOTE") } } } } struct ContentView_Previews: PreviewProvider { static let netCl = networksClient() static var previews: some View { ContentView() .environmentObject(netCl) } }
尚、次のnetworkClientクラスを全体で使えるようにするために、 SceneDelegate.swiftの一部を次のように変更する必要がある。
// SceneDelegate.swift // networkClient import UIKit import SwiftUI class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { // Create the SwiftUI view that provides the window contents. let netCl = networksClient() let contentView = ContentView().environmentObject(netCl) // Use a UIHostingController as window root view controller. if let windowScene = scene as? UIWindowScene { let window = UIWindow(windowScene: windowScene) window.rootViewController = UIHostingController(rootView: contentView) self.window = window window.makeKeyAndVisible() } } ....... (以下省略) ........... }
<NetworkClass.swift>
上記の画面のバックグラウンドで行われるTCP接続、送受信のプログラム。
class networksClientと”s”を付けているのは、swift標準のクラスnetworkClientと区別するため。init()で初期設定の引数を無くして、host, portの冗長な代入を繰り返しているのは、初期化後にhost, portを変更できるようにするためと、エラー発生を回避するため。
// NetworkClass.swift import UIKit import SwiftUI import Foundation import Network class networksClient : ObservableObject { @Published var hostP: String = "192.168.1.13" @Published var portP: UInt16 = 5000 @Published var sentText: String = "未送信" @Published var receivedText: String = "未受信" @Published var statText = "未接続" var connection: NWConnection init () { let ipS: String = "192.168.1.13" // 仮のIP let portS: UInt16 = 5000 // 仮のPort self.hostP = ipS self.portP = portS let host = NWEndpoint.Host(ipS) let port = NWEndpoint.Port(integerLiteral: portS) self.connection = NWConnection(host: host, port: port, using: .tcp) } func open(ip: String, port: UInt16) { self.hostP = ip self.portP = port let host = NWEndpoint.Host(ip) let port = NWEndpoint.Port(integerLiteral: port) self.connection = NWConnection(host: host, port: port, using: .tcp) self.connection.stateUpdateHandler = { (newState) in switch(newState) { case .ready: self.statText = "Ready" case .waiting(let error): self.statText = "Waiting - \(error)" case .failed(let error): self.statText = "Failed - \(error)" case .setup: self.statText = "Setup" case .cancelled: self.statText = "Cancelled" case .preparing: self.statText = "Preparing" default: self.statText = "Default" } } let netQueue = DispatchQueue(label: "NetworkClient") self.connection.start(queue: netQueue) } func send(sText: String) { let sendData = "\(sText)".data(using: .utf8)! connection.send(content: sendData, completion: .contentProcessed { (error) in if let error = error { self.statText = "\(#function), \(error)" } else { self.sentText = "\(sText)" self.statText = "Sent successfully" } }) } func receive() { connection.receive(minimumIncompleteLength: 0, maximumLength: Int(UInt32.max)) { (data, _, _, error) in if let data = data { let rText = String(data: data, encoding: .utf8)! self.receivedText = "\(rText)" self.statText = "Received" } else { self.statText = "\(#function), Received data is nil" } } } func sendRcv(sText: String) { self.send(sText: "\(sText)") self.receive() } func close() { self.receivedText = "" connection.cancel() } }
<testServerTCP.py>
Raspberry Pi側のTCP Serverプログラムの一例。
iPhoneから送られる操作文字列を受信して、その操作を実行する。このプログラム例では、送られる文字列を「,」で分離して、文字情報を得るところまでとしているが、その文字情報を使って様々な動きのプログラムを入れることができる。TCP送受信では、socketとselect、データ受信をthreadingを使って行っている。ここでGPIOの制御はWebIOPiではなく、RPi.GPIOなど一般的なモジュールを使うことができる。
# -*- coding: utf-8 -*- import time import socket import select import threading # Receive data from Client, by multi task svr = socket.socket(socket.AF_INET, socket.SOCK_STREAM) svr.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # 再接続時のTCPエラーを回避 svr.bind(('', 5000)) svr.listen(10) socks = [svr] rcvData="" rcvFlag = False def recv(): global rcvFlag global rcvData while True: readList, writeList, xList = select.select(socks, [], []) for sClnt in readList: if sClnt is svr: clnt, addr = svr.accept() socks.append(clnt) else: data = sClnt.recv(1024) rcvData = data.decode("utf-8") sClnt.send(("OK, Received : "+rcvData).encode('utf-8')) # return OK to client if rcvData.lower().find('bye') != -1: print("Bye!!!") rcvFlag = False socks.remove(sClnt) sClnt.close() elif len(rcvData) > 0: rcvFlag = True # Start receiving data from client recvThread = threading.Thread(target=recv) recvThread.start() print("Threading Start") try: while True: if rcvFlag: # Receive data from Client PC pdata = rcvData.split(",") rcvFlag = False # Confirm data receive if len(pdata) > 1 and pdata[1].isdecimal(): val = int(pdata[1]) else: val = 0 if pdata[0] == 'F': print("Forward", val) elif pdata[0] == "L": print("Left", val) elif pdata[0] == "R": print("Right", val) else: print("Others: ", pdata) except KeyboardInterrupt: print("Keyboard Interrupt") recvThread.deamon = True