投稿者「mars」のアーカイブ

Microk8sを試す

microk8sを用いると簡単に単ノード構成のkubernetesを構築することができ、さらにkubernetesクラスタも構築することができるようなので試してみることにしました。

インストールの手順は、microk8sを踏襲することにします。

Install MicroK8s on Linux

sudo snap install microk8s --classic

ユーザを micro8ksのグループへ追加(次のログイン時に反映される)

sudo usermod -a -G microk8s $USER
sudo chown -f -R $USER ~/.kube

microk8s起動状況の表示

$ microk8s status

microk8s is running
high-availability: yes
  datastore master nodes: 192.168.68.132:19001 192.168.68.129:19001 192.168.68.111:19001
  datastore standby nodes: 192.168.68.130:19001

addons:
  enabled:
    dashboard            # The Kubernetes dashboard
    dns                  # CoreDNS
    ha-cluster           # Configure high availability on the current node
    ingress              # Ingress controller for external access
    metallb              # Loadbalancer for your Kubernetes cluster
    metrics-server       # K8s Metrics Server for API access to service metrics
    storage              # Storage class; allocates storage from host directory
  disabled:
    ambassador           # Ambassador API Gateway and Ingress
    cilium               # SDN, fast with full network policy
    fluentd              # Elasticsearch-Fluentd-Kibana logging and monitoring
    gpu                  # Automatic enablement of Nvidia CUDA
    helm                 # Helm 2 - the package manager for Kubernetes
    helm3                # Helm 3 - Kubernetes package manager
    host-access          # Allow Pods connecting to Host services smoothly
    istio                # Core Istio service mesh services
    jaeger               # Kubernetes Jaeger operator with its simple config
    keda                 # Kubernetes-based Event Driven Autoscaling
    knative              # The Knative framework on Kubernetes.
    kubeflow             # Kubeflow for easy ML deployments
    linkerd              # Linkerd is a service mesh for Kubernetes and other frameworks
    multus               # Multus CNI enables attaching multiple network interfaces to pods
    portainer            # Portainer UI for your Kubernetes cluster
    prometheus           # Prometheus operator for monitoring and logging
    rbac                 # Role-Based Access Control for authorisation
    registry             # Private image registry exposed on localhost:32000
    traefik              # traefik Ingress controller for external access

Check the status while Kubernetes starts

microk8s status --wait-ready

disableされている中から必要なservices を有効化

microk8s enable dashboard dns registry istio

ここで microk8s enable –help を実行すると利用可能なサービスを一覧できます。

起動中の利用可能なサービスを表示

microk8s kubectl get all --all-namespaces
$ microk8s kubectl get all --all-namespaces

NAMESPACE        NAME                                             READY   STATUS        RESTARTS   AGE
ingress          pod/nginx-ingress-microk8s-controller-rj2vj      1/1     Running       0          22h
metallb-system   pod/speaker-bwpl7                                1/1     Running       0          22h
metallb-system   pod/speaker-g2kcl                                1/1     Running       1          22h
ingress          pod/nginx-ingress-microk8s-controller-dvb29      1/1     Running       1          22h
kube-system      pod/calico-node-jdpsl                            1/1     Running       1          23h
kube-system      pod/hostpath-provisioner-5c65fbdb4f-wlg45        1/1     Terminating   1          22h
kube-system      pod/coredns-86f78bb79c-r94k8                     1/1     Terminating   1          23h
kube-system      pod/hostpath-provisioner-5c65fbdb4f-6hz6n        1/1     Running       0          10h
kube-system      pod/calico-node-8hqxv                            1/1     Running       0          24h
ingress          pod/nginx-ingress-microk8s-controller-w5qv9      1/1     Running       0          22h
metallb-system   pod/speaker-wpg4z                                1/1     Running       0          22h
kube-system      pod/coredns-86f78bb79c-hl56s                     1/1     Terminating   0          10h
metallb-system   pod/controller-559b68bfd8-6nw4k                  1/1     Running       0          10h
metallb-system   pod/speaker-nrw9w                                1/1     Running       17         22h
kube-system      pod/coredns-86f78bb79c-72zp4                     1/1     Running       3          10h
kube-system      pod/calico-node-nk45x                            1/1     Running       18         24h
ingress          pod/nginx-ingress-microk8s-controller-cnpzq      1/1     Running       15         22h
kube-system      pod/metrics-server-8bbfb4bdb-9kh97               1/1     Running       0          38m
kube-system      pod/dashboard-metrics-scraper-6c4568dc68-vrmzl   1/1     Running       0          36m
kube-system      pod/calico-kube-controllers-847c8c99d-z867h      1/1     Running       0          33h
kube-system      pod/calico-node-ppczw                            1/1     Running       0          24h
kube-system      pod/kubernetes-dashboard-7ffd448895-q7zkf        1/1     Running       7          36m


NAMESPACE     NAME                                TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)                  AGE
default       service/kubernetes                  ClusterIP   10.152.183.1     <none>        443/TCP                  33h
kube-system   service/kube-dns                    ClusterIP   10.152.183.10    <none>        53/UDP,53/TCP,9153/TCP   23h
kube-system   service/metrics-server              ClusterIP   10.152.183.231   <none>        443/TCP                  38m
kube-system   service/kubernetes-dashboard        ClusterIP   10.152.183.118   <none>        443/TCP                  36m
kube-system   service/dashboard-metrics-scraper   ClusterIP   10.152.183.83    <none>        8000/TCP                 36m

NAMESPACE        NAME                                               DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR                 AGE
kube-system      daemonset.apps/calico-node                         4         4         2       4            2           kubernetes.io/os=linux        33h
metallb-system   daemonset.apps/speaker                             2         2         2       2            2           beta.kubernetes.io/os=linux   22h
ingress          daemonset.apps/nginx-ingress-microk8s-controller   2         2         2       2            2           <none>                        22h

NAMESPACE        NAME                                        READY   UP-TO-DATE   AVAILABLE   AGE
kube-system      deployment.apps/hostpath-provisioner        1/1     1            1           22h
metallb-system   deployment.apps/controller                  1/1     1            1           22h
kube-system      deployment.apps/calico-kube-controllers     1/1     1            1           33h
kube-system      deployment.apps/coredns                     1/1     1            1           23h
kube-system      deployment.apps/metrics-server              1/1     1            1           38m
kube-system      deployment.apps/dashboard-metrics-scraper   1/1     1            1           36m
kube-system      deployment.apps/kubernetes-dashboard        1/1     1            1           36m

