2020年8月20日 星期四

採用 Android Kotlin 程式庫的 NSD 網路服務先探

參考資料: https://developer.android.com/training/connect-devices-wirelessly/nsd

linux mint 上針對區域網路的名稱服務協定可以使用 avahi, 透過 avahi-browse -a  -r 就能探聽到區域網路上服務裝置所丟出的服務訊息,從而解析其 ip 位址及所接受的服務端口 port, 他支援常見的名稱服務協定 mDNS/DNS-SD, Android 裝置上使用的 NSD 也支援 mDNS/DNS-SD

這個服務協定原理是:由服務裝置先在區域網路上用 multicast 群組廣播它所服務的名稱類型,區域網路上有興趣者,擷取到該訊息或事先知道訊息就可以透過相同管道去探尋,想辦法獲得該服務裝置的 ip  位址與通信端口 port, 之後通過 ip:port 直接與對方一對一(p2p)通訊, 像這樣透過 p2p (peer to peer)的技術, 讓 app 自成一套網路, 不用伺服器,而讓 app 身兼伺服器角色, 讓ㄧ對多或是多對多通信也不成問題.

 Android 的服務裝置透過 NsdManager 就能完成包含服務公告及探索任務, 使用 kotlin 來寫程式好處是不用像 Java 要寫長長的程式碼, 善用 coroutine 也能完成非同步任務(async task), 首先編輯在 app 底下的 build.gradle, 加入支援 kotlinx-coroutines 的程式庫:
    dependencies {
        implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
        implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9'
        implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'

    }

在程式內也必須引入所用到的相關定義:
    import android.content.Context
    import android.net.nsd.NsdManager
    import android.net.nsd.NsdServiceInfo

    import kotlinx.coroutines.*
    // ...
    val _nsdManager = context.getSystemService(
                Context.NSD_SERVICE) as NsdManager


當 kotlin 的 fun 函式前面冠上保留字 suspend 時, 在裏面就能呼叫別的 suspend 函數(有點類似 javascript 的 function 前冠上 async 就能在函式裏 await 呼叫別的 async function 類似的道理), Kotlin 會將他編譯成一個可以暫停/重啟/毀滅的狀態機(state machine)函式, kotlin 程式庫裏面有一個 suspend fun delay(毫秒單位) 可以用來暫停一段時間同時切換到下一個任務繼續執行, suspend fun 函式一旦呼叫了其它 suspend fun, 函式本身將招致凍結, 等到所呼叫的函式完成後就會再度被喚醒, 用回圈定時輪詢的方式來實現一個 NSD 公告註冊程序:
    private suspend fun nsdRegisterTCP (
         protocol:Int="http",
         servicePort: Int=8080
    ): String {
        val nsdInfo = NsdServiceInfo( ).apply {
            setServiceName("MyPhone") // 名稱自由指定, 有可能會被 android 修改
            setServiceType("_$protocol._tcp") // 協定用 _xxx._xx 方式命名
            setPort(servicePort) // 公告的服務端口 port
            setAttribute("key",  "value") // 其他 TXT, key=value
        } 
        var running: Boolean = true
        _nsdManager.registerService(
              nsdInfo,
              NsdManager.PROTOCOL_DNS_SD,
              object: NsdManager.RegistrationListener {
                    override fun onRegistrationFailed(info: NsdServiceInfo, err: Int) {
                         running = false
                     }
                    override fun onUnregistrationFailed(info: NsdServiceInfo, err: Int) {
                         running = false
                    }
                    override fun onServiceUnregistered(info: NsdServiceInfo) {
                         running = false
                    }
                    override fun onServiceRegistered(info: NsdServiceInfo) {
                         nsdInfo.setServiceName(info.getServiceName())//正式註冊的網路名稱
                         running = false
                    }
              }
        )
        while (running)  { delay(100) }// 暫停 suspend, 100mS 後再回來看是否已完成
        return "$nsdInfo" // 結束後回傳字串
    }

要注意上述 servicePort 是公告所要服務的端口, 與 NSD 本身並不相關, NSD 使用的是群組廣播 multicast 通道, 可以公告多種服務類型, 公告服務單純只用來註冊網路服務, 當公告的名稱一有砥觸,Android 自動會在名稱後面添加一些文字以茲區別,探索服務則是針對服務類型主動發送請求,用以解析對方 ip 與 port, 因此等待時間可能會久一點, 程式邏輯當然也能用上述定時輪詢(pooling)的方式查看旗標狀況來實現, 但這次不使用 suspend fun 函式, 改用一般普通的函式先呼叫 runBlocking 讓裡面的 supspend fun 凍結在該區塊內, 同時藉由 launch 發動 coroutine 所得到的任務名稱 job, 作為將來解析完成後自動把 coroutine 解離, 並加以跳脫 runBlocking 區塊, 因為 runBlocking 會等所有 coroutines join 完成就自動解職離開, 當要探尋多種服務管道時, 這樣的程式運作邏輯, 或許較有效率, 詳細程式碼如下:
    import java.net.InetAddress
    data class IP_PORT(val ip: InetAddress?, val port: Int){ }
    private fun nsdDiscorverHost(
        sniffName: String="name2Find",
        sniffType: String="_http._tcp"
     ) : IP_PORT  {
        if (sniffType.length == 0 || sniffName.length == 0) return IP_PORT(null, 0)
        var sniffHost: InetAddress? = null
        var sniffPort: Int = 0
        lateinit var job: Job
        val nsdLinstener = object: NsdManager.DiscoveryListener {
            override fun onDiscoveryStarted(msg: String) { } 
            override fun onServiceLost(info: NsdServiceInfo) { }
            override fun onDiscoveryStopped(msg: String) {
                if (! job.isCompleted)  job.cancel( )
            }
            override fun onStopDiscoveryFailed(msg: String, err: Int) {
                _nsdManager.stopServiceDiscovery(this)
            }
            override fun onStartDiscoveryFailed(msg: String, err: Int) {
                _nsdManager.stopServiceDiscovery(this)
            }
            override fun onServiceFound(info: NsdServiceInfo) {
                val discovery = this // this is a DiscoveryListener
                if (info.serviceName.contains(sniffName)) {
                      _nsdManager.resolveService(
                           info,
                           object: NsdManager.ResolveListener {
                                  override fun onResolveFailed(resolv: NsdServiceInfo, err: Int) { }
                                  override fun onServiceResolved(resolv: NsdServiceInfo) {
                                       sniffHost = resolv.host
                                       sniffPort = resolv.port
                                       // if (resolv.serviceName == sniffName)
                                       _nsdManager.stopServiceDiscovery(discovery)
                                  }
                          }
                      )
                }
                // ...
            }
        }
        runBlocking { // 最多凍結 10 秒
            job = launch { // get job, launch 無傳回值:  ( )-> void
                  _nsdManager.discoverServices(
                      sniffType,
                      NsdManager.PROTOCOL_DNS_SD,
                      nsdLinstener
                  ) // 呼叫的一般函式並不會被凍結, 裏面持續在運行
                  delay(10000) // 呼叫 suspend 函式時, 本身暫時凍結 10 秒
            } // 一旦 job 被 cancel, 裏面所屬父子代程序也將一併消失
        }
        return IP_PORT(sniffHost, sniffPort)       
    }

