2022年12月16日 星期五

用 dart 語言寫簡易帶上傳功能的 http 檔案伺服器

 import 'dart:convert';
import 'dart:io';
ContentType jsType    = ContentType('application','javascript',charset:'UTF-8');
ContentType textType  = ContentType('text','plain',charset:'UTF-8');
ContentType htmlType  = ContentType('text','html',charset: 'UTF-8');
List<int> disposition = 'Content-Disposition:'.codeUnits;
List<int> multipart   = 'Content-Type:'.codeUnits;
List<int> u8x2crlf    = '\r\n\r\n'.codeUnits;
List<int> u8crlf__    = '\r\n--'.codeUnits;
const styleTag = '''<style>
  body {background:black; color:green; font-size:16px}
  a {margin-left:32px}
  .buttonLike {background:black; color:dodgerblue; cursor:pointer}
  .buttonLike:hover {background:gray; color:white}
</style>''';// css
const buttonJS = '''<input hidden id="pickOne" type="file">
<label for="pickOne" class="buttonLike" id="choose">上傳檔案 ?</label>
<button id="uploadButton" onclick="uploadFile()" disabled="true"></button><br>
<script type="application/javascript">
  pickOne.addEventListener("change", (e)=> {
    choose.innerHTML = e.target.files[0].name;
    uploadButton.disabled = false;
    uploadButton.innerHTML= "開始上傳";
  });
  const uploadFile = () => {
    if (pickOne.files.length > 0) {
      pickOne.disabled = true;
      uploadButton.disabled = true;
      uploadButton.textContent = "上傳中, 請稍候...";
      const formData = new FormData();
      formData.append("appAPK", pickOne.files[0]);
      fetch("/postTMP", {method: "POST", body: formData})
        .then((blob)   => console.log("上傳成功."))
        .catch((error) => console.log("上傳失敗!"))
        .finally(()    => location.reload());
    }
  }
</script>''';
const headTag = '<head><meta charset="UTF-8">$styleTag</head>';
const endbodyhtml = '</body></html>';
const htmlHere='<!DOCTYPE html><html>$headTag<body>It works.</body></html>';

int indexof(List<int> data, List<int> subList) {
  int matchlength = subList.length;
  int size = data.length;
  int slider = 0;
  while (slider < size) {
    int endPos = slider + matchlength;
    if (endPos > size) return -1;
    int position = slider;
    for (int i = 0; i < matchlength; i++) {
      if (data[position] != subList[i]) break;
      position ++;
    }
    if (endPos == position) return slider;
    slider ++;
  }
  return -1;
}
String formParse (List<int> data, String key) {
  List<String> args = utf8.decode(data).trim().split(';');
  for (final pair in args) {
    if (! pair.contains(key) || ! pair.contains('=')) continue;
    int start = pair.indexOf('"');
    int last  = pair.lastIndexOf('"');
    if (start >= 0 && last > 1) return pair.substring(start + 1, last);
    start = pair.indexOf("'");
    last  = pair.lastIndexOf("'");
    if (start >= 0 && last > 1) return pair.substring(start + 1, last);
    return pair.split('=').last;    
  }
  return '';
}
void postListen (HttpRequest req, String path) {
  Directory uploadDir = Directory(path);
  String? tag = req.headers.contentType?.parameters['boundary'];
  if (tag == null) req.response.close();
  else {
    List<int> boundary = tag.codeUnits;
    IOSink? sink = null;
    req.listen (
      (List<int> data) {
        int start = indexof(data, boundary);
        if (start < 0) sink ?. add(data);
        else {        
          start += 2 + boundary.length;
          List<int> form = data.sublist(start);
          int offset = indexof(form, disposition);
          if (offset >= 0) {
            offset = indexof(form, u8x2crlf);
            if (offset >= 0) {
              start += offset + u8x2crlf.length;
              int eos = indexof(form, multipart);
              if (eos > 0) {
                if (! uploadDir.existsSync()) {
                  uploadDir.createSync(recursive: true);
                }
                String name = formParse(form.sublist(0, eos), 'filename');
                if (name.length > 0) {
                  sink = File('${uploadDir.path}/$name').openWrite();
                }
              }
            }
          }
          final startData = data.sublist(start);
          int size = indexof(startData, boundary);
          if (size >= 0) {
            if (indexof(startData, u8crlf__) > 0) size -= 4;
            sink ?. add(data.sublist(start, start + size));
          } else {
            if (offset > 0) {
              sink ?. add(startData);
            } else  {
              size = indexof(data, boundary);
              if (indexof(data, u8crlf__) > 0) size -= 4;
              sink ?. add(data.sublist(0, size));
            }
          }
        }
      },
      onError: (_) => req.response.close(),
      onDone : ( ) {
        req.response .. statusCode = 200 .. write('OK') .. close();
        sink ?. close();
      }
    );
  }
}
void getPipe(String pathname, HttpResponse reply) {
  String fname = (pathname == '/index') ? './index.html' : '.$pathname';  
  final name = Uri.decodeFull(fname);
  File file2Reply = File(name);
  reply .. statusCode = 200 .. headers.contentType = htmlType;
  if (! file2Reply.existsSync() && ! name.endsWith('/')) {
    if (name == './hello' || name == './index.html') reply.write(htmlHere);
    else  reply .. statusCode = 404 ..
      write('<!DOCTYPE html><html><body>404 Not found!</body></html>');    
    reply.close();
    return;
  }
  if (name.endsWith('/')) {
    final dir = Directory(name);
    reply.write('<!DOCTYPE html><html>$headTag<body>$buttonJS 檔案清單:<br>');
    if (dir.existsSync()) dir.listSync() ..  
      sort((a, b) {
        if (a is Directory && b is File) return  1;
        if (a is File && b is Directory) return -1;
        return a.path.toLowerCase().compareTo(b.path.toLowerCase());
      }) ..
      forEach((FileSystemEntity e) {
        final url = Uri.encodeFull(e.path.replaceFirst('.', ''));
        final show = e.path.replaceFirst(name , '');
        if (e is File)      reply.write('<a href="$url">  檔案 $show</a><br>');
        if (e is Directory) reply.write('<a href="$url/"> 目錄 $show</a><br>');
      });
    reply .. write(endbodyhtml) .. close();
    return;
  }      
  reply ..
    headers.contentLength = file2Reply.lengthSync() ..
    headers.contentType   = name.endsWith('.js') ? jsType  :
      RegExp(r'(\.htm|\.html)$').hasMatch(name) ? htmlType :
      RegExp(r'(\.txt|\.dart|\.pem|\.c|\.h)$').hasMatch(name) ? textType :
      name.endsWith('.css') ? ContentType('text',  'css')  :
      name.endsWith('.jpg') ? ContentType('image', 'jpg')  :
      name.endsWith('.png') ? ContentType('image', 'png')  :
      ContentType('application', 'octet-stream');
  file2Reply.openRead().pipe(reply);
}
void main(List<String> args) {
  final ip = InternetAddress.anyIPv4;
  int   port = args.length > 0 ? int.parse(args[0]) : 8080;
  print('http://${ip.host}:$port');
  HttpServer.bind(ip, port).then((HttpServer server){
    server.listen((HttpRequest req) {       
      String pathname = req.uri.path;   
      if (req.method == 'GET') getPipe(pathname, req.response);
      else if(pathname =='/postTMP') postListen(req, './上傳暫存區');
      else req.response.close();
    });
    ProcessSignal.sigint.watch().listen((ProcessSignal signal) {// Ctrl + C
      server.close();
      exit(1);
    });
  });  
}

