UMEHOSHI ITA TOP PAGE    COMPUTER SHIEN LAB

ネットを介した遠隔操作プロジェクト(デスクトップ遠隔操作)

下記の遠隔対象PCを乗せるターンテーブルを「UMEHOSHI ITA 」を利用して作る紹介はこちらで示す別ページ(作成中)で紹介の予定

古いWindowsタブレットマシンを老人ホームやに病院など、フリーWifiが繋がる環境に置いた状態で、 そのデスクトップを、他の端末のブラウザで遠隔操作して、google Meetを起動して会話をできるようにする目標です。
(直接に操作できない環境のWindowsPCを、HTTPだけを利用したWebブラウザで遠隔操作する目標です)

動作イメージ


裏からみた状態
100円ショップで入手した
デジタルフォトフレーム立てを
改良し、粘土の重さで、
安定に立つようにした。

左のPCのデスクトップをブラウザで遠隔操作しているイメージです。
このイメージは、左のPCでgoogle Meetを起動している画面です。
左がWindow10タブレット端末で、 これをインターネットを介した他の端末のWebブラウザで、遠隔操作しているのが右のイメージです。
右のブラウザのイメージは、左Windowsのデスクトップ画面です。その中では Chromeブラウズで、Google Meetを遠隔起動した状態が見えています。
このブラウザ内のデスクトップ画像内では、ダブルクリック以外のマウス操作で、Windows Explorerに近い操作が可能です
それは、ファイル選択のクリックや、右クリックでメニューを出して、開く操作です。(ウインドウのサイズ変更のドラックは未実装です)
つまり、右の端末はChromeブラウザが起動でれば、任意のハードで任意のOSので操作可能です。

なお情報伝達サーバは、Tomcatを利用したHTTP用のアプリです。(左右との通信は全てHTTPで行っています。)
このサーバは、Rassberry Pi4に構築してポートフォワーディング(静的IPマスカレード)により、グローバルIPアクセスが可能になっています。
右のブラウザで操作したイベントを左の遠隔操作対象のWindowsPCに情報伝達サーバで伝達し、 そのPCでは受信イベントに応じた操作を行わせます。
この結果で得られた画像を、再び情報伝達サーバで右ブラウザ伝達して見せる動作です。

これを実現するために、次の3つプログラムを作りました。
左の操作対象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など他のブラウザ動作しているが、部分的に未検証)

Webブラウザでの遠隔操作方法

左がdesktop_ctrl_transfer.jspのWebページを閲覧したイメージで、 Webページ内の大きなCanvasに描いたイメージが、遠隔対象のデスクトップ画面です。

このイメージ上で、マウスボタンの押し込みや、離すボタン操作で、遠隔対象の操作が可能です。
つまり、クリック操作が可能です。
それにより、アイコンの右ボタンクリックでポップアップメニューを出し、「開く」を選択して実行できます。
しかし、一回のマウスボタン操作でデスクトップ画像が更新されるまで、2~4秒程度かかります。

この時間は、遠隔対象のPC能力や、ネットワーク環境にもよります。またそれによって、ボタン操作しても、応答画面が更新されない場合もあります。
例えば、メニューをクリックしても、ポップアップ表示が出る前の画面が送信されて、メニュー項目が並ぶポップアップが出ない場合があります。
そのような場合のため、「デスクトップ更新」ボタンが用意されており、そのクリック操作で現在のデスクトップに、ポップアップメニューが出ているか 確認できます。
つまり待っても更新されない場合は、「デスクトップ更新」ボタンを使い、現時点のデスクトップ画面に更新して、操作を継続できます。
この更新されない対策として、ブラウザからタイマーで自動的に一定間隔でデスクトップ更新を行わせることも考えましたが、 そうすると通信パケット量が増えてしまいます。
現時点では、従量制ネットワーク環境などで、余計なパケットを減らすために、自動更新を組み込みしていません。
なお、操作結果を確認しながら運用する指針で、マウスのダブルクリック操作は、実装しませんでした。
また、マウスドラックも可能にしましたが、ドラックが終わった後にしか画面更新されないので、操作性が悪いようです。

