2020年8月30日 星期日

簡單的 c++ 使用 multithread 的 localsocket


 共3個檔案: localSocket.h,   client.cpp,   server.cpp:
標頭檔 // localSocket.h
    #include <stdio.h>
    #include <string.h>
    #include <stddef.h>
    #include <stdlib.h>
    #include <unistd.h>
    #include <sys/un.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #ifndef  _default__port
    #define  _default__port  8080
    int aTcpServer (int port = _default__port, char *host=NULL, bool client = false) {
        int localSocket = socket(PF_INET, SOCK_STREAM, 0);
        if (localSocket >= 0){
            struct sockaddr_in endpoint;
            memset(&endpoint, 0, sizeof(endpoint));
            socklen_t aLen = sizeof(endpoint);
            endpoint.sin_family = AF_INET;
            endpoint.sin_port   = htons(port);
            endpoint.sin_addr.s_addr = host ?
                inet_addr(host) : 
                inet_addr("127.0.0.1");
            int result = 1;
            setsockopt(localSocket, SOL_SOCKET, SO_REUSEADDR, &result, sizeof(result));
            result = client ?
                connect(localSocket, (struct sockaddr *) &endpoint, aLen)         :
                bind(localSocket, (struct sockaddr *) &endpoint, aLen) < 0 ? -1 :
                listen(localSocket, 10) ;      
            if (result >= 0)  return localSocket;// sucess
            printf("connect result: %d\n", result);
            close(localSocket);     
        }
        printf("%s:%d socket fail!\n", host, port);
        exit(1);
    };   
    int aTcpSocket (char *host=NULL, int port = _default__port) {
        return aTcpServer(port, host, true);
    };   
    int aLocalServer (char *host = NULL, bool client = false) {
        const char *aName = host ? host : "com.example.testLocalSocket";
        int localSocket = socket(PF_UNIX, SOCK_STREAM, 0);
        if (localSocket >= 0) {
            struct sockaddr_un endpoint;
            memset(&endpoint, 0, sizeof(endpoint));
            socklen_t aLen = 1 + strlen(aName) + offsetof(struct sockaddr_un, sun_path);
            endpoint.sun_family  = AF_UNIX;
            endpoint.sun_path[0] = '\0';
            strcpy(endpoint.sun_path + 1, aName);
            int result = client ?
                connect(localSocket, (struct sockaddr *) &endpoint, aLen)         :
                bind(localSocket, (struct sockaddr *) &endpoint, aLen) < 0 ? -1 :
                listen(localSocket, 10); 
            if (result >= 0) return localSocket;// sucess
            printf("connect result: %d, fail!\n", result);
            close(localSocket);
        }
        printf("%s socket fail!\n", aName);
        exit(1);
    };   
    int aLocalSocket (char *host = NULL) {
        return aLocalServer(host, true);
    };
    #endif

客戶端: // client.cpp
    #include <random>
    #include "localSocket.h"
    using namespace std;
    int main( ) {
        random_device devSeed;
        static auto mt19937Gen  = mt19937(devSeed( ));
        static auto uniformP100 = uniform_int_distribution<int>(0, 99);
        auto randomNumberString = [ ]( ) {
            return to_string(uniformP100(mt19937Gen)).c_str( );
        };
        int localSocket = aLocalSocket( );
        const char *msg = randomNumberString( );
        write(localSocket, msg, strlen(msg));
        close(localSocket);
    }


