2021年4月24日 星期六

Android 透過網路抓圖秀在螢幕上

先修改 app/src/main/AndroidManifest.xml 檔案內容:

 <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.urlopen">
    <uses-permission android:name="android.permission.INTERNET" />
    <application
        android:networkSecurityConfig="@xml/network_security"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.UrlOpen">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

在 res 目錄底下新建一個 xml 目錄( app/src/main/res/xml), 裏面放入 netwirk_security.xml 檔案內容, 以便信認所屬網域 192.168.0.1:

 <?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <domain-config cleartextTrafficPermitted="true">
        <domain includeSubdomains="true">192.168.0.1</domain>
    </domain-config>
</network-security-config

修改 app/src/main/res/layout/activity_main.xml, 加入 android:id="@+id/...",  檔案內容:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/mainLayout"
    tools:context=".MainActivity">
    <TextView
        android:id="@+id/sample_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

主程式 app/src/main/java/com/example/urlopen/MainActivity.kt  檔案內容:

package com.example.urlopen
import android.graphics.BitmapFactory
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import java.net.URL
import java.util.concurrent.Executors
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val mainContext = this
        (findViewById(R.id.mainLayout) as ConstraintLayout) ?.apply {
            val imageView = ImageView(mainContext).apply {
                setLayoutParams(
                    ConstraintLayout.LayoutParams(
                            ViewGroup.LayoutParams.MATCH_PARENT,
                            ViewGroup.LayoutParams.WRAP_CONTENT
                    )
                )
            }
            addView(imageView)
            val url = "http://192.168.0.1/snap.jpg"
            val handler = Handler(Looper.getMainLooper())
            val getJPG = object : Runnable {
                override fun run() {
                  Executors.newSingleThreadExecutor() ?.execute {// doInBackground
                    try {
                        val stream = URL(url).openStream()
                        val jpg = BitmapFactory.decodeStream(stream)
                        handler.post { // onPostExecute
                            imageView.setImageBitmap(jpg)                          
                        }
                    } catch (e: Exception) {
                        Log.e("JNI", "network get fail!")
                    }
                  }
                }
            }
            getJPG.run()
        }
        //...
    }
}


2021年4月20日 星期二

在 Linux 系統上安裝 AndroidStudio 並加入 OpenCV 程式庫

先將 AndroidStudio 安裝完, 並下載 OpenCV 準備好:

1. 上 AndroidStudio 官網 https://developer.android.com/studio  下載壓縮包, 打開終端機, 進到 Downloads 目錄內, 解壓縮後進行安裝:

      cd Downloads

      tar  -xvf  android-studio-ide-201.7199119-linux.tar.gz

      cd  android-studio/bin &&  ./studio.sh

2. 執行 AndroidStudio (cd /home/mint/Downloads/android-studio/bin && ./studio.sh), 開啟新專案 -> 選擇 Native C++ -> 點選 Next -> Name 填入專案名稱  -> Finish, 當執行編譯時會要求安裝 NDK 編譯 c++  原始碼, 點選同意進行 NDK 的安裝

3. 再上 OpenCV 官網   https://opencv.org/releases/  點選 Android 下載 OpenCV SDK 壓縮包, 接著開啟終端機在 Downloads 目錄內解壓縮 :

     cd Downloads 

     unzip opencv-4.5.2-android-sdk.zip

執行 AndroidStudio, 打開上述 c++ 的專案, 透過 JNI 玩 OpenCV on android, 如果要使用 Java 版 openCV, 首先在Gradle Scripts 的頁籤 settings.gradle 增加以下內容:

     def opencvsdk='/home/mint/Downloads/OpenCV-android-sdk'    
     include ':opencv'
     project(':opencv').projectDir = new File(opencvsdk + '/sdk')

確認上述 opencvsdk 目錄正確, 改完後像這樣存檔 :

     def opencvsdk='/home/mint/Downloads/OpenCV-android-sdk'
     include ':app'
     include ':opencv'
     project(':opencv').projectDir = new File(opencvsdk + '/sdk')
     rootProject.name = "cvEx1"

點選 Sync-Now  同步更新 Gradle, 讓設定生效, 接著打開 Gradle Scripts 的頁籤 build.gradle(Module: ...) , 檔案內容拉到最後的地方, 加入內容像這樣:

     dependencies {

               implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
               implementation 'androidx.core:core-ktx:1.3.2'
               implementation 'androidx.appcompat:appcompat:1.2.0'
               implementation 'com.google.android.material:material:1.3.0'
               implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
               testImplementation 'junit:junit:4.+'
               androidTestImplementation 'androidx.test.ext:junit:1.1.2'
               androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
               implementation project(":opencv")

    }

