2021年4月24日 星期六

Android 透過網路抓圖秀在螢幕上

先修改 app/src/main/AndroidManifest.xml 檔案內容:

 <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.urlopen">
    <uses-permission android:name="android.permission.INTERNET" />
    <application
        android:networkSecurityConfig="@xml/network_security"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.UrlOpen">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

在 res 目錄底下新建一個 xml 目錄( app/src/main/res/xml), 裏面放入 netwirk_security.xml 檔案內容, 以便信認所屬網域 192.168.0.1:

 <?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <domain-config cleartextTrafficPermitted="true">
        <domain includeSubdomains="true">192.168.0.1</domain>
    </domain-config>
</network-security-config

修改 app/src/main/res/layout/activity_main.xml, 加入 android:id="@+id/...",  檔案內容:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:id="@+id/mainLayout"
    tools:context=".MainActivity">
    <TextView
        android:id="@+id/sample_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

主程式 app/src/main/java/com/example/urlopen/MainActivity.kt  檔案內容:

package com.example.urlopen
import android.graphics.BitmapFactory
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.constraintlayout.widget.ConstraintLayout
import java.net.URL
import java.util.concurrent.Executors
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val mainContext = this
        (findViewById(R.id.mainLayout) as ConstraintLayout) ?.apply {
            val imageView = ImageView(mainContext).apply {
                setLayoutParams(
                    ConstraintLayout.LayoutParams(
                            ViewGroup.LayoutParams.MATCH_PARENT,
                            ViewGroup.LayoutParams.WRAP_CONTENT
                    )
                )
            }
            addView(imageView)
            val url = "http://192.168.0.1/snap.jpg"
            val handler = Handler(Looper.getMainLooper())
            val getJPG = object : Runnable {
                override fun run() {
                  Executors.newSingleThreadExecutor() ?.execute {// doInBackground
                    try {
                        val stream = URL(url).openStream()
                        val jpg = BitmapFactory.decodeStream(stream)
                        handler.post { // onPostExecute
                            imageView.setImageBitmap(jpg)                          
                        }
                    } catch (e: Exception) {
                        Log.e("JNI", "network get fail!")
                    }
                  }
                }
            }
            getJPG.run()
        }
        //...
    }
}


2021年4月20日 星期二

在 Linux 系統上安裝 AndroidStudio 並加入 OpenCV 程式庫

先將 AndroidStudio 安裝完, 並下載 OpenCV 準備好:

1. 上 AndroidStudio 官網 https://developer.android.com/studio  下載壓縮包, 打開終端機, 進到 Downloads 目錄內, 解壓縮後進行安裝:

      cd Downloads

      tar  -xvf  android-studio-ide-201.7199119-linux.tar.gz

      cd  android-studio/bin &&  ./studio.sh

2. 執行 AndroidStudio (cd /home/mint/Downloads/android-studio/bin && ./studio.sh), 開啟新專案 -> 選擇 Native C++ -> 點選 Next -> Name 填入專案名稱  -> Finish, 當執行編譯時會要求安裝 NDK 編譯 c++  原始碼, 點選同意進行 NDK 的安裝

3. 再上 OpenCV 官網   https://opencv.org/releases/  點選 Android 下載 OpenCV SDK 壓縮包, 接著開啟終端機在 Downloads 目錄內解壓縮 :

     cd Downloads 

     unzip opencv-4.5.2-android-sdk.zip

執行 AndroidStudio, 打開上述 c++ 的專案, 透過 JNI 玩 OpenCV on android, 如果要使用 Java 版 openCV, 首先在Gradle Scripts 的頁籤 settings.gradle 增加以下內容:

     def opencvsdk='/home/mint/Downloads/OpenCV-android-sdk'    
     include ':opencv'
     project(':opencv').projectDir = new File(opencvsdk + '/sdk')

確認上述 opencvsdk 目錄正確, 改完後像這樣存檔 :

     def opencvsdk='/home/mint/Downloads/OpenCV-android-sdk'
     include ':app'
     include ':opencv'
     project(':opencv').projectDir = new File(opencvsdk + '/sdk')
     rootProject.name = "cvEx1"

點選 Sync-Now  同步更新 Gradle, 讓設定生效, 接著打開 Gradle Scripts 的頁籤 build.gradle(Module: ...) , 檔案內容拉到最後的地方, 加入內容像這樣:

     dependencies {

               implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
               implementation 'androidx.core:core-ktx:1.3.2'
               implementation 'androidx.appcompat:appcompat:1.2.0'
               implementation 'com.google.android.material:material:1.3.0'
               implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
               testImplementation 'junit:junit:4.+'
               androidTestImplementation 'androidx.test.ext:junit:1.1.2'
               androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
               implementation project(":opencv")

    }

同步後就會自動下載程式庫, 再將主程式 MainActivity.kt 稍作修改, 載入函式庫之後, 就能呼叫 OpenCV 類型的函數:

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.TextView
import org.opencv.android.OpenCVLoader
import org.opencv.core.Mat
import org.opencv.core.CvType.CV_32F

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val txt = findViewById<TextView>(R.id.sample_text)
        if(!OpenCVLoader.initDebug()) return
        val jMat = Mat(5, 5, CV_32F)
       
txt.setText("${stringFromJNI()}, jMat: ${jMat.rows()} * ${jMat.cols()} sucessful")
    }
    external fun stringFromJNI(): String
    companion object {
        init {
            System.loadLibrary("native-lib")
        }
    }
}

重新編譯上述專案, 並上傳到手機, 測試看是否可以成功加載 openCV 的 Java 程式庫.

上述是在 kotlin(Java)語言中呼叫 Java 版 OpenCV 函數, 下一步要在原生 c++ (native-lib)程式碼中直接呼叫 OpenCV 函數, 必須修改 app/cpp/CMakeLists.txt (實際的檔案位置是 app/src/main/cpp/CMakeLists.txt),引入標頭檔及程式庫,將以下紅色內容插入 CMakeLists.txt, 像這樣:

    cmake_minimum_required(VERSION 3.10.2)
    project("cvex1")
    set(OpenCV_DIR  /home/mint/Downloads/OpenCV-android-sdk/sdk/native/jni)
    find_package(OpenCV  REQUIRED)

    include_directories(${OpenCV_INCLUDE_DIRS})

    add_library(native-lib SHARED native-lib.cpp)
    find_library(log-lib log)
    target_link_libraries(native-lib   -ljnigraphics   ${OpenCV_LIBS}  ${log-lib})

接著修改 app/cpp/native-lib.cpp (實際的檔案位置是 app/src/main/cpp/native-lib.cpp),內容像這樣:

#include <jni.h>
#include <string>
#include <opencv2/highgui/highgui.hpp>
using namespace cv;
using namespace std;

extern "C" JNIEXPORT jstring JNICALL
Java_com_example_cvex1_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject /* this */) {
    Mat cMat(200, 320, CV_8UC3);
    char buffer[256];
    sprintf(buffer, "Hello C++ cMat:%d * %d", cMat.rows, cMat.cols);
    return env->NewStringUTF(buffer);

}

