2021年6月27日 星期日

程式流程小技巧

if { } else { } 是流程進行的分歧點, 當層級過多時, 程式碼的邏輯就變的難以理解, 例如像是:

          if(... ) {

                if(... ) {

                       if(... ) {

                                   ...

                       } else {

                       }

                } else { 

                       ...

                } ...

          } else if ( ) {

          } else if( ) {

          }

改用一次性的 while loop, 例如:

            do { 

                        if( ) { ... 

                                 break

                         } 

                        if( ) { ... 

                                break

                        } 

                        ...

             } while(false)

若沒有 do{ } while 指令, 只有 while { } 指令, 只要最後使用 break 來跳脫迴圈就可:

           while (true) {

                    if ( ) { 

                         ...

                         break

                     }  

                     ...

                    break

            } 

這樣就能將程式碼攤平, 變的較容易閱讀

2021年6月22日 星期二

使用 gradle 管理並編譯 kotlin 程式碼

1. 首先上 gradle  官網: https://gradle.org/releases 只要下載 binary-only 壓縮檔就可以了

2. 打開終端機, 把下載回來的檔案(gradle-7.1-bin.zip)解壓縮, 再將執行檔所在目錄加進 PATH 內, gradle 就安裝完成:

     cd Downloads  &&  unzip gradle-7.1-bin.zip

     PATH=$PATH:/home/mint/Downloads/gradle-7.1/bin

3.建個目錄儲存各種專案, 主程式目錄名稱, 同時也會被當成是專案名稱, 例如下述 example 次專案, 進入目錄內執行 gradle init 將會再當地目錄初始化成新專案, 接著跳出選項, 要依照畫面適當選取, 最後執行 gradle run 就能啟動樣版程式

     mkdir  Project  &&  cd Project

     mkdir  example  &&  cd example

     gradle init

     gradle run

觀察它所產生的檔案, 我打算利用 make 語法, 編寫一個簡短的 Makefile 方便以後初始化新專案,而不用每次初始化都要用選的:
# /home/mint/Project/Makefile
shellpwd  = $(shell pwd)
project  = `basename $(shellpwd)`
gradlebin  = /home/mint/Downloads/gradle-7.1/bin/gradle
templateKts  = /home/mint/Project/template.gradle.kts
templateApp  = /home/mint/Project/template.kt
appDir  = app
kotlinDir  = src/main/kotlin
targetDir  = $(appDir)/$(kotlinDir)
AppKt  = $(targetDir)/App.kt
settingsKts  = settings.gradle.kts
setProject  = "rootProject.name=(\"$(project)\")"
setInclude = "include(\"$(appDir)\")"
buildKts = $(appDir)/build.gradle.kts
ktsSetApp  = "application { mainClass.set(\"$(project).AppKt\") }"
ktsSetSrc = "sourceSets.main { java.srcDirs(\"$(kotlinDir)\") }"

name:
    @echo this is a kotlin project: $(project)

$(targetDir): $(appDir)/src/main
    @[ -d $@ ]|| mkdir $@

$(appDir)/src/main: $(appDir)/src
    @[ -d $@ ] || mkdir $@

$(appDir)/src: $(appDir)
    @[ -d $@ ] || mkdir $@

$(appDir):
    @[ -d $@ ] || mkdir $@

init: $(settingsKts) $(AppKt)
    @echo ... $(project) is ready

$(settingsKts):
    @if [ -f $@ ]; then echo .; else echo $(setProject)      > $@ && echo $(setInclude) >> $@; fi

$(AppKt): $(targetDir) $(buildKts)
    @if [ -f $@ ]; then echo .; else echo package $(project) > $@ && cat $(templateApp) >> $@; fi

$(buildKts):
    @if [ -f $@ ]; then echo .; else cp $(templateKts) $@ && echo $(ktsSetSrc) >> $@ && echo $(ktsSetApp) >> $@ ;fi
   
run:
    $(gradlebin)  run  -q  --args='arg1 arg2 arg3 ...'

build:
    $(gradlebin) build --offline

clean:
    rm -rf  build  $(appDir)/build  .gradle

再寫一個 /home/mint/Project/template.gradle.kts 樣版檔案, 用來產生 build.gradle.kts:

// template.gradle.kts
plugins {
    kotlin("jvm") version "1.5.10"
    application
}
repositories { mavenCentral() }
dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.0") }