2022年11月27日 星期日

用 dart 語言寫簡單的 SocketServer 聊天室

// server.dart
import 'dart:io';
import 'dart:convert';
class ChatRoom {
  int key = 0;
  static int sn = 0;
  static Map<int, Socket> sockets = { };
  void broadcast(List<int> data) {
    sockets.forEach((id, session) {
      if (key != id) session.add(utf8.encode('id$key:>${utf8.decode(data)}'));
    });
  }
  void relay(List<int> data) async {
    String text = utf8.decode(data).trim();
    if (text == 'quit') {
      print('relay: $text');
      final session = sockets.remove(key);
      await session?.close();
    }
    broadcast(data);
  }
  void shutdown( ) {
    sockets.remove(key);
    broadcast(utf8.encode('bye!\n'));
    print('id$key leave, remain ${sockets.length}');  
  }
  ChatRoom (Socket session) {
    key = ++sn;
    session .. add(utf8.encode('id${key} welcome!\n')) .. listen(
      relay,
      onDone: shutdown,
      onError: (_) => session.close()
    );
    sockets[key] = session;
    print('id$key join [total ${sockets.length}]');
  }
}
void main( ) {
  int port = 8080;
  print("Chat room at $port");
  ServerSocket.bind(InternetAddress.anyIPv4, port).then((ServerSocket server) {
    server.listen((socket) => ChatRoom(socket));    
  });
}
使用 dart VM 來跑: dart server.dart, 之後就可以用  telnet 127.0.0.1  8080 讓客戶端連進聊天室, 只是 linux 上的 telnet client 在中文編/解碼上, 雖然接收時解中文碼沒問題, 但傳送時編成的中文碼可能會有問題, 幸好用 dart 語言編寫相同功能的 telnet 終端機不是難事:
// client.dart
import 'dart:io';
import 'dart:convert';
void main(List<String>  args)  async {
      String ip = (args.length  >  0) ?  args[0]  :  'localhost';
      int port  = (args.length  >  1)  ?  int.parse(args[1])  :  8080;
      Socket  session = await  Socket.connect(ip,  port);
      session.listen (
           (dynamic data) => print((data is String) ? data  : utf8.decode(data).trim()),
           onDone: (  )  =>  exit(0),
           onError: (_)  =>  session.close()
      );
      stdin.listen (
         (List<int> data) {
              try {//  print(data);
                if  (utf8.decode(data).trim() == 'quit')  throw 'user quit';
                else  session.add(data);
             } catch(e) {
               print('stdin: $e, $data');
               session.close();
               exit(0);
            }
         }
     );
}
同樣用 dart VM 來跑: dart client.dart

2022年11月25日 星期五

使用 crypto 產生密鑰並自我簽證