同步後就會自動下載程式庫, 再將主程式 MainActivity.kt 稍作修改, 載入函式庫之後, 就能呼叫 OpenCV 類型的函數:

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.TextView
import org.opencv.android.OpenCVLoader
import org.opencv.core.Mat
import org.opencv.core.CvType.CV_32F

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val txt = findViewById<TextView>(R.id.sample_text)
        if(!OpenCVLoader.initDebug()) return
        val jMat = Mat(5, 5, CV_32F)
       
txt.setText("${stringFromJNI()}, jMat: ${jMat.rows()} * ${jMat.cols()} sucessful")
    }
    external fun stringFromJNI(): String
    companion object {
        init {
            System.loadLibrary("native-lib")
        }
    }
}

重新編譯上述專案, 並上傳到手機, 測試看是否可以成功加載 openCV 的 Java 程式庫.

上述是在 kotlin(Java)語言中呼叫 Java 版 OpenCV 函數, 下一步要在原生 c++ (native-lib)程式碼中直接呼叫 OpenCV 函數, 必須修改 app/cpp/CMakeLists.txt (實際的檔案位置是 app/src/main/cpp/CMakeLists.txt),引入標頭檔及程式庫,將以下紅色內容插入 CMakeLists.txt, 像這樣:

    cmake_minimum_required(VERSION 3.10.2)
    project("cvex1")
    set(OpenCV_DIR  /home/mint/Downloads/OpenCV-android-sdk/sdk/native/jni)
    find_package(OpenCV  REQUIRED)

    include_directories(${OpenCV_INCLUDE_DIRS})

    add_library(native-lib SHARED native-lib.cpp)
    find_library(log-lib log)
    target_link_libraries(native-lib   -ljnigraphics   ${OpenCV_LIBS}  ${log-lib})

接著修改 app/cpp/native-lib.cpp (實際的檔案位置是 app/src/main/cpp/native-lib.cpp),內容像這樣:

#include <jni.h>
#include <string>
#include <opencv2/highgui/highgui.hpp>
using namespace cv;
using namespace std;

extern "C" JNIEXPORT jstring JNICALL
Java_com_example_cvex1_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject /* this */) {
    Mat cMat(200, 320, CV_8UC3);
    char buffer[256];
    sprintf(buffer, "Hello C++ cMat:%d * %d", cMat.rows, cMat.cols);
    return env->NewStringUTF(buffer);

}

重新編譯上述專案, 並上傳到手機, 測試看是否可以成功連結 C++ 的程式庫. 到此已萬事俱備.

