2019年10月30日 星期三

Dart 與 Java 資料互傳


1. 陣列(array)資料結構 Dart 端稱為 Uint8List, Java 端稱為 ByteArray, 簡寫為 byte[ ]
2. 緩衝(buffer)區資料結構 Dart 端稱為 ByteData, Java 端稱為 ByteBuffer, Dart 似乎是將ByteBuffer  脫去外殼,留下裡面的 ByteArray,  用屬性 buffer 連結過去, 因此當 dart 端使用到byteData.buffer 時, 所指的內容其實就是 ByteArray. 當然還是得透過方法 Uint8List.view(byteData.buffer) 將裡面的 byteData.buffer (也就是ByteArray)轉成Uint8List 相容的資料結構.用 ByteBuffer 好處是除了可以用索引修改陣列的內容外, 也不需將陣列空間全數複製過去, 而是將連結傳送過去, 免除 copy 效率不佳的問題.
3. Dart 端透過 defaultBinaryMessenger 與 Java 端的 BinaryMessenger 用 binary 資料相互傳遞訊息, 所用就是緩衝區的資料結構來溝通, 實驗發現在 Java 端似乎只能用 ByteBuffer.allocateDirect(length) 或是 ByteBuffer.allocate(length) 分配記憶空間, 資料才能送的過去. 若是用 ByteBuffer.wrap(byte[ ]) 的方式包裝的陣列空間, 傳送似乎有問題, 詳細原因不明.
4. 若是要直接傳送原始的 binary 資料,  Java 端另外還能透過 EventChannel 將 ByteArray 直接傳到  Dart 端, 其收到的就是 Uint8List 也就是 ByteArray (byte[ ],無需再另外轉換.
5. 但若用 EventChannel 方式將 ByteBuffer.array( )當成 ByteArray 傳給 Dart, 會發現長度比原先用 allocate 分配 ByteBuffer 時的記憶空間, 整整少了 7 個位元組(bytes), 推測原因可能是 Big Endian 與 Little Endian 關係, 或是他們的資料結構儲存方式,本就不同, 尚待查證.

2019年10月24日 星期四

用 Javascript 實現簡單的 motion jpeg

伺服器上先準備好 jpeg 格式檔: mjpeg0, mjpeg1, mjpeg2  ... 等, 再寫一個 mjpeg.html 及一個 mpeg.js 檔案, mjpeg.html 範例:
<html><head><meta charset='utf-8'></head>
<body> <script type='application/javascript' src='mjpeg.js'></script></body>
</html>
底下是一個 Javascript 檔案 mjpeg.js 範例程式, 全部放到 http 伺服器同 jpeg 檔案的附錄內,  讓它執行, 用瀏覽下抓上述 mjpeg.htm, 就會定時從伺服器下載  jpeg 檔, 再繪製貼圖到 canvas, 這就是一個超簡單又名符其實的 motion jpeg 伺服器:
class MjpegViewer {
    constructor(fps, width, height) {
      this._fps     = fps;
      this._running = false;
      this._canvas  = document.createElement('canvas');
      this._canvas.width  = width;
      this._canvas.height = height;
      this._canvas.style.setProperty('border', '1px solid #000000');
      document.body.append(this._canvas);
    }
    get _duration( ){ return  0 < this._fps && this._fps < 32 ? 1000/this._fps : 100 }
    get _context( ) { return this._canvas.getContext('2d')  }    
    get stop( ) { this._running = false  }
    set host(_ip)  {
        var _counter  = 0;
        this._running = true;
        const _period = this._duration;
        const _draw = (img)=> {  this._context.drawImage(img, 0, 0) }
        const _periodRun = ( )=> {
            if(! this._running) return;
            const _img = document.createElement('img');
            _img.onerror = ( )=> { _img.remove( )  }
            _img.onload = (_)=> {
                _counter ++;
                _draw(_.target);      
                _img.remove();
            };
            _img.src = _ip + '/mjpeg' + (_counter);
            setTimeout(_periodRun, _period);
        }; 
        _periodRun( ); // fire the process
    }
};
const  viewer  = new MjpegViewer(1, 1024, 768) // fps = 1 frame/sec, canvas: 1024x768
viewer.host = 'http://192.168.0.1:8080';
setTimeout( ( )=>{ viewer.stop } , 10000);

2019年10月13日 星期日

Flutter 使用 PlatformView


vscode 開啟終端先執行以下 1 ~ 3  命令, 確認範例程式上傳至手機並執行的:
1. flutter create -i swift -a kotlin textview
2. cd textview
3. flutter run
接著 vscode 開啟 textview 專案夾, 修改以下檔案:
1. 開啟檔案 lib/main.dart, 將以下程式碼複製貼上:
import 'package:flutter/material.dart';
import 'dart:async';
import 'package:flutter/services.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
  @override build(BuildContext context) => MaterialApp(
     theme: ThemeData.dark(),
     home : NdoWidgetExample('native android widget!')
  );
}
class ViewController {
  final MethodChannel _channel;
  ViewController(int id): _channel = MethodChannel('_channel$id');  
  Future<void> setText(String text) async => await _channel.invokeMethod('setText', text);
}
typedef void NdoWidgetInit(ViewController _);// function type
class NdoWidgetExample extends StatelessWidget {
  final NdoWidgetInit _init;
  NdoWidgetExample._(this._init); 
  factory NdoWidgetExample(String text) => NdoWidgetExample._(
      (ViewController _) => _.setText(text)

  );
  @override build(BuildContext context)  => Scaffold(
    appBar: AppBar(title: Text('Android TextView')),
    body  : Column(children: [Text("Flutter!"), Container(
                width: 320.0,
                height:240.0,
                color: Colors.grey,
                child: AndroidView(
                    viewType: 'ndoView',
                    onPlatformViewCreated: (int id) => _init(ViewController(id))
                )
            )])
  );
}

