AndroidのGoogleアシスタントからパソコンを起動したい
勉強も兼ねて個人的な問題を解決するためにアプリを作成したのでご紹介します。
解決したいのは自宅のパソコン周辺の配置の問題で
パソコンの電源を投入するために
足の親指で電源ボタンを押すか、テーブルを回り込むか、テーブルの下に潜る必要がありました。
できる限り椅子から立たずに、できれば手も足も使わずにパソコンの電源を入れたいと思っていました。
幸いにも私用パソコンに乗せているマザーボードはWake On Lan機能に対応していたので、
同一Lanに接続した端末からマジックパケットを送信すれば電源を投入できるようでした。
アプリストアを探すとAndroidからマジックパケットを送信するアプリはいくつかありましたが、できることなら手も動かしたくないので、
スマートホーム家電のように自然な音声コマンドでマジックパケットを送信してパソコンの電源を投入したいな。
と思いアプリを作成しました。
ソースコードです。
class MainActivity : AppCompatActivity() {
companion object
{
// 符号なしbyteの最大値
private val BYTE_MAX_VALUE = (UByte.MAX_VALUE.toInt())
// AMD Magic Packet Format の先頭 (6byte)
private const val PRE_STR = ("FFFFFFFFFFFF")
// ポート番号
private const val PORT = (4000)
// マジックパケットのサイズ (先頭6byte + MACアドレス * 16)
private const val SIZE_MAGIC_PACKET = (6 * 17)
// 16回ループするRange構文 (MACアドレスループ用)
private val RANGE_16_TIMES = (1..16)
// マジックパケットを文字列として格納するサイズ (1byteを2文字の16進数で表す)
private const val SIZE_MAGIC_PACKET_STR = (SIZE_MAGIC_PACKET * 2)
// マジックパケット文字列の文字数分のRange構文
private val RANGE_SEND_PACKET_STR = (1..SIZE_MAGIC_PACKET_STR)
// 取得失敗時に返すブロードキャストアドレス
private const val DEFAULT_BROADCAST_ADDRESS = ("255.255.255.255")
// ユニキャスト用 Lan内の対象デバイスのIPアドレス (自宅のPC)
private const val TARGET_IP_ADDRESS = ("192.168.2.69")
// Lan内の対象デバイスのMACアドレス (自宅のPC)
private const val MAC_ADDRESS = ("XX:XX:XX:XX:XX:XX")
private const val MAC_ADDRESS_SEPARATOR = (":")
/**
* WakeOnLanを行う関数
* @param activity Activity
*/
private fun wakeup(activity: Activity)
{
// UDP送信用に準備する
val datagramChannel = DatagramChannel.open()
val address = InetSocketAddress(PORT)
// ノンブロッキングモードに設定
datagramChannel.configureBlocking(false);
// ブロードキャストする設定
datagramChannel.socket().broadcast = true
datagramChannel.socket().bind(address)
val wifiManager = activity.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager
// ブロードキャストアドレスを取得
val broadcastAddress = getBroadcastAddress(wifiManager)
val socketAddress = InetSocketAddress(broadcastAddress, PORT)
// マジックパケット文字列を作成
val packetStr = buildMagicPacket(MAC_ADDRESS)
// マジックパケット文字列が意図したサイズでなければ中断
if(packetStr.length != SIZE_MAGIC_PACKET_STR)
{
return
}
// Char配列化
val packet = packetStr.toCharArray();
// マジックパケット文字列をByteBufferにする
// サイズ分確保し初期化
val buf = ByteBuffer.allocate(SIZE_MAGIC_PACKET)
buf.clear()
// 2文字で1byte分なので2文字ずつのループ
for(cnt in RANGE_SEND_PACKET_STR step 2)
{
val index = cnt - 1
// マジックパケットChar配列から2文字ずつ取り出す
val byteStr = String(packet, index, 2)
// 16進数文字列をデコードし、Byteデータにする
val byte = Integer.decode("0x".plus(byteStr)).toByte()
// ByteBufferにセット
buf.put(byte)
}
buf.flip()
// UDP送信を行う
// ByteBufferを送信
val result = datagramChannel.send(buf, socketAddress)
if(result == SIZE_MAGIC_PACKET)
{
activity.runOnUiThread {
Toast.makeText(activity, "send success", Toast.LENGTH_SHORT).show()
}
}
datagramChannel.disconnect()
datagramChannel.close()
}
/**
* 引数に渡されたMACを用いマジックパケットを組み立てる関数
* @param mac String MACアドレス
* @return String マジックパケット
*/
private fun buildMagicPacket(mac:String):String
{
// MACアドレスから前後の空白とセパレータ文字を除去
val macAddressStr = mac.trim().replace(MAC_ADDRESS_SEPARATOR,"")
val sb = StringBuffer()
// 先頭6byte分の文字列をセット
sb.append(PRE_STR)
for (i in RANGE_16_TIMES)
{
// 16回MACアドレスをセット
sb.append(macAddressStr)
}
return sb.toString()
}
/**
* ブロードキャストアドレスを取得する関数
* @param context コンテキスト
* @return String ブロードキャストアドレス
*/
private fun getBroadcastAddress(wifiManager : WifiManager):String{
// byte配列のサイズ
val byteArraySize = (4)
// DHCP要求の結果を取得
val dhcpInfo = wifiManager.dhcpInfo
if (dhcpInfo != null)
{
// 8Bitシフトバイナリから変換された整数値からブロードキャストアドレスを求める
val broadcast = (dhcpInfo.ipAddress and dhcpInfo.netmask) or (dhcpInfo.netmask).inv()
// byte配列化
val broadcastAddress = ByteArray(byteArraySize)
for (cnt in 1..byteArraySize)
{
val index = cnt - 1
broadcastAddress[index] = ((broadcast shr index * 8) and BYTE_MAX_VALUE).toByte()
}
try {
return InetAddress.getByAddress(broadcastAddress).hostAddress
} catch (e : UnknownHostException)
{
return DEFAULT_BROADCAST_ADDRESS
}
}
else
{
return DEFAULT_BROADCAST_ADDRESS
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// WOLを実行するスレッドを起動し、このactivityを終了する
try {
thread{
wakeup(this)
}.join(1000)
}finally {
finish()
}
}
}
定数で定義したターゲットデバイスのMACアドレスを元に
buildMagicPacket関数でマジックパケットを作成し、
接続しているネットワークのブロードキャストアドレスを
getBroadcastAddress関数で求め、そのアドレスに対して
UDPブロードキャスト送信するアプリです。
アプリ名を「パソコン」等に設定し
「Ok Google, “パソコン”を起動して」というような音声コマンドでこのアプリを起動することで、
このアプリから送信されたマジックパケットにより目的のパソコンを起動する。
という使い方を想定しているため、
アクティビティはonCreateでパケットを送信し直ちに終了しています。
正確にはGoogleアシスタントからパソコンを起動している訳ではないですし、
このままだとWake On Lanで起動したいデバイスのMACアドレス毎にアプリを作成する必要があるのでスマートではないですね。
水曜担当:Tanaka
tanaka at 2019年10月16日 10:00:26