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
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
android 用 kotlin coroutine 語法測試 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 網路服務先探
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
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 阻止程式上網
#!/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>
Linux mint 玩 waydroid 一些心得
1. 目前使用 linux mint 22.1 作業系統可以順利跑起來, 可上官網去下載, 並安裝到硬碟. 2. 安裝 waydroid 可上網站 https://docs.waydro.id 參考看看: https://docs.waydro.id/usage/inst...
-
1. 上 Firebase 網站 註冊啟用 Firebase : https://firebase.google.com/ 2. 進入 Firebase Console 註冊開啟新專案 : https://console.firebase.google.com/...
-
Flutter 讓人很容易短時間就能開發出 app, 關鍵在於他開發了很多種小部件稱為 Widget, 將Widget 組合起來就是一個 app. 所謂 部 件(Widget)就是一個可以呈現在螢幕上的視覺系 物 件,幾乎可以說 Flutter 每個物件都是 部 件. 開發者透...