NAMESPACE        NAME                                                   DESIRED   CURRENT   READY   AGE
kube-system      replicaset.apps/hostpath-provisioner-5c65fbdb4f        1         1         1       22h
metallb-system   replicaset.apps/controller-559b68bfd8                  1         1         1       22h
kube-system      replicaset.apps/calico-kube-controllers-847c8c99d      1         1         1       33h
kube-system      replicaset.apps/coredns-86f78bb79c                     1         1         1       23h
kube-system      replicaset.apps/metrics-server-8bbfb4bdb               1         1         1       38m
kube-system      replicaset.apps/dashboard-metrics-scraper-6c4568dc68   1         1         1       36m
kube-system      replicaset.apps/kubernetes-dashboard-7ffd448895        1         1         1       36m

Kubernetesの起動と停止

microk8s start
or
microk8s stop

ノードをクラスターへ追加;microk8s joink~ の行をCopyし、追加するノードの端末へPasteして実行する。

microk8s add-node

/* ノード参加に必要な以下のようなメッセージ が表示されるので、参加するノードで、microk8s joinjoinを実行 */

Join node with:
microk8s join ip-172-31-20-243:25000/DDOkUupkmaBezNnMheTBqFYHLWINGDbf

If the node you are adding is not reachable through the default
interface you can use one of the following:

microk8s join 10.1.84.0:25000/DDOkUupkmaBezNnMheTBqFYHLWINGDbf
microk8s join 10.22.254.77:25000/DDOkUupkmaBezNnMheTBqFYHLWINGDbf
  • 参加しているノードの表示
microk8s kubectl get no

frirewallの設定

sudo ufw allow in on cni0 && sudo ufw allow out on cni0
sudo ufw default allow routed

dashboardの利用するには、アクセスのためのトークンを作成します。

token=$(microk8s kubectl -n kube-system get secret | grep default-token | cut -d " " -f1)
microk8s kubectl -n kube-system describe secret $token

トークンの一例(dashboardをアクセスした際に要求されるtoken。Copy&Pasteでログイン)

Name:         default-token-57rfp
Namespace:    kube-system
Labels:       <none>
Annotations:  kubernetes.io/service-account.name: default
              kubernetes.io/service-account.uid: 2f782a0f-3d04-43aa-88fe-a6d67364b297

Type:  kubernetes.io/service-account-token

Data
====
namespace:  11 bytes
token:      eyJhbGciOiJSUzI1NiIsImtpZCI6Im56Vm1vTVJyaXVJQzBaSnM4SS1PTWNrZTkzMlJMdFBqS0NMeFgxWnIzdWMifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJrdWJlLXN5c3RlbSIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VjcmV0Lm5hbWUiOiJkZWZhdWx0LXRva2VuLTU3cmZwIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQubmFtZSI6ImRlZmF1bHQiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC51aWQiOiIyZjc4MmEwZi0zZDA0LTQzYWEtODhmZS1hNmQ2NzM2NGIyOTciLCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6a3ViZS1zeXN0ZW06ZGVmYXVsdCJ9.EGkufnRoGONpt14vmBLAG8vF2khLtHZLx8J0VNzJUxX28z8NtSNB5MhRKCamChqXdTRm1iwyaPQIlgWwaKTci7CA9TNf8o70EmJBOO1AvDJ_QcC2mRpQzrkxcl0wiuLbpkHzC-wvuzqwY4b32utYikbUxHNjiDsOSyVmqN9NyDZ84FKRZUGkaWrgJmUNakBGetqaOPSpTAIC8JncPznYIOt88nyx6kCIrOFJjjh_UhPGMfevVNFZcji617uNTencBVrkwaej6O09wyqzjPVK-jWXhHaigaIb5O2TmjfcQJCyiEkF_6LYFGr7ilOzzpbAqw-iICmBQUW1Mred3FsN9Q
ca.crt:     1103 bytes

dashboardへログインした画面

dashboardでノードの状態を表示

podの追加例:

インストールの過程で気になったキーワード

multipass  Ubuntu環境へ簡単にVMを構築

helm     Kubernetes 用パッケージマネージャー

WordPress  Helm Chartのデプロイ 

3ノード以上の構成では、データストアーがクラスター内で複製され、1ノード故障に対しては耐性がある。

From the 1.19 release of MicroK8s, HA is enabled by default. If your cluster consists of three or more nodes, the datastore will be replicated across the nodes and it will be resilient to a single failure (if one node develops a problem, workloads will continue to run without interruption).

ESP32/DCモータ/Websocket UIで倒立振子

PID制御のパラメータと自立のための目標角度(TARGET)をWebsocket UIでスマホから設定します。今のところ、パラメータの値が適切でないようで、硬い床面上では短時間で倒れます。やわらかい素材の上では倒れないので、この状態で適切なパラメータを試行錯誤で探索中、、、WebベースのUIのためか、細かなパラメータ設定の操作性がいまいちなので、改善が必要かもしれない。


倒立振子のコードは、https://qiita.com/Google_Homer/items/3897e7ffef9d247e2f56 を参考にしています。

Websocketのコードは、https://github.com/mgo-tec/ESP32_SPIFFS_EasyWebSocket を参考にしています。

#include <WiFi.h>
#include <WiFiMulti.h>
#include "ESP32_SPIFFS_EasyWebSocket.h" //beta ver 1.60

#include <MadgwickAHRS.h>
Madgwick MadgwickFilter;
#include "I2Cdev.h"
#define MPU6050_PWR_MGMT_1   0x6B
#define MPU_ADDRESS  0x68

#include "Wire.h"
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels
#define OLED_RESET     4 // Reset pin # (or -1 if sharing Arduino reset pin)
#define SCREEN_ADDRESS 0x3C ///< See datasheet for Address; 0x3D for 128x64, 0x3C for 128x32
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);

//PIDパラメータ調整
//PID係数
//#define TARGET            0.4
//#define KP                100.0
//#define KI                2.0
//#define KD                2.0