重新編譯上述專案, 並上傳到手機, 測試看是否可以成功連結 C++ 的程式庫. 到此已萬事俱備.

後記: 若只想在 native-lib 內使用 openCV 函式庫處理影像甚至在作畫, 只要修改 CMakeLists.txt 及 native-lib.cpp 就夠了, 前面所說的設定 settings.gradle 就不用了, 在 build.gradle 內添加的 opencv 程式庫: dependencies { ...  implementation project(":opencv" } 也要跟著移除, 要是將兩個程式庫(C++版 及 JAVA 版)都收編進 Android app程式內, 似乎也可以, 但會有警告的訊息. 也不知道會有何副作用? 底下是一個直接在 bitmap (ARGB_8888 格式)作畫的程式碼 (native-lib.cpp):

#include <jni.h>
#include <string>
#include "opencv2/objdetect.hpp"
#include <opencv2/imgproc.hpp>
#include <opencv2/core/types_c.h>
#include <opencv2/highgui/highgui.hpp>
#include <chrono>
#include <android/bitmap.h>
using namespace cv;
using namespace std;
using namespace std::chrono;
int fps( ) {
    static auto lastTime = system_clock::now( ); // .time_since_epoch();
    static long nSxcount = 0;
    static int frames = 0;
    static int _fps = 0;
    auto currentTime = system_clock::now( );
    nSxcount += nanoseconds(system_clock::now( ) - lastTime).count( );// 累積時間差nS
    if (++ frames == 10) {// 累積10張, 算一次 fps, 並且避免數字跳動
        _fps  = 1e10/(nSxcount + 5);// 4捨5入, 並且避免除0錯誤
        frames = 0;
        nSxcount = 0;
    }
    lastTime = currentTime;// to be used next time
    return _fps;
}
extern "C" JNIEXPORT jstring JNICALL Java_com_example_cvex1_MainActivity_fpsdraw(
        JNIEnv *env,
        jobject _this,
        jstring appPath,
        jobject bitmap = NULL
    ) {
    static int id = 0;
    char buffer[1024];
    buffer[0] = 0;
    if (bitmap != NULL) {
        AndroidBitmapInfo info;
        AndroidBitmap_getInfo(env, bitmap, &info);
        if (info.format == ANDROID_BITMAP_FORMAT_RGBA_8888) {
            void *array;
            AndroidBitmap_lockPixels(env, bitmap, &array);
            Mat mat(info.height, info.width, CV_8UC4, array);
            sprintf(buffer, "fps=%3d
run@%02d", fps( ), (id ++)%100);
            putText(mat, String(buffer), Point(0, 100), 1, 4.0, Scalar(0, 255, 0));// 4x,Green color

            AndroidBitmap_unlockPixels(env, bitmap);
        }
    }
    return env->NewStringUTF(buffer);
}

主程式 MainActivity.kt:

package com.example.cvex1
import android.graphics.Bitmap
import android.graphics.Color
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.os.Handler
import android.widget.ImageView
import android.widget.TextView

class MainActivity : AppCompatActivity( ) {
    private external fun fpsdraw(pathDirectory: String, bitmap: Bitmap?): String
    private lateinit var textView: TextView
    private lateinit var imageView: ImageView
    private lateinit var bitmap:Bitmap
   
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        if(savedInstanceState == null) {
            imageView = findViewById<ImageView>(R.id.imageView)
            textView = findViewById<TextView>(R.id.sample_text) .apply {
                setTextColor(Color.RED)
                textSize = 20f
            }
            bitmap = Bitmap.createBitmap(720, 720, Bitmap.Config.ARGB_8888)// w720xh720
        }
    }   
    private fun setBitmap( ) {
        bitmap.apply {// this is bitmap
            eraseColor(0) // fill bitmap with black color
            textView.text = fpsdraw(filesDir.path , this) // textView follows bitmap update
            imageView.setImageBitmap(this) // imageView update
            Handler(mainLooper).post { setBitmap( ) }// loop this function again
        }
    }
    override fun onResume( ) {
        super.onResume( )
        setBitmap( )
    }   
    companion object {
        init { System.loadLibrary("native-lib") }
    }
}

主視窗外觀檔 activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app  ="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width     ="match_parent"
    android:layout_height    ="match_parent"
    android:background       ="@color/black"
    tools:context=".MainActivity">
    <ImageView
        android:id ="@+id/imageView"
        android:layout_width ="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintLeft_toLeftOf  ="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf ="parent"/>
    <TextView
        android:id ="@+id/sample_text"
        android:layout_width ="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintLeft_toLeftOf  ="parent"
        app:layout_constraintTop_toBottomOf ="@+id/imageView"/>
</androidx.constraintlayout.widget.ConstraintLayout>

編譯完上傳到 Android 手機, 每秒更新速度可以達到將近 60 張(fps)

後記: 一吐不快超級無敵慢的 Android Studio

1. 安裝 Android Studio 要用 SSD (固態硬碟), 否則啟動時將會慢到無法忍受, 改用 SSD 就能感受到它所帶來的快感, 估計可能是 gradle 運行時, 硬碟中太多檔案需要搜索,導致花費太多時間.

2. 隨機記憶體(DRAM)最好加裝到 8G 以上, 16G 則綽綽有餘, 4G 勉強可以運行.

3. 若只有 4G 的記憶體, 可以考慮不執行 Android Studio 來節省記憶體之消耗, 直接在命令列用 gradlew 來編譯就可. 若非得用 Android Studio, 那就不能再開啟其它視窗執行程式. 在 linux 系統上可以啟用 swap partition (記憶體交換分區)來增加執行空間, 但相對的執行速度就會慢很多.

4. 我用 Android Studio 唯一的時機是用它校正語法輸入錯誤, 他的編輯方式對習慣用 vscode 的我來說, 最大敗筆是動不動跳出一些莫名其妙的提示, 就像正在開車時,旁邊副駕駛在旁邊嘮叨,讓你無奈到快要抓狂.

2021年4月15日 星期四

在 linux 系統上接收 multicast 訊息的小程式

接收 multicast 位址  = 239.255.255.250, udp port = 1900 範例程式 mserver.c:

#include <arpa/inet.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <signal.h>
int main( ) {
    static int fd = socket(AF_INET, SOCK_DGRAM, 0);// get file descriptor by opening udp
    if (fd < 0) return 1;// error

    signal(SIGINT, [ ](int sig) {// ctrl-C handler
        close(fd);
        printf("!!! to exit\n");
        exit(1);
    });

    int value = 1;// set option SO_REUSEADDR, value type must be int!
    if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &value, sizeof(value)) == 0) {
        
        sockaddr_in addr = {
            .sin_family = AF_INET,
            .sin_port   = htons(1900),// udp port 1900
            .sin_addr   = {.s_addr = inet_addr("239.255.255.250")},// multicast only, or htonl(INADDR_ANY);
            .sin_zero   = {0}
        };
        ip_mreq req = {
            .imr_multiaddr = {.s_addr = addr.sin_addr},
            .imr_interface = {.s_addr = htonl(INADDR_ANY)} // any of local IF
        };
        sockaddr  *addrPtr = (sockaddr *)&addr;// cast to sockaddr pointer
        socklen_t addrSize = sizeof(addr);// cast to socklen_t

        if (setsockopt(fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &req, sizeof(req)) == 0 &&
            bind(fd, addrPtr, addrSize) == 0) {
                
            char buffer[256];
            int len = 0;
            do {// to receive message
                len = recv(fd, buffer, 255, 0);
                if (len > 0) {
                    buffer[len] = 0;
                    printf("%s\n", buffer);   
                }
            } while (len >= 0);
        }
    }
    close(fd);
    return 0;
}

