前回の投稿で、ESP32に接続したセンサ信号をUDPブロードキャストしました。
ESP32で計測したセンサーデータをUDPブロードキャストする
今回は、CordovaでUDPブロードキャストを受信するプラグインを作成します。
アプリケーションは、ランダム画像を表示するクロックにします。
クロック表示画面には、ランダム画像提供サイトから取得される画像を背景画像として表示します。
もし、UDPブロードキャストで受信する光センサの値から周りが暗いと判断した場合、就寝中あるいは外出中とみなして、画像の表示をやめて、黒背景にします。
また、UDPブロードキャストで赤外線リモコン信号を受信した場合には、画像の取得元を変更して、別の画像に切り替えられるようにしました。
もろもろのソースコードは前回と同じ以下に置きました。
LineBeaconSensor
スマホアプリの構成
スマホアプリとしてCordovaを採用しました。
理由は、比較的簡単にAndroidとiOS両対応のネイティブアプリを実装できるのと、プラグインが豊富であるためです。
Android、iPhone、iPadで動作を確認しました。
以下のプラグインを使いました。
-
cordova-plugin-device
AndroidとiOSどちらで動いているかを判別するために利用しています。(ただし、今回はOSごとに区別した処理はありません) -
cordova-plugin-powermanagement
スマホの画面を常に表示をOnにするために利用しています。 -
cordova-plugin-simpleudp
UDPブロードキャストを受信するために利用しています。
残念ながら、UDPを受信するのに都合の良いプラグインは見つからなかったため、自作しました。(後述)
UDP受信のためのCordovaプラグイン
以下に上げておきました。
cordova-plugin-simpleudp
プラグインの作成方法は以下を参考にします。
Android実装
Android用のネイティブ部はこんな感じです。
package jp.or.sample.plugin.SimpleUdp;
import android.app.Activity;
import android.content.Intent;
import android.util.Log;
import org.apache.cordova.CordovaInterface;
import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.CallbackContext;
import org.apache.cordova.CordovaWebView;
import org.apache.cordova.PluginResult;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.HashMap;
public class Main extends CordovaPlugin {
public static String TAG = "SimpleUdp.Main";
private Activity activity;
private static final int DEFAULT_RECEIVE_BUFFER_SIZE = 1024;
private HashMap<Integer, DatagramSocket> sockets = new HashMap<>();
@Override
public void initialize(CordovaInterface cordova, CordovaWebView webView)
{
Log.d(TAG, "[Plugin] initialize called");
super.initialize(cordova, webView);
activity = cordova.getActivity();
}
@Override
public void onResume(boolean multitasking)
{
Log.d(TAG, "[Plugin] onResume called");
super.onResume(multitasking);
}
@Override
public void onPause(boolean multitasking)
{
Log.d(TAG, "[Plugin] onPause called");
super.onPause(multitasking);
}
@Override
public void onNewIntent(Intent intent)
{
Log.d(TAG, "[Plugin] onNewIntent called");
super.onNewIntent(intent);
}
@Override
public boolean execute(String action, JSONArray args, final CallbackContext callbackContext) throws JSONException
{
Log.d(TAG, "[Plugin] execute called");
if( action.equals("receiving") ){
try {
final CallbackContext callback = callbackContext;
final int localRecvPort = args.getInt(0);
if( sockets.containsKey(localRecvPort) ){
DatagramSocket t = sockets.get(localRecvPort);
t.close();
sockets.remove(localRecvPort);
}
final DatagramSocket udpReceive = new DatagramSocket(localRecvPort);
sockets.put(localRecvPort, udpReceive);
cordova.getThreadPool().execute(new Runnable() {
@Override
public void run() {
try {
while(true){
byte[] buff = new byte[DEFAULT_RECEIVE_BUFFER_SIZE];
DatagramPacket packet = new DatagramPacket(buff, buff.length);
udpReceive.setSoTimeout(0);
udpReceive.receive(packet);
Log.d(TAG, "received");
JSONObject message = new JSONObject();
int len = packet.getLength();
String payload = new String(buff, 0, len);
message.put("payload", payload);
byte[] address = packet.getAddress().getAddress();
message.put("ipaddress", (((long)(address[0] & 0x00ff)) << 24) | (((long)(address[1] & 0x00ff)) << 16) | (((long)(address[2] & 0x00ff)) << 8) | (((long)(address[3] & 0x00ff)) << 0) );
message.put("port", packet.getPort());
final PluginResult result = new PluginResult(PluginResult.Status.OK, message);
result.setKeepCallback(true);
callback.sendPluginResult(result);
}
} catch (Exception ex) {
callbackContext.error(ex.getMessage());
if( udpReceive != null )
udpReceive.close();
}
}
});
}catch(Exception ex){
Log.d(TAG, ex.getMessage());
callbackContext.error("Invalid arg0(int)");
return false;
}
}else
if( action.equals("send") ){
try{
String message = args.getString(0);
String host = args.getString(1);
int port = args.getInt(2);
cordova.getThreadPool().execute(new Runnable() {
@Override
public void run() {
try {
DatagramSocket udpSend = new DatagramSocket();
byte[] buff = message.getBytes();
InetAddress inetAddress = InetAddress.getByName(host);
DatagramPacket packet = new DatagramPacket(buff, buff.length, inetAddress, port);
udpSend.send(packet);
Log.d(TAG, "sended");
udpSend.close();
callbackContext.success();
} catch (Exception ex) {
callbackContext.error(ex.getMessage());
}
}
});
callbackContext.success();
}catch(Exception ex){
Log.d(TAG, ex.getMessage());
callbackContext.error("Invalid arg0(String), arg1(String), arg2(int)");
return false;
}
}else {
String message = "Unknown action : (" + action + ") " + args.getString(0);
Log.d(TAG, message);
callbackContext.error(message);
return false;
}
return true;
}
}
iOS実装
iOSではこんな感じに実装しました。
(一点はまったのは、UDPパケットの1回目の受信は問題なかったのですが、2回目の受信でエラーが発生していました。理由はわかりませんが、試行錯誤して、 newConnection.forceCancel()
を呼ぶようにしたらエラーが発生しなくなりました。)
import Foundation
import Network
@objc(SimpleUdpPlugin)
class SimpleUdpPlugin : CDVPlugin
{
var sockets:[UInt16:NWListener] = [:]
override
func pluginInitialize() {
}
@objc(receiving:)
func receiving(command: CDVInvokedUrlCommand){
// port
NSLog("receiving called")
guard let port = command.arguments[0] as? UInt16 else {
let pluginResult:CDVPluginResult = CDVPluginResult(status:CDVCommandStatus_ERROR, messageAs: "Parameter invalid")
self.commandDelegate.send(pluginResult, callbackId: command.callbackId)
return
}
do {
if let listener = self.sockets[port] {
listener.cancel()
}
let localPort = NWEndpoint.Port(rawValue: port)
let listener = try NWListener(using: .udp, on: localPort!)
listener.stateUpdateHandler = { state in
switch state {
case .ready:
print("listener ready")
case .failed(let error):
print("listener2 error: \(error)")
let pluginResult:CDVPluginResult = CDVPluginResult(status:CDVCommandStatus_ERROR, messageAs: "listener failed")
self.commandDelegate.send(pluginResult, callbackId: command.callbackId)
listener.cancel()
default:
break
}
}
listener.newConnectionHandler = { newConnection in
newConnection.start(queue: .global())
newConnection.receiveMessage { (data, context, isComplete, error ) in
if let data = data {
if let receivedString = String(data: data, encoding: .utf8){
print("receive message: \(receivedString)")
newConnection.forceCancel()
let pluginResult = CDVPluginResult(status: CDVCommandStatus_OK, messageAs: ["payload": receivedString])
pluginResult?.keepCallback = true
self.commandDelegate.send(pluginResult, callbackId: command.callbackId)
}else if let error = error {
print("receive error: \(error)")
}
}
}
}
listener.start(queue: .global())
self.sockets[port] = listener
}catch{
let pluginResult:CDVPluginResult = CDVPluginResult(status:CDVCommandStatus_ERROR, messageAs: "unknown")
self.commandDelegate.send(pluginResult, callbackId: command.callbackId)
}
}
@objc(send:)
func send(command: CDVInvokedUrlCommand)
{
// message, host, port
NSLog("send called")
guard let message = command.arguments[0] as? String, let host = command.arguments[1] as? String, let port = command.arguments[2] as? UInt16 else {
NSLog("Parameter invalid")
let pluginResult:CDVPluginResult = CDVPluginResult(status:CDVCommandStatus_ERROR, messageAs: "Parameter invalid")
self.commandDelegate.send(pluginResult, callbackId:command.callbackId)
return
}
let sendPort = NWEndpoint.Port(rawValue: port)
let sendHost = NWEndpoint.Host(host)
let connection = NWConnection(host: sendHost, port: sendPort!, using: .udp)
connection.start(queue: .global())
if let messageData = message.data(using: .utf8){
connection.send(content: messageData, completion: .contentProcessed{ error in
if let error = error {
print("send error: \(error)")
let pluginResult:CDVPluginResult = CDVPluginResult(status:CDVCommandStatus_ERROR, messageAs: "send falied")
self.commandDelegate.send(pluginResult, callbackId: command.callbackId)
}else{
print("send success")
let pluginResult = CDVPluginResult(status: CDVCommandStatus_OK, messageAs: "OK")
self.commandDelegate.send(pluginResult, callbackId: command.callbackId)
}
connection.cancel()
})
}
}
}
あとは、Javascriptで以下のように呼び出せば、受信パケットを処理できます。
以下は、AndroidとiOSどちらも同じコードです。
simpleudp.receiving(UDP_PORT, packet =>{
console.log(packet);
・・・
});
UDP受信パケット
UDPパケットはこんな感じです。
{"type":"sensor","brightness":4095}
{"type":"button","isPressed":1,"wasPressed":1}
{"type":"ir","decode_type":3,"value_high":0,"value_low":50167935}
ランダム画像の取得
以下のサイトにお世話になりました。
これらランダム画像の取得URLは、以下のページを参考にさせていただきました。
クライアントソースコード
Cordovaアプリのクライアントソースコードは、Javascriptです。
'use strict';
//const vConsole = new VConsole();
//const remoteConsole = new RemoteConsole("http://[remote server]/logio-post");
//window.datgui = new dat.GUI();
const CLOCK_UPDATE_INTERVAL = 20000;
const BRIGHTNESS_THRESHOLD = 4000;
const UDP_PORT = 12346;
const IMAGE_URL_BASE_LIST = [
"https://unsplash.it/g/",
"https://picsum.photos/",
"https://p-hold.com/",
];
var vue_options = {
el: "#top",
mixins: [mixins_bootstrap],
data: {
datetime_now: new Date().getTime(),
is_dark_prev: false,
is_dark: false,
base_url_index: 0,
},
computed: {
},
methods: {
toDateString: function(tim){
const d = new Date(tim);
const weekStr = ["日", "月", "火", "水", "木", "金", "土"];
return `${d.getFullYear()}年${d.getMonth() + 1}月${d.getDate()}日(${weekStr[d.getDay()]})`;
},
toTimeString: function(tim){
const to2d = (d) => {
return ("00" + d).slice(-2);
};
const d = new Date(tim);
return `${to2d(d.getHours())}:${to2d(d.getMinutes())}`;
},
background_view_update: function(){
if( !this.is_dark ){
document.body.style.backgroundImage = 'url(' + this.background_url + ')';
}else{
document.body.style.backgroundImage = "none";
}
},
background_image_change: async function(){
console.log("background_change called");
var width = document.documentElement.clientWidth;
var height = document.documentElement.clientHeight;
var base_url = IMAGE_URL_BASE_LIST[this.base_url_index];
this.background_url = `${base_url}/${width}/${height}?p=${new Date().getTime()}`;
console.log("url: " + this.background_url);
this.background_view_update();
},
background_check_dark: function(is_dark_now){
if( (this.is_dark == is_dark_now) || (is_dark_now && !this.is_dark_prev)){
this.is_dark_prev = is_dark_now;
return;
}
this.is_dark = is_dark_now;
this.is_dark_perv = this.is_dark;
this.background_view_update();
},
background_switch_base: async function(){
this.base_url_index = ++this.base_url_index % IMAGE_URL_BASE_LIST.length;
this.background_image_change();
},
onDeviceReady: async function(){
console.log("os: " + device.platform);
window.powermanagement.acquire();
simpleudp.receiving(UDP_PORT, packet =>{
console.log(packet);
var payload = JSON.parse(packet.payload);
if( payload.type == 'sensor'){
this.background_check_dark(payload.brightness > BRIGHTNESS_THRESHOLD);
}else
if( payload.type == "ir" ){
this.background_switch_base();
}
});
},
},
created: function(){
},
mounted: function(){
proc_load();
console.log("width: " + document.documentElement.clientWidth + " height: " + document.documentElement.clientHeight);
this.background_image_change();
setInterval(() =>{
this.datetime_now = new Date().getTime();
}, CLOCK_UPDATE_INTERVAL);
}
};
vue_add_data(vue_options, { progress_title: '' }); // for progress-dialog
vue_add_global_components(components_bootstrap);
vue_add_global_components(components_utils);
/* add additional components */
window.vue = new Vue( vue_options );
HTMLです。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta http-equiv="Content-Security-Policy" content="default-src * data: gap: https://ssl.gstatic.com 'unsafe-eval' 'unsafe-inline'; style-src * 'unsafe-inline'; media-src *; img-src * data: content: blob:;">
<meta name="format-detection" content="telephone=no">
<meta name="msapplication-tap-highlight" content="no">
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Cache-Control" content="no-cache">
<!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
<script src="https://code.jquery.com/jquery-1.12.4.min.js" integrity="sha384-nvAa0+6Qg9clwYCGGPpDQLVpLNn0fRaROjHqs13t4Ggj3Ez50XnGQqc/r8MhnRDZ" crossorigin="anonymous"></script>
<!-- Latest compiled and minified CSS -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css" integrity="sha384-HSMxcRTRxnN+Bdg0JdbxYKrThecOKuH5zCYotlSAcp1+c8xmyTe9GYg1l9a69psu" crossorigin="anonymous">
<!-- Optional theme -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap-theme.min.css" integrity="sha384-6pzBo3FDv/PJ8r2KRkGHifhEocL+1X2rVCTTkUfGk7/0pbek5mMa1upzvWbrUbOZ" crossorigin="anonymous">
<!-- Latest compiled and minified JavaScript -->
<script src="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js" integrity="sha384-aJ21OjlMXNL5UyIl/XNwTMqvzeRMZH2w8c5cRVpzpU8Y5bApTppSuUkhZXN0VxHd" crossorigin="anonymous"></script>
<link rel="stylesheet" href="css/start.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/spinkit/2.0.1/spinkit.min.css" />
<script src="js/methods_bootstrap.js"></script>
<script src="js/components_bootstrap.js"></script>
<script src="js/components_utils.js"></script>
<script src="js/vue_utils.js"></script>
<script src="js/gql_utils.js"></script>
<script src="js/remoteconsole.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vconsole/dist/vconsole.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/js-cookie@2/src/js.cookie.min.js"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/toastr.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/toastr.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.7.7/dat.gui.min.js"></script>
<link rel="stylesheet" href="css/wallpaper.css">
<title>Template</title>
</head>
<body class="background">
<!--
<div id="loader-background">
<div class="sk-plane sk-center"></div>
</div>
-->
<div id="top" class="container-fluid">
<div v-bind:class="{ 'top-text-leftbottom': !is_dark, 'top-text-center': is_dark }" v-on:click="background_switch_base">
<div>
<span class="time_style">{{toTimeString(datetime_now)}}</span><br>
<span class="date_style">{{toDateString(datetime_now)}}</span><br>
</div>
</div>
<!-- for progress-dialog -->
<progress-dialog v-bind:title="progress_title"></progress-dialog>
</div>
<script src="js/start.js"></script>
<script src="cordova.js"></script>
<script src="js/index.js"></script>
</body>
文字の大きさを変えたい場合は、以下のCSSを編集してください。
.background {
width: 100%;
height: 100vh;
background-color: #000000;
background-size: contain;
background-position: center;
background-repeat: no-repeat;
}
.top-text-center {
width: 100%;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.top-text-leftbottom {
width: 100%;
height: 100vh;
display: flex;
align-items: flex-end;
}
.top-text-leftbottom * .time_style {
color: white;
font-size: 150px;
}
.top-text-leftbottom * .date_style {
color: white;
font-size: 50px;
}
.top-text-center .time_style {
color: white;
font-size: 200px;
}
.top-text-center .date_style {
color: white;
font-size: 80px;
}
コンパイル・インストール方法
cordovaがインストールされている前提です。
以下からプロジェクト一式(ZIP)をダウンロードし、ZIPを展開します。
> cd LineBeaconSensor-main\Cordova\WallPaperClock
> npm install
> cordova platform add android または ios
もしiosの場合は、XcodeでSigningやOSバージョンを指定してください。
スマホをPCまたはMacに接続します。
> cordova build android または ios
> cordova run android または ios
以上