參考討論文: https://stackoverflow.com/questions/256405/programmatically-create-x509-certificate-using-openssl
// sudo  apt-get   install   libcrypto
// g++   genrsa.c  -lcrypto  &&  ./a.out
// genrsa.c
#include <openssl/rsa.h>
#include <openssl/pem.h>
#include <stdio.h>
int main( ) {
    int nbits = 2048;
    RSA *rsaKey = RSA_new();
    BIGNUM *bignF4 = BN_new();
    BN_set_word(bignF4, 0x10001);// bignF4 = 65537
    if (RSA_generate_key_ex(rsaKey, nbits, bignF4, NULL)) {
        long days = 365;
        unsigned char* subj[3] = {
            (unsigned char *)"TW",
            (unsigned char *)"SELF",
            (unsigned char *)"0.0.0.0"
        };
        X509 *certify = X509_new();
        X509_set_version(certify, 2);// Ver 3
        ASN1_INTEGER_set(X509_get_serialNumber(certify), 1);
        X509_gmtime_adj(X509_get_notBefore(certify), 0);
        X509_gmtime_adj(X509_get_notAfter(certify), days * 86400);
        X509_NAME *subjName = X509_get_subject_name(certify);
        X509_NAME_add_entry_by_txt(subjName, "C" , MBSTRING_ASC, subj[0], -1, -1, 0);
        X509_NAME_add_entry_by_txt(subjName, "O" , MBSTRING_ASC, subj[1], -1, -1, 0);
        X509_NAME_add_entry_by_txt(subjName, "CN", MBSTRING_ASC, subj[2], -1, -1, 0);
        X509_set_issuer_name(certify, subjName);
        EVP_PKEY *keyPair = EVP_PKEY_new();
        EVP_PKEY_assign_RSA(keyPair, rsaKey);
        X509_set_pubkey(certify, keyPair);
        X509_sign(certify, keyPair, EVP_sha1());
        FILE *fout = fopen("private.pem", "wb");
        PEM_write_PrivateKey(fout, keyPair, NULL, NULL, 0, NULL, NULL);
        fclose(fout);
        EVP_PKEY_free(keyPair);
        fout = fopen("certify.pem", "wb");
        PEM_write_X509(fout,  certify);
        fclose(fout);
        X509_free(certify);
    }
    BN_free(bignF4);
    RSA_free(rsaKey);
    return 0;
}

2022年11月24日 星期四

關於 Flutter 3.3.x 黑畫面問題

在 Linux 上使用 Flutter 3.0.5 版本, 寫 linux 的處理畫面(gui)小程式, 一直以來都沒什問題, 更新到 3.3.6 版, 就是無法繪圖, 即使增開一個測試程式:  flutter create test && cd test && flutter run, 也看不到點擊畫面, 只好延用 3.0.5 版, 到今天(2022.11.25), Flutter 早已更新到 3.3.9 版, 下載的 stable 版本仍然不正常, 於是就上網路查, 有人說目前 3.3.x stable  版都有此問題, 切換到 channel master 就可以了, 好奇之下, 開啟終端機, 下個命令:

         flutter    channel    master

畫面真的恢復了, 試試把它切回 flutter  channel  stable, 還真的不正常, 結論就是目前下載的 flutter 3.3.9 版仍要再切到 flutter master channel, 接著讓它上網自動更新, 之後 linux 上處理畫面的程式就能正常運作.

2022年11月23日 星期三

20 幾行的 http 伺服器(node js)

// http.js
const http = require('http');
const fs   = require('fs');
const objHtml = {'Content-Type' : 'text/html;charset=utf-8'};
const uService = (req, reply) => {
    const filename = (req.url == '/') ? './index.html' : `.${req.url}`;
    if (! fs.existsSync(filename)) {// console.log(filename);
        reply.writeHead(404, objHtml);
        reply.write('<!DOCTYPE html><html><body>Not Found<body></html>');
        reply.end();
        return;
    }
    const mimeType = filename.endsWith('.html') ? objHtml :
          filename.endsWith('.css') ? {'Content-Type' : 'text/css'} :
          filename.endsWith('.js')  ? {'Content-Type' : 'application/javascript'} :
          {'Content-Type' : 'application/octet-stream'};
    reply.writeHead(200, mimeType);
    fs.createReadStream(filename).pipe(reply);
}
const port = 8080;
console.log(`http://0.0.0.0:${port}/`);
http.createServer(uService).listen(port);
使用 nodejs 來跑:  node  http.js, 並用瀏覽器瀏覽網址  http://127.0.0.1:8080/index.html
改用 dart 語言來寫:
// http.dart
import 'dart:io';
final objHtml = ContentType('text', 'html', charset: 'UTF-8');
final uService = (HttpRequest req) {
    final reply = req.response;
    final filename = (req.uri.path == '/') ? './index.html' : '.${req.uri.path}';
    final file = File(filename);
    if (! file.existsSync()) {
        reply .. statusCode = 404 ..
            headers.contentType = objHtml ..
            write('<!DOCTYPE html><html><body>Not Found<body></html>') ..
            close();
        return;
    }
    reply .. statusCode = 200 ..
        headers.contentType = RegExp(r'(\.htm|\.html)$').hasMatch(filename) ? objHtml :
            RegExp(r'\.css$').hasMatch(filename) ? ContentType('text', 'css') :
            RegExp(r'\.js$').hasMatch(filename) ? ContentType('application', 'javascript') :
            ContentType('application', 'octet-stream') ..
        headers.contentLength = file.lengthSync();
    file.openRead().pipe(reply);
};
main() {
    final port = 8080;
    print("http://0.0.0.0:${port}/");
    HttpServer.bind(InternetAddress.anyIPv4, port).then((http)=>http.listen(uService));
}
使用 dart 來跑:  dart  http.dart, 並用瀏覽器瀏覽網址  http://127.0.0.1:8080/index.html

2022年11月5日 星期六

玩 Arch Linux

