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();

  }

2020年11月3日 星期二

C++ 在 static 函數內使用本體

當在 class 裏面放置一個 static 函數時, 其實它只算是一個符號聯結, static 的函數實體會搬離到類型之外, 同時它已俱備唯一的函式本體,可以被呼叫, 但無法在 static 函式裏面使用 this 指標, 主因是它已脫離類型的範疇(out of scope), 但可以透過參數傳遞的方式, 利用別名 &變數, 或是指標 * 變數接收類型的本體(instance), 例如:

class  A_new_Class {

      int    aVar = 10;

      int    bVar = 5;

      static   void  function_with_instance(A_new_Class  &this_instance){

         printf("%d\n",  this_instance.aVar);// ok, it's  a parameter of instance's alias

          // printf("%d\n",  *this.aVar); // compile error!  this is invalid

      }

      static   void  function_with_this(A_new_Class  *this_pointer){

         printf("%d\n",  this_pointer -> bVar);// ok, it's a parameter of pointer

        // printf("%d\n",  this -> bVar); // compile error!  this is invalid

      }

    void call_function_with_this( ) {

            function_with_this(this); // call static function to use 'this' pointer

    } 

   void call_function_with_this_instance( ){

           function_with_instance(*this); // call static function to use 'this' instance

  }

      A_new_Class ( ){

          printf("instance of A\n");

      }

} ;


int main( ){

     class a;  // instance of A

     a.function_with_instance(a);// 10

     a.call_function_with_this_instance( );// 10


     a.function_with_this(&a);  // 5

     a.call_function_with_this( );  // 5

}


     

2020年10月1日 星期四