また、Javascriptnのkeydownイベントで得られるkeyCodeをサーバに送信することで、キー操作の遠隔操作も可能です。
これにより、Windowsタスクバーのスタートボタンクリック後のキーによるアプリ選択も可能です。
ですが、一つのキー操作に対して画面更新イベントの要求が行われるため、 一つのキー操作に対して2~4秒程度の画面更新という挙動となります。
よって例えば、メモ帳内容をこの操作で編集する用途には向きません。
そこで、まとまった文字列を一遍に送って、更新画面を得ることができる機能を別途に作っており、ページ下部でTextAreaを用意しています。
操作対象のテキスト入力部をクリックしてから、このTextAreaに設定した文字列を、隣の[TX]ボタンをクリックすることで、 対象入力部に送り込んで、そのまとまった文字列入力に対する応答画面を得ることができます。

補足: 遠隔操作でPCを操作して、Windowsタスクバーのスタートの右クリックで、「設定」を選択して起動することができます。
しかし残念ながら、管理者権限で動作するソフト(タスクマネージャやスケジューラ、デバイスマネージャなど)を起動すると、 それ以降で操作ができなくなる不具合が生じます。
現状では制御ができなくなると、どうしようもないので管理者権限で動作するソフトを操作しないように気を付けるしかありません。
操作できなくなってしまった場合、タスクスケージュールによる再起動で、復帰するまで待つしかありません。
なお原因は、デスクトップスクリーンを制御するpyautogui モジュールが、管理者権限を持つプロセスに対して、制約されるためのようです。


システム全体の動作概要




desktop_ctrl_client.html(クライアントアプリ)のソースと補足

遠隔操作パネル用の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>

desktop_ctrl_transfer.jsp(情報伝達サーバ)のソースと補足

下記で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記述を変更名にするだけで 対応できます。)


desktop_ctrl_server.py(アプリサーバ)のソースと補足

下記で   の箇所(情報伝達サーバの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()
上記ソースは、遠隔対象のWindows10の、どこに置いても構いません。
次のようなバッチファイル(remotestart.bat)で起動できるようにしています。
cd 「desktop_ctrl_server.pyを置いたフォルダの絶対パスを記述」
python desktop_ctrl_server.py
cmd
これを次のWindowsのススタートアップフォルダの中に入れています。(xxxxxユーザフォルダ名)
「C:\Users\xxxxx\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup」

そして、ログインやPIN入力なしで起動できるように、次の操作をしています。
ファイル名を指定して実行」、またはコマンドプロンプトに「netplwiz」を実行し、 「ユーザーアカウント」という画面で、ログインユーザを選択した後、 「ユーザーがこのコンピューターを使うには、ユーザー名とパスワードの入力が必要」のチェックを外しします。
以上の設定で、PCを起動やリセットで、 desktop_ctrl_transfer.jsp(情報伝達サーバ)を起動するようにしています。

遠隔対象でアプリサーバの起動のスケージュール

操作対象のPCは、電源供給を続けると、比較的温度が上がる。また、夜間も使用を続けるのは不経済です。
しかし操作対象のPCは、老人ホームなどに置くので電源ON・OFFも含めて一切の操作ができない環境に置くことを前提に運用します。
この対策として、タスクスケジューラで制御することにしました。
ハイブリッドスリープが可能なPCであれば、スリープもスケジュールもできる可能性がありますが、当方で使ったPCではその復帰スケジュールが出来ませんでした。
そこで、復帰が可能な「電源とスリープ」の設定を以下のようにしました。(WindowsCmd+R で、ms-settings:powersleepで設定)

そして最終的に、タスクスケジューラを使って、10時、14時、18時に復帰でリセット&起動させています
このような設定は、PCへ電源を常時供給した利用でも、一定時間ごとに動作させることで、内臓バッテリーの連続充電に対する劣化軽減も期待できます。
また、アプリサーバの予期していない問題でプログラム動作が止まった場合でも、リセットによる再起動で 再び動作が可能となる挙動も期待する設定です。
タスクスケジューラを使ったリセットの設定は、実行時間以外は、すべて同じで、次のようにしています。


なお 実際に運用してみると、スケジュールした時間に対して、約1時間程度、遅れる場合があるようです。
また、設定がWindows 10自動更新で変わるのを防ぐため、サービス(SERVICES.MSC )の設定画面で、Windows Updateを手動の設定に変更した。


操作対象で使用したWindowPC(raytrek DG-D08IWP)の仕様

以下のPCを遠隔対象のPCに使いましたが、desktop_ctrl_server.pyのPythonプログラムが動作できれば、任意のスペックで遠隔対象にできるでしょう。
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