2023-05-22

將 Android 裝置當作 IOT 裝置

過去在下曾將 Android 裝置當作伺服器使用
雖然效果不及正式的工具(例如 Raspberry Pi 及 NAS)
但作為既有工具,如果不轉售,當作後備裝置使用,亦可以強化其功能
只當作伺服器使用,又好像未完全發揮功能,因此在下再嘗試更多可能性

安裝 Termux 到 Android 6-

見下文
由於在下使用較舊的 Android 裝置,雖然能夠安裝 Termux ,但新版的 Termux 需要在 Android 7+ 才能使用
另外 Termux 需要 下載 bootstrap 才能啟動,而舊版的 Termux 會從 termux.net 下載,但 termux.net 已經失效
因此即使 Android 6- 的裝置即使自行下載 apk 安裝舊版 Termux 仍然無法使用

見下文
見下文
要將 Termux 安裝到 Android 6- 的裝置
要到 https://archive.org/download/termux-repositories-legacy 可以下載支援 Android 6- 的 Termux
而且 內建 bootstrap ,還有 Termux 的輔助套件

見下文
由於自行下載及安裝 APK檔案 有安全風險, Android 會提出安全警告

見下文
見下文
選取 未知的來源 ,並 確定 同意安全風險

見下文
見下文
便可以安裝 Termux

見下文
完成安裝後可以立即開啟 Termux

見下文
雖然內建 bootstrap ,仍然需要時間安裝 bootstrap

見下文
與一般 Termux 的開始畫面相同

安裝 Apache 及 PHP 到 Termux

見下文
但由於剛才提及 termux.net 已經失效,當輸入
pkg update
會顯示無法找到套件庫的資料

雖然可以從 新版 Termux 的套件庫 https://packages.termux.dev/apt 直接下載及安裝套件
但每次都要手動下載、安裝、升級,會非常浪費時間
而且無法根據相依性自動下載相關套件同樣是麻煩

見下文
令舊版 Termux 使用新版 Termux 的套件庫,除了套件來源,還需要受信任的 GPG簽署
先手動下載 新版 Termux 套件庫的 GPG簽署
輸入
file="termux-keyring_3.11_all.deb"
curl "https://packages.termux.dev/apt/termux-main/pool/main/t/termux-keyring/${file}" >"${file}"
dpkg -i "${file}"
rm "${file}"

見下文
更新 Termux 套件庫的來源,輸入
apt_path="${PREFIX}/etc/apt"
printf "deb https://packages.termux.dev/apt/termux-main-21 stable main" >"${apt_path}/sources.list"
printf "deb https://termux.dev/science-packages-21-bin science stable" >"${apt_path}/sources.list.d/science.list"
printf "deb https://termux.dev/game-packages-21-bin games stable" >"${apt_path}/sources.list.d/game.list"

見下文
見下文
更新來源後,輸入
pkg update
pkg upgrade
更新 Termux 的套件

見下文
見下文
更新套件後,輸入
pkg install termux-services openssh
安裝 OpenSSH 伺服器 及 Termux 的 服務起動器,可以讓外部裝置能夠經 OpenSSH 登入,方便操作 Termux

見下文
見下文
安裝後登出 Termux 並重新登入 Termux
輸入
sv-enable sshd
便可以在開啟 Termux 後自動起動 OpenSSH 伺服器

再輸入
passwd
修改 root 的密碼,讓外部裝置連接

見下文
使用電腦連接後,可以更方便操作

見下文
見下文
輸入
pkg install termux-api wget nano php-apache
由於要將 Android 當作 IOT 使用,必須直接能夠存取 Android 硬件的操作權限
使用 PRoot 無法存取 Android 硬件的操作權限
因此直接將在 Termux 上安裝如 Apache網頁伺服器 配合 Termux API 來控制 Android

見下文
輸入
cp "${PREFIX}/etc/apache2/httpd.conf" "${PREFIX}/etc/apache2/httpd.conf.bak"
nano "${PREFIX}/etc/apache2/httpd.conf"
先備份 Apache網頁伺服器 的設定檔,編輯 設定檔

見下文

LoadModule mpm_prefork_module libexec/apache2/mod_mpm_prefork.so
#LoadModule mpm_worker_module libexec/apache2/mod_mpm_worker.so
修改成
#LoadModule mpm_prefork_module libexec/apache2/mod_mpm_prefork.so
LoadModule mpm_worker_module libexec/apache2/mod_mpm_worker.so