//Motor
#define LED_PIN            27                      // 内蔵LED
#define MOTOR_PIN_F        A4                      // to DC Motor Driver FIN
#define MOTOR_PIN_R        A18                      // to DC Motor Driver RIN
#define MOTOR_PWM_F        0                       // PWM CHANNEL
#define MOTOR_PWM_R        1                       // PWM CHANNEL
#define MOTOR_POWER_MIN    50
int MOTOR_POWER_MAX = 200;

#define DPS                1000                    // Gscale [deg/s]

const char* ssid = "*******"; //ご自分のルーターのSSIDに書き換えてください
const char* password = "******"; //ご自分のルーターのパスワードに書き換えてください

const char* HTM_head_file1 = "/EWS/LIPhead1.txt"; //HTMLヘッダファイル1
const char* HTM_head_file2 = "/EWS/LIPhead2.txt"; //HTMLヘッダファイル2
const char* HTML_body_file = "/EWS/dummy.txt"; //HTML body要素ファイル(ここではダミーファイルとしておく)
const char* dummy_file = "/EWS/dummy.txt"; //HTMLファイル連結のためのダミーファイル

ESP32_SPIFFS_EasyWebSocket ews;
WiFiMulti wifiMulti;

IPAddress LIP; //ローカルIPアドレス自動取得用

String ret_str; //ブラウザから送られてくる文字列格納用
String txt = "text send?"; //ブラウザから受信した文字列を ESP32から再送信する文字列

int PingSendTime = 10000; //ESP32からブラウザへPing送信する間隔(ms)

long ESP32_send_LastTime;
int ESP32_send_Rate = 300;
byte cnt = 0;

//PID
float power = 0, I = 0, preP = 0, preTime;
float now = 0, Duty = 0, pitch, roll, yaw;
float KP = 100.0, KI = 2.0, KD = 2.0, TARGET = 0.0;
boolean f_disp = false;

void setup() {
  Wire.begin();
  Serial.begin(38400);
  Wire.beginTransmission(MPU_ADDRESS);
  Wire.write(MPU6050_PWR_MGMT_1);  //MPU6050_PWR_MGMT_1レジスタの設定
  Wire.write(0x00);
  Wire.endTransmission();

  Serial.print(F("Connecting to "));
  Serial.println(ssid);

  wifiMulti.addAP(ssid, password);

  Serial.println(F("Connecting Wifi..."));
  if (wifiMulti.run() == WL_CONNECTED) {
    Serial.println("");
    Serial.println(F("WiFi connected"));
    Serial.println(F("IP address: "));
    LIP = WiFi.localIP(); //ESP32のローカルIPアドレスを自動取得
    Serial.println(WiFi.localIP());
  }
  ews.EWS_server_begin();
  Serial.println(); Serial.println("Initializing SPIFFS ...");

  if (!SPIFFS.begin()) {
    Serial.println("SPIFFS failed, or not present");
    return;
  }

  Serial.println("SPIFFS initialized. OK!");
  MadgwickFilter.begin(100); //100Hz
  // SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally
  if (!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
    Serial.println(F("SSD1306 allocation failed"));
    for (;;); // Don't proceed, loop forever
  }
  pinMode(LED_PIN, OUTPUT);
  digitalWrite(LED_PIN, HIGH);
  pinMode(MOTOR_PIN_F, OUTPUT);
  pinMode(MOTOR_PIN_R, OUTPUT);

  ledcSetup(MOTOR_PWM_F, 312500, 8); //CHANNEL, FREQ, BIT
  ledcSetup(MOTOR_PWM_R, 312500, 8);
  ledcAttachPin(MOTOR_PIN_F, MOTOR_PWM_F);
  ledcAttachPin(MOTOR_PIN_R, MOTOR_PWM_R);
  // 初期化

  delay(10);

  // Clear the buffer
  display.clearDisplay();
  // テキストサイズを設定
  display.setTextSize(1);
  // テキスト色を設定
  display.setTextColor(WHITE);
  display.setCursor(20, 5);
  display.println("Start...");
  display.setCursor(20, 25);
  display.println(WiFi.localIP());
  display.display();

  delay(10); // Pause for 2 seconds
  preTime = micros();
  //  TaskHandle_t th; //ESP32 マルチタスク ハンドル定義
  //  xTaskCreatePinnedToCore(Task1, "Task1", 4096, NULL, 5, &th, 0); //マルチタスク core 0 実行

  ESP32_send_LastTime = millis();
}

void loop() {
  websocket_handshake();

  if (ret_str != "_close") {
    if (millis() - ESP32_send_LastTime > ESP32_send_Rate) {
      if (cnt > 3) {
        cnt = 0;
      }
      websocket_send(cnt, txt);
      cnt++;
      ESP32_send_LastTime = millis();
    }
    ret_str = ews.EWS_ESP32CharReceive(PingSendTime);
    if (ret_str != "\0") {
      Serial.println(ret_str);
      if (ret_str != "Ping") {
        if (ret_str[0] != 't') {
          int ws_data = (ret_str[0] - 0x30) * 100 + (ret_str[1] - 0x30) * 10 + (ret_str[2] - 0x30);
          switch (ret_str[4]) {
            case '!':
              ESP32_send_Rate = ws_data;
              break;
            case 'B':
              TARGET = float(5.0 - ws_data / 20.0);
              break;
            case 'G':
              KP = float(ws_data / 1.8) ;
              break;
            case 'R':
              KI = float(ws_data / 50.0);
              break;
            case '_':
              KD = float(ws_data / 50.0);
              break;
            case 'A':
              f_disp = ! f_disp;
              break;
            case 'O':
              KP = 100, KI = 2.0, KD = 2.0, TARGET = 0.0;
              break;
          }
        } else if (ret_str[0] == 't') {
          txt = ret_str.substring(ret_str.indexOf('|') + 1, ret_str.length() - 1);
          Serial.println(txt);
        }
      }
    }
  } else if (ret_str == "_close") {
    ESP32_send_LastTime = millis();
    ret_str = "";
  }
  PID();
}

