TCP接続でRasPiをリモート化するiPhoneアプリのプログラム例(改善版)
前回紹介したTCP接続してリモート化するiPhoneアプリでは、使用上は問題なく動作するが、『バックグラウンドで変更された状態変数を使うことを許されていない』との警告が出るので、そのような状態変数を使わないプログラムに変更したので紹介する。
●警告内容とその意味
シミュレータで動作させた時にデバッグエリアに出てくる警告は下記。
networkClient[2762:121381] [SwiftUI] Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates.
TCP接続のバックグラウンドで変更されるPublished Valuesは正しく反映されないので、メインスレッドで変更せよとの意味らしい。実際には動作に問題が出なかったので、致命的なエラーではないが、すっきりしないので対策を考えることにした。対策としては、NetworkのDispatchQueueに共存させて、DispatchQueue.main.asyncなどを使って、メインスレッドで変更しているように見せる方法もあり、試してみたが、Queueの処理順序が意図したような順でないので、Networkの処理関数からは全てreturnでメインスレッドに返す方法とした。
●プログラム例の構成
●プログラム事例
<ContentView.swift>
Home画面(HomeView.swift)と「リモコン画面」に遷移するボタンを持つ画面。RemoteView画面には、NavigationLinkで遷移する。
// ContentView.swift import SwiftUI struct ContentView: View { @EnvironmentObject var netCl: networksClient @EnvironmentObject var netV: netVar var body: some View { NavigationView { VStack { HomeView() NavigationLink(destination: RemoteView()) { Text("リモコン") .font(.system(size: 25)) .padding(6) .foregroundColor(Color.white) .background(Color.blue) .cornerRadius(10) }.padding() .navigationBarTitle("HOME") }.padding() } } } class netVar: ObservableObject { @Published var sentM = "" @Published var recvM = "" @Published var statM = "" } struct ContentView_Previews: PreviewProvider { static let netV = netVar() static let netCl = networksClient() static var previews: some View { ContentView() .environmentObject(netV) .environmentObject(netCl) } }
尚、次のnetworkClientクラスを全体で使えるようにするために、 SceneDelegate.swiftの一部を次のように変更する必要がある。
// SceneDelegate.swift import UIKit import SwiftUI class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). // Create the SwiftUI view that provides the window contents. let netCl = networksClient() let netV = netVar() let contentView = ContentView() .environmentObject(netV) .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() } } ....... (以下省略) ........... }
<HomeView.swift>
Client側としてServer側にTCP接続して、送受信を確認する画面。ここでは、接続先のIPアドレス、Port番号を入力し、接続ボタンで接続。送受信の確認のための、送信データ入力と送信ボタンがあり、その送受信データが表示される。
// HomeView.swift import SwiftUI struct HomeView: View { @EnvironmentObject var netCl: networksClient @EnvironmentObject var netV: netVar @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.netV.statM = self.netCl.open(ip: self.IPadr, port: UInt16(self.portNoS)!) }) { Text("接続") .padding(3) .foregroundColor(Color.white) .background(Color.blue) } } HStack { Text("ステータス:") Text("\(self.netV.statM)") Spacer() Button(action: { self.netV.statM = 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: { let sentF = self.netCl.send(sText: self.sendMsg) let recvF = self.netCl.receive() self.netV.sentM = sentF.sentS self.netV.recvM = recvF.recvS self.netV.statM = recvF.statS self.sendMsg = "" }) { Text("送信") .padding(3) .foregroundColor(Color.white) .background(Color.blue) } } List { Text("送信 : \(self.netV.sentM)") Text("受信 : \(self.netV.recvM)") } } .frame(alignment: .center) .padding(10) .border(Color.black, width: 1) Spacer() }.padding(10) } } struct HomeView_Previews: PreviewProvider { static let netCl = networksClient() static let netV = netVar() static var previews: some View { HomeView() .environmentObject(netCl) .environmentObject(netV) } }
<RemotViewe.swift>
方向ボタン、値を増減させるスライダー、カウント増減などいくつかの操作機能があり、操作した結果を文字列でServer側に送る。例えば、スライダーで「Power」を0~100の間で変更して、その値が「50」の時、”PWR, 50”の文字列をServer側に送る。受信側Server(Raspberry Pi)では、”PWR”、”50”を使って、制御プログラムを実行する。このGUIは一例であって、swiftUIで用意されているものは何でも使え、この送る文字列はプログラム内で自由に変えることができる。
Home画面には、トップ左の「HOME」ボタンで戻る。
// RemoteView.swift import SwiftUI struct RemoteView: View { @EnvironmentObject var netCl: networksClient @EnvironmentObject var netV: netVar @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.netV.sentM)") Text("受信: \(self.netV.recvM)") Text("ステータス: \(self.netV.statM)") } Stepper(value: $sizeVal, in: 0...2, step: 1, onEditingChanged: {_ in self.sendReceive(sendText: "SIZE,\(self.sizeVal)") }) { Text("Size: \(self.sizeLbl[self.sizeVal])") } //.padding() Stepper(value: $stepVal, in: 0...10, step: 1, onEditingChanged: {_ in self.sendReceive(sendText: "STEP,\(self.stepVal)") }) { Text("Step : \(stepVal)") } //.padding() }.padding() VStack { Button(action: { self.sendReceive(sendText: "F") }) { Image(systemName: "arrowtriangle.up.fill") .foregroundColor(.orange) .scaleEffect(4) } }.frame(width: 300, height: 100, alignment: .center) HStack { Button(action: { self.sendReceive(sendText: "L") }) { Image(systemName: "arrowtriangle.left.fill") .foregroundColor(.orange) .scaleEffect(4) } Spacer() Button(action: { self.sendReceive(sendText: "S") }) { Image(systemName: "square.fill") .foregroundColor(.orange) .scaleEffect(4) } Spacer() Button(action: { self.sendReceive(sendText: "R") }) { Image(systemName: "arrowtriangle.right.fill") .foregroundColor(.orange) .scaleEffect(4) } }.frame(width: 250, height: 100, alignment: .center) VStack { Button(action: { self.sendReceive(sendText: "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.sendReceive(sendText: "PWR,\(Int(self.powerVal))") }, minimumValueLabel: Text("0"), maximumValueLabel: Text("100"), label: {Text("POWER")} ) Text("Power : \(Int(powerVal))") }.padding() } } func sendReceive(sendText: String) { let sentF = self.netCl.send(sText: "\(sendText)") let recvF = self.netCl.receive() self.netV.sentM = sentF.sentS self.netV.recvM = recvF.recvS self.netV.statM = recvF.statS } } struct RemoteView_Previews: PreviewProvider { static let netCl = networksClient() static let netV = netVar() static var previews: some View { RemoteView() .environmentObject(netCl) .environmentObject(netV) } }
<NetworkClass.swift>
上記の画面のバックグラウンドで行われるTCP接続、送受信のプログラム。今回、全てのfuncからのデータをreturnで戻し、状態変数は、メインスレッドで書き換えることにした。Networkの処理はDispatchQueueで行われるために、その処理の結果を待つために有限回のループを回わす処理を入れた。
// NetworksClass.swift import Foundation import Network class networksClient: ObservableObject { var hostP: String var portP: UInt16 var connection: NWConnection init () { self.hostP = "localhost" // 仮のIP self.portP = 5050 // 仮のPort let host = NWEndpoint.Host(self.hostP) let port = NWEndpoint.Port(integerLiteral: self.portP) self.connection = NWConnection(host: host, port: port, using: .tcp) } func open(ip: String, port: UInt16)-> String { self.hostP = ip self.portP = port let host = NWEndpoint.Host(ip) let port = NWEndpoint.Port(integerLiteral: port) var netStat: String? self.connection = NWConnection(host: host, port: port, using: .tcp) self.connection.stateUpdateHandler = { (newState) in switch(newState) { case .ready: netStat = "Ready" case .waiting(let error): netStat = "Waiting - \(error)" case .failed(let error): netStat = "Failed - \(error)" case .setup: netStat = "Setup" case .cancelled: netStat = "Cancelled" case .preparing: netStat = "Preparing" default: netStat = "Default" } } let netQueue = DispatchQueue(label: "NetworkClient") self.connection.start(queue: netQueue) for _: Int32 in 0..<100000 { if let netStat = netStat { if netStat == "Ready" { return(netStat) } } } netStat = "Time Out Error (Open func)" return netStat! } func send(sText: String) -> (sentS: String, statS: String) { var netStat: String? let sendData = "\(sText)".data(using: .utf8)! self.connection.send(content: sendData, completion: .contentProcessed { (error) in if let error = error { netStat = "\(#function), \(error)" } else { netStat = "Sent successfully" } }) for _: Int32 in 0..<100000 { if let netStat = netStat { if netStat.contains("Sent") { return(sText, netStat) } } } netStat = "Time Out Error (Send func)" return (sText, netStat!) } func receive() -> (recvS: String, statS: String){ var netStat: String? var recvT: String? connection.receive(minimumIncompleteLength: 0, maximumLength: Int(UInt32.max)) { (data, _, _, error) in if let data = data { let rText = String(data: data, encoding: .utf8)! recvT = "\(rText)" netStat = "Received" } else { netStat = "\(#function), Received data is nil" } } for _: Int32 in 0..<100000 { if let netStat = netStat { return(recvT!, netStat) } } recvT = "" netStat = "Time Out Error (Receive func)" return (recvT!, netStat!) } func close() -> String { let netStat: String = "Close" connection.cancel() return netStat } }
<testServer.py>
Raspberry Pi側の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