1. 官網下載 Iso 檔 https://archlinux.org/download/, 目前 2022.11.01 版支援 Kernel 6.06, youtube 有許多影片教導重頭至尾的安裝步驟
2. 我用 pacstrap下載以下套件,就足以實現一套完整的中文 linux x11 桌面系統在 VirtualBox 上跑:
base   linux   linux-firmware   base-devel   linux-headers    grub   dosfstools  squashfs-tools   xorriso   efibootmgr   os-prober   gdisk   parted   dhcpcd   networkmanager   network-manager-applet    dialog   wireless_tools   wpa_supplicant    openssh   git   virtualbox-guest-utils   xorg   lightdm   lightdm-gtk-greeter   mate   mate-extra     xorg-server     wqy-bitmapfont   wqy-zenhei   wqy-microhei   wqy-microhei-lite   opendesktop-fonts   ttf-arphic-ukai   ttf-arphic-uming    pulseaudio   pulseaudio-alsa   pavucontrol   materia-gtk-theme   papirus-icon-theme    firefox    xed    geany    gcin   vim

後記:
1. 下載並安裝完後再將  /var/cache/pacman/pkg 刪除可以能節省 1G 左右空間
2. 假設 EFI filesystem 在 /dev/sda1, 安裝 grub bootloader 到 /dev/sda2:
   mkdir   /mnt/efi   &&   mkdir   /mnt/boot
   mount   /dev/sda1  /mnt/efi   &&   mount   /dev/sda2    /mnt/boot
   grub-install   --target=x86_64-efi    --efi-directory=/mnt/efi    --boot-directory=/mnt/boot --bootloader-id=grub    /dev/sda

2022年8月18日 星期四

c++ 語言將陣列當成 bitmap 畫線方式

參考畫線演算法: https://en.wikipedia.org/wiki/Bresenham%27s_line_algorithm
用 c++ 語言將 unsigned char arra[w * h] 陣列當灰階 bitmap, 在裏面畫線範例程式:         
// draw.cpp
#include "stdio.h"
void drawline (unsigned char *p, int w, int h, int cch, int x1, int y1, int x2, int y2, int c=0xffffff) {
        unsigned char *pixel = p + (y1 * w + x1) * cch;
        int dx = x2 - x1;
        int dy = y2 - y1;
        int sx = dx > 0 ? 1 : -1;
        int sy = dy > 0 ? 1 : -1;
        int xbytes = cch * sx;
        int ybytes = w * cch * sy;
        
        if (dx < 0) dx = -dx;// abs, positive
        if (dy > 0) dy = -dy;//-abs, negative
        int de = dx + dy;
        while (true) {
            if (0 <= x1 && x1 < w &&
                0 <= y1 && y1 < h) {                    
                if (cch == 1) *pixel = c & 0xff;
                else {
                    pixel[2] = c >> 16 & 0xff;// R
                    pixel[1] = c >> 8  & 0xff;// G
                    pixel[0] = c       & 0xff;// B                    
                }
            }
            if (x1 == x2 && y1 == y2) break;
            int ex2 = de << 1;
            if (ex2 >= dy) {
                if (x1 == x2) break;
                de += dy;
                x1 += sx;
                pixel += xbytes;
            }
            if (ex2 <= dx) {
                if (y1 == y2) break;
                de += dx;
                y1 += sy;
                pixel += ybytes;
            }
        }
    };
    int main( ){
        int w = 80;
        int h = 25;
        unsigned char bitmap[w * h] ={0};// bitmap:  width * height, gray set black color
        
        auto grayDraw= [&](int x1, int y1, int x2, int y2) {
                int cch = 1;// color channel
                int grayLevel = 0xff;// white color
                drawline(bitmap, w, h, cch, x1, y1, x2, y2, grayLevel);
        };
       
        grayDraw(    0  ,   0,      0, h-1);
        grayDraw(    0,     0, w-1,     0);
        grayDraw(    0, h-1, w-1, h-1);
        grayDraw(w-1,    0, w-1, h-1);
        
        grayDraw(  0,   0, w-1, h-1);
        grayDraw(  0, h-1, w-1,   0);
       
        unsigned char *pixel = bitmap;
        for(int y = 0; y < h; y++) {
            for(int x = 0; x < w; x ++){
                *pixel ++ > 0 ? printf("O") : printf("."); // 文字模式         
            }           
            printf("\n");
        }
    }
編譯並執行  g++  draw.cpp && ./a.out

2022年8月10日 星期三

Linux 上讓 Flutter 3.0 支援 opencv

opencv 用在影像處理上, 非常方便, 且原始碼用 c++ 寫的, dart 語言透過 ffi 便能與 c 語言相互交流, 近來 Flutter 3.0 支援 linux desktop 也趨成熟, 運用 cmake 結合 c 程式庫, 在 linux 上寫 GUI 程式, 用來處理影像就不再是難事.
1. 先用終端機建立一個專案
    flutter  create  project1
        
2.在專案的 lib 目錄下建立一個 cpp 目錄, 將  c 原始碼放於此, 另外 build 目錄放編譯時的檔案
    cd  project1/lib  && mkdir  cpp  && mkdir  build
       
3. 安裝 opencv-dev 開發程式庫及 cmake,  pkg-config 等工具程式:
    sudo  apt  install   libopencv-dev   cmake   pkg-config
      
4. 在 lib 目錄下建立一個檔案 CMakeLists.txt, 將以下內容存檔:
# lib/CMakeLists.txt
    cmake_minimum_required(VERSION 3.16.3)
    project("native")      
    find_package(PkgConfig  REQUIRED)
    pkg_check_modules(OpenCV   REQUIRED   IMPORTED_TARGET   opencv4)

    include_directories(
        cpp
        ${OpenCV_INCLUDE_DIRS}
    )
    add_library(native  SHARED
        cpp/native.cpp
    )   
    target_link_libraries(native
        ${OpenCV_LIBRARIES}
    )

    