//************* 倒立振り子 ****************************************
void PID() {
  float  P, D, dt, Time;
  static float  powerI = 0;

  Wire.beginTransmission(0x68);
  Wire.write(0x3B);
  Wire.endTransmission(false);
  Wire.requestFrom(0x68, 14, true);
  while (Wire.available() < 14);
  int16_t axRaw, ayRaw, azRaw, gxRaw, gyRaw, gzRaw, Temperature;

  axRaw = Wire.read() << 8 | Wire.read();
  ayRaw = Wire.read() << 8 | Wire.read();
  azRaw = Wire.read() << 8 | Wire.read();
  Temperature = Wire.read() << 8 | Wire.read();
  gxRaw = Wire.read() << 8 | Wire.read();
  gyRaw = Wire.read() << 8 | Wire.read();
  gzRaw = Wire.read() << 8 | Wire.read();

  // 加速度値を分解能で割って加速度(G)に変換する
  float acc_x = axRaw / 16384.0;  //FS_SEL_0 16,384 LSB / g
  float acc_y = ayRaw / 16384.0;
  float acc_z = azRaw / 16384.0;

  // 角速度値を分解能で割って角速度(degrees per sec)に変換する
  float gyro_x = gxRaw / 131.0;  // (度/s)
  float gyro_y = gyRaw / 131.0;
  float gyro_z = gzRaw / 131.0;

  /*
    //c.f. Madgwickフィルターを使わずに、PRY(pitch, roll, yaw)を計算
    double roll  = atan2(acc_y, acc_z) * RAD_TO_DEG;
    double pitch = atan(-acc_x / sqrt(acc_y * acc_y + acc_z * acc_z)) * RAD_TO_DEG;
  */

  //Madgwickフィルターを用いて、PRY(pitch, roll, yaw)を計算
  MadgwickFilter.updateIMU(gyro_x, gyro_y, gyro_z, acc_x, acc_y, acc_z);

  //PRYの計算結果を取得する
  roll  = MadgwickFilter.getRoll();
  pitch = MadgwickFilter.getPitch();
  yaw   = MadgwickFilter.getYaw();
  now     = TARGET - roll                 ; // 目標角度から現在の角度を引いて偏差を求める
  if (f_disp) {
    display.clearDisplay();
    display.setCursor(20, 5);
    display.println( roll);
    display.setCursor(20, 25);
    display.println( now);
    display.display();
  }
  if (-20 < now && now < 20) {
    Time    = micros()                    ;
    dt      = (Time - preTime) / 1000000  ; // 処理時間を求める
    preTime = Time                        ; // 処理時間を記録
    P       = now / 90                    ; // -90~90→-1.0~1.0
    I      += P * dt                      ; // 偏差を積分する
    D       = (P - preP) / dt             ; // 偏差を微分する
    preP    = P                           ; // 偏差を記録する
    power  += KP * P + KI * I + KD * D    ; // 出力を計算する
    if (power < -1) power = -1            ; // →-1.0~1.0
    if (1 < power)  power =  1            ;
    //Motor駆動
    Duty = (int)((MOTOR_POWER_MAX - MOTOR_POWER_MIN) * abs(power) + MOTOR_POWER_MIN);
    ledcWrite( MOTOR_PWM_F, (power < 0 ?    0 : Duty) );
    ledcWrite( MOTOR_PWM_R, (power < 0 ? Duty :    0) );
    digitalWrite(LED_PIN, HIGH);
    if (f_disp) {
      display.clearDisplay();
      display.setCursor(20, 5);
      display.println( roll);
      display.setCursor(20, 25);
      display.println( Duty);
      display.setCursor(20, 45);
      display.println( power);
      display.display();
    }
  } else {                                                  // 転倒したら停止
    ledcWrite(MOTOR_PWM_F, 0);
    ledcWrite(MOTOR_PWM_R, 0);
    power = 0;
    I = 0;
    digitalWrite(LED_PIN, LOW);
  }

}

//**************************************************************
void LED_PWM(byte Led_gr, byte channel, int data_i) {
  Serial.println(data_i);
}
//*********************************************
void websocket_send(uint8_t count, String str_txt) {
  String str, tmp;
  //※WebSocketへのテキスト送信は110 byte 程度なので、全角35文字程度に抑えること
  tmp = "AGL=" + String(roll) + ",TGT=" + String(TARGET) + ",P=" + String(KP) +  ",I=" + String(KI) + ",D=" + String(KD);
  switch (cnt) {
    case 0:
      //str = str_txt;
      str = tmp;
      break;
    case 1:
      str = tmp;
      break;
    case 2:
      str = tmp;
      break;
    case 3:
      str = tmp;
      break;
  }

  ews.EWS_ESP32_Str_SEND(str, "wroomTXT"); //ブラウザに文字列を送信
}
//************************* Websocket handshake **************************************
void websocket_handshake() {
  if (ews.Get_Http_Req_Status()) { //ブラウザからGETリクエストがあったかどうかの判定
    String html_str1 = "", html_str2 = "", html_str3 = "", html_str4 = "", html_str5 = "", html_str6 = "", html_str7 = "";

    //※String変数一つにEWS_Canvas_Slider_T関数は2つまでしか入らない
    html_str1 += "<body style='background:#000; color:#fff;'>\r\n";
    html_str1 += "<font size=3>\r\n";
    html_str1 += "ESP-WROOM-32(ESP32)\r\n";
    html_str1 += "<br>\r\n";
    html_str1 += "ESP32_SPIFFS_EasyWebSocket Beta1.60 Sample\r\n";
    html_str1 += "</font><br>\r\n";
    html_str1 += ews.EWS_BrowserSendRate();
    html_str1 += "<br>\r\n";
    html_str1 += ews.EWS_ESP32_SendRate("!esp32t-Rate");
    html_str1 += "<br>\r\n";
    html_str1 += ews.EWS_BrowserReceiveTextTag2("wroomTXT", "from ESP32 DATA", "#555", 20, "#00FF00");
    html_str1 += "<br>\r\n";
    html_str1 += ews.EWS_Status_Text2("WebSocket Status", "#555", 20, "#FF00FF");
    html_str1 += "<br><br>\r\n";

    html_str2 += ews.EWS_TextBox_Send("txt1", "Hello Easy WebSocket Beta1.60", "送信");
    html_str2 += "<br><br>\r\n";
    html_str2 += "SETTING \r\n";
    html_str2 += ews.EWS_On_Momentary_Button("ALL", "ALL-ON", 80, 25, 15, "#000000", "#AAAAAA");
    html_str2 += ews.EWS_On_Momentary_Button("OUT", "DEFALT", 80, 25, 15, "#FFFFFF", "#555555");
    html_str2 += "<br>\r\n";

    html_str3 += "<br>TGT_ :\r\n";
    html_str3 += ews.EWS_Canvas_Slider_T("BLUE", 240, 40, "#777777", "#0000ff"); //CanvasスライダーはString文字列に2つまでしか入らない
    html_str3 += "<br>PID P:\r\n";
    html_str3 += ews.EWS_Canvas_Slider_T("GREEN", 250, 40, "#777777", "#00ff00"); //CanvasスライダーはString文字列に2つまでしか入らない

    html_str4 += "<br>PID I:\r\n";
    html_str4 += ews.EWS_Canvas_Slider_T("RED", 250, 40, "#777777", "#ff0000"); //CanvasスライダーはString文字列に2つまでしか入らない
    html_str4 += "<br>PID D:\r\n";
    html_str4 += ews.EWS_Canvas_Slider_T("_RGB", 250, 40, "#777777", "#ffff00");

    html_str7 += "<br><br>\r\n";
    html_str7 += ews.EWS_WebSocket_Reconnection_Button2("WS-Reconnect", "grey", 200, 40, "black" , 17);
    html_str7 += "<br><br>\r\n";
    html_str7 += ews.EWS_Close_Button2("WS CLOSE", "#bbb", 150, 40, "red", 17);
    html_str7 += ews.EWS_Window_ReLoad_Button2("ReLoad", "#bbb", 150, 40, "blue", 17);
    html_str7 += "</body></html>";

    //WebSocket ハンドシェイク関数
    ews.EWS_HandShake_main(3, HTM_head_file1, HTM_head_file2, HTML_body_file, dummy_file, LIP, html_str1, html_str2, html_str3, html_str4, html_str5, html_str6, html_str7);
  }
}