見下文
在 設定檔 最後加上
LoadModule php7_module libexec/apache2/libphp7.so
AddHandler php7-script .php
Android 7+ 改為
LoadModule php_module libexec/apache2/libphp.so
AddHandler php-script .php
完成後,儲存並離開
(php_module 使用 底線(underscore), php-script 使用 減號(minus))

見下文
輸入
apachectl start

見下文
使用網頁瀏覽器輸入 http://<Android裝置 的 IP>:8080
顯示 It Works! 表示 Android 的 Apache網頁伺服器 正常運作

見下文
輸入
nano "${PREFIX}/share/apache2/default-site/htdocs/phpinfo.php"
製作 phpinfo.php 檔案

見下文
輸入
<?php phpinfo();
完成後,儲存並離開

見下文
使用網頁瀏覽器輸入 http://<Android裝置 的 IP>:8080/phpinfo.php
顯示 PHP 的資料

見下文
輸入
nano "${PREFIX}/share/apache2/default-site/htdocs/led.php"

輸入
<?php
if (filter_input(INPUT_GET, "led") == 1) {
	system("termux-torch on");
} else {
	system("termux-torch off");
}
?>
<!DOCTYPE html>
<html lang="en">
	<head>
		<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
		<title>LED Control</title>
		<style>
a { font-size: 128pt; }
		</style>
	</head>
	<body>
		<div><a href="?led=1">LED On</a></div>
		<div><a href="?led=0">LED Off</a></div>
	</body>
</html>

見下文
使用網頁瀏覽器輸入 http://<Android裝置 的 IP>:8080/led.php
顯示網頁效果

安裝 Termux:API

見下文
由於操作涉及 Termux API 因此需要安裝 Termux:API 的 Android 套件

見下文
見下文
提供操作權限給予 Termux 及 Termux:API

見下文
效果如同使用 ESP8266 等具備 WiFi 的微控制器經網頁控制一些 Android裝置 的硬件

製作 HTTP伺服器

由於 Java 能夠編譯成能在 Android 執行的程式,而且 Java 亦可以自行製作 HTTP伺服器
雖然 Java 提供 com.sun.net.httpserver 的 HTTP伺服器,但 Android 並沒有提供,因此無法使用
網上很多自製 Android HTTP伺服器 建議使用 NanoHTTPD ,但在下使用時發現有些功能無法使用
結果還是自己製作 HTTP伺服器

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
public class HttpServer implements Runnable {
	private ServerSocket server;
	private boolean running = false;
	public HttpServer(int port) throws IOException {
		this.server = new ServerSocket(port);
	}
	public boolean isRunning() {
		return this.running;
	}
	public void start() {
		this.running = true;
	}
	public void stop() {
		this.running = false;
	}
	@Override
	public void run() {
		while (true) {
			if (this.isRunning()) {
				try {
					Socket client = this.server.accept();
					InputStream input = client.getInputStream();
					ByteArrayOutputStream bytes = new ByteArrayOutputStream();
					while (input.available() > 0) {
						bytes.write(input.read());
					}
					bytes.flush();
					bytes.close();
					OutputStream output = client.getOutputStream();
					output.write("HTTP/2 200 OK\n".getBytes());
					output.write("Connection: close\n".getBytes());
					output.write("\n".getBytes());
					output.write("hello, world".getBytes());
					output.flush();
					output.close();
					input.close();
					client.close();
				} catch (Exception ex) {
					ex.printStackTrace();
				}
			}
		}
	}
	public static void main(String[] args) throws IOException {
		// could change other unused port
		HttpServer server = new HttpServer(8080);
		server.start();
		Thread thread = new Thread(server);
		thread.start();
	}
}
見下文
輸入
javac HttpServer.java
編譯 Java 程式
輸入
java HttpServer
執行 Java 程式

見下文
使用網頁瀏覽器或能發送 HTTP請求 的工具瀏覽 Java 的 HTTP伺服器
例如在下使用 Curl ,輸入
curl -i "http://127.0.0.1:8080/"
發送成功,會顯示與 OutputStream 相同的輸出結果

編寫 Android 應用程式

編寫 Android 應用程式 最簡單是使用 Android Studio
安裝的步驟在下不詳細說明,但由於 Android Studio 需要使用 Java 編譯程式碼
因此仍然需要安裝 Java 的編譯器

見下文
在建立專案的範本中,選擇 Empty Activity

見下文
輸入專案的名稱
程式語言可以選擇 KotlinJava (在下使用 Java 編寫)
Minimum SDK 可以根據需要的程式碼,選擇較舊或較新的 SDK
較舊的 SDK 可以支援較多 Android 裝置,但支援的功能較少;反之亦然

