TCP接続でRasPiをリモート化するiPhoneアプリのプログラム例(改善版)

iPhoneHome
TCP接続リモートアプリ

前回紹介した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でメインスレッドに返す方法とした。

●プログラム例の構成

programOutline
プログラム概要

●プログラム事例

<ContentView.swift>
iPhoneHome

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>
iPhone-Remote

方向ボタン、値を増減させるスライダー、カウント増減などいくつかの操作機能があり、操作した結果を文字列で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