5. 編輯 porject1/lib/cpp/native.h , porject1/lib/cpp/native.cpp 示範呼叫 opencv:
// lib/cpp/native.cpp
#include <vector>
#include "native.h"
using namespace std;
using namespace cv;
extern "C" { // export as C function
    ImgStruct  grayJpeg(unsigned char *bin = nullptr, int size = 0) {// 解碼 bin, 轉成 jpeg 灰階影像   
        static vector<unsigned char> jpg;
        vector<unsigned char>().swap(jpg);
        if (bin == nullptr) return ImgStruct {.size = 0, .data = nullptr};
        imencode(".jpg",
            imdecode(Mat(1, size, CV_8UC1, bin), IMREAD_GRAYSCALE),
            jpg
        );
        return ImgStruct {
            .size = (int) jpg.size(),
            .data = jpg.data()
        };
    }
}

// lib/cpp/native.h
    #ifdef __cplusplus
        extern "C" {
    #endif
    #ifndef Native_H
    #define Native_H
        struct ImgStruct {
            int  size;
            unsigned char *data;
        };

        ImgStruct  grayJpeg(unsigned char*, int);        
    #endif
    #ifdef __cplusplus
        }
    #endif   

6.進入 project1/linux 建好程式庫目錄的聯結:
            cd   porject1/linux  &&  ln  -sf  ../lib  .
   接著編輯  project1/linux/CMakeLists.txt, 拉到最後面, 加入以下文字:   
    add_subdirectory("./lib")
    set(nativeSO "${PROJECT_BINARY_DIR}/lib/libnative.so")
    install(FILES  ${nativeSO}  DESTINATION  ${INSTALL_BUNDLE_LIB_DIR}  COMPONENT Runtime)
     
7. 編輯主程式   project1/lib/main.dart 及繪圖程式 project1/lib/mjpgViewer.dart:
//主程式:  lib/main.dart
    import 'package:flutter/material.dart';
    import 'mjpgViewer.dart';
    void main() {
        WidgetsFlutterBinding.ensureInitialized();
        runApp(MyApp());
    }
    class MyApp extends StatelessWidget {
        const MyApp({Key? key}) : super(key: key);
        @override
        Widget build(BuildContext context) {
            return MaterialApp(
                theme: ThemeData.dark(),
                home: const HomePage(),
                title:"MjpgViewer"
            );
        }
    }
    class HomePage extends StatelessWidget{
        const HomePage({Key? key}) : super(key: key);
        @override
        Widget build(BuildContext context) {
            return Scaffold(
                body: MjpgViewer(MediaQuery.of(context).size)  
            );
        }
    }
   
// 繪圖程式: lib/mjpgViewer.dart
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'dart:ui' as ui;
import 'dart:ffi' hide Size;
import "package:ffi/ffi.dart";
import 'package:flutter/services.dart';

typedef  CharPtr = Pointer<Uint8>;
class ImgStruct extends Struct {
  @Int32() external int length;
  external CharPtr data;
  Uint8List asTypedList() => data.asTypedList(length); // method to get data
}


    class MjpgViewer extends StatefulWidget {
        final Size bodySize;
        MjpgViewer(Size? size): bodySize = size ?? Size(400, 400);
        @override _mjpgViewerState createState() => _mjpgViewerState();
    }
    class _mjpgViewerState extends State<MjpgViewer> {
        ui.Image? assetsImage;
        @override  void  initState( ) {
            super .initState( );
            final libso = DynamicLibrary.open("libnative.so");// load share library
            
            final grayJpeg = libso.lookupFunction<ImgStruct Function(CharPtr, Int),
               
ImgStruct Function (CharPtr, int)
            >("grayJpeg");


            rootBundle.load("assets/test.jpg").then((ByteData bytes) {
                final Uint8List u8List = bytes.buffer.asUint8List();
                final int len = u8List.lengthInBytes;
                final CharPtr jpg = malloc.allocate<Uint8>(len);//預先分配動態記憶空間         
                jpg.asTypedList(len).setAll(0, u8List);// 因無法獲得 u8List 的指標, 只能複製後再傳過去, 從 u8List 位置 0 開始, 複製到底.
                final ImgStruct  img = grayJpeg(jpg, len);
                decodeImageFromList(img.asTypedList( )).then(
                  (_uiImg) => setState(()=> assetsImage = _uiImg)
                );
                malloc.free(jpg);// 動態分配的空間, 不用時必須釋放掉
            });
        }
        @override Widget build(BuildContext context) => CustomPaint(
            painter: CavasDraw(assetsImage),
            size: widget.bodySize,
        );
    }
    class CavasDraw extends CustomPainter {
        final ui.Image? background;
        CavasDraw(this.background);
        @override bool shouldRepaint(CavasDraw old) => true;
        @override void paint(Canvas canvas, Size size){
            final bg = background;   
            if (bg != null) {
                final pen = Paint();
                pen.strokeWidth = 1;
                pen.style = PaintingStyle.stroke;
                final src = Rect.fromLTWH(0, 0, bg.width.toDouble(), bg.height.toDouble());
                final dst = Rect.fromLTWH(0, 0, size.width, size.height);
                canvas.drawImageRect(bg, src, dst, pen);
            }
        }
    }

// project1/pubspec.yaml
name: project1
description: A new Flutter project.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
version: 1.0.0+1