見下文
建立專案需要一些初始化時間

見下文
修改 java/<project-package>/MainActivity
將先前測試成功的 HttpServer.java 類別內容複製到 MainActivity.java (作為內部類別會比較容易測試)

見下文
將 HttpServer.java 的 public static void main 內容
複製到 MainActivity.java 的 protected void onCreate
由於 protected void onCreate 不能使用 throws 語法,因此需要改為 try catch

見下文
由於 HTTP伺服器 需要使用 存取網絡,因此需要加入網絡使用權限
manifests/AndroidManifest.xml 中加入
<uses-permission android:name="android.permission.INTERNET"/>
(大小寫相符)

建立 APK 檔案及安裝

見下文
見下文
Build > Build Bundle(s) / APK(s) > Build APK(s) 建立 APK檔案

見下文
APK檔案 建立後, Android Studio 會顯示完成資訊
locate 開啟保存 APK檔案 的目錄

見下文
APK檔案 預設名稱為 app-debug.apk
並保存在 <project-package>/app/build/outputs/apk/debug 的目錄中

見下文
將 Android裝置 連接到 電腦,同並允許電腦能夠存取 Android裝置 的內容

見下文
將 app-debug.apk 複製到 Android裝置
(複製位置並沒有限制)

見下文
見下文
見下文
見下文
見下文
見下文
見下文
在 Android裝置 尋找 app-debug.apk 並安裝
(安裝流程與 Termux 相同,因此在下不重覆說明)

使用 ADB 安裝及執行

見下文
設定 > 關於裝置

見下文
見下文
見下文
點擊 內部版本號 7次,以啟用 開發人員選項

見下文
見下文
見下文
在 開發人員選項 ,啟用 USB除錯

見下文
將 Android裝置 連接到 電腦,按 允許 讓電腦能夠以 USB除錯 控制 Android裝置

見下文
Android Studio 能偵測到已啟動 USB除錯 的 Android裝置
並選取需要測試的 Android裝置

見下文
見下文
見下文
Run > Run 'app' 或 按 F6 編譯專案並安裝到已選擇的 Android裝置

使用 APK檔案 或 ADB 安裝結果都是相同
APK檔案 可以方便傳送給不同 Android裝置 安裝,除了 USB ,亦可以使用 藍牙、網絡 等方法傳送檔案
ADB 會自動安裝並執行應用程式比較簡單及快捷

見下文
不論用甚麼方法,安裝完成後,開啟應用程式

見下文
再次使用 Curl 測試
(對應 Android裝置 的 IP地址)

指派不同 HTTP請求

見下文
確認 HTTP伺服器 能夠在 Android 運作後,修改 HTTP伺服器 的程式碼
接收不同的 HTTP請求 ,傳回相對的 HTTP回應

String[] requests = bytes.toString().split("\r\n\r\n", 2);
String[] heads = requests[0].split(" ");
if (heads[1].equals("/torch-on")) {
	CameraManager cameraManager = getSystemService(CameraManager.class);
	for (String cameraId : cameraManager.getCameraIdList()) {
		if (cameraManager.getCameraCharacteristics(cameraId).get(CameraCharacteristics.FLASH_INFO_AVAILABLE)) {
			cameraManager.setTorchMode(cameraId, true);
		}
	}
	OutputStream output = client.getOutputStream();
	output.write("HTTP/2 200 OK\n".getBytes());
	output.write("Connection: close\n".getBytes());
	output.write("\n".getBytes());
	output.write("torch on".getBytes());
	output.flush();
	output.close();
} else if (heads[1].equals("/torch-off")) {
	CameraManager cameraManager = getSystemService(CameraManager.class);
	for (String cameraId : cameraManager.getCameraIdList()) {
		if (cameraManager.getCameraCharacteristics(cameraId).get(CameraCharacteristics.FLASH_INFO_AVAILABLE)) {
			cameraManager.setTorchMode(cameraId, false);
		}
	}
	OutputStream output = client.getOutputStream();
	output.write("HTTP/2 200 OK\n".getBytes());
	output.write("Connection: close\n".getBytes());
	output.write("\n".getBytes());
	output.write("torch off".getBytes());
	output.flush();
	output.close();
} else {
	OutputStream output = client.getOutputStream();
	output.write("HTTP/2 200 OK\n".getBytes());
	output.write("Connection: close\n".getBytes());
	output.write("\n".getBytes());
	output.write("hello, world".getBytes());
	output.flush();
	output.close();
}