raspberry pi4でTansorFlow-Lightを使ってみた

このサイトの手順に従ってインストール:

https://github.com/EdjeElectronics/TensorFlow-Lite-Object-Detection-on-Android-and-Raspberry-Pi/blob/master/Raspberry_Pi_Guide.md

カメラ動画の認識実行例:

航空機の映像が小さい(遠方の目標)と、凧(kite)や鳥(bird)として誤認識されることが多い。再生速度を遅くしてご覧ください。

手順の全般

  • 1a. Update the Raspberry Pi
  • 1b. パッケージをrepository からダウンロードし、仮想環境を作成
  • 1c. TensorFlow and OpenCVその他必要なライブラリーのインストール
  • 1d. TensorFlow Lite detection modelのセットアップ
  • 1e. TensorFlow Lite model!の実行

Step 1a. Update the Raspberry Pi

sudo apt-get update
sudo apt-get dist-upgrade

Step 1b. リポジトリからダウンロードして仮想環境を作成

$ git clone https://github.com/EdjeElectronics/TensorFlow-Lite-Object-Detection-on-Android-and-Raspberry-Pi.git

ディレクトリー名が長いので、短めな名称にリネーム
$ mv TensorFlow-Lite-Object-Detection-on-Android-and-Raspberry-Pi tflite1
$ cd tflite1

virtualenvを利用した仮想環境を構築:
$sudo pip3 install virtualenv
次のコマンドで仮想環境 "tflite1-env" を作成
$ python3 -m venv tflite1-env

"tflite1-env"の活性化
$ source tflite1-env/bin/activate

Step 1c. Install TensorFlow Lite dependencies and OpenCV

$ bash get_pi_requirements.sh
次のURLから、自分の環境にあったバージョンを選んでインストールする。
https://github.com/google-coral/pycoral/releases/
例えば、python3.8 arm64bitの場合
$pip3 install pip3 install https://github.com/google-coral/pycoral/re
leases/download/v1.0.1/tflite_runtime-2.5.0-cp38-cp38-linux_aarch64.whl

Step 1d. Set up TensorFlow Lite detection model

認識のモデルをスクラッチから作るのは大変なので、ここではGoogle’s sampleをダウンロードして拝借

$ wget https://storage.googleapis.com/download.tensorflow.org/models/tflite/coco_ssd_mobilenet_v1_1.0_quant_2018_06_29.zip
$ unzip coco_ssd_mobilenet_v1_1.0_quant_2018_06_29.zip -d Sample_TFLite_model

Step 1e. TensorFlow Lite modelの実行

静止画像、動画、カメラ入力などを対象としたpythonスクリプトが用意されていますが、とりあえず、同梱されているtest.mp4動画でテスト。

$ python3 TFLite_detection_video.py --modeldir=Sample_TFLite_model

別の動画を指定する場合, --videoオプションでファイル名(パス)を指定する。
$ python3 TFLite_detection_video.py --video 動画のファイル名 --modeldir=Sample_TFLite_model

youtubeから拾ってきた岐阜航空祭の動画で試してみたら、思った以上に良好に検出してくれました。(稀に航空機を凧、鳥と誤認識)

Webカメラを利用する場合は;

$ python3 TFLite_detection_webcam.py --modeldir=Sample_TFLite_model

Googleの学習済のモデルには、数十種類の認識対象が含まれている。対象のリストはSample_TFLite_modelディレクトリーの中に、labelmap.txtという名称で入っている。同じディレクトリー内に、ファイルdetect.tfliteがあり、これが学習済のデータ(バイナリー)のようだ。

TFLite_detection_video.pyスクリプトを少し改変して、例えば航空機を検出した場合に限定して、検出枠の座標を取り出すこともできたので、これまでに実装したステップモータやサーボモータでカメラを動かす実験と合体させてみたい。

更新:サーボモータで追尾するコード

import time
import math
import datetime
import cv2
import pigpio
import queue
import numpy as np
import sys
from threading import Thread
import importlib.util
import os

face_cascade_path = '/home/pi/opencv/data/haarcascades/haarcascade_frontalface_default.xml'
face_cascade = cv2.CascadeClassifier(face_cascade_path)
usleep = lambda x: time.sleep(x/1000000.0)

TILT=17
PAN=27
RPi=False
GP=pigpio.pi('localhost',8880)
GP.set_mode(PAN,pigpio.OUTPUT)
GP.set_mode(TILT,pigpio.OUTPUT)

