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