主程式如要註冊及探尋網路服務可以用 GlobalScope.launch 同時運行上述兩個 coroutines:
    GlobalScope.launch(Dispatchers.Main) {
        launch {
            val msg  = async {// async 有傳回值: Deferred<String>
                    nsdRegisterTCP("http",  8080) // 這是 async 最後一行函數值
            }
            print("${msg.await( )}")// method Deferred.await( ) to get future value
        }
        launch {
            val find = async {{// async 有傳回值: Deferred<String>
                      nsdDiscorverHost("MyPhone",  "_http._tcp") // 這是 async 最後一行函數值
            }
            print("${find.await( )}")// method Deferred.await( ) to get future value
        }
    }

上面用的  runBlocking, GlobalScope.launch 區塊是在函式內用來發動 coroutine 的工具, 也就是說他是介於一般 routine 與 coroutine 之間運作的橋樑, 當然放在 suspend fun 也是可以的, runBlocking 會凍結函式的運作, 但 GlobalScope.launch 不會, 至於裏面的 launch 及 async 兩種區塊, 他們之間的差異點是: launch 後的 job 不會傳回值, 但 async 區塊可以傳回區塊裏面最後一行的函數值, 發動之後要用方法 await( ) 等待 async 內所有 coroutine 完成後就能取得函數值, 類似 Javascript async/await 的用法, 當 coroutines 放在不同的 launch 區塊時, 他們是獨立並行運作的, 無法斷定何者會先完成, 至於區塊裡面的 coroutines 卻是循序運作的, 而且 runBlocking, GlobalScope.launch, launch , async 等區塊內並不侷限用 suspend fun, 一般方式寫的同步函式也可以放在裏面, 只是它無法被暫停或重置.

最後要再次強調的是 suspend fun 只能由同樣是 suspend fun 類型的函式去呼叫, 或是交由 Kotlin CoroutineScope builder  像是 runBlocking 或 GlobalScope.launch 讓它在區塊範圍內運作

2020年8月12日 星期三

linux 上用 iptables 阻止程式上網

在 Linux mint , 預設使用者是 mint, 可以切換成 su, 用  groupadd 建立一個新群組(例如 wine), 後續就能用 usermod -aG 添加使用者
#!/bin/bash
wineuser=mint
egrep -qs "^wine:" /etc/group || sudo groupadd wine
egrep -qs "^wine:.*:$wineuser" /etc/group ||  usermod -aG wine $wineuser

使用 iptables 的匹配選項 owner 可以針對群組開創一條新規則, 將該群組的使用者與網路斷然隔絕:sudo  iptables  -A  OUTPUT  -m  owner  --gid-owner  wine  -j  REJECT

 要注意的是 group 增添新成員,  必須重新開機才會生效, 因此若寫成 script 執行, 必須先執行一次再重開機,若將上述指令一同放在 /etc/rc.local 裏面時,至少要重新開機兩次才能用 sg 切換群組!

未來開啟終端機, 要執行程式時, 只要下命令前加上 sg   wine  先切換成新群組 wine 後才執行程式, 就可以限制該程式上網的能力,例如寫程式常用的 vscode, 執行命令是 code, 用以下命令, 就能防止它無端傳送封包:
       sg    wine    code
若想要暫時解禁, 可以在 vscode 裏面先開啟終端機, 接著切換成 mint 原始群組, 讓終端機未來所要執行的程式恢復使用網路功能:
       sg  mint
       ...
運作到最後,在終端機下達離開指令
       exit
終端機就會再度禁止網路運作

2020年8月7日 星期五

關於 RC 低通濾波器

一個電阻串接一個下地電容就可以當低通濾波器, 如下所示:
                    Vi     ---> R     +  --- > Vo
                                            C
                                         GND
                   電容阻抗 = 1/sC
                   Vo = Vi * ( 1/sC) ÷ ( R + 1/sC) = Vi  / (1 + sRC)
                   Vo÷Vi =  1 / (1 + sRC)
                    s =  jω
                   Gain = | Vo/Vi|  = 1 / √(1+ (ωRC)^2)
- 3 db 頻率點是當輸出是原輸入的"根號 2分之一" =  1/√ 2 時:
                   20log( 1/√2) = - 3 = 20log(Gain)
                  Gain = 1/√2
                  1 +    (ωRC)^2  = 2 => ωRC = 1
                  ω = 2πf
                  f = 1/2πRC
也就是說將時間常數 RC 乘上 2π 之後, 再取倒數就是該頻率點, 將該頻率以 1V 輸入至此低通濾波器輸出就會得到  0.707 V, 也就是說會衰減 3db
以通式 20 log(Gain) 來看:
                  20log[1 / √(1+ (ωRC)^2)] = - 10log[ 1 + (ωRC)^2]
                  當 (ωRC)^2 >> 1 時
                  20 log(Gain) = - 10log[ 1 + (ωRC)^2] ~= - 20log[ ωRC]
意思是說若以 f = 1/2πRC 當作基準頻率的 -3db 點(信號強度剩 1/√ 2), 則未來該基準頻率 10 倍頻的地方將會衰減 20 db(信號強度剩 1/10), 而 100 倍頻時就會衰減 40 db(信號強度剩 1/100), 而 1000 倍頻時就會衰減至 60 db(信號強度剩 1/1000), ... 以此類推, 換句話說就是每 10 倍頻以斜率  20db 在線性衰減

使用 OP LM324 當作緩衝器注意事項

參考文章: https://www.analog.com/en/analog-dialogue/articles/avoiding-op-amp-instability-problems.html#

LM324 是內含 4 個 OP 的運算放大器, 參考技術資料 : https://www.onsemi.com/pub/Collateral/LM324-D.PDF, 工作電壓是 3V ~ 32V , 可以單電壓工作, 但有些事項需要特別注意:

1. 輸出端 Vo 線性範圍,  供應電壓 Vcc (正端電源), Vee (負端電源), 與 Vb 偏壓(bias voltage)

從 datasheet 可看出 LM324 是 bipolar 輸出, 當偏壓 Vb = (Vcc - Vee) ÷ 2 時, 負輸出(Vo < Vb) ,可以接近 Vee, 但正輸出(Vo > Vb) 受限電晶體的特性, 最大輸出電壓將會比正端電源 Vcc 稍低個 1 ~ 1.3, 因此若想用電壓 3.3V 當電源工作時, 會看到最大輸出到接近 2V 時就飽和被截掉了, 輸出若是 Vo < Vee 時,當然也會被截掉變成 Vee

2. 放大器輸入電阻 Ri, 回受電阻 Rf,  信號源 Vs, 信號源電阻 Rs

若是操作運算放大器在反相放大模式, 當回受電阻等於 Rf, 輸入電阻等於 Ri, 放大率就是 - Rf ÷Ri, 也就是說可以將信號放大或縮小, 端視 Rf 與 Ri 的比率而定, 而且輸出信號相位與原輸入信號還差了 180 度:

         Vo = - Vi  * Rf ÷ Ri   , 反相放大

若是操作在非反相放大模式, 放大率則變成 1 + Rf ÷Ri, 永遠大於 1, 換句話說只能把信號放大:

        Vo =  Vi  * (1 + Rf ÷ Ri) = Vi *(Ri + Rf) ÷ Ri   , 非反相放大

當信號源 Vs 的輸出阻抗 Rs 遠小於 Ri, 就可以忽略掉信號衰減效應 Vi = Vs, 否則就要考慮進去:

         Vi = Vs * Ri ÷ (Ri + Rs)

因此, 反相放大器輸出:

        Vo = -  Vs * [Ri  ÷ (Ri + Rs)]  *  Rf ÷ Ri 

              = -  Vs * Rf ÷ (Ri + Rs) 

非反相放大輸出 :
        Vo  =  Vs * [Ri  ÷ (Ri + Rs)] * [(Ri + Rf) ÷ Ri]

               =  Vs * (Ri + Rf )÷ (Ri + Rs)

雖然可以透過 Rs 把數值加大將信號衰減, 但同時也把熱雜訊引入, 因此 Rs 要儘量小, 參考文章: https://www.analog.com/en/analog-dialogue/raqs/raq-issue-25.html

2020年8月6日 星期四

使用 Javascript 用 webGL 畫線

參考資料:   https://www.tutorialspoint.com/webgl/index.htm             
<html><head><meta charset='utf-8'>    
    <script>
        class PixelRGBA { // float32[4] RGBA, 1 pixel color
            constructor(r, g, b, α = 1) { this.color = new Float32Array([r, g, b, α]);  }
        }
        class ColorLine extends PixelRGBA {// default α = 1
            constructor(nXnY = 512, r = 8, g = 0, b = 0, isSolid = true, aspect = 1.0) {
                super(r, g, b);
                this.pixels = nXnY;// , nX = nY = nXnY, number of X, Y
                this.isSolid= isSolid;
                this.visible = true;
                this.lineID = 0;// ID allocated by ScopeWebGL addLine()
                this.eXoY = new Float32Array(nXnY << 1);// even X, odd Y for (x, y)
                this.viewStart = 0;
                this.η = 0;// 0 <= η < this.pixels
                // keep const λ = 2 = N * Δx, so that x begin with -1 to be in range [-1, 1]
                const λ  = 2.0;// x range [-1, 1], max length = 2, to be a float number
                const N  = isSolid ? nXnY : nXnY >> 3;// line pixels,to be a interger number
                const Δx = λ / N;// x interval, will be a float number
                const Δy = aspect * Δx;// y dependent on x for vertical/horizontal line
                for (let i = 0, n = 0, x = -1.0, y = -aspect; i < N; i ++, n += 2) {
                    this.eXoY[n] = x; // even: x coordinate [-1, 1], Δx = 2 ÷ nXnY
                    this.eXoY[n + 1] = y;// odd: ycoordinate [-1, 1],Δy = aspect * Δx
                    x += Δx;
                    y += Δy;
                }// line.eXoY is Float32Array, 4 bytes/float, 8 bytes per (x,y)

                this.vertical = (x) => { // set vertical line @x
                    for (let i = 0, n = 0; i < this.pixels; i ++, n += 2) {
                        this.eXoY[n] = x;// todo: validate
                    }
                    return this;// to be chain this member
                }
                this.horizon = (y) => { // set horizon line@y
                    for (let i = 0, n = 1; i < this.pixels; i ++, n += 2) {
                        this.eXoY[n] = y;// todo: validate
                    }
                    return this;// to be chain this member
                }
                this.injectFrame = (data) => { // to inject a frame of data at tail
                    if (data && (data.length > 0)) {
                        let moveEnd = this.pixels - data.length;
                        if (moveEnd < 0) moveEnd = 0;
                        const start = data.length * 2;
                        for (let i = 0, n = 1; i < moveEnd; i ++, n += 2) {
                            this.eXoY[n] = this.eXoY[start + n];// only copy y at odd
                        }
                        for (let i = 0, n = (moveEnd * 2) + 1; i < data.length; i ++, n += 2){
                            this.eXoY[n] = data[i];// only copy y at odd
                        }
                    }
                    return this;// to be chain this member
                }
            }            
            // amplitude(n, y) { this.eXoY[n * 2 + 1] = y;  }
            get y() {
                let n = this.η + this.viewStart ;
                if (n >= this.pixels) n = this.pixels - 1;
                return this.eXoY[(n << 1) + 1];
            } // read only
            /*set y(tempy) {// write only
                let n = this.η + this.viewStart ;
                if (0 > n || n >= this.pixels) return;                
                this.eXoY[(n << 1) + 1] = tempy;
            }*/
            
            get position() { return this.η - (this.pixels >> 1); } // read only, refer to center point
            set position(n){ // to set position @n sample, 0<= n < pixels
                if(n >= this.pixels) n = this.pixels - 1;// saturation
                else if(n < 0) n = 0;                
                this.η = n;// 0<= η < pixels
            } // write only

            get x() { return this.eXoY[this.η << 1];  }// read only
            set x(tempx){// to set position @x [-1.0<= x <=1.0] mapto [0 - pixels]
                let n = Math.floor((tempx + 1) * this.pixels) >> 1;// level shift
                this.position = n;
            } // write only
        }

        class ScopeLineGL {// https://www.tutorialspoint.com/webgl/index.htm              
            constructor() {
                const gpuVertexSource =`
                    uniform mat2 scaling;
                    attribute vec2 coordinates;
                    uniform vec2 translation;
                    void main(void) {
                        gl_Position = vec4(scaling * coordinates + translation, 0, 1.0);
                    }
                `;
                const gpuShaderSource =`
                    precision mediump  float;
                    uniform highp vec4 color;
                    void main(void) {
                        gl_FragColor = color;
                    }
                `;
                //const filterGain  = document.createElement('span');// to output text
                const yscaleInput = document.createElement('input');// to input gain value    
                const xcursorInput= document.createElement('input');// to input level trigger value
                const ycursorInput= document.createElement('input');// to input level trigger value   
                const xscaleInput = document.createElement('input');// to input level trigger value   
                const spanBreak   = document.createElement('br');   // spanBreak.cloneNode(true));
                const divScope    = document.createElement('div');// to group following elements
                const canvas      = document.createElement('canvas');// to display oscilloscope
                const coordinatex = document.getElementById('coordinatex');
                const edgeToggle  = document.getElementById('edgeToggle');
                const xcursorToggle= document.getElementById('xcursorToggle');
                const ycursorToggle= document.getElementById('ycursorToggle');
                this.resetToDefault= document.getElementById('resetToDefault');
               
                document.body.style='background-color:black;';
                document.body.append(yscaleInput);
                document.body.append(spanBreak);
                document.body.append(xcursorInput);
                document.body.append(divScope);
                divScope.append(ycursorInput);
                divScope.append(spanBreak.cloneNode());
                divScope.append(canvas);
                divScope.append(spanBreak.cloneNode());
                divScope.append(xscaleInput);
                
                const pixelRatio = window.devicePixelRatio || 1;
                this.width = 2048;
                this.height= 256;
                this.aspectratio = this.width / this.height;
                canvas.width = Math.round(this.width * pixelRatio);// pixels@foreground, to fix center point?
                canvas.height= Math.round(this.height* pixelRatio);// canvas.getContext('2d');

                const webgl  = canvas.getContext('webgl', {antialias: true, transparent: false});//'experimental-webgl'
                webgl.enable(webgl.DEPTH_TEST);
                webgl.clear(webgl.COLOR_BUFFER_BIT || webgl.DEPTH_BUFFER_BIT);
                webgl.viewport(0, 0, canvas.width, canvas.height);
                const gpuProgram = webgl.createProgram();
                const vertexCode = webgl.createShader(webgl.VERTEX_SHADER);            
                const fragmentCode = webgl.createShader(webgl.FRAGMENT_SHADER);
                webgl.shaderSource(vertexCode, gpuVertexSource);
                webgl.shaderSource(fragmentCode, gpuShaderSource);
                webgl.compileShader(vertexCode);
                webgl.compileShader(fragmentCode);
                webgl.attachShader(gpuProgram, vertexCode);
                webgl.attachShader(gpuProgram, fragmentCode);
                webgl.linkProgram(gpuProgram);
                this.clear = () => webgl.clear(webgl.COLOR_BUFFER_BIT || webgl.DEPTH_BUFFER_BIT);
            
                this.lines = [];// to store all lines, todo: use map { } , not array []
                this.lineID = 0;// serial number to be allocated
                this.clientWidth = canvas.width; // scaled by drag
                this.clientHeight = canvas.height;// scaled by drag
                this.disableLine = (id) => { // mark as invisible
                    if(id < this.lineID) this.lines[id].visible = false;
                    return this;
                }
                this.addLine = (line) => {
                    line.lineID = this.lineID ++;
                    this.lines.push(line);
                    return line.lineID;
                }
                
                this.addLine(new ColorLine(this.width, 10, 10, 10).horizon(0));// solid grey horizontal line at x=0
                this.addLine(new ColorLine(this.height, 10, 10, 10, true, this.aspectratio).vertical(0));// solid grey vertical line at y=0
                this.triggerLineID1 = this.addLine(new ColorLine(this.width, 10, 0, 0, false));// dash red trigger line;
                this.triggerLineID2 = this.addLine(new ColorLine(this.width, 10, 0, 0, false));// dash red trigger line;
                this.timeLineID1 = this.addLine(new ColorLine(this.height, 0, 10, 0, true, this.aspectratio));// solid green time line;   
                this.timeLineID2 = this.addLine(new ColorLine(this.height, 0, 10, 0, true, this.aspectratio));// solid green time line;         
                this.scopeLineID = this.addLine(new ColorLine(this.width, 10, 10, 0));// slop = 1, solid yellow line                      
                
                const abs = (v) => v < 0 ? -v : v;// 取絕對值
                const round = (f, d = 2) => {// default 小數 2 位
                    let dot = 10 ** d;
                    return Math.floor(f * dot + 0.5) / dot;
                }
                const tText = (uS) => abs(uS) >= 1e6 ? `${round(uS / 1e6)}sec` :
                                      abs(uS) >= 1e3 ? `${round(uS / 1e3)} ms` :
                                      `${round(uS, 0)} uS`;// 單位四捨五入, 小數點 2 位
                const reportCoordinate = () => {
                    const scopeLine = this.lines[this.scopeLineID];
                    const  fs = this.sampleRate;
                    const  Δx = abs(this.lines[this.timeLineID1].eXoY[0] - this.lines[this.timeLineID2].eXoY[0]);
                    const  Δy = abs(this.lines[this.triggerLineID1].eXoY[1] - this.lines[this.triggerLineID2].eXoY[1]);
                    const tΔx = round(scopeLine.pixels * Δx * 5e5 / fs, 0); // uS, [-1, 1] ↕↔
                    const ΔHz = (tΔx == 0) ? 'Hz' : `${round(1e6 / tΔx, 0)} Hz`;
                    const   n = scopeLine.position;// refer to O, may be a negative number
                    const nΔt = n * 1e6 / fs;// uS unit
                    let y = round(scopeLine.y);
                    if (y > 0) y = `+${y}`;
                    coordinatey.innerHTML =`<span>↕@y= ${round(this.triggerLevel)} v, Δy= ${round(Δy)} v</span>`;
                    coordinatex.innerHTML =`<span'>↔@fs= ${fs} Hz, y= ${y} v<br>n= ${n}, t= ${tText(nΔt)}<br>Δt= ${ΔHz}<sup>-1</sup> = ${tText(tΔx)}</span>`;
                }
                
                const scaleTable  = [16, 8, 4, 2, 1, 1.0/2, 1.0/4, 1.0/8, 1.0/16];
                yscaleInput.type  = 'range';   
                yscaleInput.min   = 0;   
                yscaleInput.max   = scaleTable.length - 1;
                xscaleInput.type  = 'range';// rotate 180 degreen
                xscaleInput.min   = 0;
                xscaleInput.max   = scaleTable.length - 1;
                ycursorInput.type = 'range';// rotate 90 degreen  
                xcursorInput.type = 'range';   
                
            // to calculate global offset
                const ox0 = yscaleInput.clientWidth;// pixels
                const oy0 = ycursorInput.clientHeight;// pixels
                const oTranslate = `translate(0, ${oy0 - ox0}px)`;
                const rotate90deg= 'transform-origin:0 100%; transform: rotate(90deg)';//clockwise
                const rotate180deg='transform-origin:50% 50%; transform: rotate(180deg)';               
                xcursorInput.style= `width:${this.clientWidth}; ${rotate180deg} translate(${-ox0}px, 0)`;
                ycursorInput.style= `width:${this.clientHeight}; ${rotate90deg} ${oTranslate};`;
                yscaleInput.style= rotate90deg;
                xscaleInput.style= rotate180deg;
                const xoffset = ox0 * 2 / this.clientWidth; // Δx = 2.0 ÷ nX;
                const yoffset = 0;
                this.xcursorID = false;
                this.ycursorID = false;
                canvas.style.border= '1px solid';// 外框 1 點
                // canvas.style= `transform-origin:0% 0%; transform: translate(${ox0}px, 0)`;
                
                webgl.bindBuffer(webgl.ARRAY_BUFFER, webgl.createBuffer());
                webgl.useProgram(gpuProgram);
                const gpuColor  = webgl.getUniformLocation(gpuProgram, 'color');  
                const gpuScaling = webgl.getUniformLocation(gpuProgram,'scaling');
                const coordinates = webgl.getAttribLocation(gpuProgram,'coordinates');
                const gpuTranslate= webgl.getUniformLocation(gpuProgram,'translation');
                webgl.uniform2fv(gpuTranslate, new Float32Array([xoffset, yoffset]));
                webgl.vertexAttribPointer(coordinates, 2, webgl.FLOAT, false, 0, 0);
                webgl.enableVertexAttribArray(coordinates);
                
                const xyDraw = (eXoY, pixels, color = PixelRGBA(10,10,10), isSolid = true) => {
                    if (line) { // lambda can bind this & webgl to useProgram ;// webgl.useProgram(gpuProgram);
                        webgl.uniform4fv(gpuColor, color);// transfer color to GPU
                        webgl.uniformMatrix2fv(gpuScaling, false, new Float32Array([
                            this.xscale, 0,
                            0, this.yscale
                        ])); // transfer a matrix for global scaler to GPU
                        webgl.bufferData(webgl.ARRAY_BUFFER, eXoY, webgl.STREAM_DRAW);// stream the position (x,y)
                        webgl.drawArrays(isSolid ? webgl.LINE_STRIP : webgl.LINES, 0, pixels);// line render
                        // webgl.drawArrays(isSolid ? webgl.POINTS : webgl.LINES, 0, viewPixels);
                    }
                }                
                this.draw = (line, eXoY = line.eXoY, pixels = line.pixels) =>
                    xyDraw(eXoY, pixels, line.color, line.isSolid);

                edgeToggle.onclick = () => {
                    this.positiveTrigger = ! this.positiveTrigger;// toggle
                    edgeToggle.innerHTML = `${this.positiveTrigger ? '↑正緣':'↓負緣'}觸發`;
                }                            
            // cursor y to capture position as triggerLevel and show horizontal line
                ycursorInput.oninput  = () => { // direction inverse
                    // console.log(ycursorInput.value);
                    const temp = - ycursorInput.value * 2 / this.height;// mirror and scale down to [-1, 1]
                    this.triggerLevel = temp;
                    this.lines[this.ycursorID ? this.triggerLineID1 : this.triggerLineID2].horizon(temp);
                    reportCoordinate();// todo: report y only
                }
            // cursor x to capture position as time@x and show vertical line
                xcursorInput.oninput = () => { // direction inverse
                    // console.log(xcursorInput.value);
                    const temp = - xcursorInput.value * 2 / this.width;// mirror and scale down to [-1, 1]
                    this.lines[this.scopeLineID].x = temp;
                    this.lines[this.xcursorID ? this.timeLineID1 : this.timeLineID2].vertical(temp);
                    reportCoordinate();// todo: report x only
                }
            // toggle y cursor ID  
                ycursorToggle.onclick = () => {
                    this.ycursorID = ! this.ycursorID;// toggle
                    ycursorInput.oninput();// to sync with y-cursor
                }
            // toggle xcursor ID
                xcursorToggle.onclick = () => {
                    this.xcursorID = ! this.xcursorID;// toggle
                    xcursorInput.oninput();// to sync with x-cursor
                }
            // amplitude scale
                yscaleInput.oninput  = () => {// inverse by scaleTable
                    this.yscale = scaleTable[yscaleInput.value];
                    const temp = 1.0 / this.yscale;// to syn with _gpuYscale
                    ycursorInput.step = temp;
                    ycursorInput.min = -temp * this.height / 2;// Δy = 2/this.height
                    ycursorInput.max = temp * this.height / 2;
                    ycursorInput.oninput();// to sync with y-cursor
                };
            // time scale
                xscaleInput.oninput  = () => {// inverse by scaleTable
                    this.xscale = scaleTable[xscaleInput.value];
                    const temp = 1.0 / this.xscale;// to syn with _gpuXscale
                    xcursorInput.step = temp;
                    xcursorInput.min = -temp * this.width / 2;// Δx = 2/this.width
                    xcursorInput.max = temp * this.width / 2;
                    xcursorInput.oninput(); // to sync with x-cursor
                }
                this.resetToDefault.onclick = () => {
                    yscaleInput.value = 5; // 0.5X => use table to invese direction
                    xscaleInput.value = 4; // 1X  => use table to invese direction
                    ycursorInput.value= 0;// +:down, 0:original@center, -:up
                    xcursorInput.value= 0;// 1:left, 0:original@center, -1:right
                    this.positiveTrigger = false;
                    this.triggerLevel = 0.0;
                    this.lines[this.triggerLineID1].horizon(0);
                    this.lines[this.triggerLineID2].horizon(0);
                    this.lines[this.timeLineID1].vertical(0);
                    this.lines[this.timeLineID2].vertical(0);  
                    this.lines[this.scopeLineID].viewStart = 0;                
                    edgeToggle.onclick();
                    yscaleInput.oninput();
                    xscaleInput.oninput();
                }   
                this.xcursorInput = xcursorInput;// to update cursor infomation realtime
                this.resetToDefault.onclick();
            }// end of constructor
            get render () { // getter can bind this
                this.lines.forEach( (line) => { if (line.visible) {
                    if(line.lineID == this.scopeLineID) { // scopeLine need to be trigger
                        const level = this.triggerLevel;
                        const positive = this.positiveTrigger;
                        const halfPoints = line.pixels >> 1; // begin from center point
                        let n = halfPoints * 2 + 1;// index of center y
                        let py = line.eXoY[n];// store first point
                        n += 2; // go ahead next point, to skip x coordinate
                        for(let i = 1; i < halfPoints; i ++, n += 2) {
                          let y = line.eXoY[n];// trigger seraching            
                          if (positive && y > py &&
                                py < level && level <= y) {
                                line.viewStart = i; // positive trigger
                                break;
                          } else if (! positive && y < py &&
                                py > level && level >= y) {
                                line.viewStart = i; // negative trigger
                                break;
                          }
                          py = y; // keep previously value to check edge
                        }
                    }
                    if(line.viewStart == 0) this.draw(line);                       
                    else {
                        const eXoY = new Float32Array(line.eXoY);//to keep same x coordinate
                        const pixels = line.pixels - line.viewStart;// pixels to move
                        const indexShift= line.viewStart << 1; // index shift
                        let n = 1;// to update y coordinate only, it begins at 1
                        for(let i = 0; i < pixels; i ++, n += 2) {
                            eXoY[n] = line.eXoY[n + indexShift];
                        }    
                        this.draw(line, eXoY, pixels);// pixels has been shrink
                        // for(let i = pixels; i < line.pixels; i ++, n += 2) eXoY[n] = 0;
                        // this.draw(line, eXoY);            
                    }
                }});
            }
            set triggerLevel(level) {
                if(-1<= level && level <=1) this._triggerLevel = level;  
            }
            get triggerLevel() { return this._triggerLevel || 0.0; } // to prevent Null Pointer Exception

            get sampleRate() { return this._sampleRate || 1; } // to prevent Null Pointer Exception
            set sampleRate (fs) {// fs per second
                if(fs >= 1) {
                    this._sampleRate = fs;
                    this.resetToDefault.onclick();
                    this.render;
                }
            }
        }
    </script>
</head>
<body>
    <button id='resetToDefault' style="float: left;">重設 Reset</button>
    <button id='ycursorToggle' style="float: right; color: red">標記相對振幅 y 起始點</button>
    <div id='coordinatey' style='font-size: 48px; text-align:left; color:red'>coordinatey</div>
    <button id='xcursorToggle' style="float: right; color:green">標記相對時間 t 起始點</button>  
    <button id='edgeToggle' style="float: left;">↑ 邊緣觸發</button>
    <div id='coordinatex' style='font-size: 48px; text-align:left; color:green'>coordinatex</div>
    <script>
        const oscScope= new ScopeLineGL();
        const line = oscScope.lines[oscScope.scopeLineID];
        
        const data = new Float32Array(128);
        const randomSingal = () => {
            for(let i = 0; i < data.length ; i ++) {
                data[i] = 2 * Math.random() - 1;
            }
            line.injectFrame(data);
            oscScope.render;
            requestAnimationFrame(randomSingal);     
        }
        randomSingal();
    </script>
</body>
</html>

2020年7月2日 星期四

用 ESP8266 驅動 BLDC

之前硬碟故障後一直保存在庫房裏, 裏面有一棵 3 相 BLDC 包在鋁合金裏面, 無法拆, 索性上網買兩顆橋式電路板練習用 PWM(pulse width modulation)來驅動看看, 3 相 BLDC 端點電壓 A, B,  C 在相對應的 U, V, W 相位差需彼此相差 120 度才能驅動, 同一時間點 BLDC 的其中兩端點根據電流流向產生磁極去吸引馬達轉子(永久磁鐵)達到正轉或逆轉的目的, 第 3 端點必須保持高阻抗讓它自由滑行, BLDC 雖中文翻譯為無刷直流馬達, 實際上卻是一個名不符實的交流馬達, 只是他不用負電壓去驅動, 而是靠端點相對電壓產生負電流去驅動, 透過 PWM 信號控制橋式電路板就能產生交流(正負)電流, 網上買的橋式 IC 是 MX1508, 他有兩個全橋式通道, 每個通道是 2 個輸入, 去控制 2 個橋式輸出,真值表如下:
         In+    In-     Out+    Out-
           0       0       HiZ       HiZ
           0       1       GND     VCC
           1       0       VCC      GND
           1       1       煞車      煞車

驅動 3 個相位的 BLDC 需要用 2 棵 MX1508 IC 將直流 PWM 信號轉換成交流的橋式電流去推動馬達的電感性負載, 2 棵 MX1508 就有 4 個全橋式通道可以使用, 只要取其中 3 個通道, 另外一個通道的兩個輸入端直接接地避免誤動作,  順轉時,電流推導順序如下:
    step 1.   A -> B (C HiZ) : A+ = 1, (A- = 0) , B+ = 0, (B- = 1), C+ = 0, (C- = 0)
    step 2.   A -> C (B HiZ) : A+ = 1, (A- = 0) , B+ = 0, (B- = 0), C+ = 0, (C- = 1)
    step 3.   B -> C (A HiZ):  A+ = 0, (A- = 0) , B+ = 1, (B- = 0), C+ = 0, (C- = 1)
    step 4.   B -> A (C HiZ):  A+ = 0 , (A- = 1), B+ = 1, (B- = 0), C+ = 0, (C- = 0)
    step 5.   C -> A (B HiZ):  A+ = 0 , (A- = 1), B+ = 0, (B- = 0), C+ = 1, (C- = 0)
    step 6.   C -> B (A HiZ):  A+ = 0 , (A- = 0), B+ = 0, (B- = 1), C+ = 1, (C- = 0)

如果要逆轉, 只要將順序反過來就可:
    step 1.   C -> B (A HiZ):  A+ = 0 , (A- = 0), B+ = 0, (B- = 1), C+ = 1, (C- = 0)
    step 2.   C -> A (B HiZ):  A+ = 0 , (A- = 1), B+ = 0, (B- = 0), C+ = 1, (C- = 0)
    step 3.   B -> A (C HiZ):  A+ = 0 , (A- = 1), B+ = 1, (B- = 0), C+ = 0, (C- = 0)
    step 4.   B -> C (A HiZ):  A+ = 0, (A- = 0) , B+ = 1, (B- = 0), C+ = 0, (C- = 1)
    step 5.   A -> C (B HiZ) : A+ = 1, (A- = 0) , B+ = 0, (B- = 0), C+ = 0, (C- = 1)
    step 6.   A -> B (C HiZ) : A+ = 1, (A- = 0) , B+ = 0, (B- = 1), C+ = 0, (C- = 0)

之後循環重複  1 ~ 6 步驟就能產生 120 度相位差, 持續推動 BLDC, 底下只看順轉的情形, 搭配上述真值表, 把電流的流向控制邏輯數位化成一個 6x6 矩陣表來表示, 橫軸是時間軸, 縱軸是每個數位化控制邏輯:

  const int motorPhases[6][6] = {
     {1, 1, 0, 0, 0, 0}, // A+: 1, 1, z, 0, 0, z
     {0, 0, 0, 1, 1, 0}, // A- : 0, 0, z, 1, 1, z
     {0, 0, 1, 1, 0, 0}, // B+: 0, z, 1, 1, z, 0
     {1, 0, 0, 0, 0, 1}, // B- : 1, z, 0, 0, z, 1
     {0, 0, 0, 0, 1, 1}, // C+: z, 0, 0, z, 1, 1
     {0, 1, 1, 0, 0, 0}  // C- : z, 1, 1, z, 0, 0
  };
  mx1508 真值表:
  輸入(+-)    輸出(+-)
     00       -> GND, GND : 高阻抗 HiZ
     10       ->  VCC, GND : 電流順向 Forward
     01       ->  GND, VCC : 電流反向 Reverse
     11       ->  VCC, VCC  : 煞車 Brake

另外要特別注意的是 MX1508 耐電壓只到 9V, 驅動力有限, 可能無法產生太大扭力, 尤其使用 PWM 訊號去驅動時, 有效扭力與 duty cycle 成正比, 因此可能還要打個折. 想要讓 BLDC 保持高速運動, 就算沒有 sensor 也可以推動, 就要像盪鞦韆一樣慢慢的定時加速. 下面要利用 ESP8266 的 timer1 中斷服務程式(interrupt service routine) 從 GPIO 輸出正相位 phase0A, phase0B, phase0C 去控制橋式電路的 U, V, W 相, 也就是說將相對應橋式輸出端分別連接馬達的 U, V, W 相, 至於負相位 phase_A, phase_B, phase_C 主要是控制MX1508 的輸出邏輯, 因此負相位要與橋式輸入相對應的相位連接, 但橋式輸出端就空著不用接,換句話說連接 BLDC 的其實只用通道的半橋式輸出電路, 而控制時運用全橋式輸入邏輯. 相位順序必須連接正確否則是無法推動 BLDC 的,完整 arduino 程式碼如下:
// app.ino
#include "ESP8266WiFi.h"
#define phase0A   14 // A+ use GPIO14: D5
#define phase_A   12 // A- use GPIO12 : D6
#define phase0B   13 // B+ use GPIO13 : D7
#define phase_B   15 // B-  use GPIO15 : D8
#define phase0C   4  //  C+ use GPIO4 : D2
#define phase_C   5  //  C- use GPIO5 : D1
#define RED_LED  2  // GPIO2  is D4
const int motorPhases[6][6] = {
  {1,1,0,0,0,0},// A+: 1, 1, z, 0, 0, z
  {0,0,0,1,1,0},// A- : 0, 0, z, 1, 1, z
  {0,0,1,1,0,0},// B+: 0, z, 1, 1, z, 0
  {1,0,0,0,0,1},// B- : 1, z, 0, 0, z, 1
  {0,0,0,0,1,1},// C+: z, 0, 0, z, 1, 1
  {0,1,1,0,0,0} // C- : z, 1, 1, z, 0, 0
};
int currentPhase = 0;
uint32_t prescaler = 5e6;
int downCounter = 5e6/prescaler;// counter upto 1 second

void ICACHE_RAM_ATTR  onTimeCounter( ) {
  timer1_write(prescaler);
  if(prescaler < 2e6) {     
    digitalWrite(RED_LED, 1);// Turn off LED, begin to speed up ...
    digitalWrite(phase0A , motorPhases[0][currentPhase]);
    digitalWrite(phase_A , motorPhases[1][currentPhase]);
    digitalWrite(phase0B , motorPhases[2][currentPhase]);
    digitalWrite(phase_B , motorPhases[3][currentPhase]);
    digitalWrite(phase0C , motorPhases[4][currentPhase]);
    digitalWrite(phase_C , motorPhases[5][currentPhase]);
    if (++  currentPhase > 5) currentPhase = 0;  
  } else { // to brake BLDC
    digitalWrite(phase0A, 1);
    digitalWrite(phase_A, 1);
    digitalWrite(phase0B, 1);
    digitalWrite(phase_B, 1);
    digitalWrite(phase0C, 1);
    digitalWrite(phase_C, 1);
  }
  if (downCounter > 0) downCounter --;
  else { // time to speed up
    if(prescaler >= 2e5) {
      prescaler = prescaler * 90 / 100; // to speed up 1/0.9    ~= 1.1
    } else if(prescaler >= 2e4) {
      prescaler = prescaler * 95 / 100; // to speed up 1/0.95 ~= 1.05
    } else if (prescaler >= 10e3) {
      prescaler = prescaler * 98 / 100; // to speed up 1/0.98 ~= 1.02
    } else if (prescaler >= 5e3 ) {
      prescaler = prescaler * 99 / 100; // to speed up 1/0.99 ~= 1.01
    } // else keep speed constant, if prescaler smaller than 4500, kernel panic!
    downCounter  = (625e3/prescaler); // counter upto 1/8 second
  } 
}

void setup( ) {
  Serial.begin(115200);
  pinMode(RED_LED, OUTPUT); 
  digitalWrite(RED_LED, 0);// Turn on LED
  pinMode(phase0A, OUTPUT);// A+
  pinMode(phase_A, OUTPUT);// A-
  pinMode(phase0B, OUTPUT);// B+
  pinMode(phase_B, OUTPUT);// B-
  pinMode(phase0C, OUTPUT);// C+
  pinMode(phase_C, OUTPUT);// C-
  timer1_attachInterrupt(onTimeCounter);
  timer1_enable(TIM_DIV16, TIM_EDGE, TIM_SINGLE);
  timer1_write(prescaler);
}

void loop( ) {
}

仔細觀查 6x6 矩陣表會發現一個有趣的現象  A+  與 A- , B+ 與 B- , C+ 與 C- 都相差(超前或是延後) 3 個時間單位(全相是 6 個時間單位, 一圈是 360 度), 稍微運算後會發現正好就是 + 與 - 相位差 180 度, 而 A+ 與 B+  與 C+ 之間也都相互差了 2 個時間單位, 也是因為相位差 120 度的必然結果:
    360 * 3 / 6 = 180
    360 * 2 / 6 = 120
因此只要保存 baseVector= {1, 1, 0, 0, 0, 0} 這一基礎向量, 其它時間的相位用"取模 6 運算"(% 6) 來得到, 因此推動 BLDC 可以簡單用下面的虛擬運算碼來呈現:
const bool  baseVector[6] = {true, true, false, false, false, false};//向量, 共 6 個時間相 0 ~ 5
int n = 0 ;  // 初始相 0, in phase
while (true) {
    digitalWrite(A+,  baseVector[n % 6]);// in  phase  0
    digitalWrite(A- , baseVector[(n - 3) % 6]);// phase lag 3
    digitalWrite(B+, baseVector[(n - 2) % 6]);// phase lag 2
    digitalWrite(B- , baseVector[(n - 5) % 6]);// phase lag 5
    digitalWrite(C+, baseVector[(n - 4) % 6]);// phase lag 4
    digitalWrite(C- , baseVector[(n - 1) % 6]);// phase lag 1
    n = (n + 1) % 6;
    delay(.);
}

另一種演算法是先分別取出每個相, 結束後再準備下個時間單位的向量, 只要把基礎向量旋轉一個時間單位, 同樣可以達到相同目的:
bool  baseVector[6] = {true, true, false, false, false, false};//初始向量, 共 6 個時間相 0 ~ 5
while (true) {
    digitalWrite(A+, baseVector[0]);// phase 0
    digitalWrite(A- , baseVector[3]);// phase -3 % 6 = phase 3
    digitalWrite(B+, baseVector[4]);// phase -2 % 6 = phase 4
    digitalWrite(B- , baseVector[1]);// phase -5 % 6 = phase 1
    digitalWrite(C+, baseVector[2]);// phase -4 % 6 = phase 2
    digitalWrite(C- , baseVector[5]);// phase -1 % 6 = phase 5
    bool temp =  baseVector[0];
    for (int  i = 0; i < 5 ; i ++) baseVector[i] = baseVector[i + 1];// 順時移動
    baseVector[5] = temp;
    delay(.);
}

因為沒使用 sensor 去感應磁鐵的位置, 因此很有可能一開始的慣性力矩造成旋轉方向錯誤,或是有阻力導致後續驅動 BLDC 的相位無法與旋轉運動產生有效共振而停止運動, 或許 BLDC 必須用 hall sensor 的助力才能穩定運行!

2020年6月5日 星期五

從 esp-open-sdk 的原始碼自行編譯, 並建立 esp8266 開發環境

參考資料: https://github.com/cesanta/esp-open-sdk
1. 先安裝編譯工具
sudo apt-get install make unrar-free autoconf automake libtool gcc g++ gperf   flex  bison texinfo gawk ncurses-dev libexpat-dev python-dev python python-serial  sed git unzip bash help2man wget bzip2  libtool-bin

2. 建立工作目錄, 下載 esp-open-sdk 原始碼
cd  &&  mkdir  esp8266
cd  esp8266
git  clone  --recursive  https://github.com/pfalcon/esp-open-sdk.git
cd  esp-open-sdk
make
 cd  ..

3.  到 github 找一個範例來練習,  Wifi Repeater 就是一個很實用的案例, 透過它可以延伸 Wifi 通訊的有效距離, 上官網https://github.com/martin-ger/esp_wifi_repeater 下載原始碼並解壓縮至工作目錄 esp_wifi_repeater-master, 接著在 linux 開發環境設定好環境變數 PATH 加入已編譯好的 cross compiler 所在路徑:
cd  esp8266
wget  https://github.com/martin-ger/esp_wifi_repeater/archive/master.zip
unzip  master.zip
PATH=~/esp8266/esp-open-sdk/xtensa-lx106-elf/bin:$PATH

4. 修改檔案內容 esp_wifi_repeater-master/user/user_config.h, 改連家裡的 Wifi
    #define        WIFI_SSID         "ssid"
    #define        WIFI_PASSWORD     "password"
    #define        REMOTE_MONITORING   0

5. 修改檔案內容 esp_wifi_repeater-master/user/user_main.c,  找到 4301 行的定義
     int i;
將上述行從 #if ACLS ... #endif 搬出來

6. 修改完存檔, 切到 esp_wifi_repeater-master 目錄, 開始編譯
cd  esp8266/esp_wifi_repeater-master
make

7. 將 esp8266 的開發板接上 USB, 將上述編譯完的程式寫入
make flash

Note: 若無法寫入可能是 /dev/ttyUSB0 權限沒開, 將 linux 使用者 mint 加入 dialout 群組再重新開機
sudo  usermod -a  -G  dialout  mint

8.把 esp8266 開發板重新開機, 用手機掃描上述設定的 ssid, 得到 ip 應該是 192.168.4.x, 再用瀏覽器打開網頁連上 http://192.168.4.1, 如有需要可以將它修改存檔, 這個 wifi repeater 一開始不會問密碼? 那是因為密碼設定為 none, 因此可以透過網頁將他改設為 WPA2  後再存檔就能解決, 不用的時候也最好把電源關掉,避免被侵入手機透過家裡的 Wifi 連上網際網路.一切順利的話, 恭喜完成超便宜的 Wifi repeater.



2020年6月4日 星期四

使用 esp8266 開發板, 設定 freeRTOS 開發環境

參考資料:https://github.com/espressif/ESP8266_RTOS_SDK
1. 下載 xtensa toolchain, 設定環境變數  PATH
cd  &&  mkdir  toolchain
cd toolchain
wget  https://dl.espressif.com/dl/xtensa-lx106-elf-linux64-1.22.0-100-ge567ec7-5.2.0.tar.gz
tar -xvf xtensa-lx106-elf-linux64-1.22.0-100-ge567ec7-5.2.0.tar.gz
PATH=~/toolchain/xtensa-lx106-elf/bin:$PATH
cd ..

2. 下載支援 esp8266 版本的 RTOS SDK, 設定環境變數  IDF_PATH
cd toolchain
git clone --recursive https://github.com/espressif/ESP8266_RTOS_SDK.git
export  IDF_PATH=~/toolchain/ESP8266_RTOS_SDK

3. 安裝 python 工具
sudo   apt-get   install   python-pip   python-setuptools
pip install wheel
python -m pip install --user -r $IDF_PATH/requirements.txt

4. 複製範例
cp -r  $IDF_PATH/examples/get-started/hello_world .
cd  hello_world
make menuconfig
make

p.s. 安裝好 toolchain 後, 只要將下列環境變數的設定加入 .bashrc 後面, 下次開啟終端機時就能直接 compile 程式了
PATH=~/toolchain/xtensa-lx106-elf/bin:$PATH
export IDF_PATH=~/toolchain/ESP8266_RTOS_SDK








2020年5月30日 星期六

理解 Arduino 運作方式

參考資料:
1. https://medium.com/home-wireless/use-visual-studio-code-for-arduino-2d0cf4c1760b
2. https://tttapa.github.io/ESP8266/Chap01%20-%20ESP8266.html


Arduino 用的是 c/c++ 語言來開發韌體(firmware), Arduino 的主程式碼通稱為 sketch (例如副檔名是 .ino 的 app.ino), 至於 c 語言的主程式進入點是 main( ), Arduino 的 sketch 實際將它分成 setup( ) 及 loop( ) 兩部份, setup( ) 只會執行一次, loop( ) 則會不斷重複執行, 也就是說 sketch 可以看成是在 c 語言 main( ) 裡會呼叫執行的兩道副程式:
void main( ) {
     setup( );
     while( true )  loop( );
}

在 linux 底下要開發 Arduino, 須下載 arduino SDK, 目前最新版本是 1.8.12:
https://www.arduino.cc/download_handler.php?f=/arduino-1.8.12-linux64.tar.xz
將它解壓縮到指定目錄 (ex. /home/mint/Downloads/arduino-1.8.12), 例如
cd /home/mint/Downloads
tar -xvf  arduino-1.8.12-linux64.tar.xz

若要用 vscode 開發 Arduino, 首先要安裝 Arduino 插件(Extensions), 接著在 vscode 開啟 Settings 設定環境變數,  搜尋 Arduino:Path, 將上述目錄例如 /home/mint/Downloads/arduino-1.8.12 填入, 如果要開發 ESP8266 需要到 Settings 再搜尋 arduino.additionalUrls, 點進去編輯 settings.json , 在 arduino.additionalUrls 欄位填入 "https://arduino.esp8266.com/stable/package_esp8266com_index.json", 像是這樣:
{
    "arduino.enableUSBDetection": false,
    "arduino.path": "/home/mint/Downloads/arduino-1.8.12",
    "workbench.startupEditor": "newUntitledFile",
    "arduino.additionalUrls": "https://arduino.esp8266.com/stable/package_esp8266com_index.json"
}

之後在命令列 View -> Command Palette (或快捷鍵 F1) 搜尋 Arduino 就能看到一堆如同在 Arduino IDE 上可以使用的 cli 命令. 不過剛開始時要先開啟一個專案夾, 並在 View -> Command Palette 選擇 Arduino:Initialize 跑一遍, 初始化一個 Arduino 開發環境, 接著像是 Board Manager, Board Config, Select Serial Port , Library Manager 才能開始使用, 在 Board Manager 可以安裝開發板的 SDK, 例如搜尋 ESP8266 接著安裝, 程式就會安裝到  ~/.arduino15/packages 目錄裡面, 在 Board Config 可以直接去更改開發板用的 SOC, 在 Select Serial Port 可以改變作業系統序列裝置的虛擬位置, 根據選擇也會相對應改變 arduino.json 裡面 board, port 及 configuration 的內容, 至於 Library Manager 可以安裝社群上已經寫好的程式庫, 下載的程式庫, 將會放在目錄  ~/Arduino/libraries 裡面.

以下是修改 .vscode/arduino.json 及 .vscode/c_cpp_properties.json 的檔案內容, 例如:
# arduino.json
{
    "sketch": "app.ino",
    "output": "./build",
    "port": "/dev/ttyUSB0",
    "board": "esp8266:esp8266:d1",
    "configuration": "xtal=80,vt=flash,exception=legacy,ssl=all,eesz=4M2M,ip=lm2f,dbg=Disabled,lvl=None____,wipe=none,baud=921600"
}
上面輸出目錄  output 可以加速後續的 compiler 時間, 可以自行指定到快速區, sketch 就是主程式

# c_cpp_properties.json
{
    "configurations": [
        {
            "name": "Linux",
            "includePath": [
                "${workspaceRoot}",
                "/home/mint/.arduino15/packages/esp8266/tools/**",
                "/home/mint/.arduino15/packages/esp8266/hardware/esp8266/2.7.1/**",
                "/home/mint/Arduino/libraries"
            ],
            "forcedInclude":[ ],
            "defines": ["USBCON"],
            "compilerPath": "/usr/bin/gcc",
            "cStandard": "c11",
            "cppStandard": "c++17",
            "intelliSenseMode": "clang-x64"
        }
    ],
    "version": 4
}
上面 defines 的 USBCON 是支援讓作業系統可以打開序列終端機(Serial port console), 到此設定完成後, vscode 版的 Arduino IDE 應該就能正常運作了. 一個超簡單運行在 ESP8266 上的 http 伺服器範例:
// app.ino
#include "ESP8266WiFi.h"
#include <ESP8266WebServer.h>
char str[2000];
char placeHolder[] = {"<html><head><meta charset='utf-8'></head><body>it works: %ld</body></html>"};
ESP8266WebServer http(80);
void setup( ) {
  const bool softAP    = false;
  const char* ssid     = "myAP";
  const char* password = "myPW";
  Serial.begin(115200);
  if (softAP) {
    WiFi.softAP(ssid, password);
    Serial.println(WiFi.softAPIP( ));
  } else {
    Serial.println("Link to wifi ap to get ip address");
    WiFi.begin(ssid, password);
    while (WiFi.status() != WL_CONNECTED) {
      delay(1000);
      Serial.print(".");
    }
    Serial.println(WiFi.localIP( ));
  }
  http.on("/", [ ]( ) {
    sprintf(str, placeHolder, millis( ));
    http.send(200, "text/html", str);
  });
  http.onNotFound( [ ]( ){
    http.send(404, "text/plain", "404: Not found");
  });
  http.begin( );
}

void loop( ) {
  http.handleClient( );
}


在 vscode 下方狀態列有個插頭的符號可以開/關序列終端機, 為了透過序列阜與開發板作溝通, 在 linux 上可以針對撥接群組 dialout 添加使用者 mint 來賦予使用權限:
sudo  usermod -a  -G  dialout  mint

重新開機讓它生效, 最後在 vscode 右上方中間的磚牆符號可以驗證並編譯 sketch 程式碼的完整性, 其左邊注滿容器的符號則是編譯 sketch 主程式後就直接上傳到開發板運行.


後記 ESP8266 GPIO:
1. 總共17個3.3V的GPIO, 最高承受到3.6V, 電流驅動能力最高12mA, D0~D15內有提升(pull up)電阻, 透過呼叫 pinMode(gpio#, INPUT_PULLUP) 就能減少外掛提升電阻, 但要注意 GPIO0, GPIO2, GPIO15 帶有 bootstrapping 功能, 很有可能外加的電阻使得內建的高提升電阻值而失效,甚至造成輸入邏輯的準位判斷異常, 至於 GPIO16 則是內建下拉(pull down)電阻.
    GPIO 0(D3): Boot SPI, 開發板通常會外加提升電阻, 見下表
    GPIO 1(TX): UART TX0
    GPIO 2(D4): Boot SDIO, 開發板通常會外加提升電阻, 見下表
    GPIO 3(RX): UART RX0
    GPIO 4(D2): I²C_SDA, I²C data
    GPIO 5(D1): I²C_SCK, I²C clock
    GPIO 6~11: 外掛 Flash ROM
    GOIO 12(D6):  MISO, 高速 SPI 主輸入僕輸出
    GPIO 13(D7):  MOSI, 高速 SPI 主輸出僕輸入
    GPIO 14(D5):  SCK, 高速 SPI clock
    GPIO 15(D8):  SS, 高速 SPI select, 開發板通常會外拉下地電阻,見下表
    GPIO 16(D0):  Wakeup,  喚醒
設定 GPIO15, GPIO0, GPIO2  的開機模式 bootstrapping:
     low , low, high  : 從 UART 載入程式
     low , high, high :  從 SPI 載入程式
     high, x, x :  從 SDIO 載入程式
2. 另外還有一個類比通道(10 bits adc), 最高承受到 3V, 測量範圍是 0 ~ 1 V

2020年5月26日 星期二

Solder Mask 與 Paste Mask 的差別


1. 就字面意義來說 Solder Mask (防焊層)就是要把焊接(電氣)層遮掉, Paste Mask 則是要把護膠層遮掉,兩者使用時機並不同, 前者用在 PCB 製程, 後者用於主機板組立製程(PCBA)開鋼板.
2. 在 PCB 的製程中,一塊銅箔基板,為了要線路化,會貼上阻光膠,然後將 Solder Mask 底片放到含阻光膠的銅箔基板上,用紫外線曝光,經過紫外線曝曬,未遮住的地方就能用洗滌劑把該處阻光膠清除,留下的殘膠位置就是線路圖,接者將曝光洗滌過線路板放入強酸池加以蝕刻,曝光部位洗除後只會留下線路圖.
3. 主機板組立時為了要讓 SMT 零件上件,須先刷一層錫膏黏上零件, 因此要用 Paste Mask 層去製作鋼板,將鋼板對準 PCB 後在開孔處刷上一層錫膏,經過熱處理錫膏熔化貼合就能完成 SMT 製程
4. 因此 Solder Mask 是用於 pcb 的線路化製程. 而 Paste Mask 是在 PCBA 開 SMD PAD 的鋼板用的, 開孔大小可根據經驗適時加以調整.


2020年4月7日 星期二

世紀之毒

近期的武漢肺炎疫情在全世界肆虐,主因中國共產黨的隱匿資訊與錯誤思維所造成的世界災難,巧合的是100多年前,當時的清廷鼓勵義和團扶清滅洋導致了八國聯軍攻擊中國的行動.歷史似乎正在重演,今日武漢肺炎流行之際,中共利用大外宣對內用仇外情緒搭配一些不可思議的媒體神話病態的在操弄愛國情操,對外用醫療物資勒索全世界,像是綁架法國的醫療物資用來要換取採用中國華為的 5G 技術,像是武漢封城後開始搜刮全世界醫療口罩,像是派遣醫療團隊到世界各地去"表演",一付事不關己的態度,連對世界道歉的誠意都沒有,正當全世界鬧口罩荒之際,又逼迫各國低頭向中國高價購買醫療設備等等,都是名副其實的趁人之危之舉. 對比之下, 台灣就大方多了,慷慨捐了 1000 萬口罩到西方世界,這就是獨裁政權與民主國家的分別,人道與人權主義應該是超越種族主義的,這點恐怕只有在民主國家的人民才有辦法去體會,這也正是全世界要去聲討中國,要中國付出代價的最大原因.有點諷刺好笑的是,這次疫情比較像是中共發動的第三次世界大戰,想要報復100多年前的舉動.小時候常聽人說過一句話:君子報仇,三年不晚,不是不報,只是時候未到,也許中共認為該是武漢肺炎放出來的時候了吧!網路上對 covid-19 有個新奇的註解是: China Output Virus In December 2019, 真是一絕又經典.世界如能慢慢了解到中共的邪惡本質,反擊與劃清界線,這才是正解,至於亡羊補牢或許尤未晚矣. 八國聯軍實際上是因滿清政府無能腐敗的政權所導致,與現今共產主義的侵略各國行動或許主客觀立場迥異,但本質卻如出一徹,都是獨裁政權導致的世紀災難,這次武漢肺炎造成全世界大流行,與其說是一個規模如世界大戰的一場戰疫,不如說是暴露中共政權狐狸尾巴的冰山一角.要知道共產主義早在世界各地滲透多時,要根除並不容易,中國大陸的人民長久以來被洗腦,終其一生都被蒙在鼓裡,無法了解與認清事實或許還情有可原,但身在牆外的世界,卻不得不認清一個事實,那就是獨裁政權(中國共產黨)才是世紀之毒瘤.

後記:
今天(2020.4.9)在 youtube 上,看到 WHO 的 Tedros 盡然怪罪台灣對它作人身攻擊說什麼 "種族歧視" 莫須有的罪名,台灣並非聯合國一員,在世界上沒有話語權,Tedros 利用 WHO 的地位,在會議上公開譴責台灣,實在有欠公道,甚至還罵美國總統川普將它政治化,殊不知當 Tedros 站在 Taiwan 對立面譴責時,本身就已帶了親中的政治色彩正在替匪宣傳,全世界唯有中國共產黨看不慣台灣的作為,無所不用其極想將台灣邊緣化,台灣在這場戰役堅守下來,尚未大規模感染,全世界有目共睹,Tedros 卻在旁吃味,盡是胡言亂語,可惡無恥之極,無怪乎大家都看不下去,要它下台的呼聲會這麼高,這是它喪失領導威信最主要原因,非洲一些國家的總統儘然還力挺 Tedros,當歐美各國都被武漢病毒打的快招架不住之際,非洲國家如果還信 WHO/Tedros 那一套,下一個爆發地點,我們只能說,那就等著看吧,台灣只能默默祈禱,盼能度過武漢肺炎這難關.

今天(2020.4.19)看到新聞關於 Tedros:
1. https://news.ltn.com.tw/news/world/breakingnews/3135105
2.https://tw.news.yahoo.com/%E8%AD%9A%E5%BE%B7%E5%A1%9E%E9%81%AD%E8%B5%B7%E5%BA%95%E6%8E%8C%E6%AC%8A-%E6%81%90%E6%80%96%E7%B5%84%E7%B9%94-%E8%AC%80%E6%AE%BA-%E6%BF%AB%E6%8D%95-%E7%94%A8%E5%88%91%E6%A8%A3%E6%A8%A3%E4%BE%86-104647758.html
原來 Tedros 有共產黨的的背景,它在衣索匹亞所屬政黨 "提格雷人民解放陣線" 實際上就是共產黨,統治人民的方式與共產黨如出一冊就是槍桿子出政權,毫無人權可言,難怪 Tedros 所作的決定都親中,這下命案揭曉也終於明白了.共產黨是一個善長用民族主義來製造仇恨鬥爭的邪惡組織,難怪 Tedros 想如法泡製,利用種族主義以 "種族歧視" 莫須有罪名污衊台灣,台灣是一個愛好和平的國家怎麼會輕易上當呢? 所以一系列的美食攻擊就耀上國際版面,許多在台外國人紛紛響應 #thisattackcomefromtaiwan 力挺台灣,也要讓全世界了解台灣的真善和美,相信事實是勝於雄辯的!

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

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