# Define VideoStream class to handle streaming of video from webcam in separate processing thread
# Source - Adrian Rosebrock, PyImageSearch: https://www.pyimagesearch.com/2015/12/28/increasing-raspberry-pi-fps-with-python-and-opencv/
class VideoStream:
    """Camera object that controls video streaming from the Picamera"""
    def __init__(self,resolution=(640,480),framerate=30):
        # Initialize the PiCamera and the camera image stream
        self.stream = cv2.VideoCapture(0)
        #self.stream = cv2.VideoCapture('rtsp://admin:@192.168.68.128:554/1/h264major')
        ret = self.stream.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc(*'MJPG'))
        ret = self.stream.set(3,resolution[0])
        ret = self.stream.set(4,resolution[1])

        # Read first frame from the stream
        (self.grabbed, self.frame) = self.stream.read()

        # Variable to control when the camera is stopped
        self.stopped = False

    def start(self):
        # Start the thread that reads frames from the video stream
        Thread(target=self.update,args=()).start()
        return self

    def update(self):
        # Keep looping indefinitely until the thread is stopped
        while True:
            # If the camera is stopped, stop the thread
            if self.stopped:
                # Close camera resources
                self.stream.release()
                return

            # Otherwise, grab the next frame from the stream
            (self.grabbed, self.frame) = self.stream.read()

    def read(self):
        # Return the most recent frame
        return self.frame

    def stop(self):
        # Indicate that the camera and thread should be stopped
        self.stopped = True
        
def move(p0,p1,dev):
    global tPos,pPos
    global tMin,tMax,pMin,pMax
    if dev==PAN:
        if p1 > pMax or p1 < pMin:
            return
    else:
        if p1 > tMax or p1 < tMin:
            return
            
    deg=p0
    dx=0.4
    counts=int(abs(p1-p0)/dx)
    if p1<p0:
        dx=-dx
    for i in range(0,counts):
        deg=deg+dx
        pw=500+int(deg*2000/270)
        GP.set_servo_pulsewidth(dev,pw)
        #time.sleep(0.005)
        #GP.set_servo_pulsewidth(dev,0)
        if dev==TILT:   
            tPos=deg
        else:
            pPos=deg

def key(k):
    global pPos,tPos,PAN,TILT,track,f_all
    global capture,fontFace,color,Green,Red

    if k == ord('j'):
        new=pPos+2
        move(pPos,new,PAN)
        return
    elif k == ord('k'):
        new=pPos-2
        move(pPos,new,PAN)
        return
    elif k == ord('m'):
        new=tPos-2
        move(tPos,new,TILT)
        return
    elif k == ord('i'):
        new=tPos+2
        move(tPos,new,TILT)
        return
    elif k == ord('p'):
        tmp=input()
        move(pPos,int(tmp),PAN)
    elif k == ord('t'):
        tmp=input()
        move(tPos,int(tmp),TILT)
    elif k == ord('a'):
        f_all = not f_all
    elif k == ord('f'):
        track = not(track)
        if  track:
            color=Red
        else:
            color=Green
 
    elif k == ord('z'):
        move(tPos,0,TILT)
        move(pPos,90,PAN)

def tracking(dX,dY):
    global xW,yW,pPos,tPos,tW
    ret=False
    if dX >0 :
        move(pPos,pPos+1,PAN)
    elif dX < 0:
        move(pPos,pPos-1,PAN)
    if dY > 0:
        move(tPos,tPos+1,TILT)
    elif dY < 0:
        move(tPos,tPos-1,TILT)
    return ret

# 移動体検知
def detectMOV(tm, tc):
    global avg,  img1,frame
    ret = False
    x,y=0,0
    if avg is None:
        avg = img1.copy().astype("float")
    else:
        cv2.accumulateWeighted(img1, avg, 0.5)
        frameDelta = cv2.absdiff(img1, cv2.convertScaleAbs(avg))
        thresh = cv2.threshold(frameDelta, tm,  255, cv2.THRESH_BINARY)[1]
        #cv2.imshow('th',thresh)
        contours,hierarchy = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    #    contours=cv2.drawContours(img,contours,-1,(0,255,0),2)
        for i in range(0,len(contours)):
            if len(contours[i]) > 0:
                 if cv2.contourArea(contours[i]) > tc:
                    rect = contours[i]
                    x, y, w, h = cv2.boundingRect(rect)
                    cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 0, 255), 2)
                    ret=True
                        
    return ret,x,y

   
def detect_face(frame,gray):
    global xW,yW,xC,yC
    faces = face_cascade.detectMultiScale(gray, scaleFactor=1.3, minNeighbors=5)
    xC,yC=xW,yW
    for x, y, w, h in faces:
        cv2.rectangle(frame, (x, y), (x + w, y + h), (0, 255, 0), 2)
        face = frame[y: y + h, x: x + w]
        try:
            xC,yC=x+w/2,y+h/2
            dX,dY=xC-xW,yC-yW
            if track:
                if(abs(dX)>tW or (abs(dY)>tW)):
                    tracking(dX,dY)
        except:
            xC,yC=xW,yW    
        
if __name__ == "__main__":

    avg=None
    tc=350     # Minimum area  moving detection
    tm=10      # Threshold vale to BINARY
    before = None
    tPos,pPos=0,0
    track=False
    fontFace =cv2.FONT_HERSHEY_SIMPLEX
    Red=(0,0,255)
    Blue=(255,0,0)
    Green=(0,255,0)
    TGT=['airplane','bird','kite']
    #capture = cv2.VideoCapture(1)
    #
    tMin,tMax=0,90  # minimum/Maximum setting for TILT 
    pMin,pMax=0,180 # minimum/Maximum setting for PAN
    move(tPos,0,TILT)
    move(pPos,0,PAN)
    wMax=50
    f_count=wMax

    f_all=True
    MODEL_NAME = 'Sample_TFLite_model'
    GRAPH_NAME = 'detect.tflite'
    LABELMAP_NAME = 'labelmap.txt'
    min_conf_threshold = 0.5
    #resW, resH =1280,720
    resW, resH =640,480
    imW, imH = int(resW), int(resH)
    use_TPU = False
    size=(resW, resH)
    Cx=int(resW/2)
    Cy=int(resH/2)