2.開啟檔案 android/app/src/main/kotlin/com/example/textview/MainActivity.kt, 將以下程式碼複製貼上:
package com.example.textview;
import android.os.Bundle
import io.flutter.app.FlutterActivity
import io.flutter.plugins.GeneratedPluginRegistrant
import io.flutter.plugin.common.PluginRegistry.Registrar
class MainActivity: FlutterActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        GeneratedPluginRegistrant.registerWith(this)
        registrarFor("com.example.textview").apply {  

          val ndoView = Factory_NdoWidget(messenger())
          platformViewRegistry().registerViewFactory("ndoView",ndoView)        
       }
    }       
}

3. 新增 android/app/src/main/kotlin/com/example/textview/TextViewPlugin.kt, 將以下程式碼複製貼上:
package com.example.textview
import io.flutter.plugin.common.BinaryMessenger
import io.flutter.plugin.common.StandardMessageCodec
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.platform.PlatformView
import io.flutter.plugin.platform.PlatformViewFactory
import android.widget.TextView
import android.content.Context
class Factory_NdoWidget(private val messenger: BinaryMessenger)
: PlatformViewFactory(StandardMessageCodec.INSTANCE) {
    override fun create(context: Context, id: Int, o: Any?)
    = _NdoWidget(context, id, messenger)
}
class _NdoWidget(context: Context, id: Int, messenger: BinaryMessenger)
: PlatformView, MethodCallHandler { // interface to implement
    private val _channel = MethodChannel(messenger, "_channel$id")
    private val _view    = TextView(context)
    init {  _channel.setMethodCallHandler(this) }
    override fun getView() = _view

    override fun dispose() {   }
    override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
        when (call.method) {
            "setText" -> {
                _view.setText(call.arguments as String);
                result.success(null)
            }
            else -> result.notImplemented()
        }
    }
}
4.重新編譯並上傳到手機執行

2019年10月8日 星期二

Flutter 與原生插件溝通