編譯及執行:

   g++ mserver.c -o mserver && ./server 

傳送 multicast 位址  = 239.255.255.250, udp port = 1900 範例程式 mclient.c:

#include <arpa/inet.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <signal.h>
int main( ) {
    static int fd = socket(AF_INET, SOCK_DGRAM, 0);// udp
    if (fd < 0) return 1;

    signal(SIGINT, [ ](int sig) {// ctrl-C handler
        close(fd);
        printf("!!! to exit\n");
        exit(1);
    });

    sockaddr_in addr = {
        .sin_family = AF_INET,
        .sin_port   = htons(1900),// udp port 1900
        .sin_addr   = {.s_addr = inet_addr("239.255.255.250")},
        .sin_zero   = {0}
    };
    sockaddr  *addrPtr = (sockaddr *)&addr;// cast to sockaddr pointer
    socklen_t addrSize = sizeof(addr);// cast to socklen_t 

    auto mcastSend = [&addrPtr, &addrSize](const char *msg) {
        printf("%s %d\n", msg, fd);
        return sendto(fd, msg, strlen(msg), 0, addrPtr, addrSize);
    };
    while (mcastSend("hello") >= 0) sleep(1);// wait 1 second to send again
    return 0;
}

開啟另一終端機, 編譯並執行:

   g++ mclient.c -o mclient && ./mclient

2021年4月13日 星期二

理解數位正旋波(digital sin wave)產生器的原理

要將類比電壓轉換成數位電壓(ADC), 有個很簡單的方式是透過比較器輸入一個高頻率的三角波當基準電壓,當輸入的電壓大於三角波電壓時輸出 V+(1), 若小於則輸出 V-(0), 產生的數位(1,0,...)電壓波形也就是俗稱的 PWM(pulse width modulation),要將上述數位電壓轉回類比電壓(DAC), 一個非常經濟的方式是透過低通濾波器(RC 或 LC)將數位電壓轉回類比訊號. 因此一個微處理機(uP)可以很容易經由 PWM 方式外接一個低通濾波器就能產生所需要的 sin 波,而且三角波函數輕而易舉用查表(lookup table)方式就能得到準位 y(n), 最後將比較 結果 sin(n) >  y(n)  ? 輸出到 GPIO ,  當  GPIO 接上低通濾波器後就能產生完美的 sin 波, 當 PWM 的頻率越高, 所能產生的 sin 波的頻率就能越高, 而且產生的誤差也會愈小, 參考資料:

1. https://www.renesas.com/us/en/document/apn/pwm-sine-wave-generation-sinewave?language=en

2. https://www.ti.com/lit/an/spna217/spna217.pdf?ts=1618361816463&ref_url=https%253A%252F%252Fwww.google.com.tw%252F

2021年3月10日 星期三

實現一個簡單的數位 PID, 利用 opencv 程式庫繪出控制曲線

// g++ pid.c `pkg-config --cflags --libs opencv4` && ./a.out

#include <opencv2/imgproc.hpp>
#include <opencv2/highgui.hpp>
using namespace cv;
class DPID { // discrete PID
    private:
        double Kp, Ti, Td, dt;// PID parameters
        double et_1 = 0.0;// initial e[t-1]
        double et_2 = 0.0;// initial e[t-2]
        double ut_1 = 0.0;// initial u[t-1]
    public:
        double operator( )(const double target) {            
            auto et = target - ut_1;
            auto err = (1 + dt/Ti + Td/dt)*et - (1 + 2*Td/dt)*et_1 + (Td/dt)*et_2;           
            // auto err = (1 + dt/(2*Ti) + Td/dt)*et - (1 - dt/(2*Ti) + 2*Td/dt)*et_1 + (Td/dt)*et_2;
            auto ut = ut_1 + Kp * err;
            ut_1 = ut < 0 ? 0 : ut > 1000 ? 1000 : ut; // update output
            et_2 = et_1;
            et_1 = et;
            return ut_1;
        }
        DPID(double Ku = 1, double fMax = 1e8)  {   
            dt  = 1.0/(2*fMax);
            Ti  = dt < 1e-9 ? 1e-9 : dt;
            Td = dt/4;
            Kp = Ku;
        }
};
int main( ) {
    const int pixels = 800;
    const double target = 400.0;
    const double yX2 = 2 * target;// 2x target
    auto map2D = [&yX2](Point p) {// 線性座標轉換
        return Point(p.x, yX2 - p.y);
    };
    Mat mat2D(Size(pixels, yX2), CV_8UC3);
    auto figure = [&mat2D]( ) {
        static String name = "DPID";
        imshow(name, mat2D);
        return name.c_str( );
    };
    auto line2D = [&mat2D, &map2D](Point p1, Point p2) {
        static auto green = Scalar(0, 255, 0);
        line(mat2D, map2D(p1), map2D(p2), green);
    };
    Point last;// the last point
    Scalar red(0, 0, 255);
    Scalar black(0, 0, 0);
    // printf("       t,       y\n");// csv
    for(double k=0.1; k<=0.5; k+=0.01) {
        mat2D.setTo(black);
        line(mat2D, Point(0, 0), Point(pixels, 0)  , red, 2, FILLED);
        line(mat2D, Point(0, 0), Point(0, yX2)     , red, 2, FILLED);
        line(mat2D, Point(0, yX2), Point(pixels, yX2), red, 2, FILLED);
        line(mat2D, Point(pixels, 0), Point(pixels, yX2), red, 2, FILLED);
        auto pid = DPID(k);// foreground
        for (int t = 0; t < pixels; t ++) {
            auto y = pid(target);
            // printf("%8d,%8.2f\n", t, y);
            auto point = Point(t, y);
            if (t > 0) line2D(last, point);
            last = point;
        }
        figure( );
        if(waitKey(50) > 0) break;
    }
    return waitKey(0);
}

2021年2月26日 星期五

linux c++ 掃描按鍵, 透過 UDP 遙控裝置

#include <sys/time.h> // struct timeval
#include <termios.h>  // struct termios
#include <unistd.h>   // close
#include <stdio.h>    // printf
#include <string.h>   // memset
#include <stdlib.h>   // atoi
#include <netdb.h>    // gethostbyname
#include <arpa/inet.h>// inet_ntoa
#include <signal.h>  // signal