# Import TensorFlow libraries
# If tflite_runtime is installed, import interpreter from tflite_runtime, else import from regular tensorflow
# If using Coral Edge TPU, import the load_delegate library
    pkg = importlib.util.find_spec('tflite_runtime')
    if pkg:
        from tflite_runtime.interpreter import Interpreter
        if use_TPU:
            from tflite_runtime.interpreter import load_delegate
    else:
        from tensorflow.lite.python.interpreter import Interpreter
        if use_TPU:
            from tensorflow.lite.python.interpreter import load_delegate

    # If using Edge TPU, assign filename for Edge TPU model
    if use_TPU:
        # If user has specified the name of the .tflite file, use that name, otherwise use default 'edgetpu.tflite'
        if (GRAPH_NAME == 'detect.tflite'):
            GRAPH_NAME = 'edgetpu.tflite'

    # Get path to current working directory
    CWD_PATH = os.getcwd()

    # Path to .tflite file, which contains the model that is used for object detection
    PATH_TO_CKPT = os.path.join(CWD_PATH,MODEL_NAME,GRAPH_NAME)

    # Path to label map file
    PATH_TO_LABELS = os.path.join(CWD_PATH,MODEL_NAME,LABELMAP_NAME)

    # Load the label map
    with open(PATH_TO_LABELS, 'r') as f:
        labels = [line.strip() for line in f.readlines()]

    # Have to do a weird fix for label map if using the COCO "starter model" from
    # https://www.tensorflow.org/lite/models/object_detection/overview
    # First label is '???', which has to be removed.
    if labels[0] == '???':
        del(labels[0])

    # Load the Tensorflow Lite model.
    # If using Edge TPU, use special load_delegate argument
    if use_TPU:
        interpreter = Interpreter(model_path=PATH_TO_CKPT,
                                  experimental_delegates=[load_delegate('libedgetpu.so.1.0')])
        print(PATH_TO_CKPT)
    else:
        interpreter = Interpreter(model_path=PATH_TO_CKPT)

    interpreter.allocate_tensors()

    # Get model details
    input_details = interpreter.get_input_details()
    output_details = interpreter.get_output_details()
    height = input_details[0]['shape'][1]
    width = input_details[0]['shape'][2]
    W,H = width,height
    xW,yW =int( W/2),int(H/2)
    tW=W/80         # minimum offcenter distance

    floating_model = (input_details[0]['dtype'] == np.float32)

    input_mean = 127.5
    input_std = 127.5
    move(tPos,20,TILT)
    move(pPos,120,PAN)
    # Initialize frame rate calculation
    frame_rate_calc = 1
    freq = cv2.getTickFrequency()

    # Initialize video stream
    videostream = VideoStream(resolution=(imW,imH),framerate=30).start()
    time.sleep(1)
    frame_rate = 24.0 # フレームレート
    now=datetime.datetime.now().strftime("%Y%m%d_%H%M")
    fmt = cv2.VideoWriter_fourcc('m', 'p', '4', 'v') # ファイル形式(ここではmp4)
    writer = cv2.VideoWriter('SV_'+now+'.mp4', fmt, frame_rate, size) # ライター作成
    frames=0
    #for frame1 in camera.capture_continuous(rawCapture, format="bgr",use_video_port=True):
    while True:
        now=datetime.datetime.now().strftime("%Y%m%d_%H:%M:%S")
        # Start timer (for calculating frame rate)
        t1 = cv2.getTickCount()

        # Grab frame from video stream
        frame1 = videostream.read()

        # Acquire frame and resize to expected shape [1xHxWx3]
        frame = frame1.copy()
        frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
        frame_resized = cv2.resize(frame_rgb, (width, height))
        input_data = np.expand_dims(frame_resized, axis=0)

        # Normalize pixel values if using a floating model (i.e. if model is non-quantized)
        if floating_model:
            input_data = (np.float32(input_data) - input_mean) / input_std

        # Perform the actual detection by running the model with the image as input
        interpreter.set_tensor(input_details[0]['index'],input_data)
        interpreter.invoke()

        # Retrieve detection results
        boxes = interpreter.get_tensor(output_details[0]['index'])[0] # Bounding box coordinates of detected objects
        classes = interpreter.get_tensor(output_details[1]['index'])[0] # Class index of detected objects
        scores = interpreter.get_tensor(output_details[2]['index'])[0] # Confidence of detected objects
        #num = interpreter.get_tensor(output_details[3]['index'])[0]  # Total number of detected objects (inaccurate and not needed)
        # Draw framerate in corner of frame
        msg='FPS: {0:.2f}'.format(frame_rate_calc)
        msg = msg + ' Track:'+str(track)+ ' F:' + str(frames) + ' T:'+ str(f_all) + ' ' + now
        cv2.putText(frame,msg,(30,50),cv2.FONT_HERSHEY_SIMPLEX,0.8,(255,255,0),1,cv2.LINE_AA)
        # Loop over all detections and draw detection box if confidence is above minimum threshold
        for i in range(len(scores)):
            object_name = labels[int(classes[i])] # Look up object name from "labels" array using class index
            if f_all or (object_name in TGT):
                if ((scores[i] > min_conf_threshold) and (scores[i] <= 1.0)):
                    # Get bounding box coordinates and draw box
                    # Interpreter can return coordinates that are outside of image dimensions, need to force them to be within image using max() and min()
                    ymin = int(max(1,(boxes[i][0] * imH)))
                    xmin = int(max(1,(boxes[i][1] * imW)))
                    ymax = int(min(imH,(boxes[i][2] * imH)))
                    xmax = int(min(imW,(boxes[i][3] * imW)))
                    x,y=Cx,Cy
                    if (xmax-xmin)*(ymax-ymin)<10000:
                        x=xmin+int((xmax-xmin)*0.5)
                        y=ymin+int((ymax-ymin)*0.5)
                        if f_all:
                            # Draw label
                            label = '%s: %d%%' % (object_name, int(scores[i]*100))
                            labelSize, baseLine = cv2.getTextSize(label, cv2.FONT_HERSHEY_SIMPLEX, 0.7, 2) # Get font size
                            label_ymin = max(ymin, labelSize[1] + 10) # Make sure not to draw label too close to top of window
                            cv2.rectangle(frame, (xmin,ymin), (xmax,ymax), (10, 255, 0), 2)
                            cv2.rectangle(frame, (xmin, label_ymin-labelSize[1]-10), (xmin+labelSize[0], label_ymin+baseLine-10), (255, 255, 255), cv2.FILLED) # Draw white box to put label text in
                            cv2.putText(frame, label, (xmin, label_ymin-7), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 0), 2) # Draw label text
                        else:
                            x=xmin+int((xmax-xmin)*0.5)
                            y=ymin+int((ymax-ymin)*0.5)
                            cv2.circle(frame,(x,y),4,color=Green,thickness=1)
                            cv2.circle(frame,(x,y),10,color=Green,thickness=1)
                            cv2.circle(frame,(x,y),16,color=Green,thickness=1)
                            frames=frames+1
                            writer.write(frame)
                            f_count=wMax
                        dW = Cx - x
                        dH = Cy - y
                        msg='dW:'+str(dW)+' dH:'+ str(dH)
                        cv2.putText(frame, msg, (30, 80), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 0, 0), 1) 
                        #msg=tgt_track(track,dH,dW,tH,tW,ptz,moverequest)
                        msg=tracking(dW,dH)

        # All the results have been drawn on the frame, so it's time to display it.
        cv2.imshow('Object detector', frame)
        if f_count>0 and f_count !=wMax:
            if not f_all:
                writer.write(frame)
        f_count=f_count-1
        # Calculate framerate
        t2 = cv2.getTickCount()
        time1 = (t2-t1)/freq
        frame_rate_calc= 1/time1
        # Press 'q' to quit
        k=cv2.waitKey(1) & 0xFF
        key(k)       
        if k == ord('q'):
            break
        for i in range(5):
            frame1 = videostream.read()
    # Clean up
    cv2.destroyAllWindows()
    videostream.stop()
    if writer is not None:
        writer.release()
    move(tPos,10,TILT)
    move(pPos,90,PAN)
    GP.stop()
    print('Finish!')