伺服端 // server.cpp
    // g++  server.cpp  -pthread  -o  server
    #include <future>
    #include <thread>
    #include "localSocket.h"
    using namespace std;
    int main( ) {
        int localSocket = aLocalServer( );
        volatile static int socketNumber = 0;
        static int sockets[10];
        static future<long> futures[10];
        static FILE *fin = fopen("./localSocket.h","rb");
        static pthread_mutex_t f1mutext = PTHREAD_MUTEX_INITIALIZER;
        static pthread_mutex_t t1mutext = PTHREAD_MUTEX_INITIALIZER;
        while (true) { // Main thread
            int socketfd = accept(localSocket, NULL, NULL);
            if(socketfd < 0) continue;
            if(socketNumber == 10) { close(socketfd); continue; }      
            pthread_mutex_lock(&t1mutext);
            auto aPromise = promise<long>( );
            futures[socketNumber] = aPromise.get_future( );
            sockets[socketNumber] = socketfd;
            socketNumber ++;
            auto await = [socketfd]( ) {
                pthread_mutex_lock(&t1mutext);
                socketNumber --;
                for(int i = 0; i < socketNumber; i ++) if(sockets[i] == socketfd) {
                    swap(sockets[i] , sockets[socketNumber]);
                    swap(futures[i] , futures[socketNumber]);
                    break;
                }
                long value = futures[socketNumber].get( );
                pthread_mutex_unlock(&t1mutext);
                return value;
            };
  
            auto aThread = [socketfd, await](int buflen) {
                long limit = await( );
                long position = 0;
                char *buffer = (char *) malloc(buflen + 2);
                if (buffer == nullptr) {
                    close(socketfd);
                    return;
                }
                auto sprintfd = [socketfd] (char *buf, const char *fmt, long arg) {
                    sprintf(buf, fmt, arg);
                    write(socketfd, buf, strlen(buf));
                };
                int len = 0;
                fseek(fin, 0L, SEEK_END);
                long fileLength = ftell(fin);
                do { // read command from network
                    len = read(socketfd, buffer, buflen);
                    buffer[len] = 0;// append EOS
                    if(len > 0) printf("%s\n", buffer);          
                    if (strstr(buffer, "ftell=?")) {
                        sprintfd(buffer, "ftell=%ld", position);
                    } else if (strstr(buffer, "length=?")) {                          
                        sprintfd(buffer, "length=%ld", limit);
                    } else if (strstr(buffer, "fseek=")) {
                        sscanf(buffer, "fseek=%ld", &position);
                        if (position < 0) position = limit - 1;
                        sprintfd(buffer, "ftell=$ld", position);
                    } else if(strstr(buffer, "fread=")) {
                        sscanf(buffer, "fread=%d", &len);// re-use len
                        if(len > 0) {
                            pthread_mutex_lock(&f1mutext);
                            fseek(fin, position, SEEK_SET);
                            len = fread(buffer, 1, len, fin);
                            pthread_mutex_unlock(&f1mutext);
                            if (len > 0) {
                                write(socketfd, buffer, len);
                                position += len;
                            }
                        }
                    }
                } while (len > 0);// EOF
                free(buffer);              
                close(socketfd);
            };
            pthread_mutex_unlock(&t1mutext);
            aPromise.set_value(fileLength);
            thread(aThread, 1024 * 1024).detach( );// thread split
        }
        fclose(fin);
        close(localSocket);
    }

編譯並執行看看:
g++   server.cpp  -pthread  -o  server  &&  ./server &
g++   client.cpp   -o   client
./client    |    ./client   |   ./client   |   ./client   |    ./client   |    ./client   |    ./client   |    ./client

2020年8月25日 星期二

android 用 kotlin coroutine 語法測試 LocalSocket

Java 有內建 LocalSocket 及 LocalServerSocket 兩種類型的程式庫, 是作為內部進程溝通管道(IPC), 進程甚至包括用 c 語言寫的外部 JNI 程式都可以, 一般的 client 就是 LocalSocket 類型 , 若要當伺服器時用 LocalServerSocket , 延續上一篇, 同樣採用 coroutine 方式:
    import kotlinx.coroutines.*
    import android.net.LocalSocket
    import android.net.LocalServerSocket
    import android.net.LocalSocketAddress
    import java.io.IOException 

    private val localSocketName = "com.example.testLocalSocket"   
    private suspend fun localServerListen() {
        val localServerSocket = LocalServerSocket(localSocketName) 
        while(true) {
            try {
                println("server listen...")
                val localSocket = localServerSocket.accept()// will block to listen
                println("server accept $localSocket")
                val array32 = ByteArray(32)
                val len = localSocket.getInputStream().read(array32)// will block to read
                println("server receive:")
                for (x in 0 until len) print("${array32.get(x)},")           
                println("\nserver finish: $len")
                localSocket.close()
            } catch (e: IOException) {
                e.printStackTrace();
                break;
            }
        }
        localServerSocket.close();
    }
    private suspend fun testLocalSocket(start:Int = 100) {
        try {
            val localSocket = LocalSocket()
            val endpointServer = LocalSocketAddress(
                localSocketName,
                LocalSocketAddress.Namespace.ABSTRACT
            )
            println("client connect ...")
            localSocket.connect(endpointServer)
            println("client send: $localSocket")
            val len = 32
            val array32 = ByteArray(len)
            for (x in 0 until len) array32.set(x, (start + x).toByte())
            localSocket.getOutputStream().write(array32, 0, len)
            localSocket.close()
        } catch (e: IOException) {
            e.printStackTrace();
        }
    }
在主程序用 GlobalScope.launch 同時發動三條 coroutines 一起執行:
    GlobalScope.launch(Dispatchers.IO) {
        launch { localServerListen() }
        launch { testLocalSocket(90) }
        launch { testLocalSocket(80) }
    }


後記: 用 c 寫 LocalSocket 通信程式, 使用 Unix Socket_Stream,並採用"虛根"的命名空間 (abstract namespace):
1. // localSocket.h   used  in  client  and  server
  char localSocketName[ ] = "com.example.testLocalSocket";// strlen < 106