class DeltaPWM {
  private:
    unsigned char value, click;
  public:
    void reset() {// reset to default value
      click  = 0;
      value  = 1;
    }
    auto operator ()(int key = 0) {
      if (key > 0) {
        if (key != click) { // key changes
          value = 1;// begin from 1
          click = key;// keep to compare next time
        } else if (value < 10) value ++;// same key pressed
      }// saturat at 10, increase nonlinearly
      return value;
    };
    DeltaPWM() { reset(); }
};

int main(void) {
  const char *hostname = "myDecive.local";// remote target
  const int udpPort = 8888;// service port number
  signal(SIGINT, [](int sig) {
    printf("CTRL-C\n");  
  });

  printf("Waiting: %s...\n", hostname);
  struct hostent *host = gethostbyname(hostname);
  if (host == NULL) {
    printf("%s not known\n", hostname);
    return 1; // NS lookup fail, nothing to do
  }
  int udpFD = socket(AF_INET, SOCK_DGRAM, 0);
  if (udpFD < 0) return 2; // socket init fail, nothing to do
    
  printf("'q' or double click 'ESC' to break, Arrow keys to remote control.\n");
  struct sockaddr_in udpAddr; // allocate a struct sockaddr_in from stack
  memset(&udpAddr, 0, sizeof(udpAddr));// empty udpAddr
  udpAddr.sin_family = AF_INET;
  bcopy(host->h_addr_list[0], (caddr_t) &udpAddr.sin_addr, host->h_length);
  // udpAddr.sin_addr.s_addr = inet_addr(inet_ntoa(*(struct in_addr *) host->h_addr));
  udpAddr.sin_port = htons(udpPort);
  auto udpSend = [&](const char *msg) {
      int len = strlen(msg);
      if(len > 0) sendto(udpFD, msg, len, 0,
          (const struct sockaddr *) &udpAddr,
           sizeof(struct sockaddr_in)
      );              
  };
 
  static char keyBuffer[1024]; // share keyBuffer
  static auto parseKey = [](const char *key, int *value) -> bool {
    static char valBuffer[32];
    if (keyBuffer[0] == 0 || ! key) return false;// scan fail
    char *findKey = strstr(keyBuffer, key);
    if (findKey) {
      char *rewind = findKey;
      while(rewind > keyBuffer) {// check if not comment
        rewind --;
        char &ar = *rewind;
        if (ar == '\n' || ar == '\r' || ar == ',' || ar == '#') break;
      }
      if(*rewind != '#') { // discard comment
        findKey += strlen(key);
        if(*findKey == '=') findKey ++;// skip '=' if provide
        char *dest = valBuffer;
        for(int i = 0; i < 31; i ++) { // tupto 31 characters
          char &af = *findKey ++;
          if (af == 0 || af == '\n' || af == '\r' || af == ' ' || af == ',' || af == '#') break;
          *dest ++ = af;
        }
        *dest = 0;// EOS to split string
        *value = atoi(valBuffer);// convert to integer
        return true;// sucess
      }
    }
    *value = 0;  // value invalid
    return false;// scan fail
  };
      
  struct termios keepTerm;
  tcgetattr(STDIN_FILENO, &keepTerm);
  auto term = keepTerm;// duplicate
  term.c_lflag &= ~(ICANON | ECHO); // mask for ICANON + ECHO
  tcsetattr(STDIN_FILENO, TCSANOW, &term);// set a new configuration

  DeltaPWM dp, dz, dr;
  int power =  0;// initial power to send
  int yaw   = 16;// initial yaw to send
  int pitch =  0;// initial pitch to send
  int roll  =  0;// initial roll to send
  int maxFD  = udpFD + 1;
  char txtBuffer[1024];
  sprintf(txtBuffer, "power=%d,yaw=%d,pitch=%d,roll=%d", power, yaw, pitch, roll);
  udpSend(txtBuffer);

  while (true) {
    fd_set fdBits;
    FD_ZERO(&fdBits);// reset all bits
    FD_SET(STDIN_FILENO, &fdBits);
    FD_SET(udpFD, &fdBits);
    struct timeval timeout = {.tv_sec = 0, .tv_usec = 40000};// 40 mSec = 0.04 Sec
    int event = select(maxFD, &fdBits, NULL, NULL, &timeout);
    if (event < 0) break; // error event proceed first

    if(FD_ISSET(udpFD, &fdBits)) { // udp received event
      int len = recv(udpFD, keyBuffer, 1023, 0);
        if (len > 0) {
        keyBuffer[len] = 0;
        printf("###:%s\n", keyBuffer);
        int temp = 0;
        if(parseKey("power", &temp)) power = temp;
        if(parseKey("yaw"  , &temp))   yaw = temp;
        if(parseKey("pitch", &temp)) pitch = temp;
        if(parseKey("roll" , &temp))  roll = temp;
        printf("power:%d@%d, yaw:%4d, pitch:%4d@%4d, roll:%4d@%4d\n",
          power, dp(), yaw, pitch, dz(), roll, dr());
      }// udp packet received, update and show current status
    }

    if(FD_ISSET(STDIN_FILENO, &fdBits)) { // user input event
      int ch = getchar();// printf("KEY CODE = %d\n", ch);
      if (ch == 'q') break;// user break by pressing 'q'
      if (ch == 27) {
        ch = getchar();  // printf("ESC KEY= %d\n", ch);
        if(ch == 27) break;// user break by double ESC clicks
        if(ch == 91) {// keypad scan
          ch = getchar();// printf("ARROW= %d\n", ch);
          switch(ch) {
            case 65: // arrow up, to increase power
                power += dp(ch);
                sprintf(txtBuffer, "power=%d", power);
                break;
            case 66: // arrow down, to decrease power
                power -= dp(ch);
                sprintf(txtBuffer, "power=%d", power);
                break;
            case 67:// arrow right,
                yaw ++;
                sprintf(txtBuffer, "yaw=%d", yaw);
                break;
            case 68:// arrow left,
                yaw --;
                sprintf(txtBuffer, "yaw=%d", yaw);
                break;
            case 50: //Ins, to toward right
                roll += dr(ch);
                sprintf(txtBuffer, "roll=%d", roll);
                break;
            case 51: //Del, to toward left
                roll -= dr(ch);
                sprintf(txtBuffer, "roll=%d", roll);
                break;         
            case 53: // pageup to forward
                pitch += dz(ch);
                sprintf(txtBuffer, "pitch=%d", pitch);
                break;
            case 54: // pagedown to backward                  
                pitch -= dz(ch);
                sprintf(txtBuffer, "pitch=%d", pitch);
                break;
                
            case 70: // home
            case 72: // end
            default:
                txtBuffer[0] = 0;// clear txtBuffer
                printf("KEY: %d not defined\n", ch);
                break;
          }// keyCode validate
          if (strlen(txtBuffer) > 0) udpSend(txtBuffer);
        }// remote control
      }// keyCode scan
    }

    if (event == 0) {// timeout event finally
      dp.reset();
      dz.reset();
      dr.reset();
    }
  }// loop until error happen or user break

  udpSend("power0,yaw0,pitch0,roll0");// set all to zero before end
  close(udpFD);// close udp port
  tcsetattr(STDIN_FILENO, TCSANOW, &keepTerm);// restore termial
  return 0;// no error
}