environment:
  sdk: ">=2.17.6 <3.0.0"

dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^1.0.2
  ffi: ^2.0.1
 
flutter:
  assets:
    - assets/test.jpg
  uses-material-design: true

最後進入專案 project1 的目錄, 建個子目錄 assets, 將 test.jpg 放進該目錄內, 執行:
    cd project1  &&  flutter  pub get  &&  flutter  run  --no-version-check

後記:

1. 整個專案目錄結構
project1/
    pubspec.yaml
    assets/
        test.jpg
    lib/
        main.dart
        mjpgViewer.dart
        CMakeLists.txt
        CMakeLists.android

        cpp/
            native.cpp
            native.h
    linux/
        CMakeLists.txt
        lib  -> ../lib
    android/
        lib/
            CMakeLists.txt  -> ../../lib/CMakeLists.android
            cpp  -> ../../lib/cpp

        app/
           build.gradle

2. 參考之前文章 https://masontseng.blogspot.com/search?q=opencv 用 OpenCV jni 來編譯能在 Android 上跑的 share library, 稍微修改 CMakeLists.txt, 將它取名為 CMakeLists.android
# CMakeLists.android
    cmake_minimum_required(VERSION 3.16.3)
    project("native")   
    set(OpenCV_DIR  /home/mint/Downloads/OpenCV-android-sdk/sdk/native/jni)
    find_package(OpenCV  REQUIRED)

    include_directories(
        cpp
        ${OpenCV_INCLUDE_DIRS}
    )
    add_library(native SHARED
        cpp/native.cpp
    )
    target_link_libraries(native
        -ljnigraphics
        ${OpenCV_LIBS}
    )

 但 cmake 只認得 CMakeLists.txt, 因此用 ln -sf 符號聯結的方式來解決檔名的問題, 也不需複製.
        cd Project1/android &&  mkdir  lib  &&  cd  lib
        ln   -sf   ../../lib/cpp   .
        ln   -sf   ../../lib/CMakeLists.android    CMakeLists.txt
 
最後編輯 project1/android/app/build.gradle, 加入 externalNativeBuild, 將 CMakeLists.txt 路徑設定好就可以了:
 android {
    //...
    externalNativeBuild {
        cmake {
            path "../lib/CMakeLists.txt"
        }
    }

}
p.s. 編譯成 Android 可以用的 share library, 參考文章:   https://levelup.gitconnected.com/port-an-existing-c-c-app-to-flutter-with-dart-ffi-8dc401a69fd7

2022年8月6日 星期六

測試dart 的 c interop 並繪圖

//main.dart
import 'package:flutter/material.dart';
import 'draw2d.dart';
void main( ) {
  runApp(new MyApp( ));
}
class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData.dark( ),
      home: HomePage( ),
      title:"draw2d",// id, it's not title name!
    );
  }
}
class HomePage extends StatelessWidget{
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Draw2D(MediaQuery.of(context).size)  
    );
  }
}