見下文
見下文
由於部分程式碼無法在 版本較低的 Android裝置 中執行,因此會出現 警告或提示
左邊的紅色警告圖示 查看修正方法
在下使用 Add @RequiresApi(M) Annotation
Android Studio 會將 @RequiresApi(api = Build.VERSION_CODES.M) 安置到功能註解

當 @RequiresApi 已經安置
當程式碼中再有相同的修正建議
Android Studio 不會更新已存在的註解
需要手動更新 或 刪除已存在的註解再使用自動修正

見下文
向 HTTP伺服器 發送 指定 HTTP請求,達至遙距操作效果

製作操作界面

雖然使用 Curl 發送 HTTP請求 很方便,但一般使用者並不容易使用指令操作的工具
製作只需要點擊操作的界面便可以更簡單控制 Android裝置

見下文
到 res > New > Directory

見下文
目錄名稱為 raw
(必須是 raw ,大小寫相符)

見下文
到 res > raw > New > File

見下文
檔案名稱為 index
(檔案名稱並沒有限制,但不能具備副檔名,大小寫相符)

見下文
製作 HTML檔案
<!DOCTYPE html>
<html lang="zh-yue">
	<head>
		<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
		<title>Android As IOT Index</title>
		<script>
// <!--
function sendHttpRequest(form, data) {
	var xhr = new XMLHttpRequest();
	var method = form.getAttribute("data-method").toUpperCase();
	if (method == "GET") {
		xhr.onreadystatechange = function() {
			if (xhr.readyState == XMLHttpRequest.DONE) {
				window.alert(xhr.responseText);
			}
		}
	}
	xhr.open(form.getAttribute("data-method").toUpperCase(), form.action, true);
	xhr.send(data);
	return method == "GET";
}
// -->
		</script>
	</head>
	<body>
		<form action="/torch-on" method="get" data-method="PUT" onsubmit="return sendHttpRequest(this, null);">
			<input type="submit" value="Torch On"/>
		</form>
		<form action="/torch-off" method="get" data-method="PUT" onsubmit="return sendHttpRequest(this, null);">
			<input type="submit" value="Torch Off"/>
		</form>
	</body>
</html>
由於在下使用不同 請求方法 區分 操作類型,例如:
  • 讀取資料 使用 GET
  • 寫入資料 使用 POST
  • 修改資料 使用 PUT
  • 刪除資料 使用 DELETE
避免單純的連結便可以執行寫入、修改、刪除等操作
但 HTML 的 表單提交 不支援 GET 及 POST 以外的 請求方法
因此使用 XMLHttpRequest 來代替 表單提交 ,並使用 data-method 的取代 method

見下文
將 HttpServer 修改,加入索引頁,並將 index HTML檔案引導至 HttpServer
if (heads[1].equals("/index")) {
	ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
// R.raw.index matches file located in raw/index
	InputStream inputStream = getResources().openRawResource(R.raw.index);
	byte[] buffer = new byte[1024 * 1024 * 16];
	for (int length; (length = inputStream.read(buffer)) > 0; ) {
		byteArrayOutputStream.write(buffer, 0, length);
	}
	inputStream.close();
	byteArrayOutputStream.flush();
	byteArrayOutputStream.close();
	OutputStream output = client.getOutputStream();
	output.write("HTTP/2 200 OK\n".getBytes());
	output.write("Content-Type: text/html; charset=UTF-8\n".getBytes());
	output.write("Connection: close\n".getBytes());
	output.write("\n".getBytes());
	output.write(byteArrayOutputStream.toByteArray());
	output.flush();
	output.close();
} else if (heads[0].toUpperCase().equals("GET")) {
// do something with GET method
} else if (heads[0].toUpperCase().equals("POST")) {
// do something with POST method
} else if (heads[0].toUpperCase().equals("PUT")) {
// do something with PUT method
} else if (heads[0].toUpperCase().equals("DELETE")) {
// do something with DELETE method
}

見下文
測試效果

使用上,自製HTTP伺服器 的回應速度比 Termux 上的 HTTP伺服器 快

總結

由於 Android裝置 通常都已經內建大量模組,除了能控制閃光燈外
還可以監察定位、陀螺儀、文字轉語音等服務;亦有儲存空間以紀錄資訊

美中不足的是普遍常見既 Android裝置 都沒有 GPIO 能夠使用

參考資料

沒有留言 :

張貼留言