以及另一個  /home/mint/Project/template.kt 樣版範例, 用來產生 kotline 的程式碼:

// template.kt
import kotlinx.coroutines.*
import kotlin.coroutines.*
fun main() {
    runBlocking{
        launch {
            println("Hello")
        }
        launch {
            println("world")
        }
        launch {
            println("you are welcome")
        }
    }
}

將上述 3 個檔案各自存檔後, 在 Makefile 所在目錄內執行 make init, 或是使用參數 -f 指定 Makefile 位置, 上述 make init 用來先產生 settings.grtadle.kts, app/build.gradle.kts, 及 app/src/main/kotlin/App.kt 等範例代碼, 這樣 gradle run 就能編譯並且執行程式:

       make   init   -f   /home/mint/Project/Makefile

      gradle  run

不用寫程式, 就能讓上面的樣版程式跑起來, 夠神奇吧! 實際上是 gradle 先根據 build.gradle.kts 內容,先上網路倉庫(repositories), 找尋插件(plugins)將 kotlin 編譯程式  kotlin("jvm") version "1.5.10" 以及各種需要的相關(dependencies)程式庫,下載後存入硬碟, 再呼叫 kotlin 去編譯成 Java 代碼(byte code), 最後在 J(ava)V(irtual)M(achine 內執行程式. 一切全自動完成, 這就是 gradle 厲害的地方.

ps.

1. gradle 缺點是網路一定要通暢才能運作, 主要是 gradle daemon 要先跑起來.雖然可以用 gradle --offline build 離線編譯程式, 但沒了網路, 例如用 iptables 先把網路的輸出通道關閉:

    sudo  iptables  -A  OUTPUT  -m  owner  --gid-owner  mint  -j  REJECT

    gradle  --offline  build

他就無法編譯了?  這參數 --offline 目的就真的有點奇怪, 似乎名不正言不順.

 2. 如果用複製/貼上面的 Makefile 內容, 無法讓 make 運作, 可能是縮排(tab)的問題, 可以用文字編輯程式稍微修改, 因為 make 規定所有標的物的執行代碼(冒號: 下一行)必須要先縮排(tab)才能運作, 否則會出現錯誤訊息.

2021年6月13日 星期日

使用 kotlin 語言把 http 協定連線升級成 WebSocket 協定所需要的回應編碼: base64( sha1(clientKey) )

 參考資料: https://datatracker.ietf.org/doc/html/rfc6455

    // echo -n dGhlIHNhbXBsZSBub25jZQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11 | sha1sum 
    // sha1Hex  : b37a4f2cc0624f1690f64606cf385945b2bec4ea
    // keyBase64: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
   
    val clientKey = "dGhlIHNhbXBsZSBub25jZQ=="  
    val guID      = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
    val base64sym = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
    val key_GUID  = "${clientKey.trim()}$guID"
    val binary    = MessageDigest.getInstance("SHA-1").digest(key_GUID.toByteArray())
    val sha1Hex   = StringBuilder()
    val keyBase64 = StringBuilder()
    val temp3 = IntArray(3)
    var accumulate = 0
    for (c in binary)  {
        sha1Hex.append(String.format("%02x", c))
        temp3[accumulate ++] = c.toInt()
        if (accumulate == 3) {
            accumulate = 0;
            keyBase64.append(base64sym[ (temp3[0] and 0xfc) shr 2])
            keyBase64.append(base64sym[((temp3[0] and 0x03) shl 4) + ((temp3[1] and 0xf0) shr 4)])
            keyBase64.append(base64sym[((temp3[1] and 0x0f) shl 2) + ((temp3[2] and 0xc0) shr 6)])
            keyBase64.append(base64sym[  temp3[2] and 0x3f])
        }
    }
    if (accumulate > 0) {
        keyBase64.append(base64sym[ (temp3[0] and 0xfc) shr 2])
        if (accumulate == 2) {
            keyBase64.append(base64sym[((temp3[0] and 0x03) shl 4) + ((temp3[1] and 0xf0) shr 4)])
            keyBase64.append(base64sym[ (temp3[1] and 0x0f) shl 2])
        }
        keyBase64.append('=')
        if(accumulate == 1) keyBase64.append('=')
    }
    Log.d("WebSocket_key:", "$clientKey, $keyBase64, $sha1Hex") 

2021年6月10日 星期四

寫一個 Makefile 利用 openssl 產生自我簽章(selfsign)的證書(certification) 讓 Android SSLServerSocket 正常運作

將下列文字存成 Makefile, 複製到 Android 專案目錄內, 接著在終端機下指令 Make, 就會產生一個鑰匙庫(keystore.p12)放在 app/src/main/res/raw 目錄內, 而私鑰則放在 key 目錄下:
#Makefile
storepass=PKCS12_keystore_for_android_app
tempDir=/dev/shm
saveDir=app/src/main/res/raw
privateDir=key

gen: keystore.p12

keystore.p12: selfsign
    openssl pkcs12 -export -inkey $(privateDir)/prvkey.pem -passout pass:$(storepass) -in $(tempDir)/$< -out $(saveDir)/$@
    
selfsign: buildDirectory
    openssl req -x509 -nodes -days 3650 -newkey rsa:2048 -keyout $(privateDir)/prvkey.pem -subj "/C=TW/ST=Taiwan/L=TaipeiCity/CN=localhost" -out $(tempDir)/$@
    
buildDirectory:
    [ -d $(saveDir) ] || mkdir $(saveDir)
    [ -d $(privateDir) ] || mkdir $(privateDir)

clean:
    rm -rf $(tempDir)/selfsign

Android app 可以利用 resources 將它載入, 之後就能讓 SSL Server Socket 正常運作:

        // ...
        val tls = SSLContext.getInstance("TLS").apply {
            val keyStore = KeyStore.getInstance("PKCS12")
            val storepass = "PKCS12_keystore_for_android_app"//pass phrase define in Makefile
            resources.openRawResource(R.raw.keystore).apply {
                keyStore.load(this, storepass.toCharArray())
                close()
            }
            val kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()).apply {
                init(keyStore, storepass.toCharArray())
            }
            init(kmf.keyManagers, null, null)
        }
        val sslServer = tls.serverSocketFactory.createServerSocket(8443) .apply{
            setReuseAddress(true)
        }