WP Mail SMTP利用をしてWordPressからメールを送信

プラグインWP Mail SMTPをインストール。設定の手順は、WP Mail SMTPのドキュメントを参照。

WP Mail SMTPのドキュメント:

https://wpmailsmtp.com/docs/how-to-set-up-the-gmail-mailer-in-wp-mail-smtp/#create-app

Gmailを利用して、上記のURLを参照しながら設定を完了させ、無事利用できるようになった。Google APIを利用するためのクライアントIDとクライアントシークレットをGoogleから取得することが必要。(手順は、上記URLのドキュメントに詳しく書かれている)

サーボモータを利用したPan/Tiltカメラ台の制御(pigpioの利用)

こんな記事を発見:RPi.GPIOよりもpigpioの方が精度が良いらしい。

「RPi.GPIO と pigpio のパルス幅の精度を測定」

テストのコード

#!/usr/bin/python
# -*- coding: utf-8 -*-
import time
import pigpio
PAN=27
GP = pigpio.pi()
GP.set_mode(PAN, pigpio.OUTPUT)
GP.set_servo_pulsewidth(PAN, 500)   # gpio18   500us

time.sleep(60)
GP.stop()

実測結果

標準偏差が348nSと、OSのオーバーヘッドがないArduinoと比較すると2桁程大きいが、サーボモータの制御には十分な精度が得られそう、、、

サーボモータを利用したPan/Tiltカメラ台の制御(GPIOライブラリーでは正確な制御は無理?)

サーボモータでPan/Tiltカメラ台を制御する実験をやってみたが、意図した方向から少し外れては正しい方向へカメラ台が回転する現象が頻発している。ステッピングモータの時には、このような現象に遭遇した記憶がない。サーボモータは、制御信号のパルス幅に対応した角度まで回転するように作られているので、意図しない回転の原因は、次のように推定できる。

  • 制御信号のパルス幅がバラついている
  • サーボモータの精度が悪い(制御信号のパルスに正確に追従していない)
  • その他?(上記の両方、またはその他の原因)

安価なサーボモータを使っているので、最初はサーボモータの精度の問題と思ったりしたが、制御信号のパルス幅をオシロで観測してみることにした。オシロには測定した値の統計情報を表示する機能があり、パルス幅の評価にはこれを利用することにした。

サーボモータの制御は、次のPythonスクリプトでRaspberry pi4bのGPIOからPWM信号を発生させて行っている。500HzでDuty50%の矩形波を生成。

#GPIOの初期設定
import RPi.GPIO as GPIO
GPIO.setwarnings(False)
GPIO.setmode(GPIO.BCM)
PAN=27
GPIO.setup(PAN, GPIO.OUT)
PWM_pan = GPIO.PWM(PAN, 500)
PWM_pan.start(0.0)

PWM_pan.ChangeDutyCycle(50)
while True:
    pass

オシロで観測した波形と統計情報

赤枠部分に表示された情報を信用すると、パルス幅の平均値が1.08mS、最大値が2.34mS、標準偏差が88.96μなどどなっている。ここで、利用したサーボモータは0.5mSから2.5mSの制御信号のパルス幅に対応して、270度の範囲で回転することになっている。回転角度を1度変更するにために必要なパルス幅の変化は、約4.7μS(=2mS/270)。ところが、観測したパルス幅の標準偏差が88.96μということなので、サーボモータが制御信号に忠実に反応した場合、回転角度が数度以上変動することになる。実際に、この制御信号をサーボモータへ与えると、モータは静止せずに微動を繰り返す状態となった。

制御パルス生成にArduino Microを用いた場合

PIN6からPWM信号を生成

int PAN = 6;

void setup() {
  pinMode(PAN, OUTPUT);
  analogWrite(PAN,128);
}

void loop() {
}

オシロで観測した様子

なんと標準偏差が1桁のnSオーダー。Raspberry Pi4の1/1000以下となりました。

サーボモーターを用いたPan/Tiltカメラマウント

以前、ステッピングモータでPan/Tilt制御可能なカメラマウントを作ってみましたが、今回はサーボモータを利用してみました。サーボモータは、手軽にドライブできる利点はありものの、角度の情報を取り出す仕組がないという利用目的次第では致命的な欠点もあることに気が付きました。特にTilt方向が問題で、気を付けないとマウントに搭載しているカメラが座台と干渉し、カメラなどを破損する可能性があります。

ステッピングモータでPan/Tiltの制御し、画像の重心を追尾する実験の様子。

Pan/Tiltの台座

Pan/Tiltの台座を購入してみました。ネジが曲がっていたり数が合わないのは想定内?パーツの不具合を補うため、3Dプリンターでパーツを作る。

組み立ての途中の様子。組み立ててみたら回転の動きが少し怪しい。ベアリングの精度の問題か?この上に、Tilt制御用のサーボを搭載する予定。