2. // localSocket.c    for   client
  #include <stdlib.h>
  #include <stdio.h>
  #include <stddef.h>
  #include <sys/socket.h>
  #include <sys/un.h>
  #include <string.h>
  #include <unistd.h>
  #include "localSocket.h"
  int main() {
        int localSocket = socket(AF_UNIX, SOCK_STREAM, 0);
        if (localSocket < 0) exit(1);           
        struct sockaddr_un endpointServer; // socket for loopback addr
        memset(&endpointServer, 0, sizeof(endpointServer));// init  
        endpointServer.sun_family = AF_UNIX;
        int sunOffset = offsetof(struct sockaddr_un, sun_path);
        endpointServer.sun_path[0]  = '\0';
        strcpy(endpointServer.sun_path + 1, localSocketName);     
        socklen_t len = 1 + strlen(localSocketName) + sunOffset;

        if(connect(localSocket, (struct sockaddr *)&endpointServer, len) >= 0) {
            char msg[] = "Hello";
            send(localSocket, msg, strlen(msg), 0);
        }
        close(localSocket);
    }

3. // localServerSocket.c   for   server
    #include <stdlib.h> 
    #include <stdio.h> 
    #include <stddef.h> 
    #include <sys/socket.h> 
    #include <sys/un.h> 
    #include <string.h> 
    #include <unistd.h> 
    #include "localSocket.h"
    int main() {
        int localSocket = socket(AF_UNIX, SOCK_STREAM, 0);
        if (localSocket < 0) exit(1);                   
        struct sockaddr_un endpointServer; // socket for loopback addr
        memset(&endpointServer, 0, sizeof(endpointServer));// init   
        endpointServer.sun_family = AF_UNIX;
        int sunOffset = offsetof(struct sockaddr_un, sun_path);          
        endpointServer.sun_path[0]  = '\0';
        strcpy(endpointServer.sun_path + 1, localSocketName);    
        socklen_t len = 1 + strlen(localSocketName) + sunOffset;
       
        if (bind(localSocket, (struct sockaddr *)&endpointServer, len) >= 0 && listen(localSocket, 5) >= 0) {
            int socketfd = accept(localSocket, (struct sockaddr *)&endpointServer, &len);
            if(socketfd >= 0) {
                char buf[1024];
                int n = read(socketfd, buf, sizeof(buf));
                buf[n] = 0;
                printf("server receive: %s\n", buf);
                close(socketfd);
            }
        }
        close(localSocket); 
    }

編譯上述兩個程式並執行看看:
    g++  localServerSocket.c   -o   server   &&   ./server  &
    g++  localSocket.c   -o  client   &&   ./client

理解"虛根"(abstract root)的用意:上述 endpointServer.sun_path[0] = '\0'一開頭處填入  0 (零, 也是字串結尾 End Of String), 會讓 bind localSoket 時, 不用在檔案系統內產生檔案,否則執行的程式將在檔案系統目錄內建構出一個 domain name(長度為 0 的檔案 com.example.testLocalSocket),導致不同目錄下執行的程式將有不同的 domain name(整個 domain name 包含前置目錄,除非在同根的目錄用相同名稱,才會有相同 domain name), 執行 domain name 各異的 LocalSocket 程式,除非在命名上取巧,基本上是無法相互溝通的.LocalSocket 採用"虛根"是一個很聰明的方式,可想像 0 就是一個虛根,當執行的程式在虛根上長出 domain name,也就不需在檔案系統下留下任何軌跡,LocalSocket 通信程式只要在虛根上採用了相同名稱,自然就能相互通訊,解決了 domain name 命名的麻煩.

android 用 kotlin coroutine 語法測試 Socket

以往使用 tcp Socket 通信免不了要用 Thread 處理非同步的工作(async  task), 但有了  couroutines 程式庫後就容易多了, 但要注意的是 Android 不能在主程(MainThread 又稱為 UI Thread)使用網路的功能,這時就要用派任工 Dispatchers.IO 分配到後台去執行, Java 有內建 Socket 及 ServerSocket 兩種類型的程式庫, 若要當 tcp  伺服器時用 ServerSocket , 一般的 tcp client 就是 Socket 類型:

    import kotlinx.coroutines.*
   //  import java.net.ServerSocket // tcp server class
    import java.net.Socket  // tcp client class

    private suspend fun testSocket()  {
        try {
            val socket  = Socket("www.google.com.tw", 443) // TCP socket for client
            val connect = socket.isConnected()
            println("$socket : $connect")
            socket.close()
        } catch (e: Exception) {
            println("Socket Error@testSocket: $e")
        }
    }
主程序用 GlobalScope.launch 直接發動 coroutines:
    GlobalScope.launch(Dispatchers.IO) {                      
        launch {
            testSocket()
        }
        // ...
    }

備註:
AndroidManifest.xml 添加網路授權:
          <uses-permission android:name="android.permission.INTERNET" />
build.gradle 添加 kotlin 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'
     }


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>

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

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