如果寫的是 ssl client 端程式, 則要將信任網站的簽署檔(cert file) 放入 keystore, 交由 TrustManager 來管理, Android 可以使用 BKS 格式檔:

        val tls = SSLContext.getInstance("TLS").apply {
            val keyStore = KeyStore.getInstance("PKCS12")
            val storepass = "PKCS12_keystore_for_android_app"//pass phrase define in Makefile
            resources.openRawResource(R.raw.keystore).apply {
                keyStore.load(this, storepass.toCharArray())
                close()
            }
            val kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()).apply {
                init(keyStore, storepass.toCharArray())
            }
            val trustStore = KeyStore.getInstance("BKS")
            val storepass = "PKCS12_keystore_for_android_app"//pass phrase define in Makefile
            resources.openRawResource(R.raw.truststore).apply {
                trustStore.load(this, storepass.toCharArray())
                close()
            }
            val tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()).apply {
                init(trustStore)
            }
            init(kmf.keyManagers, tmf.trustManagers, SecureRandom())
        }

2021年5月31日 星期一

理解血氧機 Pulse oximetry 的原理

參考資料

1. https://www.ti.com/lit/an/slaa655/slaa655.pdf?ts=1622422930797&ref_url=https%253A%252F%252Fwww.google.com%252F

2. https://www.nxp.com/docs/en/application-note/AN4327.pdf

3. https://www.ncbi.nlm.nih.gov/pmc/articles/PMC4099100

4. https://highscope.ch.ntu.edu.tw/wordpress/?p=40839

動脈血(arterial blood)中含血紅素(red blood cell),血紅素中會攜帶氧氣(oxygen)的分子蛋白(protein)稱為血氧蛋白(hemoglobin), 血裏面混有含氧的血紅蛋白(oxyHemoglobin) 簡稱 HBO2, 及未攜氧的血紅蛋白 deoxyHemoglobin 簡稱 HB, 血氧飽和度(oxygen saturation) 簡稱 SaO2 定義為:

                         SaO2 = HBO2濃度 / (HBO2濃度 + HB濃度) = 1 / (1 + HB / HBO2)