2021年2月5日 星期五

Linux mint 上安裝 opencv c++ 開發環境, 並展示範例程式

先安裝好開發環境所需相關程式庫:

      sudo apt-get  install  libopencv-dev

opencv 文件參考: https://docs.opencv.org/master/d8/dfe/classcv_1_1VideoCapture.html ,儲存下列範例程式碼:

//example.cpp
#include <opencv2/highgui/highgui.hpp>
using namespace cv;
int  main( ) {
    String title = "computerVision";
    printf("Press ESC or Ctrl-C to exit...\n");
    VideoCapture video;
    Mat srcImage;
    while (waitKey(200) != 27) {
        video.open("http://192.168.12.102/snap.jpg");// it will release first, than open
        if(video.isOpened( ) && video.read(srcImage)) {// read from file
            imshow(title, srcImage);
        }
    }
    video.release( );
}

編譯上述程式碼並執行:

      g++  example.cpp   `pkg-config --cflags --libs opencv4` && ./a.out

若是使用 vscode 編寫程式, 需在目錄內先建一個目錄  .vscode, 並於該目錄裡, 將下列內容存成 c_cpp_properties.json 檔案,  這樣 vscode 才能找到相關標頭檔

{
    "configurations": [
        {
            "name": "Linux",
            "includePath": [
                "${workspaceFolder}/**",
                "/usr/include/**",
                "/usr/local/include/**"
            ]
        }
    ],
    "version": 4
}

基本繪圖(矩形, 直線, 圓形, 橢圓形)範例程式: 

// g++ draw.cpp `pkg-config --cflags --libs opencv4`
// draw.cpp
#include <opencv2/core.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/highgui.hpp>
#include <unistd.h>
using namespace cv;

int main(void) {
    int pixels = 512; // pixels in one dimension
    Size size2D(pixels, pixels);// 2 dimension pixels: pixels L*L pixels
    Mat figure2D(size2D, CV_8UC3);// 2 dimension figure
    double fx1 = pixels / 4;// one quarter
    double fx2 = fx1 * 2 ;// double quarter = half
    double fx3 = fx1 * 3 ;// triple quarter

    static int lineThick = 2;
    static int lineType  = FILLED;
    static Scalar whiteColor(255, 255, 255);// B, G, R
    static Scalar blueColor (255,   0,   0);
    static Scalar greenColor(0,   255,   0);
    static Scalar redColor  (0,     0, 255);

    auto drawLine = [&figure2D](Point p1, Point p2) {
        line(figure2D, p1, p2, greenColor, lineThick, lineType);
    };// a lambda to capture an image of Mat and draw a line

    auto drawCircle = [&figure2D](Point c, double r) {
        circle(figure2D, c, r, redColor, lineThick, lineType);
    };// a lambda to capture an image of Mat and draw a circle

    auto drawHalf = [&figure2D](Point c, Size r, double deg){
        ellipse(figure2D, c, r, deg, 0, 180, blueColor, lineThick, lineType);
    };// a lambda to capture an image of Mat and draw a half of ellipse

    auto drawRectangle = [&figure2D](Point p1, Point p2) {
        rectangle(figure2D, p1, p2, whiteColor, lineThick, lineType);
    };// a lambda to capture an image of Mat and draw a rectangle
   
    auto drawShow = [&figure2D]( ){
        static String name = "figure";
        imshow(name, figure2D);
        return name.c_str( );
    };// a lambda to capture an image of Mat and show

    Size radius(fx1, fx2);// size of 2D
    Point center(fx2, fx2);
    drawRectangle(Point(fx1, fx1), Point(fx3, fx3));
    int  key = 0;   
    for (int rotate = 0; rotate < 360; rotate += 45) {
        double theta = rotate * M_PI / 180;// degree to theta
        Point pth(fx2*(1 + cos(theta) / 2), fx2*(1 + sin(theta) / 2));
        drawCircle(pth, 20);
        drawLine(pth, center);
        drawHalf(center, radius, rotate);
        drawShow( );       
        key = waitKey(1000);
        if(key > 0) break; // any key to break
    }
    return key > 0 ? key : waitKey(0);
}

使用已訓練好的資料庫(xml 格式檔)偵測臉部及眼部, 參考資料: https://docs.opencv.org/3.4/db/d28/tutorial_cascade_classifier.html

// g++ a.c `pkg-config --cflags --libs opencv4`
#include "opencv2/objdetect.hpp"
#include <opencv2/imgproc.hpp>
#include <opencv2/core/types_c.h>
#include <opencv2/highgui/highgui.hpp>
using namespace cv;
using namespace std;
 
int  main( ) {
    String faceXMLPath = "/usr/share/opencv4/haarcascades/haarcascade_frontalface_default.xml";
    String eyeXMLPath  = "/usr/share/opencv4/haarcascades/haarcascade_eye.xml";
    CascadeClassifier faceFilter, eyeFilter;
    if (!faceFilter.load(faceXMLPath)){
        printf("%s not exist", faceXMLPath.c_str( ));
        return -1;
    }
    if (!eyeFilter.load(eyeXMLPath)){
        printf("%s not exist", eyeXMLPath.c_str( ));
        return -1;
    }

    //Scalar blueColor= Scalar(255,0  ,0);// B, G, R
    Scalar greenColor = Scalar(0  ,255,0);
    Scalar redColor   = Scalar(0  ,0  ,255);
    Mat colorFrame, grayFrame;
    VideoCapture video;
    String title = "computerVision";
    printf("Press ESC to exit...\n");

    while (waitKey(200) != 27) { // FPS = 1000/200 = 5
        video.open("http://192.168.12.102/snap.jpg");// release first, than open

        if(video.isOpened( ) && video.read(colorFrame)) {// read a jpg
            cvtColor(colorFrame, grayFrame, COLOR_BGR2GRAY);
            equalizeHist(grayFrame, grayFrame);

            vector<Rect> faces;
            faceFilter.detectMultiScale(grayFrame, faces);
            for (size_t i = 0; i < faces.size( ); i++) {
                auto ox = faces[i].x;
                auto oy = faces[i].y;
                Rect edgeFace = cvRect(ox, oy, faces[i].width, faces[i].height);
                rectangle(colorFrame, edgeFace, redColor);

                vector<Rect> eyes;
                Mat roi = grayFrame(faces[i]);
                eyeFilter.detectMultiScale(roi, eyes);
                for (size_t j = 0; j < eyes.size( ); j++)  { 
                    auto x = ox + eyes[j].x;
                    auto y = oy + eyes[j].y;
                    Rect edgeEye = cvRect(x, y, eyes[j].width, eyes[j].height);
                    rectangle(colorFrame, edgeEye, greenColor);
                }
            }

            imshow(title, colorFrame);
            // imshow(title + "-grayFrame", grayFrame);
        }
    }
    video.release( );
    return 0;
}

2021年2月3日 星期三

使用 opencv.js 作簡單的邊緣偵測

 

參考: https://docs.opencv.org/master/d3/de6/tutorial_js_houghlines.html