用 c++ 將 jpeg 檔封裝成 avi 格式檔

    #include <stdio.h>
    #include <string.h> 
    #include <stdlib.h> 
    #include <unistd.h>
   
    struct RCwindow    {
        unsigned short int left;
        unsigned short int top;
        unsigned short int right;
        unsigned short int bottom;     
    };

    struct AviChunkHdr  {
        unsigned char fourcc[4];
        unsigned int  dataSize;
        // ... data
    };// Chunk header, dataSize = sizeof(whole structure) - 8

    struct AviListHdr  {
        unsigned char fourcc[4];
        unsigned int  dataSize;
        unsigned char listtype[4];
        // ... list
    };// List header, dataSize = sizeof(whole structure) - 8

    struct AviStrf {
        unsigned char fourcc[4];
        unsigned int dataSize;

        unsigned int strucSize;
        unsigned int width;
        unsigned int height;
        unsigned int bitsCount;
        unsigned char compression[4];
        unsigned int imgSize;
        unsigned int xpelsMeter;
        unsigned int ypelsMeter;
        unsigned int numColors;
        unsigned int impColors;
    };// Chunk: avi stream format

    struct AviHeader   {
        unsigned char fourcc[4];
        unsigned int dataSize;

        unsigned int usFrame;
        unsigned int bytesPS;
        unsigned int padding;
        unsigned int flags;
        unsigned int totalFrames;
        unsigned int initFrame;
        unsigned int streams;
        unsigned int bufferSize;
        unsigned int width;
        unsigned int height;
        unsigned int reserved[4];
    };// Chunk: avi header
   
    struct AviListStrh   {
        unsigned char fourcc[4];
        unsigned int  dataSize;
        unsigned char listtype[4];

        unsigned char stype[4];
        unsigned int flags;
        unsigned int priority;
        unsigned int start;
        unsigned int scale;
        unsigned int rate;
        unsigned int initFrame;
        unsigned int totalFrames;
        unsigned int bufferSize;
        unsigned int quality;
        unsigned int samples;
        RCwindow rectangle;
    };// List: stream header


    struct AviListOdml {
        unsigned char fourcc[4];
        unsigned int  dataSize;
        unsigned char listtype[4];

        unsigned char stype[4];
        unsigned int  intSize;
        unsigned int  jpgFrames;
    };// List: openDML

    struct AviListStrl {
        unsigned char fourcc[4];
        unsigned int  dataSize;
        unsigned char listtype[4];

        AviListStrh strh;
        AviStrf strf;
        AviListOdml list_odml;
    };//List: stream list

    struct AviListHdrlist  {
        unsigned char fourcc[4];
        unsigned int  dataSize;
        unsigned char listtype[4];

        AviHeader avih;
        AviListStrl strl;
    };// List: aviheader + streamlist

    struct AviRiff {
        AviListHdr  riffhdr;
        AviListHdrlist headerList;
        AviListHdr  movieList;
    };// Lists: RIFF avi Tag + headerList + movieList
   
    struct Jpeg {
        unsigned short int soi;
        unsigned short int app0;
        unsigned short int hlen;
        char id[5];
    };

    int main(int argc, char **argv)  {
        unsigned int width  = 0; 
        unsigned int height = 0;
        sscanf(argv[argc - 1], "%ux%u", &width, &height);
        unsigned short jpgFrames = argc - 1;
        if (width > 1 && height > 1) jpgFrames --;       
        else { // default size
            width  = 800;
            height = 600;
        }
        if(jpgFrames == 0) {
            printf("usage:\n\t%s %s\n", argv[0], "1.jpg [...] [widthxheight]");
            exit(1);
        }
       
        unsigned int aviSize = 0;// max 2^32 = 4G       
        int bufferSize = 1024;
        char buffer[bufferSize + 1];
        char aviName[] = "test.avi";
        FILE *avi = fopen(aviName, "wb");
        if(avi == nullptr) {           
            printf("create avi file fail!\n");
            exit(1);
        }

        int fps = 1;// frame per second
        unsigned int usScale = 1e6;
        unsigned int sFrame = usScale/fps;

        AviListOdml odml = {           
            {'L', 'I', 'S', 'T'}, sizeof(AviListOdml) - 8,
            {'o', 'd', 'm', 'l'}, {'d', 'm', 'l', 'h'},
            sizeof(unsigned int), jpgFrames  
        };// open dml extention

        AviStrf strf = {
            {'s', 't', 'r', 'f'}, sizeof(AviStrf) - 8,
            sizeof(AviStrf),  
            width, height, 1 + (24 << 16),
            {'M', 'J', 'P', 'G'},  
            width * height * 3,  
            0,  0,  0,  0
        };// stream format, video
       
        AviListStrh strh = {
            {'s', 't', 'r', 'h'}, sizeof(AviListStrh) - 8,
            {'v', 'i', 'd', 's'}, {'m', 'j', 'p', 'g'},
            0, 0, 0,
            sFrame, usScale, 0, jpgFrames,  
            0, 0, 0,
            {0, 0, (unsigned short int)width, (unsigned short int)height}
        };// stream header       

        AviListStrl streamlist = {              
            {'L', 'I', 'S', 'T'}, sizeof(AviListStrl) - 8,
            {'s', 't', 'r', 'l'}, strh, strf, odml
        };// stream list

        AviHeader avih  = {
            {'a', 'v', 'i', 'h'}, sizeof(AviHeader) - 8,
            sFrame, fps * aviSize/jpgFrames, 0, 0, jpgFrames,  
            0, 1, 0, width, height, {0}
        };// avi header without index

        AviRiff riff = {
            {// AviListHdr
                {'R', 'I', 'F', 'F'}, 0,
                {'A', 'V', 'I', ' '}
            },
            {// AviListHdrlist                
                {'L', 'I', 'S', 'T'}, sizeof(AviListHdrlist) - 8,
                {'h', 'd', 'r', 'l'}, avih, streamlist
            },
            { // AviListHdr
                {'L', 'I', 'S', 'T'}, 0,
                {'m', 'o', 'v', 'i'}
            }    
        };

        auto fwritei = [](unsigned int i, FILE *f){
            for(int j = 0; j < 4 ; j ++) {
                fputc(i & 0xff, f);// LSB first out
                i >>= 8;
            }
        };
       
        long totalBytes = 0;
        fseek(avi, sizeof(riff), SEEK_SET);
        for(int k = 0; k < jpgFrames; k ++) {
            int index = k + 1;
            FILE *jpgFile= fopen(argv[index], "rb");
            if (jpgFile == nullptr) continue;

            long flen = fread(buffer, 1, 11, jpgFile);
            buffer[flen] = 0;
            auto &jpeg = * (Jpeg *)buffer;
            if  (flen == 11 && jpeg.soi == 0xd8ff && jpeg.app0 == 0xe0ff &&
                    strcmp(jpeg.id,"JFIF") == 0) {
                fseek(jpgFile, 0L, SEEK_END);
                flen = ftell(jpgFile);
                printf("%04d. %-12s %-12ld bytes\n", index, argv[index], flen);

                fwrite("00dc", 4, 1, avi);
                fwritei(flen, avi);
                fwrite(buffer, 6, 1, avi);
                fwrite("AVI1", 5, 1, avi);
                fseek(jpgFile, 11L, SEEK_SET);
                int n = 0;
                do {
                    n = fread(buffer, 1, bufferSize, jpgFile);
                    if(n > 0) fwrite(buffer, n, 1, avi);
                } while(n == bufferSize);
                totalBytes += flen;
                if(flen % 2) {
                    flen ++;
                    fputc(0, avi);
                }
                aviSize += flen + 8;
            }
            fclose(jpgFile);
        }
       
        riff.movieList.dataSize  = 4 + aviSize;
        riff.riffhdr.dataSize = riff.movieList.dataSize  + sizeof(AviRiff) - 8;
        riff.headerList.avih.bytesPS = fps * totalBytes / jpgFrames;
       
        fseek(avi, 0l, SEEK_SET);
        fwrite(&riff, sizeof(riff), 1, avi);

        printf("\nmovieSize=%d, riffSize=%d, maxBytesPS=%d\n",
            riff.movieList.dataSize,
            riff.riffhdr.dataSize,
            riff.headerList.avih.bytesPS
        );
        printf("%s %ld/(%d bytes) includes %u frames(%u*%u):\n\n",
            aviName,
            sizeof(riff),
            4 + riff.riffhdr.dataSize,
            jpgFrames,
            width,
            height
        );
        fclose(avi);
    }

2020年8月30日 星期日

簡單的 c++ 使用 multithread 的 localsocket


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

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


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

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

用 emcc compiler 寫簡單的 openGL 繪圖, 讓瀏覽器也能觀看

在瀏覽開啟 html 檔, 裡面除了可以用 javascript 語言來運行openGL ES, 也能用 wasm 語言來運作 , 透過 emcc 編譯器可以將 c 語言翻譯成 wasm, 安裝方式詳如 Emscripten 官網: https://emscripten.org/...