Flutter 與原生插件主要是透過通道來溝通,除了 Flutter 官網上所展示的 MethodChannel 外, 還有 EventChannel 及 MessageChannel.  他們之間的最大不同點是: EventChanel 僅能單向從原生插件端往 Flutter 平台推送資料流, 也就是說原生插件是主動發送端, Fultter 處於被動的接收資料流, 而 MethodChannel 主動權是在 Flutter 端, 原生插件處在被動回應的角色, 嚴格來說是半雙工通道(需求發送完要等待回應), 至於 MessageChannel, 視情況兩端都能各自指定為主動端或是被動端, 是一個全雙工的通道(發送與接收分開同時運作)在互相傳輸資料, 理論上來說 MessageChannel 是可以取代上述兩種通道的, 只是運作介面之不同而已.通常要馬上回應的溝通管道直接使用 MethodChannel, 需長時間的回應就就交由 EventChannel, 比方說錄影時要馬上回應機器狀態, 就直接通過 MethodChannele 溝通與控制, 當錄影開始到結束需要花費較長時間,不可能讓程序長時間等待,就可以另闢 EventChannel 管道,後續用廣播方式作通知. 用 EventChannl 與 MethodChannel 就能滿足大部份文字訊息溝通的需求, 關於 EventChannel 與 MessageChannel 的使用方式,參考範例程式:
 // 原生 flutter plugin 端 kotlin 原始碼
    import android.os.Bundle
    import io.flutter.app.FlutterActivity
    import io.flutter.plugin.common.StringCodec
    import io.flutter.plugin.common.MethodChannel
    import io.flutter.plugin.common.EventChannel
    import io.flutter.plugin.common.BasicMessageChannel
    import io.flutter.plugins.GeneratedPluginRegistrant    // ...   
    var stream : EventChannel.EventSink? = null
    EventChannel(flutterView , "com.example.event").setStreamHandler(//事件通道註冊
        object:  EventChannel.StreamHandler { // 繼承 StreamHandler 並實現
                      override fun onCancel(arguments: Any?) { stream = null  }
                      override fun onListen(arguments: Any?, sink: EventChannel.EventSink?) {
                            stream = sink
                            stream?.success("init complete")   // 資料流非同步發送一個事件
                     }
    })
    //... async send event
    stream?.success("event !") // 資料流非同步再發送一個事件
    val msg  = BasicMessageChannel<String>(flutterView, "channel", StringCodec.INSTANCE)//簡單訊息通道註冊
    msg.send("Hello!") { reply: String? -> println(reply); } // 簡單訊息通道發送一則訊息  
    msg.setMessageHandler { req, reply ->  reply.reply("$req: plugin")  }// 同時能回應訊息
    // ...

// Flutter 端 dart 原始碼
   final _event  =  EventChannel('com.example.event'); //事件通道註冊
   _event.receiveBroadcastStream().listen((onData) {  //監聽資料流所發生的事件
         print('$onData');
   });
   final _msg = BasicMessageChannel<String>('channel', StringCodec());//簡單訊息通道   
  _msg.setMessageHandler( (String onData) async { // 簡單訊息通道等待接收訊息  
        print('receive: $onData');
        return 'hi';  // 回應資料
    });
  _msg.send('who').then(( x) =>  print('answer:$x')); // 簡單訊息通道同時能傳送訊息
總結來說, EventChannel 專用於插件的廣播頻道, MethodChannel 專用於插件的回應需求通道, 而 MessageChannel 則是一個通用的全雙工溝通管道.
使用 BinaryMessenger 通道傳輸, 參考文章:
        https://medium.com/flutter/flutter-platform-channels-ce7f540a104e