將以下內容存成 html 檔案,  先抓取 http://192.168.12.102/snap.jpg 檔案後, 再作影像處理範例:

 <html>
    <head><meta charset="utf-8"><title>OpenCV demo</title>
        <script async src="https://docs.opencv.org/3.4.0/opencv.js" onload="onOpenCvReady( );" type="text/javascript"></script>
    </head>
    <body>
        <div><canvas id="computerVision"></canvas></div>
        <script>                       
            async function onOpenCvReady() {
                let result = await fetch("http://192.168.12.102/snap.jpg");
                let jpgURL = URL.createObjectURL(await result.blob());
                let resolv = false;   
                let future = new Promise((ready, _) => resolv=ready);
                let img    = new Image();
                img.onload = () => {
                    resolv && resolv(img);
                    URL.revokeObjectURL(jpgURL);
                    img.remove();
                };
                img.src = jpgURL;
               
                let src = cv.imread(await future);// cv: computer vision
                let dst = new cv.Mat();// an instance of matrix
                cv.cvtColor(src, dst, cv.COLOR_RGBA2GRAY, 0);// color to gray, 轉成灰階
                cv.Canny(dst, dst, 50, 200, 3); // gray to edge, 過濾成邊際線
               
                let lines = new cv.Mat();
                cv.HoughLinesP(dst, lines, 1, Math.PI / 180, 2, 0, 0)// 從邊際線提取線段
                for (let i = 0; i < lines.rows; ++i) {
                    let startPoint = new cv.Point(lines.data32S[i * 4], lines.data32S[i * 4 + 1]);
                    let endPoint = new cv.Point(lines.data32S[i * 4 + 2], lines.data32S[i * 4 + 3]);
                    cv.line(src, startPoint, endPoint, [255, 0, 0, 255]);// 將線段畫在原圖上
                }
                cv.imshow('computerVision', src);// 秀出合成圖
                lines.delete();
                dst.delete();
                src.delete();
            }
        </script>
    </body>
</html>

使用瀏覽器開啟上述檔案就可, 每次都要上網抓 opencv.js 感覺很麻煩, 可以先抓下來存成 opencv.js, 將它放在與 html 檔案同一目錄下, 稍微修改檔案路徑:
        <script async src="opencv.js" onload="onOpenCvReady();" type="text/javascript"></script>



2021年1月25日 星期一

網購的詐術

跨境買中國大陸的貨, 常常會遇到缺件或到貨數量不足的問題, 若是能退款的就還好, 有的商家會要求下次再補, 若是同意下次回購, 無疑助長他的騙術施與另一買家, 這種商家聲譽掃地, 管理不善, 刻意製造下次回購的機會, 真是很要不得的舉動.

2021年1月4日 星期一

安裝 bCNC 用來遙控 Grbl

官網 https://github.com/vlachoudis/bCNC 是一個用  python 語言寫的控制軟體, 須事先安裝 python 的  Tkinter 模組, 之後才能執行 bCNC, 利用 python 的套件管理程式  pip 直接安裝 bCNC 模組:

      sudo apt-get install python-tk
      pip install --upgrade bCNC

接著沒意外的話就能執行 bCNC 模組

      python -m bCNC

打開文件後, 序列阜(Port)可以使用 USB Serial 轉接線, 開啟裝置

     /dev/ttyUSB0

或是 telnet 協定來連接:

     socket://esp8266.local:23

    使用 esp8266 寫一個內建 telnet server 並利用 software Serial port 摹擬出一個序列阜(swSerial)與 ATmega328P 的序列裝置對接(swRX <-> TX, swTX<->RX) , 實作出 telnet  協定後,就能打造出一個無線遙控平台, 底下是用 Arduino 方式寫的 telnet server 原始碼, 所摹擬的 UART 使用 GPIO4(swRX), GPIO5(swTX), 並結合上一篇的 AVRISP :

#include <SoftwareSerial.h>
#include <ESP8266AVRISP.h>
ESP8266AVRISP avrISP(328, 16);// AVRISP: TCP port 328, RESET: GPIO16


WiFiServer telnetServer(23); // Telnet: TCP port 23
WiFiClient telTerm;
SoftwareSerial swSerial;

void setup( ) {
  Serial.begin(115200);

     // ...

      swSerial.begin(19200, SWSERIAL_8N1, 4, 5); //swRX:GPIO4, swTX: GPIO5


      telnetServer.begin( );
      telnetServer.setNoDelay(true);


      avrISP.setReset(false); // let the AVR run
      avrISP.begin( );

      MDNS.begin("esp8266");

}