實驗發現攜氧血紅蛋白(HBO2)與未攜氧血紅蛋白(HB)在紅光(波長 660nm波段)與近紅外光(波長 940nm波段)的摩爾消光係數(molar extinction coefficient)會有明顯的差異, 在紅光波段觀察, 當打到 HB 時, 因吸收了較多紅光, 出來時剩下微弱紅光而呈現暗紅色, 而打到 HBO2 則多數穿透過去, 因此呈現的是鮮(亮)紅色, 至於發射紅外光時則呈現相反的狀況, HBO2 反而因吸收較多紅外光譜, 出來的則是較弱(暗)紅外光,用示波器觀察就會發現, HB 與 HBO2 會隨著心跳(cardiac cycle)而產生週期性脈動信號(包含著直流與交流信號, 當輸入信號越強, 交直流也會等比例放大?).信號強度也隨著波長各異, 為了拉開差異解析出紅光與紅外光, 同時校正輸入信號準位(從 SaO2 的定義來看, 只要能測出 HB/HBO2 相對濃度, 就能得出 SaO2, 因為 HB 較吸紅光, 而 HBO2 比較吸收紅外光, 因此看起來測量紅光吸收率/紅外光吸收率, 其值必定與 HB/HBO2 濃度比成一定的比例關係 ?), 因此定義:

                         R = 測試紅光吸收率/測試紅外光吸收率  %

                         測試光譜吸收率 = 量測交流準位/量測直流準位

                         準位則是量測均方根 RMS (root mean square)

                        直流準位: 可以量波峰準位: ?

                        交流準位: 波峰準位 - 波谷準位 ?

利用上述參數的定義,使用標準血氧飽和度(0% ~ 100%), 用紅光及紅外光打進去分別量測光吸收率換算成 R 值 %, 作圖後會發現(例如將自變數 x 當作血氧飽和度 %,  因變數 y 則 是 R 值 %),大多數測試點會散落在負相關的直線方程式附近(這可以從 SaO2 的定義看出其成反比的關係式, 或是經驗值來看因 HB 較吸收紅光, 而 HBO2 比較吸收紅外光的方向來理解), 因此可以定義一個 SpO2 的直線方程:

                         SpO2: f(R) =  m * R + b, 其中 m 係數是斜率常數, b 係數是 bias 常數

實驗數據似乎會得到 m = -25 ? 及 b = 110 ?, 也許隨儀器參數選用也會有所不同, 這種用線性回歸分析法算出係數(m, b) 去對應標準 SaO2 數值就稱為 SpO2 ? 經上述數學推導及實驗校正就能得到精確結果(因為直線方程式是 1 對 1 的關係式, 量出 R % 就相當於測得血氧飽和度 %), 俗稱的血氧儀(Pulse oximetry)就是一種可以解析 SpO2 的工具,他需要的是發射兩種光譜(紅光及紅外光, 常用的遙控器內便有一棵紅外線 LED 發射器, 用手機照相機就能看到所發出的光強度, 面板上也會搭一棵紅光 LED 當作按鍵指示器, 可以說非常容易取得)再搭配一個接收感測器(像是 TSL235, 線性光譜轉換器)及信號運算放大器(OP)取得這兩種光譜的信號強度.

光吸收度(量)是一個相對數值(實際上沒有單位,只是一個純量 scalar), 用來度量輸出與輸入的相對衰減值, 或者換算為差異值. 通常用 log 來衡量,  數學上光吸收度 A 就能定義為:

                       A = - log(O/I) = log(I/O) = log(I) -  log(O) , 其中 I 是入射強度, O 是出射強度

根據比爾定律, 光被吸收量 absorbance = ε * c *d, 其中 c 是摩爾濃度單位, d 是光線經過介質的長度, ε 是消光係數或稱光衰係數 absorptivity, 因此輸入與輸出的關係式就可以推導出來:

                       - log(O/I)  = εcd

                       O = I * exp(-εcd)

備註: 文章內的問號 ? , 是還不理解是否正確.


2021年5月27日 星期四

Android 簡單利用 ZXing 產生 QR code, 並將它解碼

 1.用 AndroidStudio 產生樣板程式, 將 ZXing 程式庫加入 build.gradle, 並按下同步鈕,像這樣:
dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    implementation 'androidx.core:core-ktx:1.5.0'
    implementation 'androidx.appcompat:appcompat:1.3.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 'com.google.zxing:core:3.4.1'
}

2. 修改 activity_main.xml 內容,加入 ImageView 及 Button 像這樣:

<?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"
        android:visibility="visible"
        tools:ignore="MissingConstraints" />
    <Button
        android:id="@+id/bgButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:gravity="left|center_vertical"
        android:textAllCaps="false"
        android:background="@android:color/transparent"
        app:layout_constraintBottom_toBottomOf="parent"
        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"
        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> 

3.修改主程式 MainActivity.kt , 像這樣:

package com.example.zxex1
import android.content.pm.ActivityInfo
import android.graphics.*
import android.graphics.drawable.GradientDrawable
import android.os.Bundle
import android.widget.Button
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import com.google.zxing.*
import com.google.zxing.common.HybridBinarizer
class MainActivity : AppCompatActivity() {
    private val qrBitmap = {text: String, square: Int ->
        val intArray = IntArray(square * square)
        try {//產生 QR code
            val bCode = MultiFormatWriter().encode(text,
                BarcodeFormat.QR_CODE,
                square,
                square,
                mapOf(EncodeHintType.CHARACTER_SET to "UTF8")
            )
            var k = 0
            for (row in 0 until square) {
                for (col in 0 until square) {
                    if (! bCode[row, col]) intArray[k + col] = Color.WHITE
                }
                k += square // next row
            }
        } catch (e:IllegalArgumentException) {  }
        Bitmap.createBitmap(intArray,
            square,
            square,
            Bitmap.Config.ARGB_8888
        )
    }
    private val painter = Paint() .apply {
        textAlign   = Paint.Align.CENTER
        style       = Paint.Style.STROKE
        color       = Color.RED
        textSize    = 24f
        strokeWidth = 4f
        isAntiAlias = true
    }
    private val shrinkSize = 640
    private val bgGround = Bitmap.createBitmap (
            shrinkSize,
            shrinkSize,
            Bitmap.Config.ARGB_8888
        )  .apply {            
            val centerX= width.toFloat() / 2
            val centerY= height.toFloat() / 2
            val left = centerX / 2     - 4// = 1 unit = 1/4
            val top  = centerY / 2     - 4// = 1 unit = 1/4
            val right= centerX * 3 / 2 + 4// = 3 unit = 3/4
            val down = centerY * 3 / 2 + 4// = 3 unit = 3/4
            Canvas(this).drawRect(left, top, right, down, painter)
        }
    private lateinit var bgButton:Button
    private lateinit var bgImageView:ImageView
    private fun testQRcode(code: String = "中文也可以") {
        try {
            val sqrSize= shrinkSize/2
            val sRGB = IntArray(sqrSize * sqrSize)
            val qrCode = qrBitmap(code, sqrSize) .apply { // 正方形
                getPixels(sRGB, 0, sqrSize, 0, 0, sqrSize, sqrSize)
            }// 解析 QR code
            MultiFormatReader()
                .decode (BinaryBitmap (HybridBinarizer (RGBLuminanceSource (
                    sqrSize, sqrSize, sRGB
                )))) .text ?. also { qrText -> // 成功解出文字
                Bitmap.createBitmap(bgGround) .apply {
                    val x = width.toFloat()/4
                    val y = height.toFloat()/4
                    Canvas(this).drawBitmap(qrCode, x, y, painter)
                    bgImageView.setImageBitmap(this)// 顯示合成照
                }
                bgButton.apply {
                    setTextColor(Color.GREEN)
                    text = qrText
                }
            }            
        } catch (e: Exception) {// QR code not found exception
            bgImageView.setImageBitmap(bgGround)
        }
    }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
        if (savedInstanceState == null) {
            findViewById<TextView>(R.id.sample_text).text = "Hello"
            bgImageView = findViewById<ImageView>(R.id.imageView)
            bgButton = findViewById<Button>(R.id.bgButton).apply{
                background = GradientDrawable() .apply {
                    cornerRadius = 24f
                    setColor(Color.GREEN)
                }
                isAllCaps = false
                textSize = 24f
                val scanHint= "掃描 QR code"
                var id = 0
                text = scanHint
                setTextColor(Color.RED)
                setOnClickListener { _->
                    text = scanHint
                    setTextColor(Color.RED)
                    testQRcode("數字(${id ++})")
                }
            }
        }
        testQRcode()
    }
    override fun onResume(){ super.onResume()  }
    override fun onPause() {  super.onPause() }
}

4. 編譯並上傳到手機上執行看看

2021年5月7日 星期五

