
ブレッドボード上に、Raspberry Pi Pico WとLSM9DS1(9軸センサ)モジュールを取りつ付けています。
これに小型のモバイルバッテリーに固定し、手に持って空中で操作するすることで、Unityアプリを操作しようとする試みです。
下記で示すUnityアプリは、TCPサーバーを起動して、Pico WからTCP接続して、次のようなデータを送信する仕組みにしています。
| Accelerometerの'A'、またはgyroscopeの'G'、またはmagneticの'M'の1byte | Xの2byte情報 | Yの2byte情報 | Zの2byte情報 |
この各センサーの受信データは、Unityのトグルボタンで、使うか使わないか?の切り替えて実験できるようにしています。
なおUnit側では起動時にTCPサーバーとして起動し、接続がなければ その"TCPサーバーとしてのIPv4ドレス:ポート番号"を、
マルチキャストで送信します。
Raspberry Pi Pico W側ではWifiに接続後、マルチキャストで受信した"サーバーのIPv4ドレス:ポート番号"で、
そのサーバーに接続して、そこにLSM9DS1の9軸センサー情報を送り続けます。
using UnityEngine;
using System.Net.Sockets;
using System.Net;
using System;
using System.Threading.Tasks;
using System.Threading;
public class SensorServer : MonoBehaviour
{
IPAddress multicastIP = IPAddress.Parse("239.192.0.0");
const int multicastPortNo = 59001;
Task ipSendTask = null;//マルチキャストでip送信するタスク
const int tcpServerPortNo = 59002;// TCPサーバで使うポート番号
public IPAddress ipAddress;// TCPサーバで使うデバイスのIPアドレス
public TcpListener tcpListener = null; //サーバー用オブジェクト(接続待機用)
TcpClient tcpClient = null;//
NetworkStream networkStream = null;// クライアントと通信するストリーム
bool toggleM= true, toggleG = true,toggleA = true;// OnGUIトグルボタン状態
bool receiveFlagM = false, receiveFlagG = false, receiveFlagA = false;// 受信制御
long receiveCount = 0;//受信ブロックデータ数
Vector3 v3M = Vector3.zero;// Magnetometerセンサーの受信情報
float degreeZ; // 上記から計算したZ軸の角度
Vector3 v3G = Vector3.zero;// Gyroscopeセンサーの受信情報
Vector3 v3A = Vector3.zero;// Accelerometerセンサーの受信情報
Vector3 v3M_rest = Vector3.zero;
Vector3 v3A_rest = Vector3.zero;
void Start()
{
this.ipAddress = MyServer.GetMyIPv4();// このマシンのIPv4アドレスを取得
this.tcpListener = new TcpListener(this.ipAddress, SensorServer.tcpServerPortNo); //サーバー用オブジェクト生成
this.tcpListener.Start();
Debug.Log($"Use ip of address with {this.ipAddress.ToString()}-----Sever Strat-----" );
Task task = MyServer.udpReceiveAsyncLoop(multicastIP.ToString(), 59000);
}
void FixedUpdate()
{
if (tcpListener.Pending())// 保留中の接続要求があるか(接続依頼が来た?)
{
this.tcpClient = tcpListener.AcceptTcpClient();// 接続を許可
Debug.Log("-----Accept-----");
this.networkStream = this.tcpClient.GetStream();// クライアントと通信するストリーム取得
Thread receiveThread = new System.Threading.Thread(new System.Threading.ThreadStart(receiveLoop));
receiveThread.Start();
}
if (this.networkStream == null)
{
if (this.ipSendTask == null)
{ // 自身のサーバとなるIPアドレスの文字列を、マルチキャストで送信
string sendData = this.ipAddress.ToString() + "," + tcpServerPortNo;
byte[] buf = System.Text.Encoding.UTF8.GetBytes(sendData);
this.ipSendTask = MyServer.SendMulticast(multicastIP, multicastPortNo, buf);
}
else if (this.ipSendTask.IsCompleted) this.ipSendTask = null;
}
if (this.networkStream == null) return;// 接続状態でない
// 以下は、このアプリのTCPに接続してきたクライアントと接続状態で実行--------
while (this.receiveFlagM || this.receiveFlagG || this.receiveFlagA) {
if (!this.toggleM) this.receiveFlagM = false;
if (!this.toggleG) this.receiveFlagG = false;
if (!this.toggleA) this.receiveFlagA = false;
if (this.receiveFlagM)
{// 磁気センサーの情報で、このGameObjectを姿勢(rotation)を設定
// Quaternion参考:https://xr-hub.com/archives/11515
Quaternion rotation = Quaternion.LookRotation(v3M);//方向を、回転情報に変換 // y と xを、逆に指定
if(this.v3M_rest != Vector3.zero)
{
Quaternion rotation2 = Quaternion.LookRotation(this.v3M_rest);//方向を、回転情報に変換
rotation = rotation * Quaternion.Inverse(rotation2);//Quaternionの引き算に相当する計算
}
transform.rotation = rotation;
this.receiveFlagM = false;
}
if (this.receiveFlagA)
{// 加速度センサーの情報で、このGameObjectを移動
Vector3 acc = this.v3A - this.v3A_rest;
//Vector3 acc = this.v3A ;
if (this.receiveCount >= 10) this.gameObject.transform.Translate(acc * 0.00001f);
this.receiveFlagA = false;
}
if (this.receiveFlagG)
{// ジャイロセンサーの情報で、このGameObjectを制御。
Quaternion rotation = Quaternion.LookRotation(this.v3G);
Vector3 g = rotation.eulerAngles;
this.transform.Rotate(this.v3G * 0.01f);
this.receiveFlagG = false;
}
}
}
private void receiveLoop()// 別スレッドで動作させるTCP受信ループ
{
byte[] buf = new byte[7]; // 受信用バッファ
int idx = 0;
int c;
byte[] buffer2 = new byte[sizeof(UInt16)];// バイナリから整数に変換用バッファ
while (this.networkStream != null)
{
try
{
c = this.networkStream.ReadByte();
if (c == -1) break;
}
catch (Exception e)
{
Debug.Log(e);
break;
}
buf[idx++] = (byte)c;
if (idx < buf.Length) continue;
idx = 0;
this.receiveCount++; // センサー受信回数(M,A,Gの合計)
if (this.receiveFlagM == true && this.receiveCount > 100) continue;
Buffer.BlockCopy(buf, 1, buffer2, 0, buffer2.Length);
short x = System.Buffers.Binary.BinaryPrimitives.ReadInt16BigEndian(buffer2);
Buffer.BlockCopy(buf, 1 + 2, buffer2, 0, buffer2.Length);
short y = System.Buffers.Binary.BinaryPrimitives.ReadInt16BigEndian(buffer2);
Buffer.BlockCopy(buf, 1 + 4, buffer2, 0, buffer2.Length);
short z = System.Buffers.Binary.BinaryPrimitives.ReadInt16BigEndian(buffer2);
if(buf[0] == 'M' && this.receiveFlagM == false)
{
v3M = new Vector3(y, -x, z);
degreeZ = Mathf.Atan(v3M.y / v3M.x) * 360 / Mathf.PI / 2; // 角度算出
this.receiveFlagM = true;
}
else if(buf[0] == 'G' && this.receiveFlagG == false)
{
v3G.x = -y; v3G.y = -x; v3G.z = z;
this.receiveFlagG = true;
}
else if(buf[0] == 'A' && this.receiveFlagA == false)
{
v3A.x = y; v3A.y = x; v3A.z = -z;// y と xを逆、zは逆に指定
this.receiveFlagA = true;
}
}
this.networkStream = null;
Debug.Log("break receiveLoop");
}
private void OnGUI() // 参照:https://docs.unity3d.com/ja/2022.3/Manual/gui-Controls.html
{
GUIStyle lableStyle = GUI.skin.label;// ---Labelスタイル設定
lableStyle.fontSize = 26;
GUI.color = new Color(0.9f, 0.0f, 0.9f); //文字色設定
float whidth = 720;
float height = 100;
float xPos = 120;
float yPos = 50;
toggleM = GUI.Toggle(new Rect(0, yPos, 120, 50), toggleM, " Magnetometer");
string msg = $"x={v3M.x,6:F}, y={v3M.y,6:F}, z={v3M.z,6:F} degZ={degreeZ,6:F2}";//受信データの確認
GUI.Label(new Rect(xPos, yPos, whidth, height), msg);
yPos += 50;
toggleG = GUI.Toggle(new Rect(0, yPos, 120, 50), toggleG, " Gyroscope");
msg = $"x={v3G.x,6:F}, y={v3G.y,6:F}, z={v3G.z,6:F} ";//受信データの確認
GUI.Label(new Rect(xPos, yPos, whidth, height), msg);
yPos += 50;
toggleA = GUI.Toggle(new Rect(0, yPos, 120, 50), toggleA, " Accelerometer");
msg = $"x={v3A.x,6:F}, y={v3A.y,6:F}, z={v3A.z,6:F} ";//受信データの確認
GUI.Label(new Rect(xPos, yPos, whidth, height), msg);
yPos += 50;
Vector3 acc = this.v3A - this.v3A_rest;
msg = $"x={acc.x,6:F2}, y={acc.y,6:F2}, z={acc.z,6:F2} ";//実験データの確認
//GUI.Label(new Rect(xPos, yPos, whidth, height), msg);
yPos += 50;
GUI.color = new Color(1f, 1f, 0.0f); //文字色設定
if (GUI.Button(new Rect(xPos + 100, yPos, 100, 60), "リセット"))
{
this.transform.position = Vector3.zero;
this.transform.rotation = Quaternion.identity;
this.v3M_rest = v3M;
this.v3A_rest = v3A;
Debug.Log($"---------------- REST -----{v3M_rest}----------------");
}
}
}
上記で使っているMyServer.csのコードを下記に示します。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Net;
using System.Net.Sockets;
using System;
public class MyServer
{
// Unity C# 実行マシンのIPv4のアドレス群の検索で、最初に見つかったアドレスを返すメソッド(見つからない場合はnullを返す)
public static System.Net.IPAddress GetMyIPv4()
{
int idxIp = -1;
string hostName = System.Net.Dns.GetHostName();
Debug.Log($"HostName==============================={hostName}");
IPAddress[] ipAddressArray = Dns.GetHostAddresses(hostName);
for (int i = 0; i < ipAddressArray.Length; i++)
{
Debug.Log($"{ipAddressArray[i]}======IPv4 :{ipAddressArray[i].AddressFamily == AddressFamily.InterNetwork}");
if (ipAddressArray[i].AddressFamily == AddressFamily.InterNetwork) // 最初に見つかったIPv4
{
idxIp = i;
break;
}
}
if (idxIp == -1) return null;
// 以上で、使うIPアドレスが、ipAddress[idxIp]に決定
return ipAddressArray[idxIp];
}
// ip:portNo のマルチキャストアドレス用Socketを生成して、送信準備をして、そのソケットを返す
public static Socket getMulticastSocket(IPAddress ip, int portNo)
{
Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(ip));
socket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.MulticastTimeToLive, 2);
IPEndPoint ipep = new IPEndPoint(ip, portNo);
socket.Connect(ipep);
return socket;
}
// 2秒後に、ip:portNo のマルチキャストでsendDataを送信する
public static async System.Threading.Tasks.Task SendMulticast(IPAddress ip, int portNo, byte []sendData)
{
System.Net.Sockets.Socket udpSocket = getMulticastSocket(ip, portNo);
await System.Threading.Tasks.Task.Delay(2000); // 2秒の遅延を非同期に待機
udpSocket.Send(sendData);
udpSocket.Close();
}
// マルチキャスト受信と、Debug.Log表示
public static async System.Threading.Tasks.Task udpReceiveAsyncLoop(string multicast_IP, int portNo)
{
UdpClient client = new UdpClient();
client.ExclusiveAddressUse = false;
client.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);
IPEndPoint localEp = new IPEndPoint(IPAddress.Any, portNo);
client.Client.Bind(localEp);
IPAddress multicastIP = IPAddress.Parse(multicast_IP);
client.JoinMulticastGroup(multicastIP, IPAddress.Any);
//client.JoinMulticastGroup(IPAddress.Parse("239.255.0.1"), IPAddress.Any);//複数登録可能
while (true)
{
try
{
UdpReceiveResult udpReceiveResult = await client.ReceiveAsync();
string text = System.Text.Encoding.ASCII.GetString(udpReceiveResult.Buffer);
Debug.Log($"udpReceiveAsyncLoop:{multicastIP}:{text}");
}
catch (System.Exception e)
{
Debug.Log($"{e.ToString()}");
break;
}
}
}
}
from machine import Pin, I2C
import picowtcp
import utime
import _thread
target_pin = Pin(14,Pin.OUT) # 使用LED利用の準備
onboard_led = Pin( "LED", Pin.OUT )
target_pin.high()# 外付けLED点灯(GP14接続のLEDを点灯)
print( picowtcp.wifi__connect() ) # Wifi接続
target_pin.low()# 外付けLED消灯(GP14接続のLEDを消灯)
ip_port_bin = picowtcp.receiveIpMulticast("239.192.0.0", 59001) # 接続サーバIPアドレス受信
ip_port_a = ip_port_bin.decode('utf-8').split(',')
picowtcp.ip=ip_port_a[0]
picowtcp.portnumber=int(ip_port_a[1])
print(f"Multicast Recive IP:PORT:{picowtcp.ip}:{picowtcp.portnumber}")
target_pin.high()# 外付けLED点灯(GP14接続のLEDを点灯)
client=None# TCPサーバに接続で得られる自身のクライアント用socket記憶用
def receive(sock): #TCP受信のスレッド用関数
print( "start receiveData" )
while True:
bin = sock.recv(1)#1byte受信
print(f"----{bin}")
#
print( "receiveData ended." )
from machine import Pin, I2C
import utime
I2C_SDA=16# GP16のジェネラル端子をSDA「Serial Data」に指定用変数
I2C_SCL=17# GP17のジェネラル端子をSCL「Serial Clock」に指定用変数
I2C_CH=0# I2Cのチャンネル指定用変数(Picoでは0と1のチャンネルがある)
I2C_M_ADDR=0x1C#磁力センサのアドレス
I2C_AG_ADDR=0x6a#加速センサとジャイロセンサのアドレス
# SCL はスタンダードモードの400KHzの通信モードで、I2Cを初期化
i2c=I2C( I2C_CH, scl=Pin(I2C_SCL), sda=I2C_SDA, freq=400000)#★
for addr in i2c.scan(): print( f"enabled address:{addr:02x}" )
# 磁気センサー関連のレジスタ番号と設定値(lsm9ds1.pdf参照)
CTRL_REG1_M_N=0x20# レジスタ番号
CTRL_REG1_M=0b10010000# 温度補正 Low-power mode ODR:10Hz デフォルト:00010000
CTRL_REG2_M_N=0x21
CTRL_REG2_M=0b00000000# Full scale:±4gauss デフォルト:00000000
CTRL_REG3_M_N=0x22
CTRL_REG3_M=0b00000000# I2Cのenable Continuous conversion mode デフォルト:00000011
CTRL_REG4_M_N=0x23
CTRL_REG4_M=0b00000000# Z-axis:low power, data LSb at lower address, デフォルト: 00000000
CTRL_REG5_M_N=0x24
CTRL_REG5_M=0b00000000# continuous update デフォルト: 00000000
STATUS_REG_M=0x27# 状態
OUT_X_L_M=0x28 # このアドレスより、OUT_X_H_M、OUT_Y_L_M、OUT_Y_H_M、OUT_Z_L_M、OUT_Z_H_Mレジスタが並ぶ
OFFSET_X_REG_L_M=0x05
# ジャイロのセンサー関連のレジスタ番号と設定値
CTRL_REG1_G_N=0x10# レジスタ番号
CTRL_REG1_G=0b11010000# 角速度センサ制御レジスタ デフォルト:00000000
#CTRL_REG1_G[7〜5bit]のODR_G:110 測定周期952Hz
#CTRL_REG1_G[4〜3bit]のFS_G:01 Gyroscope full-scaleを 58dps
#CTRL_REG1_G[1〜0bit]のBW_G:00 帯域幅
CTRL_REG2_G_N=0x11
CTRL_REG2_G=0b00000000# デフォルト:00000000
CTRL_REG3_G_N=0x12
CTRL_REG3_G=0b00000000# デフォルト:00000000
#CTRL_REG3_G でLow-power disabled、High-pass filter desable,Gyroscope high-pass filter cutoff frequency set
STATUS_REG=0x17# 状態
OUT_X_G=0x18 # このアドレスより、OOUT_Y_G (1Ah - 1Bh)、OUT_Z_G (1Ch - 1Dh)のレジスタが並ぶ
# 加速度のセンサー関連のレジスタ番号と設定値
CTRL_REG4_N=0x1E
CTRL_REG4=0b00111000#0,0,Zen_G,Yen_G,Xen_G,0,LIR_XL1,4D_XL1 #デフォルト値:00111000
CTRL_REG5_N=0x1F
CTRL_REG5=0b00111000#0,0,Zen_G,Yen_G,Xen_G,0,LIR_XL1,4D_XL1 #デフォルト値:00111000
CTRL_REG6_XL_N=0x20
CTRL_REG6_XL=0b00000000#デフォルト値:00000000
#CTRL_REG6_XL[7〜5bit]はジャイロスコープ利用時無効?で、ジャイロスコープと同じ測定周期
#CTRL_REG6_XL[4〜3bit]はAccelerometer full-scale:±2g
#CTRL_REG6_XL[4〜3bit]
OUT_X_L_XL=0x28#OUT_X_H_XL,OUT_Y_L_XL,OUT_Y_H_XL,OUT_Z_L_XL,OUT_Z_H_XL
#磁力センサのアドレスを指定して、各制御レジスタを設定
i2c.writeto_mem( I2C_M_ADDR, CTRL_REG1_M_N, bytes([CTRL_REG1_M]))
i2c.writeto_mem( I2C_M_ADDR, CTRL_REG2_M_N, bytes([CTRL_REG2_M]))
i2c.writeto_mem( I2C_M_ADDR, CTRL_REG3_M_N, bytes([CTRL_REG3_M]))
i2c.writeto_mem( I2C_M_ADDR, CTRL_REG4_M_N, bytes([CTRL_REG4_M]))
i2c.writeto_mem( I2C_M_ADDR, CTRL_REG5_M_N, bytes([CTRL_REG5_M]))
#各ジャイロセンサレジスタをを上記の通り設定
i2c.writeto_mem( I2C_AG_ADDR, CTRL_REG1_G_N, bytes([CTRL_REG1_G]))
i2c.writeto_mem( I2C_AG_ADDR, CTRL_REG2_G_N, bytes([CTRL_REG2_G]))
i2c.writeto_mem( I2C_AG_ADDR, CTRL_REG3_G_N, bytes([CTRL_REG3_G]))
#各加速度のセンサーレジスタを上記の通り設定
i2c.writeto_mem( I2C_AG_ADDR, CTRL_REG4_N, bytes([CTRL_REG4]))
i2c.writeto_mem( I2C_AG_ADDR, CTRL_REG5_N, bytes([CTRL_REG5]))
i2c.writeto_mem( I2C_AG_ADDR, CTRL_REG6_XL_N, bytes([CTRL_REG6_XL]))
target_pin.low()# 外付けLED消灯(GP14接続のLEDを消灯)
while True:
try:
while client == None: # サーバが受け入れるまで、TCP接続要求を繰り返す
target_pin.high()# 外付けLED点灯(GP14接続のLEDを点灯)
client=picowtcp.tcp_connect()
if client != None: t_id = _thread.start_new_thread(receive, (client,) )
#
statu=i2c.readfrom_mem( I2C_M_ADDR, STATUS_REG_M,1)
#print(f"statu:{statu[0]:08b}", end=" ")
if statu[0] & 0x80 :
bin=i2c.readfrom_mem( I2C_M_ADDR, OUT_X_L_M, 6) # X,Y,Z軸の磁気センサー情報取得
#print( [v for v in bin] )
magbin=[bin[n] + (bin[n+1]<<8) for n in range(0,len(bin),2)] # 2byteからuint16へ変換
magbin=[v if v <= 32767 else v - 65536 for v in magbin] # uint16からint16へ変換
#deg = math.atan(magbin[1]/magbin[0]) * 360 / math.pi / 2 # 角度算出
#print( f"M:{magbin[0]:6}, Y:{magbin[1]:6}, Z:{magbin[2]:6} , deg={deg:5.2f}" )
picowtcp.send_data(client, 'M', magbin)
if statu[0] & 0x02 : # ジャイロセンサー情報あり?
bin=i2c.readfrom_mem( I2C_AG_ADDR, OUT_X_G, 6) # X,Y,Z軸のジャイロセンサー情報取得
#print( [v for v in bin] )
gyro=[bin[n] + (bin[n+1]<<8) for n in range(0,len(bin),2)]
gyro=[v if v <= 32767 else v - 65536 for v in gyro]
picowtcp.send_data(client, 'G', gyro)
if statu[0] & 0x01: # 加速度情報情報あり?
bin=i2c.readfrom_mem( I2C_AG_ADDR, OUT_X_L_XL, 6) # X,Y,Z軸の加速度情報取得
#print( [v for v in bin] )
accel=[bin[n] + (bin[n+1]<<8) for n in range(0,len(bin),2)]
accel=[v if v <= 32767 else v - 65536 for v in accel]
#print( f"Accelerometer:{accel}" )
picowtcp.send_data(client, 'A', accel)
if (statu[0] & 0x80) or (statu[0] & 0x02) or (statu[0] & 0x01):
if onboard_led.value(): onboard_led.low()
else: onboard_led.high()
#utime.sleep(0.5)
except Exception as e:
print( e )
client = None
utime.sleep(0.5)
client.close()
import socket import network import utime import struct SSID='〇〇'# Wifi用 PW = '〇〇〇'# 上記に接続するためのパスフレーズ ip, portnumber="192.168.0.110",59001#接続先サーバのアドレス def wifi__connect():# Wifiに接続するまで繰り返す wlan = network.WLAN(network.STA_IF) wlan.active(True) wlan.connect(SSID, PW) while wlan.isconnected() == False: print('Connecting to Wi-Fi AP') utime.sleep(1) # wlan_status = wlan.ifconfig() return f'''Connected! Pico IP Address:{wlan_status[0]} Netmask:{wlan_status[1]} Default Gateway:{wlan_status[2]} Name Server: {wlan_status[3]}''' # https://micropython-docs-ja.readthedocs.io/ja/latest/library/socket.html def ipv4_inet_pton(ip: str)->bytes:# iPv4のアドレス文字列に対応する4byteバイナリを返す a=ip.split('.') return bytes(map(int,a)) def receiveIpMulticast(multicast_group, portNo): #マルチキャスト受信 server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # ソケットの作成 server_socket.bind(('', portNo)) # ソケットにアドレスをバインド # マルチキャストグループに参加 group = ipv4_inet_pton(multicast_group) # 引数のIPアドレス文字列に対応する4byteバイナリ取得 mreq = struct.pack('4sL', group, 0) server_socket.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq) # 受信 data, addr = server_socket.recvfrom(1024) # データとアドレスを受信 return data def tcp_connect(): client=socket.socket(socket.AF_INET, socket.SOCK_STREAM) print(f'Connecting to {ip}:{portnumber}') client.connect((ip, portnumber)) client.setblocking(True)# client.settimeout(None) # タイムアウトを秒数で指定 #client.setblocking(False) return client def send_data(sock:socket, prefix:str, d16s:list): bin=prefix.encode('utf-8') + int(d16s[0]).to_bytes(2,'big') bin+=int(d16s[1]).to_bytes(2,'big')+int(d16s[2]).to_bytes(2,'big') n=0 while n < len(bin):#上記の(1+2*3)=7byteを送る。(非ブロッキングにも対応) n+=sock.write(bin[n:]) #sock.write(bin)、 sock.send(bin)、sock.sendall(bin)# などで全てOSError: [Errno 103] ECONNABORTED return bin if __name__ == '__main__': print( wifi__connect() ) client = tcp_connect() bin=send_data(client, 'M', [50, 127, -1]) # 7byteのバイト例で送信 print( f"送信データ:{bin}") while True: client.write(bin) #utime.sleep(0.001) client.close()