void loop( ) { 
    MDNS.update( );
    static AVRISPState_t  last_state = AVRISP_STATE_IDLE;
    AVRISPState_t new_state = avrISP.update( );
    if (last_state != new_state) {     
      switch (new_state) {
        case AVRISP_STATE_IDLE: Serial.printf("[AVRISP] idle\n");
            break;         
        case AVRISP_STATE_PENDING:Serial.printf("[AVRISP] pending\n");
            break;         
        case AVRISP_STATE_ACTIVE:
            Serial.printf("[AVRISP] programming\n");
            break;
        default: break;         
      }     
      last_state = new_state;
    } 
    if (last_state != AVRISP_STATE_IDLE) avrISP.serve( ); 

    // ...
    static char txtBuffer[1024];

   if (telnetServer.hasClient( )) {
      Serial.printf("Accept new connection\n");
      if (telTerm.connected( )) telTerm.stop( ); // stop old connection      
      telTerm = telnetServer.available( );
      telTerm.flush( );
      swSerial.flush( );
    }
        
    if (telTerm.connected( )) {
      int length = swSerial.available( );
      if (length > 0) {// swSerial -> telTerm
        for(int n = 0; n < length ; n ++) txtBuffer[n] = swSerial.read( );
        telTerm.write(txtBuffer, length);
      }
      int n = 0;
      length = telTerm.available( );
      while(length -- > 0) {// telTerm -> swSerial
        int ch = telTerm.read( );
        Serial.printf((31 < ch && ch < 127 || ch==0xd || ch==0xa) ? "%c" : "_%02x_", ch);
        if (ch == 0x0d) continue;// drop useless CR
        txtBuffer[n ++] = ch;
        if (n < sizeof(txtBuffer)) {
          txtBuffer[n] = 0;
          if ((strstr(txtBuffer,"\xff\xfb")||strstr(txtBuffer,"\xff\xfd")) && ch > 0) break;// drop
          if (strstr(txtBuffer,"exit!") || strstr(txtBuffer,"quit!")) {
            telTerm.stop( );
            break;
          }
        }       
        if (ch==0x0a || ch=='?' || n>=80) {
          if (ch != 0x0a || n > 1) swSerial.write((const uint8_t *)txtBuffer, n);                 
          break;
        }
      }
  }// bridge packet between termial and swSerial

}

    實驗發現 software uart 當 baud rate 設定 115200 時, 會掉封包或是有誤碼的情形, 改成 19200 後就能改善, 且能順利與 bCNC 連接. 記得同時要更改 grbl/config.h 內的 baud rate 設定(將 #define BAUD_RATE  115200 改成 #define BAUD_RATE  19200), 重新編譯後再用上一篇所說 avrdude 重新燒錄. 這樣就不用放一台 PC 在 GRBL 控制器的旁邊了, 一個內建軔體燒錄器的 GRBL 無線遙控器順利誕生.

    Arduino Uno 燒錄了 GRBL 軔體到主控板後, 接上 CNC shield 板子後就能直接控制步進馬達(可支援四組), CNC shield 上步進馬達驅動器的電源與主控板電源是分開的,  我使用筆電(Notebook)的 19V 直流電直接供應, 為了簡化, 使用單一電源, 同時供應主板電源, 我將 Arduino Uno 上的1117 (LDO, 用來將輸入 Vin 轉 5V 電源) 的輸入腳(第3支腳) 翹開, 也可以直接拔掉, 上網買一個, 大約 10 幾塊台幣而已, 可耐 24V 電壓的降壓型切換式電源模組(Buck Switching  Mode DC to DC regulator) 作替換, 輸出先調好成 DC 5V 才焊接上去, 之後主板就能接上 19V 的電源,最後焊一條粗的電源線將 19V 與 CNC shiled 的步進電源相連就可, 最後弄個轉接板(AVRISP + UART)讓 ESP8266 直接吃 AVRISP 上頭的 5V 電源, 整個電源路徑示意圖:

        DC 19V  Vin    +-> Ardunio Uno -> CNC shiled

                                   |->  Buck  DC to DC -> 5V   -> AVRISP ->  ESP8266 PCB

                                   xxx -> 1117 -> 5V 

    步進馬達的推動力(應該說是扭力)與所供應的電流成正比, CNC shield  可以接上 4 組 A4988 或是 DRV8825 模組 , 輸出電流可以透過模組上面的可變電阻來調整, 以 A4988 來說, 電流調整公式是:

        Imax  = Vref / (8 * Rsense)

    上述 Rsense 是模組上面的精密電阻(共有兩顆, 分別偵測 A/B 線圈電流用), 電阻上壓差(注意: 並不是 Vref)上限是  0.5 V, 而 A4988 最大輸出上限是 2 A(注意: 並不是底下公式的 Imax), 我看模組上 Rsense 用的是 R100, 也就是等於 0.1 Ohm, 因此

        Imax = Vref / 0.8 = 1.25 * Vref   A

    其中規格書上寫到  Vref 可調區間是 0 到 4 V, 也就是 Imax 可調區間是 0 ~ 5 A, 但受 A4988 輸出最大 2 A 的限制, 所以使用 Rsense 等於 0.1 Ohm 的 A4988, 其 Vref 最大也只能調到 1.6 V, 當 Vref 電壓調整到 0.5 V時, 最大驅動電流約 0.625 A, 這還得要看步進馬達是否能承受, 42 步進馬達應該沒什問題,  3 個軸的推動, 除了 Z 軸因為要承受重力負載的關係, 當返回向上時需要比較大的拉力, 網路上 DIY CNC 時常用的是 T8 全牙螺桿來傳動, 有分 2mm 或是 8mm 行程, 兩者比較來說, 8mm 行程的 T8 螺桿傳動時雖跑的比較快, 但因螺紋坡度較陡峭, 相對就需要較大的推/拉力, 而 2mm 行程的 T8 螺桿就顯的較省力, 我除了買了 2mm 與 8mm 行程的 T8 螺桿外, 還另外買了 1.26mm  行程的 M8 全牙螺桿,  用 4 爪 M8 螺帽塞進 11 mm 孔徑的木塊裏, 用來帶動 X 軸, 經實驗相互比較, 整體來說, 傳動正常, 只是 M8 螺桿轉動時, 噪音大了點, 這是摩擦力振動所產生的噪音. 目前 X 軸使用 1.26mm 行程的 M8 螺桿, Y 軸使用 8mm 行程的 T8 螺桿, Z 軸使用 2mm 行程的 T8 螺桿, A4988 全都設定為 16 micro steps, 因此需要修改 grbl/defaults.h 的設定:

  #define DEFAULT_X_STEPS_PER_MM 2540.0 // 16 microsteps, 200 steps per revolution/1.26mm = 2540 usteps/mm
  #define DEFAULT_Y_STEPS_PER_MM  400.0 // 16 microsteps, 200 steps per revolution/8mm = 400 usteps/mm
  #define DEFAULT_Z_STEPS_PER_MM 1600.0 // 16 microsteps, 200 steps per revolution/2mm = 1600 usteps/mm

    有時候行進速率太高或是摩擦力太大導致步進馬達失步, 這時可以調整軸的最大行進速率, 或是干脆換一棵扭力較強的步進馬達,  若是允許,  還能調大電流(A4988 最大只能推到 2 A) , 我目前 Y 軸使用 8mm 行程的關係, 所以使用了強力步進馬達, 速率也就調的高一點, 以下是我目前 grbl/defaults.h 的設定:

  #define DEFAULT_X_MAX_RATE 1000.0
  #define DEFAULT_Y_MAX_RATE 4000.0
  #define DEFAULT_Z_MAX_RATE  250.0

    關於主(旋)軸(spindle)的控制, 可以用 telnet 終端機先下一個命令 $$ , 列出所有設定值, 其中 $30 指示目前最大轉速(rpm)的數值 (內定 default 設定值是 1000 rpm), 對應的是當 PWM  輸出 5V 的數值. 透過 G-CODE 代碼 M3 S1000 設定順時鐘(CW)的轉速為 1000 rpm, 或是 M4  S1000 設定逆時鐘(CCW)的轉速為 1000 rpm, 當設定為 0 rpm 就相當於關閉主軸, 也就是說輸出等於 0V, 用三用電表量測 CNC Shielle 板子上標註 End Stop 的  Z+ 或 Z- (實際上 Z+ 與 Z- 是短路的)排針, 附帶一提的是, Z+ 與 Z- 在舊版的 Grbl 是拿來當作讀取 Z 軸限位開關(Limit  switch)之用, 目前在 Grbl 1.1 版本中, 為了支援 VARIABLE_SPINDLE, 已將讀取 Z 軸限位開關改到標註著 SpnEn 的位置. 改變轉速(S0 ~ S1000) 就可分別量測到 0 ~ 5V 的數值. 因此只要利用 Z+(或 Z-)腳位,  接上電阻及電容濾波器再用電晶體去推動繼電器(relay), 就能當作電源的開(ON)與關(OFF),  達成讓軟體自動控制主軸的目的.

    最後說一下摩擦係數極低的滑塊與光軸, 使用它的目的, 除了承重並維持上下及左右之間的平衡外, 還能減少行進間的摩擦力, 摩擦力相當於法向(垂直)力*摩擦係數. 而螺桿則是專門負責傳動, 兩根光軸除了要保持平行外, 和傳動螺桿間也須同時保持平行, 當鎖上螺絲時更要注意, 光軸與螺桿之間要避免拉力, 否則將會增加行進間的摩擦力, 因此光軸與螺桿定位孔以及滑塊鎖上負載的螺絲孔位置都必須垂直與精準,  這樣滑動時就不會有阻礙, 馬達推動也才不會卡卡的引起很大振動, 甚至造成像是空襲警報的噪音, 尤其是當振動久了, 很容易造成連軸螺絲脫落, 導致傳動軸空轉, 因此有必要用膠帶將連軸螺絲處多纏繞幾圈, 避免它鬆脫.

2020年11月10日 星期二

在 Linux mint 上玩 ATmega328P

先安裝好 binutils-avr  gcc-avr  avr-libc:

      sudo apt-get install binutils-avr  gcc-avr  avr-libc

使用 open source 的 grbl 當範例程式(官網 https://github.com/gnea/grbl/releases), 下載解壓縮後, 進入主目錄並編譯 :

      wget  https://github.com/grbl/grbl/archive/master.zip  && unzip master.zip

      cd grbl-master  && make 

安裝燒程式:

      sudo apt-get install  avrdude

參考資料:https://github.com/esp8266/Arduino/tree/master/libraries/ESP8266AVRISP 

空的 avr uP 必須先透過 ICP (in-circuit programing) 的方式預先燒錄一段內建 ISP(n-System programing)的 bootloader, 之後才能透過 serial port 來更新軔體(Firmaware), 另一種是花錢買 ICSP (in-circuit serial programming)燒器,  而 AVR 的 uP 像是 ATmega328P 能透過 SPI 介面來燒內部的 flash.

我之前買過一個 ESP8266 的開發板, 本身也可以拿來當作 AVR ICSP 的燒, 安裝完 ESP8266 的 arduino 後, 打開位在 ~/home/mint/.arduino15/packages/esp8266/hardware/esp8266/2.7.4/libraries/ESP8266AVRISP, 可以將他複製到專案裏,稍作修改, 編譯後再燒到 ESP8266, 他就變成一個名副其實的無線 AVRISP 燒錄器. 燒錄 AVR uP流程要使用 avrdude 命令, 把 Hex 檔案傳送給 ESP8266, 由 ESP8266 用自身的 SPI 介面燒寫到 ATmega328P 的 flash 上, 命令列執行格式:

  avrdude -C avrdude.conf -c avrisp -p ATmega328P -P net:avrisp -U flash:w:grbl.hex:i

上述 -C avrdude.conf avrdude 的事前設定檔(可以將參數放在檔案裏面), 而 -P net: 參數中的 avrisp 是 esp8266 執行 mdns 的 localhost:port(例如用 -P net:esp8266.local:328), 或是直接用 -P net:ip:port, 例如 -P net:192.168.1.12:328, 至於 grbl.hex 正是前面編譯 grbl 完, 要燒的軔體程式(hex 格式檔),至於 esp8266 與 ATmega328P 接線方式要參考 AVRISP 原始碼裡面的 README.rst 說明

-----------------+-------------
|ESP8266 IO/JTAG |AVR
ICSP/SPI|
|================+============|
| GPIO14(D5/TMS)>| SCK        |
|----------------+------------|
| GPIO12(D6/TDI)<| MISO       |
|----------------+------------|
| GPIO13(D7/TCK)>| MOSI       |
|----------------+------------|
| GPIO16(D0)    >| RESET      |
-----------------+-------------


AVR ICSP SPI(Slave)/ESP8266 SPI(Master):
  MISO  --|1 2|-- VCC
  SCK   --|3 4|-- MOSI
  RESET --|5 6|-- GND

ESP8266 的 GPIO12(TDI), GPIO13(TCK), GPIO14(TMS), GPIO15(TDO) 是內建的 SPI/JTAG 硬體介面, 我改用 GPIO16(D0) 作為 RESET 腳, 要注意的是 ESP8266 是 3.3V 的裝置, 介面之間的邏輯準位若電壓太高恐會把 IC 弄壞或是造成無法開機. 而 MISO (GPIO12)對於 ESP8266 來說是輸入接腳, 可以簡單用 1k 串聯 2k 下地電阻的分壓器(Voltage divider)作為 Level shifter 來連接, 另外因為在 Arduino UNO( ATmega328P) 板子上的 RESET 接腳有個 10k Ohm 提升電阻拉到 5V, 為免影響 ESP8266 開機,可以在 ESP8266 端加個電晶體(buffer)把它隔開, 亦或是簡單用 1.5k 下地電阻把把它拉到 0.7V 以下.

以下執行命令不使用 -C avrdude.conf, 而是直接下命令燒錄 grbl.hex, 假設 esp8266 在區域網路獲得的 ip 位址是 192.168.12.106, 而 service tcp port 是 328, 執行 avrdude 燒錄:

   avrdude -c avrisp -p ATmega328P -P net:192.168.12.106:328 -U flash:w:grbl.hex:i

警告:使用 avrdude 直接燒錄若下的參數錯誤, 恐會造成無法再燒錄的悲劇, 因為可能會清掉原來的 bootloader, 若是內部的 fuse 燒入錯誤, 將造成無法透過 SPI 再更新軔體若真的想要燒入內部 fuse 可先參考

   https://www.engbedded.com/fusecalc

燒錄程序畫面截錄:

avrdude: AVR device initialized and ready to accept instructions

Reading | ################################################## | 100% 0.01s

avrdude: Device signature = 0x1e950f (probably m328p)
avrdude: NOTE: "flash" memory has been specified, an erase cycle will be performed
         To disable this feature, specify the -D option.
avrdude: erasing chip
avrdude: reading input file "grbl.hex"
avrdude: writing flash (29864 bytes):

Writing | ################################################## | 100% 8.66s

avrdude: 29864 bytes of flash written
avrdude: verifying flash memory against grbl.hex:
avrdude: load data flash data from input file grbl.hex:
avrdude: input file grbl.hex contains 29864 bytes
avrdude: reading on-chip flash data:

Reading | ################################################## | 100% 7.34s

avrdude: verifying ...
avrdude: 29864 bytes of flash verified

avrdude: safemode: Fuses OK (E:FD, H:DA, L:FF)

avrdude done.  Thank you.

 

後記:

使用 arduino SDK 修改 AVRISP 程式啟用 TCP port 328 當 sevice port, 以及 GPIO16 作為 RESET 腳:

  #include <ESP8266AVRISP.h>

  ESP8266AVRISP avrISP(328, 16);

  void setup(){

    Serial.begin(115200);

    // ...

    avrISP.begin(); 

  }

  void loop() {

    // ... 

    static AVRISPState_t last_state = AVRISP_STATE_IDLE;
    AVRISPState_t new_state = avrISP.update();
    if (last_state != new_state) {
      switch (new_state) {
        case AVRISP_STATE_IDLE: Serial.printf("[AVRISP] idle\n");
            break;         
        case AVRISP_STATE_PENDING:Serial.printf("[AVRISP] pending\n");
            break;         
        case AVRISP_STATE_ACTIVE:
            Serial.printf("[AVRISP] programming\n");
            break;
        default: break;         
      }
      last_state = new_state;
    }
    if (last_state != AVRISP_STATE_IDLE) avrISP.serve();

  }

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

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