後記: 若只想在 native-lib 內使用 openCV 函式庫處理影像甚至在作畫, 只要修改 CMakeLists.txt 及 native-lib.cpp 就夠了, 前面所說的設定 settings.gradle 就不用了, 在 build.gradle 內添加的 opencv 程式庫: dependencies { ...  implementation project(":opencv" } 也要跟著移除, 要是將兩個程式庫(C++版 及 JAVA 版)都收編進 Android app程式內, 似乎也可以, 但會有警告的訊息. 也不知道會有何副作用? 底下是一個直接在 bitmap (ARGB_8888 格式)作畫的程式碼 (native-lib.cpp):

#include <jni.h>
#include <string>
#include "opencv2/objdetect.hpp"
#include <opencv2/imgproc.hpp>
#include <opencv2/core/types_c.h>
#include <opencv2/highgui/highgui.hpp>
#include <chrono>
#include <android/bitmap.h>
using namespace cv;
using namespace std;
using namespace std::chrono;
int fps( ) {
    static auto lastTime = system_clock::now( ); // .time_since_epoch();
    static long nSxcount = 0;
    static int frames = 0;
    static int _fps = 0;
    auto currentTime = system_clock::now( );
    nSxcount += nanoseconds(system_clock::now( ) - lastTime).count( );// 累積時間差nS
    if (++ frames == 10) {// 累積10張, 算一次 fps, 並且避免數字跳動
        _fps  = 1e10/(nSxcount + 5);// 4捨5入, 並且避免除0錯誤
        frames = 0;
        nSxcount = 0;
    }
    lastTime = currentTime;// to be used next time
    return _fps;
}
extern "C" JNIEXPORT jstring JNICALL Java_com_example_cvex1_MainActivity_fpsdraw(
        JNIEnv *env,
        jobject _this,
        jstring appPath,
        jobject bitmap = NULL
    ) {
    static int id = 0;
    char buffer[1024];
    buffer[0] = 0;
    if (bitmap != NULL) {
        AndroidBitmapInfo info;
        AndroidBitmap_getInfo(env, bitmap, &info);
        if (info.format == ANDROID_BITMAP_FORMAT_RGBA_8888) {
            void *array;
            AndroidBitmap_lockPixels(env, bitmap, &array);
            Mat mat(info.height, info.width, CV_8UC4, array);
            sprintf(buffer, "fps=%3d
run@%02d", fps( ), (id ++)%100);
            putText(mat, String(buffer), Point(0, 100), 1, 4.0, Scalar(0, 255, 0));// 4x,Green color

            AndroidBitmap_unlockPixels(env, bitmap);
        }
    }
    return env->NewStringUTF(buffer);
}

主程式 MainActivity.kt:

package com.example.cvex1
import android.graphics.Bitmap
import android.graphics.Color
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.os.Handler
import android.widget.ImageView
import android.widget.TextView

class MainActivity : AppCompatActivity( ) {
    private external fun fpsdraw(pathDirectory: String, bitmap: Bitmap?): String
    private lateinit var textView: TextView
    private lateinit var imageView: ImageView
    private lateinit var bitmap:Bitmap
   
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        if(savedInstanceState == null) {
            imageView = findViewById<ImageView>(R.id.imageView)
            textView = findViewById<TextView>(R.id.sample_text) .apply {
                setTextColor(Color.RED)
                textSize = 20f
            }
            bitmap = Bitmap.createBitmap(720, 720, Bitmap.Config.ARGB_8888)// w720xh720
        }
    }   
    private fun setBitmap( ) {
        bitmap.apply {// this is bitmap
            eraseColor(0) // fill bitmap with black color
            textView.text = fpsdraw(filesDir.path , this) // textView follows bitmap update
            imageView.setImageBitmap(this) // imageView update
            Handler(mainLooper).post { setBitmap( ) }// loop this function again
        }
    }
    override fun onResume( ) {
        super.onResume( )
        setBitmap( )
    }   
    companion object {
        init { System.loadLibrary("native-lib") }
    }
}

主視窗外觀檔 activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app  ="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width     ="match_parent"
    android:layout_height    ="match_parent"
    android:background       ="@color/black"
    tools:context=".MainActivity">
    <ImageView
        android:id ="@+id/imageView"
        android:layout_width ="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintLeft_toLeftOf  ="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf ="parent"/>
    <TextView
        android:id ="@+id/sample_text"
        android:layout_width ="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintLeft_toLeftOf  ="parent"
        app:layout_constraintTop_toBottomOf ="@+id/imageView"/>
</androidx.constraintlayout.widget.ConstraintLayout>

編譯完上傳到 Android 手機, 每秒更新速度可以達到將近 60 張(fps)

後記: 一吐不快超級無敵慢的 Android Studio

1. 安裝 Android Studio 要用 SSD (固態硬碟), 否則啟動時將會慢到無法忍受, 改用 SSD 就能感受到它所帶來的快感, 估計可能是 gradle 運行時, 硬碟中太多檔案需要搜索,導致花費太多時間.

2. 隨機記憶體(DRAM)最好加裝到 8G 以上, 16G 則綽綽有餘, 4G 勉強可以運行.

3. 若只有 4G 的記憶體, 可以考慮不執行 Android Studio 來節省記憶體之消耗, 直接在命令列用 gradlew 來編譯就可. 若非得用 Android Studio, 那就不能再開啟其它視窗執行程式. 在 linux 系統上可以啟用 swap partition (記憶體交換分區)來增加執行空間, 但相對的執行速度就會慢很多.

4. 我用 Android Studio 唯一的時機是用它校正語法輸入錯誤, 他的編輯方式對習慣用 vscode 的我來說, 最大敗筆是動不動跳出一些莫名其妙的提示, 就像正在開車時,旁邊副駕駛在旁邊嘮叨,讓你無奈到快要抓狂.

2021年4月15日 星期四

在 linux 系統上接收 multicast 訊息的小程式

接收 multicast 位址  = 239.255.255.250, udp port = 1900 範例程式 mserver.c:

#include <arpa/inet.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <signal.h>
int main( ) {
    static int fd = socket(AF_INET, SOCK_DGRAM, 0);// get file descriptor by opening udp
    if (fd < 0) return 1;// error

    signal(SIGINT, [ ](int sig) {// ctrl-C handler
        close(fd);
        printf("!!! to exit\n");
        exit(1);
    });

    int value = 1;// set option SO_REUSEADDR, value type must be int!
    if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &value, sizeof(value)) == 0) {
        
        sockaddr_in addr = {
            .sin_family = AF_INET,
            .sin_port   = htons(1900),// udp port 1900
            .sin_addr   = {.s_addr = inet_addr("239.255.255.250")},// multicast only, or htonl(INADDR_ANY);
            .sin_zero   = {0}
        };
        ip_mreq req = {
            .imr_multiaddr = {.s_addr = addr.sin_addr},
            .imr_interface = {.s_addr = htonl(INADDR_ANY)} // any of local IF
        };
        sockaddr  *addrPtr = (sockaddr *)&addr;// cast to sockaddr pointer
        socklen_t addrSize = sizeof(addr);// cast to socklen_t

        if (setsockopt(fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &req, sizeof(req)) == 0 &&
            bind(fd, addrPtr, addrSize) == 0) {
                
            char buffer[256];
            int len = 0;
            do {// to receive message
                len = recv(fd, buffer, 255, 0);
                if (len > 0) {
                    buffer[len] = 0;
                    printf("%s\n", buffer);   
                }
            } while (len >= 0);
        }
    }
    close(fd);
    return 0;
}

