UMEHOSHI ITA TOP PAGE COMPUTER SHIEN LAB
下記の遠隔対象PCを乗せるターンテーブルを「UMEHOSHI ITA 」を利用して作る紹介はこちらで示す別ページ(作成中)で紹介の予定
古いWindowsタブレットマシンを老人ホームやに病院など、フリーWifiが繋がる環境に置いた状態で、
そのデスクトップを、他の端末のブラウザで遠隔操作して、google Meetを起動して会話をできるようにする目標です。
(直接に操作できない環境のWindowsPCを、HTTPだけを利用したWebブラウザで遠隔操作する目標です)
裏からみた状態 100円ショップで入手した デジタルフォトフレーム立てを 改良し、粘土の重さで、 安定に立つようにした。 ![]() |
![]() |
![]() |
左のPCのデスクトップをブラウザで遠隔操作しているイメージです。 このイメージは、左のPCでgoogle Meetを起動している画面です。 ![]() |
左の操作対象Windows用プログラム | TomcatのWebサーバアプリ(情報伝達サーバ) | 右の操作用のパネルとなるHTML |
---|---|---|
desktop_ctrl_server.py(アプリサーバ) pythonのプログラムです。pyautoguiモジュールで デスクトップを制御する。 情報伝達サーバよりGETしたイベントで操作し、 キャプチャーしたデスクトップイメージをPOST。 |
desktop_ctrl_transfer.jsp(情報伝達サーバ) ブラウザで操作したイベントを、アプリサーバからの リクエストで伝達し、アプリサーバからPOSTされた デスクトップイメージを、ブラウザに応答メッセージで 伝達する。(JSPのプログラム) |
desktop_ctrl_client.html(クライアントアプリ) 受信したデスクトップ画像上で操作したイベントで GETリクエストを行い、応答で得られた デスクトップイメージをCanvasに描画表示する。 (配置は、Tomcatサーバ内で、 desktop_ctrl_transfer.jspと同じ位置) |
raytrek DG-D08IWP (他のWindows端末でも可) OS: Windows 10 Home Python 3.9.13 |
Raspberry PI 4 Raspbian GNU/Linux 9 javac 1.8.0_212 Tomcat8 (ポートフォワーディングでグローバルIPアクセス可に設定) |
Chromeブラウザでgoogle Meet が動作可能な任意端末 (Edgeなど他のブラウザ動作しているが、部分的に未検証) |
左がdesktop_ctrl_transfer.jspのWebページを閲覧したイメージで、
Webページ内の大きなCanvasに描いたイメージが、遠隔対象のデスクトップ画面です。
このイメージ上で、マウスボタンの押し込みや、離すボタン操作で、遠隔対象の操作が可能です。
つまり、クリック操作が可能です。
それにより、アイコンの右ボタンクリックでポップアップメニューを出し、「開く」を選択して実行できます。
しかし、一回のマウスボタン操作でデスクトップ画像が更新されるまで、2~4秒程度かかります。
この時間は、遠隔対象のPC能力や、ネットワーク環境にもよります。またそれによって、ボタン操作しても、応答画面が更新されない場合もあります。
例えば、メニューをクリックしても、ポップアップ表示が出る前の画面が送信されて、メニュー項目が並ぶポップアップが出ない場合があります。
そのような場合のため、「デスクトップ更新」ボタンが用意されており、そのクリック操作で現在のデスクトップに、ポップアップメニューが出ているか
確認できます。
つまり待っても更新されない場合は、「デスクトップ更新」ボタンを使い、現時点のデスクトップ画面に更新して、操作を継続できます。
この更新されない対策として、ブラウザからタイマーで自動的に一定間隔でデスクトップ更新を行わせることも考えましたが、
そうすると通信パケット量が増えてしまいます。
現時点では、従量制ネットワーク環境などで、余計なパケットを減らすために、自動更新を組み込みしていません。
なお、操作結果を確認しながら運用する指針で、マウスのダブルクリック操作は、実装しませんでした。
また、マウスドラックも可能にしましたが、ドラックが終わった後にしか画面更新されないので、操作性が悪いようです。
また、Javascriptnのkeydownイベントで得られるkeyCodeをサーバに送信することで、キー操作の遠隔操作も可能です。
これにより、Windowsタスクバーのスタートボタンクリック後のキーによるアプリ選択も可能です。
ですが、一つのキー操作に対して画面更新イベントの要求が行われるため、
一つのキー操作に対して2~4秒程度の画面更新という挙動となります。
よって例えば、メモ帳内容をこの操作で編集する用途には向きません。
そこで、まとまった文字列を一遍に送って、更新画面を得ることができる機能を別途に作っており、ページ下部でTextAreaを用意しています。
操作対象のテキスト入力部をクリックしてから、このTextAreaに設定した文字列を、隣の[TX]ボタンをクリックすることで、
対象入力部に送り込んで、そのまとまった文字列入力に対する応答画面を得ることができます。
補足:
遠隔操作でPCを操作して、Windowsタスクバーのスタートの右クリックで、「設定」を選択して起動することができます。
しかし残念ながら、管理者権限で動作するソフト(タスクマネージャやスケジューラ、デバイスマネージャなど)を起動すると、
それ以降で操作ができなくなる不具合が生じます。
現状では制御ができなくなると、どうしようもないので管理者権限で動作するソフトを操作しないように気を付けるしかありません。
操作できなくなってしまった場合、タスクスケージュールによる再起動で、復帰するまで待つしかありません。
なお原因は、デスクトップスクリーンを制御するpyautogui モジュールが、管理者権限を持つプロセスに対して、制約されるためのようです。
遠隔操作パネル用のHTML内容で、Tomcat8構築内のdesktop_ctrl_transfer.jsp(情報伝達サーバ)と同じディレクトリに配置して使います。
Canvasにおけるマウス操作イベントを、XMLHttpRequestを使ったリクエストで伝達し、その応答としてデスクトップ画像を取得して描画しています。
このページのURLが漏れてしまうと、をれを知った誰でもが遠隔操作できてしまうので、URLの取り扱いには注意が必要です。
001 002 003 004 005 006 007 008 009 010 011 012 013 014 015 016 017 018 019 020 021 022 023 024 025 026 027 028 029 030 031 032 033 034 035 036 037 038 039 040 041 042 043 044 045 046 047 048 049 050 051 052 053 054 055 056 057 058 059 060 061 062 063 064 065 066 067 068 069 070 071 072 073 074 075 076 077 078 079 080 081 082 083 084 085 086 087 088 089 090 091 092 093 094 095 096 097 098 099 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157
<!DOCTYPE html> <html><head> <meta charset="UTF-8"> <title>Desktop Control </title> <script type="text/javascript"><!-- var canvas2 ; // 操作対象のCanvas要素 var context ; // Canvas描画用 var msgElement ; // メッセージ文字列の対象要素 var waitResponse=false; // マウスメッセージの表示フラグ var imageElem;//描画画像を管理するImage要素(動的に生成) var mouseDownPos = {x:0, y:0};// マウスを押した位置記憶用 var IMG_RATIO=1; function getMousePosition(canvas, e) {// canvas内のマウス座標を返す var rect = canvas.getBoundingClientRect(); return { x: Math.floor(e.clientX - rect.left), y: Math.floor(e.clientY - rect.top) }; } function getAccesskey(){// これが合わないと、情報伝達サーバーで伝達されない。予定 return "1001"; } function set_desktop_image(){ // 起動時のサーバ側にある前回の操作時の最後のデスクトップ画像を、キャンバスに描画する。 imageElem = new Image(); imageElem.onload = function () { canvas2.width=imageElem.width; canvas2.height=imageElem.height; context.drawImage(imageElem, 0, 0); }; imageElem.src = "desktop.png"; //相対URLで指定した画像 } // マウスのボタンアップとドラックとキーを押した時のイベントを情報伝達サーバーに送る function init(){// HTML body onload のイベント canvas2 = document.getElementById('canvas2'); context = canvas2.getContext('2d');//上記Canvasに描画する時に使うオブジェクト所得 msgElement = document.getElementById('MSGP');//テキストメッセージの出力対象取得 set_desktop_image(); //初期表示 canvas2.addEventListener('mousemove', function (e) { if( waitResponse == true) return; var mousePos = getMousePosition(canvas2, e); var message = 'マウス位置 X:' + mousePos.x*IMG_RATIO + ', Y:' + mousePos.y*IMG_RATIO; msgElement.textContent=message;//マウス位置をテキスト表示 }, false); canvas2.addEventListener('mousedown', function (e) { if( waitResponse == true) return; mouseDownPos = getMousePosition(canvas2, e);//マウスボタンを押し込み位置を記憶 }, true); canvas2.addEventListener('mouseup', mouseUp, true);// GETで情報伝達サーバーへ送信 window.addEventListener('keydown', keyDown);// GETで情報伝達サーバーへ送信 document.getElementById('TXAREA').addEventListener( 'keydown', function (e) { e.stopPropagation(); console.log( e.keyCode ); }); document.oncontextmenu = function() { return false; }//右クリックメニューを無効化 } function getStringByUint8Array(a){// aのバイナリから文字列取得 var msg = ""; for (var i=0; i < a.length ; i++){ msg += String.fromCharCode(a[i]); } return msg; } // キーが押されたら、情報伝達サーバーへ送信し、受信した相手のデスクトップイメージを受信して表示 function keyDown(e){ if(waitResponse) return; var date_obj = new Date();// 現在のローカル時間が格納された、Date オブジェクトを作成する var mmsec = date_obj.getTime(); // 測定開始時に経過時間を変数に残す var reqString = "desktop_ctrl_transfer.jsp?A="+getAccesskey()+"&T=" + mmsec;// 時間(ミリ秒)埋め込み reqString += "&K=" + e.keyCode;// キーコード埋め込み msgElement.textContent=reqString; requestDeskTop(reqString)// 画像のバイナリイメージをリクエストして表示 } // マウスアップ時に、情報伝達サーバーへ送信し、受信した相手のデスクトップイメージを受信して表示 function mouseUp(e){ if(waitResponse) return; var date_obj = new Date();// 現在のローカル時間が格納された、Date オブジェクトを作成する var mmsec = date_obj.getTime(); // 測定開始時に経過時間を変数に残す var mousePos = getMousePosition(canvas2, e); var reqString = "desktop_ctrl_transfer.jsp?A="+getAccesskey()+"&T=" + mmsec;// 時間(ミリ秒)埋め込み reqString += "&X=" + mousePos.x*IMG_RATIO + "&Y=" + mousePos.y*IMG_RATIO;// マウスUP位置埋め込み reqString += "&B=" + e.button; //マウスボタン情報埋め込み、QueryStringを生成 ボタン左:0,右:2 reqString += "&DX=" + mouseDownPos.x*IMG_RATIO + "&DY=" + mouseDownPos.y*IMG_RATIO;// マウスDOWN位置埋め込み msgElement.textContent=reqString;// GET の送信パラメタの表示 requestDeskTop(reqString)// 画像のバイナリイメージをリクエストして表示 } function requestDeskTop(reqString){// 画像のバイナリイメージをリクエストして表示 waitResponse = true;// 応答メッセージの待ち状態へ移行 var httpRequest = new XMLHttpRequest(); httpRequest.responseType = "arraybuffer"; httpRequest.onload = function (oEvent) { if (httpRequest.readyState === 4 && httpRequest.status === 200) { var arrayBuffer = httpRequest.response; // Note: not httpRequest.responseText if (arrayBuffer) { var byteArray = new Uint8Array(arrayBuffer); if( byteArray.byteLength > 500){ var imgblob = new Blob([byteArray],{type:"image/png"});//Binary Large OBject imageElem = new Image(); imageElem.onload = function () { context.drawImage(imageElem, 0, 0); msgElement.textContent=byteArray.byteLength + "byte受信、更新"; }; imageElem.src = URL.createObjectURL(imgblob); } else { var s=getStringByUint8Array(byteArray);//バイナリから文字列取得 msgElement.textContent=byteArray.byteLength + "byte受信:" + s; } waitResponse = false;// 応答メッセージの待ち状態を終了 } } }; httpRequest.open("GET", reqString, true);// 非同期で要求(操作で更新された画像イメージが戻る) httpRequest.send(null);//(GET送信で、情報伝達サーバーが応答しないと、例外が発生) } function update_desktop_image(){ var date_obj = new Date();// 現在のローカル時間が格納された、Date オブジェクトを作成する var mmsec = date_obj.getTime(); // 測定開始時に経過時間を変数に残す var reqString = "desktop_ctrl_transfer.jsp?A="+getAccesskey()+"&T=" + mmsec;// 時間(ミリ秒)埋め込み msgElement.textContent=reqString; requestDeskTop(reqString)// 画像のバイナリイメージをリクエストして表示 } // id="TXAREA"の文字列を送る function sendText(){ if(waitResponse) return; var date_obj = new Date();// 現在のローカル時間が格納された、Date オブジェクトを作成する var mmsec = date_obj.getTime(); // 測定開始時に経過時間を変数に残す var reqString = "desktop_ctrl_transfer.jsp?A="+getAccesskey()+"&T=" + mmsec;// 時間(ミリ秒)埋め込み reqString += "&TX=" + encodeURIComponent(document.getElementById('TXAREA').value); msgElement.textContent=reqString; requestDeskTop(reqString)// 画像のバイナリイメージをリクエストして表示 } // --> </script> </head> <body onload="init()" style="background-color: rgb(207, 247, 147)"> <canvas id="canvas2" width="640" height="480"></canvas><br> <p id="MSGP"></p> <p style="text-align: center;"><input type="button" value="デスクトップ更新" onclick="update_desktop_image()"></p> <br> <textarea cols="1" id="TXAREA" style="vertical-align: middle;"></textarea><button><input value="TX" type="button" onclick="sendText()"> </body> </html>
下記でxxxxxxの箇所は、desktop_ctrl_server.pyから送信されたデスクトップ画像の保存パス を指定する記述で、ご使用の環境に合わせて、適当に変更する必要があります。
001 002 003 004 005 006 007 008 009 010 011 012 013 014 015 016 017 018 019 020 021 022 023 024 025 026 027 028 029 030 031 032 033 034 035 036 037 038 039 040 041 042 043 044 045 046 047 048 049 050 051 052 053 054 055 056 057 058 059 060 061 062 063 064 065 066 067 068 069 070 071 072 073 074 075 076 077 078 079 080 081 082 083 084 085 086 087 088 089 090 091 092 093 094 095 096 097 098 099 100 101 102 103 104 105 106 107 108 109 110
<%@page contentType="text/html; charset=UTF-8" language="java"
import="java.util.*"
import="java.io.*"
%><%!
static String fileName="desktop.png";//保存ファイル名
static String absolutePath="/home/xxxxx" + fileName;//ファイルパス
static byte[] binaryData = null;
static String requestString = null;//サーバアプリのポーリング(定期的な問合せ)で利用
//クライアント側からのリクエストから応答するまで期間だけnull以外が記憶
// POSTでbinaryDataに記憶されるデスクトップのバイナリイメージをresponseで送信する。
static void responseImage(HttpServletResponse response){
response.setContentType("image/png");
response.setStatus(HttpServletResponse.SC_OK);
response.setHeader( "Content-Length", ""+binaryData.length);
try {
OutputStream os = response.getOutputStream();
os.write(binaryData); // データを書き込む
os.close();
} catch (Exception e) {
e.printStackTrace();
}
}
%><%
String queryString = request.getQueryString();
String exist=request.getParameter("E");//サーバアプリのポーリング用リクエスト判定用
//-- ブラウザからのマウスイベント応答
if(queryString != null && exist == null){ // ブラウザからは'E'のパラメタは送信しないため
long t = new Date().getTime() + 1000*15;// 15秒後にタイムアップ
requestString = queryString;
for(;;){
try{
Thread.sleep(10);
if( requestString == null ){// サーバーアプリの処理が終わるのを待つ。
if( binaryData == null ) break;// サーバーアプリからのデータ取得失敗
responseImage(response); // アプリサーバでのイベント処理後の画面で応答
return;
}
if(new Date().getTime() > t) break;// タイムアップ!
}
catch(Exception e){
out.println(e.getMessage());
}
}
response.setStatus(HttpServletResponse.SC_EXPECTATION_FAILED);
out.println("Error!!");//417のHTTPレスポンスで期待に応えられなかったエラー
return;
}
//-- サーバアプリからイベント有無の確認()-----
if(exist != null){//-- サーバアプリからイベント有無の確認-----
if(requestString == null){
out.print("NONE");// 依頼が存在しない
} else {
out.print(requestString);// クライアントのイベントを転送する。
}
return;
}
//-- サーバアプリからデスクトップイメージがPOSTされた場合の受信-----
// Content-Type: application/octet-stream の場合、POST取得方法は次のようにできる。
// リクエストヘッダーからContent-Lengthを取得
String contentLengthHeader = request.getHeader("Content-Length");
if ( contentLengthHeader == null ) {
out.println("contentLengthHeader : null");
return;
}
int contentLength = Integer.parseInt(contentLengthHeader);
if ( contentLength == 0 ) {
out.println("contentLength : 0");
return;
}
// データを受信するためのバッファを作成
binaryData = new byte[contentLength];
int bytesRead = 0;
int totalBytesRead = 0;
// リクエストボディからデスクトップデータを読み取る
try {
InputStream is = request.getInputStream();
while (totalBytesRead < contentLength) {
bytesRead = is.read(binaryData, totalBytesRead, contentLength - totalBytesRead);
if (bytesRead == -1) {
break;
}
totalBytesRead += bytesRead;
}
} catch (Exception e) {
e.printStackTrace();
}
// データの受信が完了したことを確認(absolutePathのパスのファイル(例"desktop.png")に保存)
if (totalBytesRead == contentLength) {// ここでdataバイト配列に含まれるバイナリデータを処理します
if(binaryData != null) {
java.io.FileOutputStream outputStream = new java.io.FileOutputStream(absolutePath);
outputStream.write(binaryData);
outputStream.close();
}
} else {
// データの受信が失敗した場合のエラー処理
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
out.println("<p>データの受信エラー</p>");
}
requestString = null;
%>
<%= absolutePath %>保存<br>
<%= totalBytesRead %>byte受信<br>
OK
上記はJSPが動作するサーバ(Tomcatなど)で、desktop_ctrl_client.html(クライアントアプリ)と同じ位置に配置します。
配置したURLが分かると、第三者によって乗っ取りされる可能性があり、注意が必要です。
なお、このdesktop_ctrl_transfer.jspを名前を変えて配置することで、遠隔対象を増やすことが可能です。
その場合は、対応するdesktop_ctrl_client.html(クライアントアプリ)のソースと
desktop_ctrl_server.py(アプリサーバ)のソースも、
desktop_ctrl_transfer.jspの記述を変更して、それぞれで配置することで実現できます。
(一つの遠隔対象に、desktop_ctrl_transfer.jsp、desktop_ctrl_client.html、desktop_ctrl_server.pyの3つが必要で、
遠隔対象を増やす場合は、desktop_ctrl_transfer.jspとdesktop_ctrl_client.htmlを名前の変えて複製し、
desktop_ctrl_client.htmlと、desktop_ctrl_server.pyの複製ファイルのソースはdesktop_ctrl_transfer.jsp記述を変更名にするだけで
対応できます。)
下記で の箇所(情報伝達サーバのIPアドレスとポート番号とパス)は、ご使用の環境に合わせて変更する必要があります。
001 002 003 004 005 006 007 008 009 010 011 012 013 014 015 016 017 018 019 020 021 022 023 024 025 026 027 028 029 030 031 032 033 034 035 036 037 038 039 040 041 042 043 044 045 046 047 048 049 050 051 052 053 054 055 056 057 058 059 060 061 062 063 064 065 066 067 068 069 070 071 072 073 074 075 076 077 078 079 080 081 082 083 084 085 086 087 088 089 090 091 092 093 094 095 096 097 098 099 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158
import pyautogui # デスクトップスクリーンを制御するモジュール import socket # TCP通信用 import io # 画像のバイナリ変換用 import time # スリープ用 import urllib.parse # クェリーストリング取り扱い用 #import ipadr # 情報伝達サーバのグローバルIP取得モジュール #import bootlog # 起動時間を起動するモジュール IP_ADDRESS=ipadr.ip # 情報伝達サーバのグローバルIP PORT_NUMBER=8080 # 情報伝達サーバのポート番号 ABSOLUTE_URL=f"http://{IP_ADDRESS}:{PORT_NUMBER}/xxxxxx/desktop_ctrl_transfer.jsp" # 情報伝達サーバのURL IMG_RATIO=1 # デスクトップキャプチャ画像の倍率を決める分数のパラメタ def get_desktop_image(): # デスクトップの辺を半分にした画像のイメージを返す img=pyautogui.screenshot() # キャプチャー(class 'PIL.Image.Image'取得) (width, height) = (img.width // IMG_RATIO, img.height // IMG_RATIO) img_resized = img.resize((width, height)) img_resized.save("screen.png" ) # 確認用保存 # print( img.mode ) # 'RGB' return img_resized # 'PIL.Image.Image' def exc_by_event( queryString ): # 「引数:クライアント側のイベント情報」で実行 params = urllib.parse.parse_qs(queryString) if 'K' in params: keycode = int( params['K'][0] ) c=chr(keycode) pyautogui.keyDown(c) # キーイベントをWindowsシステムに送る pyautogui.keyUp(c) print(f"-----------------pyautogui.keyDown({c}, keyUp ") return True # if 'TX' in params: chars = params['TX'][0] pyautogui.typewrite(chars, interval=0.2) print(f"-----------------pyautogui.typewrite({chars}, interval=0.2") return True # x=int( params['X'][0] ) if 'X' in params else None y=int( params['Y'][0] ) if 'Y' in params else None btn=None if 'B' in params and params['B'][0] == '0': btn="left" if 'B' in params and params['B'][0] == '1': btn="middle" if 'B' in params and params['B'][0] == '2': btn="right" if None in (x, y) : return False # 何もしない if btn == None: pyautogui.moveTo(x, y, duration=0.5) print(f"------------pyautogui.moveTo({x}, {y}, duration=0.5)") return False dx=int( params['DX'][0] ) if 'DX' in params else None dy=int( params['DY'][0] ) if 'DY' in params else None if dx != None and dy != None : if abs(x-dx) > 5 or abs(y-dy) > 5: # マウスドラック操作? #pyautogui.moveTo(dx,dy) #pyautogui.dragTo(x, y, duration=0.001, button=btn) # pyautogui.mouseDown(dx,dy) pyautogui.moveTo(x, y, duration=0.2) pyautogui.mouseUp(x, y, button=btn) # print(f"--------------マウスドラック操作{dx},{dy}→{x}, {y}, duration=0.2, button={btn})") return True # pyautogui.click(x, y, button=btn) # マウスクリック操作 # pyautogui.click(x, y, button=btn, interval=0.5) # マウスクリック操作 pyautogui.moveTo(x, y) pyautogui.mouseDown(x, y, button=btn) time.sleep(0.5) pyautogui.mouseUp(x, y, button=btn) print(f"-----------------pyautogui.click({x}, {y}, button={btn}) ") return True def receive(sock:socket, printFlag:bool=True): # 情報伝達サーバからの受信データ確認用 buf=b"" body_bin = b"" content_length=0 while True: c = sock.recv(1)#1byte受信 if c == b'': break buf += c if len(buf) >= 2 and buf[-2::]==b'\r\n': s=buf.decode('utf-8') if s.startswith('Content-Length:') : content_length=int(s[len('Content-Length:'):]) if printFlag: print(s, end="")#文字列へ変換して表示 if(len(buf)==2): break # 応答ヘッダ部の終了 buf=b"" # for i in range(content_length): body_bin+=sock.recv(1)#1byte受信 print(f"receive body:{body_bin}") return body_bin.decode('utf-8') def send_get(sock:socket, bin_body:bytes,printFlag:bool=True): msg_header=f"GET {ABSOLUTE_URL}?{bin_body.decode('utf-8')} HTTP/1.1\r\n" msg_header+=f"HOST: {IP_ADDRESS}:{PORT_NUMBER}\r\n" msg_header += 'Connection: close\r\n' msg_header+="\r\n" bin_header=msg_header.encode("utf-8")#binaryへ変換 sock.sendall(bin_header)#HTTPリクエストメッセージ送信 if printFlag :print(msg_header) #print("----- 以上がリクエストメッセージ-----") def send_post(sock:socket, bin_body:bytes, printFlag:bool=True): ''' HTTP のPOSTでbin_bodyのバイナリーイメージを送信''' msg_header=f"POST {ABSOLUTE_URL} HTTP/1.1\r\n" msg_header+=f"HOST: {IP_ADDRESS}:{PORT_NUMBER}\r\n" # msg_header+="Content-Type: application/x-www-form-urlencoded\r\n" msg_header+="Content-Type: application/octet-stream\r\n" msg_header+=f"Content-Length: {len(bin_body)}\r\n" msg_header += 'Connection: close\r\n' # ←が無いとすぐ閉じない。 msg_header+="\r\n" bin_header=msg_header.encode("utf-8")#binaryへ変換 sock.sendall(bin_header)#HTTPリクエストメッセージ送信 if printFlag: print(msg_header, end='') sock.sendall(bin_body) #print(bin_body) print("----- 以上がHTTP POSTのリクエストメッセージ送信-----") def post_desktop(printFlag:bool=True): try: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect((IP_ADDRESS, PORT_NUMBER)) print("-----JSPサーバに接続!") output = io.BytesIO() img=get_desktop_image() # 以前に作った関数で、デスクトップイメージを取得 img.save(output, format='PNG') #PNGのファイルとして、binaryデータを得る bin_body=output.getvalue()#送信するbinaryを得る send_post(sock, bin_body, printFlag) # HTTPリクエスト送信 receive(sock) # レスポンス受信表示 sock.close() except Exception as e: print(e) post_desktop() #デスクトップのバイナリーを情報伝達サーバに送信 # 情報伝達サーバに、要求信号があれば、それを処理する繰り返し while True: try: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect((IP_ADDRESS, PORT_NUMBER)) print("-----JSPサーバに接続!") query="E=1".encode("utf-8") #binaryへ変換 send_get(sock,query, printFlag=False) # イベントが情報伝達サーバに在るかを定期的な問合せ body_str=receive(sock,printFlag=False) # レスポンス受信 if body_str.strip() == 'NONE' : print(body_str) # イベントが情報伝達サーバにない場合の受信文字確認表示 time.sleep(0.5) pass else : print(body_str) # イベントが情報伝達サーバある場合の受信文字確認表示 exc_by_event(body_str) # イベント処理 time.sleep(0.5) post_desktop() # デスクトップをキャプチャして情報伝達サーバにPOSTする time.sleep(0.5) # (0.5+0.5)=1秒ごとにチェックする。 sock.close() except Exception as e: print(e) time.sleep(2) # 実行エラーは2秒ごと input("Eneterで終了>") sock.close()
cd 「desktop_ctrl_server.pyを置いたフォルダの絶対パスを記述」 python desktop_ctrl_server.py cmdこれを次のWindowsのススタートアップフォルダの中に入れています。(xxxxxユーザフォルダ名)
操作対象のPCは、電源供給を続けると、比較的温度が上がる。また、夜間も使用を続けるのは不経済です。
しかし操作対象のPCは、老人ホームなどに置くので電源ON・OFFも含めて一切の操作ができない環境に置くことを前提に運用します。
この対策として、タスクスケジューラで制御することにしました。
ハイブリッドスリープが可能なPCであれば、スリープもスケジュールもできる可能性がありますが、当方で使ったPCではその復帰スケジュールが出来ませんでした。
そこで、復帰が可能な「電源とスリープ」の設定を以下のようにしました。(WindowsCmd+R で、ms-settings:powersleepで設定)
そして最終的に、タスクスケジューラを使って、10時、14時、18時に復帰でリセット&起動させています。
このような設定は、PCへ電源を常時供給した利用でも、一定時間ごとに動作させることで、内臓バッテリーの連続充電に対する劣化軽減も期待できます。
また、アプリサーバの予期していない問題でプログラム動作が止まった場合でも、リセットによる再起動で 再び動作が可能となる挙動も期待する設定です。
タスクスケジューラを使ったリセットの設定は、実行時間以外は、すべて同じで、次のようにしています。
なお 実際に運用してみると、スケジュールした時間に対して、約1時間程度、遅れる場合があるようです。
また、設定がWindows 10自動更新で変わるのを防ぐため、サービス(SERVICES.MSC )の設定画面で、Windows Updateを手動の設定に変更した。
raytrek DG-D08IWP OS: Windows 10 Home インストール済み CPU: インテル Atom x5-Z8350 プロセッサ(クアッドコア, 定格 1.44GHz, キャッシュ2MB) デジタイザ:Wacom feel IT technologies デジタイザ4096階調 スキャンレート180Hz メモリ: 4GB DDR3L ディスプレイアダプター: インテル HDグラフィックス400 (CPU内蔵) ディスプレイ: 8インチ液晶 (※1) (1280×800ドット表示 / マルチタッチ対応) ストレージ: 64GB eMMC 無線LAN: IEEE802.11 ac/a/b/g/n Bluetooth: Bluetooth 4.0 センサー: 加速度センサー、GPS I/O: microUSB×1(給電兼用) 映像出力: microHDMI ×1 サウンド: ヘッドフォン出力×1 (ステレオミニプラグ), スピーカー内蔵, マイク内蔵 カードスロット: microSDカードスロット(SDXC) ウェブカメラ: 約 200万画素 WEBカメラ フロント ×1,リア ×1 サイズ: 約 214(幅)×128(奥行き)×10.1(高さ)mm 重量: 本体 約400g ペン 約5g