底下使用 android 端的 Kotlin 原始碼:  android platform -> flutter 溝通:
    import android.os.Bundle
    import io.flutter.app.FlutterActivity
    io.flutter.plugin.common.BinaryMessenger
    import io.flutter.plugin.common.StringCodec
    import java.nio.ByteBuffer
    import java.nio.ByteOrder
    // import io.flutter.plugin.common.BinaryCodec
    // import io.flutter.plugin.common.StandardMessageCodec
    // 若要接收監聽處理範例:
    flutterView.setMessageHandler("binary")  { buffer: ByteBuffer?,  reply ->    
      buffer?.order(ByteOrder.nativeOrder())      
      buffer?.putDouble(3.1415)
      buffer?.putInt(123456789)
      reply.reply(buffer)
    }
   // 若要傳送並等待處理範例
    val buffer = ByteBuffer.allocateDirect(12)
    buffer.putDouble(3.1415)
    buffer.putInt(123456789)  
    flutterView.send("binary", buffer) { _ -> } // 忽略回傳值
    // {  x: ByteBuffer? -> val y =  BinaryCodec.INSTANCE.decodeMessage(x) }//處理回傳值

底下使用 Flutter 端的 dart 原始碼:  flutter  -> android platform 溝通:
    // 若要接收監聽處理範例:
    BinaryMessages.setMessageHandler('binary', (ByteData buffer) async {
      final readBuffer = ReadBuffer(buffer);
      final x = readBuffer.getFloat64( );
      final n = readBuffer.getInt32( );
      print('Received $x and $n');
      return null;
    });
    // 若要傳送並等待處理範例
    final writeBuffer = WriteBuffer( ) ..
                      putFloat64(3.1415) ..
                      putInt32(12345678);
    final buffer = writeBuffer.done();
    await BinaryMessages.send('binary', buffer);
上述 BinaryMessages 通道若是透過 StringCodec( ), 就能用字串加以溝通:
    // 若要接收監聽處理範例:
    final codec = StringCodec( );
    BinaryMessages.setMessageHandler('binary', (ByteData buffer) async {
      print('${codec.decodeMessage(buffer)}');
      return codec.encodeMessage('Hi');
    });
    // 若要傳送並等待處理範例:
    final reply = await BinaryMessages.send('binary', codec.encodeMessage('Hello'))
    print(codec.decodeMessage(reply));

2019年10月2日 星期三

Flutter 透過高階函式(high orfer function)溝通 Widget

當參數帶有迴調函式時稱為高階函式,  StatefulWidget 與下層 Widget 溝通就可透過建構式傳遞高階函式方式當參數, 架起溝通管道, 底下 MyButton 雖然是一個 StatelessWidget 創造的 widget, 透過技巧可讓它看起來多彩多姿, 範例程式:
import 'package:flutter/material.dart';
void main() => runApp(MyApp( ));
class MyApp extends StatelessWidget {
  @override build(BuildContext context) => MaterialApp(
      theme : ThemeData.dark(),
      home  : MyHomePage()
  );
}
class MyHomePage extends StatefulWidget {
   @override  createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
  final colors = <Color>[Colors.red, Colors.orange, Colors.yellow, Colors.green, Colors.blue, Colors.cyan,Colors.purple];
  String  title = '按下按鍵更改標題';
  int     index = 0;  // 顏色索引
  Color color( ) { // callback function to send color
    index = (index +1) % colors.length;
    return colors[index];
  }
  void changeTitle(String _) {  // callback function to change title
    setState( ( ) => title =_ ); // setState to rebuild Scaffold widget
  }
  @override build(BuildContext context) => Scaffold(
      appBar: AppBar(title: Center(child:Text(title))),
      body  : Center(child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[MyButton(color, changeTitle)],
      ))
  ); 
}
class MyButton extends StatelessWidget {
  final Color Function( ) colorIn;
  final void Function(String) titleOut;
  MyButton(this.colorIn, this.titleOut);
  @override build(BuildContext context) => FlatButton.icon(
      onPressed : ( ) => titleOut('這是新標題'),
      color     : colorIn(), // get color
      label     : Text('按下按鍵更改顏色'), 
      icon      : Icon(Icons.color_lens)
  ); 
}

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

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