編譯及執行:

   g++ mserver.c -o mserver && ./server 

傳送 multicast 位址  = 239.255.255.250, udp port = 1900 範例程式 mclient.c:

#include <arpa/inet.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <signal.h>
int main( ) {
    static int fd = socket(AF_INET, SOCK_DGRAM, 0);// udp
    if (fd < 0) return 1;

    signal(SIGINT, [ ](int sig) {// ctrl-C handler
        close(fd);
        printf("!!! to exit\n");
        exit(1);
    });

    sockaddr_in addr = {
        .sin_family = AF_INET,
        .sin_port   = htons(1900),// udp port 1900
        .sin_addr   = {.s_addr = inet_addr("239.255.255.250")},
        .sin_zero   = {0}
    };
    sockaddr  *addrPtr = (sockaddr *)&addr;// cast to sockaddr pointer
    socklen_t addrSize = sizeof(addr);// cast to socklen_t 

    auto mcastSend = [&addrPtr, &addrSize](const char *msg) {
        printf("%s %d\n", msg, fd);
        return sendto(fd, msg, strlen(msg), 0, addrPtr, addrSize);
    };
    while (mcastSend("hello") >= 0) sleep(1);// wait 1 second to send again
    return 0;
}

開啟另一終端機, 編譯並執行:

   g++ mclient.c -o mclient && ./mclient

2021年4月13日 星期二

理解數位正旋波(digital sin wave)產生器的原理

要將類比電壓轉換成數位電壓(ADC), 有個很簡單的方式是透過比較器輸入一個高頻率的三角波當基準電壓,當輸入的電壓大於三角波電壓時輸出 V+(1), 若小於則輸出 V-(0), 產生的數位(1,0,...)電壓波形也就是俗稱的 PWM(pulse width modulation),要將上述數位電壓轉回類比電壓(DAC), 一個非常經濟的方式是透過低通濾波器(RC 或 LC)將數位電壓轉回類比訊號. 因此一個微處理機(uP)可以很容易經由 PWM 方式外接一個低通濾波器就能產生所需要的 sin 波,而且三角波函數輕而易舉用查表(lookup table)方式就能得到準位 y(n), 最後將比較 結果 sin(n) >  y(n)  ? 輸出到 GPIO ,  當  GPIO 接上低通濾波器後就能產生完美的 sin 波, 當 PWM 的頻率越高, 所能產生的 sin 波的頻率就能越高, 而且產生的誤差也會愈小, 參考資料:

1. https://www.renesas.com/us/en/document/apn/pwm-sine-wave-generation-sinewave?language=en

2. https://www.ti.com/lit/an/spna217/spna217.pdf?ts=1618361816463&ref_url=https%253A%252F%252Fwww.google.com.tw%252F

簡單 c 程式碼, 根據五行八卦相生相剋推斷吉凶

#include "stdio.h" // 五行: //               木2 //      水1           火3 //         金0     土4 // // 先天八卦 vs 五行 //                    ...