Andoid 使用 kotlin 的 coroutine 實現簡單的 http 伺服器

程式複製到  MainActivity.kt:

package com.example.simplehttp
import android.os.Bundle
import android.net.wifi.WifiManager
import androidx.appcompat.app.AppCompatActivity
import java.net.ServerSocket
import java.net.Socket
import java.net.SocketTimeoutException
import java.io.IOException
import java.io.PrintWriter
import java.util.*
import kotlinx.coroutines.*
typealias _ReplyURI = (Socket) -> Unit
class MainActivity : AppCompatActivity() {
    private val patternBegin = "GET "
    private val patternEnd = " HTTP/1.1"
    private val hformat = "HTTP/1.0 200 OK\nServer: Simple\nContent-Type: %s\nContent-Length: %11d\nConnection: close\n\n"
    private val socketPrintHTML:(Socket, String) -> Unit = {sfd, html ->
        try {
            val writer = PrintWriter(sfd.getOutputStream(), true)
            writer.printf(hformat, "text/html", html.length)
            writer.printf("%s", html)
        } catch (e: IOException) {
            e.printStackTrace()
        }
    }
    data class OneResponse(val uri:String, val lambda: _ReplyURI)
    inner class ToResponse(val fd: Socket) {
        private val field = ArrayList<OneResponse>()
        private val lambdaNull: _ReplyURI = { socketPrintHTML(it, "is lambda Null ok?") }
        val execute:(String) -> Unit = { uri ->
            if(! fd.isClosed) {
                val matchURI = uri.toLowerCase()
                for(i in field.indices) if(field[i].uri == matchURI) {
                    field[i].lambda(fd)
                }
                socketPrintHTML(fd, "not found $uri")
            }
        }
        fun toReply(uri:String, lambda:_ReplyURI? = null) {
            field.add(OneResponse(
                uri.toLowerCase(),
                lambda ?: lambdaNull
            ))
        }
    }
    private fun uhttpRequest(res:ToResponse) {
        val rxSize = 1024
        val rxBuffer = ByteArray(rxSize)
        val len = res.fd.getInputStream().read(rxBuffer, 0, rxSize - 1)
        if(len > 0) {
            rxBuffer[len] = 0
            val str = String(rxBuffer)
            if (str.startsWith(patternBegin) && str.contains(patternEnd)) res.
                execute( str.substring( patternBegin.length, str.indexOf(patternEnd) ))
        }
    }
    private lateinit var serverJob: Job
    private var portTCP: Int = 8080
    private var snapURI: String = "/"
    private  fun uListen() {
        var streamingLock = false
        val tcpServer = ServerSocket(portTCP).apply {
            soTimeout = 40
        }
        while (serverJob.isActive) {
            try {
                val socketfd = tcpServer.accept()
                val req = ToResponse(socketfd). apply {
                    toReply("/Hello1")
                    toReply("/Hello2") { sfd ->
                        socketPrintHTML(sfd, "You are welcome")
                    }
                    toReply(snapURI) { sfd -> runBlocking {
                        while(streamingLock) delay(100)
                        streamingLock = true //...
                            socketPrintHTML(sfd, "Coroutine runBlocking ok")                        
                        streamingLock = false
                    }}
                }
                GlobalScope.launch(Dispatchers.IO) {
                    uhttpRequest(req)
                    socketfd.close()
                }
            } catch (e: SocketTimeoutException) {                
            }
        }
        tcpServer.close()
    }    
    override fun onResume() {        
        val info = (applicationContext.
            getSystemService(WIFI_SERVICE) as WifiManager).connectionInfo
        val a = info.ipAddress        and 0xff
        val b = info.ipAddress shr  8 and 0xff
        val c = info.ipAddress shr 16 and 0xff
        val d = info.ipAddress shr 24 and 0xff
        title = "http://$a.$b.$c.$d:${portTCP}${snapURI}"
        serverJob = GlobalScope.launch(Dispatchers.IO) {
            uListen()            
        }
    }    
    override fun onPause() {
        runBlocking {
            serverJob.cancelAndJoin()
        }
        super.onPause()
    }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        if (savedInstanceState == null) {
        }
    }
}

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

用 python 解簡單的常微分方程式

# sudo apt install python3-pip # python3 -m venv venv # cd venv # . bin/activate # pip3 install numpy matplotlib import numpy as np import m...