// draw2d.dart
import 'dart:ffi'  hide Size;// to use Size in dart:ui
import 'dart:math';
import 'dart:ui';
import 'package:flutter/material.dart'  hide TextStyle;// to use TextStyle in dart:ui
import "package:ffi/ffi.dart";
import "dart:async";
import "dart:isolate";
class Draw2D extends StatefulWidget {
  final Size bodySize;
  Draw2D(Size? size): bodySize = size ?? Size(400, 400);
  @override _Draw2DState createState( ) => _Draw2DState( );
}
class _Draw2DState extends State<Draw2D> {
  List<double> figure = [ ];
  final iso = ReceivePort( );
  final dataOffset = 16;
  static Future<void> bgIsolate(SendPort port) async { // isolate function must be static
    final tag = Isolate.current.debugName.toString( );
    final tagLog= (_)=> print("${DateTime.now( )}(${tag}) $_");
    final libso = DynamicLibrary.open("lib/libnative.so");// load share library
    final sttyReady = libso.lookupFunction<Bool Function( ),
        bool Function ( )
      >("ready");        
    final sopen = libso.lookupFunction<Int32 Function(Pointer<Utf8>, Int32),
        int Function(Pointer<Utf8>, int)
      >("sopen");
    final sttyOpen = ([String dev = "/dev/ttyACM0", baud = 250000]) {
      final _str = dev.toNativeUtf8( );
      sopen(_str, baud);
      malloc.free(_str);
    };
    final swrite = libso.lookupFunction<Int32 Function(Pointer<Utf8>),
      int Function(Pointer<Utf8>)
    >("swrite");    
    final sttyWrite = (String str) {
      final _str = str.toNativeUtf8( );
      swrite(_str);
      malloc.free(_str);
    };
    final sttyPoll = libso.lookupFunction<Int32 Function( ),
        int Function ( )
      >("spoll");    
    final sttyBuffer = libso.lookupFunction<Pointer<Uint8> Function( ),
        Pointer<Uint8> Function ( )
      >("adcBuffer");
    final portSendADC = ( ) {
      final n = sttyPoll( );
      if (n > 0) {// make sure null safety
        port.send(sttyBuffer( ).asTypedList(n));
      }
    };
    tagLog("debugName is $tag\n");  
    if (!sttyReady( )) sttyOpen("/dev/ttyACM0");

    if (sttyReady( )) {
      sttyWrite("fson\n");// command to send ADC data
      while (true) {// event loop
        await Future.delayed(Duration(milliseconds: 0));
        portSendADC( );
      }
      sttyWrite("fsoff\n");// command to turn off ADC
    }
    Isolate.exit(port, null);// isolate bgChild exit and return a null back.
  }
  @override void dispose( ) {
    iso.close( );
    super.dispose( );
  }
  @override void initState( ) {
    super.initState( );
    final mainLog = (_) => print("${DateTime.now( )}(${Isolate.current.debugName}) $_");
    mainLog("Future<Isolate> attach sendPort");
    figure = List<double>.filled(widget.bodySize.width.toInt( ), 0);
    final flen = figure.length;
    for (int i = 0; i < flen; i ++) {// normalize f(θ) =  (1 + sin(θ)) / 2;
      figure[i] = (1 - sin(2 * 3.14159 * i / flen)) / 2;// normallize, mirror to speed up CavasDraw.paint
    }
    Isolate.spawn(bgIsolate, iso.sendPort, debugName: "bgChild");
    iso.listen((adcData) {
      if (adcData == null) {
        iso.close( );
      } else {
        int i = 0;
        for (final adc in adcData) {
          if (i < dataOffset) figure[i] = 0.5;// header, fill base point 128 / 256 = 0.5
          else if (i < flen)  figure[i] = adc / 256.0;// normallize adc data, mirror already.
          i ++;// next location
        }
        setState(( ){ });
      }
    });
  }
  @override Widget build(BuildContext context) => Listener (
    onPointerSignal: (evt) => setState(( ) {
      // print(evt);
    }),
    onPointerHover: (evt)  => setState(( ) {
      // print(evt);
    }),
    child: CustomPaint(
      painter: CavasDraw(widget.bodySize, figure),
      size: widget.bodySize,
    )
  );
}
class CavasDraw extends CustomPainter {
  final Size drawSize;
  final List<double> figure;// normalize/mirror figure [0 ~ 1]
  final pen     = Paint( );
  final outline = Path( );
  final dx, cx, cy, w, h;
  late Offset east, west, south, north, center;    
  Paragraph sentence(String str, [double? sz, Color? c]) {
    final font = ParagraphBuilder(ParagraphStyle(fontSize: sz ?? 24))..
                  pushStyle(TextStyle(color: c ?? Colors.white))..
                  addText(str);
    return font.build( ).. layout(ParagraphConstraints(width: drawSize.width));
  }
  CavasDraw(this.drawSize, this.figure):
    dx    = drawSize.width  / figure.length,
    cx    = drawSize.width  / 2,
    cy    = drawSize.height / 2,
    w     = drawSize.width,
    h     = drawSize.height {
    pen.style = PaintingStyle.stroke;
    center = Offset(cx, cy);
    east   = Offset(w - 1, cy);
    west   = Offset(0, cy);
    south  = Offset(cx, h - 1);
    north  = Offset(cx, 0);
    outline.moveTo(0, 0);
    outline.lineTo(0, h - 1);
    outline.lineTo(w - 1, h - 1);
    outline.lineTo(w - 1, 0);
    outline.lineTo(0, 0);
    outline.close( );
  }
  @override bool shouldRepaint(CavasDraw old) => true;
  @override void paint(Canvas canvas, Size size) {
    pen.strokeWidth = 1;
    pen.color = Colors.green;
    double x = 0.0;
    Offset lastone = Offset(x, cy);// base point
    for (final y in figure) {// draw the figure
      final fxy = Offset(x, h * y);
      canvas.drawLine(lastone, fxy, pen);
      lastone = fxy;
      x += dx;
    }
    pen.color = Colors.red;
    canvas.drawLine(north, south, pen);
    canvas.drawLine(west, east, pen);
    canvas.drawParagraph(sentence("觸發點0"), west);// draw text
    pen.strokeWidth = 8;
    pen.color = Colors.grey;
    canvas.drawPath(outline, pen);
  }
}

#CMakeLists.txt
cmake_minimum_required(VERSION 3.7)
project(native)
add_library(native SHARED
    native.cpp
 )

//native.h
#ifdef __cplusplus
   extern "C" {
#endif
#ifndef Native_H
#define Native_H
bool    ready( );// device ready
int     sopen(char *buffer, int baud);
int     spoll( );// number of data available in serial port
int     swrite(char *buffer);// serial port write c_str
unsigned char *adcBuffer( );
#endif
#ifdef __cplusplus
   }
#endif
   
//native.cpp
#include "UBitSerial2.h"
#include "native.h"
static UBitSerial   serialPort = UBitSerial( );
extern "C" { // export as C function
        bool ready( ) {
                return serialPort.ready( );
        }              
        int spoll( ) {
                return serialPort.adcPoll( );
        }
        int swrite(char *buffer){
                return serialPort.write(buffer);
        }
        int sopen(char *buffer, int baud) {
                return serialPort.sopen(buffer, baud);
        }        
        unsigned char *adcBuffer( ) {
                return serialPort.adcBuffer( );
        }        
}

//UbitSerial2.h
#ifndef UBitSerial2_H
#define UBitSerial2_H
#include <unistd.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <asm/termbits.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <stdarg.h>
#define defaultBaudrate 250000
#define maxCountSPF 2048
#define dataOffset  16
class UBitSerial {
  private:
    int fd = 0;
  public:
    ~UBitSerial( ) {
      if (fd) close(fd);
      fd = 0;
    }
    int sopen(char *dev, int baud) {
      if (fd > 0) close(fd);
      int ttyFD = open(dev, O_RDWR | O_NONBLOCK);
      if (ttyFD > 0) {
        fd = ttyFD;
        struct termios2 uart2;
        ioctl(fd, TCGETS2, &uart2);
        uart2.c_iflag     = 0;
        uart2.c_oflag     = 0;
        uart2.c_lflag     = 0;
        uart2.c_cc[VMIN]  = 1;
        uart2.c_cc[VTIME] = 0;
        uart2.c_cflag     = CS8 | CREAD | CLOCAL;
        uart2.c_cflag    &= ~CBAUD;
        uart2.c_cflag    |= CBAUDEX;
        uart2.c_ispeed = baud;
        uart2.c_ospeed = baud;
        ioctl(fd, TCSETS2, &uart2);
        ::printf("%s baud=%d ready. serial port fd=%d\n", dev, baud, fd); // use global printf
      }
      return ttyFD;
    }
    UBitSerial(const char *dev = "/dev/ttyUSB0", int baud = defaultBaudrate) {   
      sopen((char *)dev, baud);
    }
    bool ready( ) { return fd > 0;  }
    unsigned char rawBuffer[2][maxCountSPF];// byte buffer
    int qSize = sizeof(rawBuffer)/sizeof(rawBuffer[0]);
    int head = 0;
    int tail = 0;
    int frameSync = 0;
    int nIndex  = 0;
    char chunkBuffer[1024];
    const int lastone = sizeof(chunkBuffer) - 1;
    int adcPoll( ) {
        if (fd > 0) {
        int fig_1  = 0;
        int chunk = 0;    
        unsigned char *fillBuffer = rawBuffer[head];// raw figure buffer
        while (true) {
          ioctl(fd, FIONREAD, &chunk);
          if (chunk <= 0) break;// poll until empty
          if (chunk > lastone) chunk = lastone;
          ::read(fd, chunkBuffer, chunk);
          chunkBuffer[chunk] = 0;// append EOS
          if (strstr(chunkBuffer, "stop!!!")) continue;
          
          for (int i = 0; i < chunk; i ++) {// check one by one          
              unsigned char figY = (unsigned char) chunkBuffer[i];
              switch (frameSync) {// state macine to detect frameSync 0xff 0x0 ...
               case 0: if (figY == 0xff) {
                              frameSync = 1;
                              fig_1 = 0xff;
                          }
                          break;
                  case 1: if ((figY & 0xf0) != 0 || fig_1 != 0xff) frameSync = 0;
                          else {
                              frameSync = 2;
                              nIndex = 0;
                              fillBuffer[nIndex ++] = 0xff;//insert 0xff at the begining
                          }
                          break;
                  default:break;
              }
              if (frameSync != 2) continue;// until frameSync == 2
              if (nIndex < dataOffset) fillBuffer[nIndex ++] = figY;
              else fillBuffer[nIndex ++]  = ~figY;
              if (nIndex == maxCountSPF) {
                frameSync = 0;
                int next = head + 1;
                if (next == qSize) next = 0;
                if (next != tail) head = next;// head to next rawBuffer
            }
          }
        }
      }
      return (head == tail) ? 0 : maxCountSPF;
    }
    unsigned char *adcBuffer( ) {// no copy
      if (adcPoll( ) > 0) {
        unsigned char *ptr = rawBuffer[tail];
        if (++ tail == qSize) tail = 0;
        return ptr;
      }
      return nullptr;
    }
    int write(char *buffer) {
      if (fd && buffer) return ::write(fd, buffer, strlen(buffer));// use global write      
      return 0;
    }
    int printf(const char *fmt, ...) {// todo: prevent size of strBuffer overflow.
      static char strBuffer[1024];// upto 1024 characters
      va_list parameters;
      va_start(parameters, fmt);
      vsprintf(strBuffer, fmt, parameters);
      va_end(parameters);
      return write(strBuffer);// this->write
    }
};
#endif
 

2022年8月3日 星期三

關於 flutter Matrix4 部件

 Flutter 可以將部件用 Matrix4 包裝起來, 透過它將部件作旋轉, 位移, 放大等特效.可以參考文章:https://medium.com/flutter-community/advanced-flutter-matrix4-and-perspective-transformations-a79404a0d828

1. 旋轉 X 軸時, 使用的轉換矩陣是[1,0,0,0;  0, cos, sin, 0;  0,-sin, cos, 0; 0,0,0,1], 座標運算一下:

  x' = x,   y' = y * cos(θ) + z * sin(θ),   z' = -y * sin(θ) + z * cos(θ)

因為畫面 z = 0 =>  x' = x,    y' = y * cos(θ), 可以預見 X 軸不變, 但 Y 軸變短了, 因為|cos(θ)| <= 1

 

2. 旋轉 Y 軸時, 使用的轉換矩陣是[cos,0,-sin,0;  0, 1, 0, 0;  sin,0,cos,0;  0,0,0,1], 座標運算一下:

  x' = x * cos - z *sin,     y' = y,     z' = x * sin + z *cos

因為畫面 z = 0 =>  x' =  x * cos,    y' = y , 可以預見 Y 軸不變, 但 X 軸變短了, 因為|cos(θ)| <= 1

 

3.旋轉 Z 軸, 使用的轉換矩陣是[cos, sin,0, 0;  -sin,cos,0,0;  0,0,1, 0;  0,0,0,1], 座標運算一下:

         x' = x * cos(θ) + y *sin(θ),          y' = -x * sin(θ) + y * cos(θ),         z' = z

當所有座標點, 以左上角當原點 (0,0, 0)為參考點, 同時旋轉旋轉 Z 軸 θ 時, 各座標點相對位置將一如往常, 物體並不會變形

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

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