Arduinoで掃除しないルンバを作った

これは何?

f:id:inajob:20180331231312p:plain

ロボット作りの習作ということで、Arduinoを使っていわゆるルンバみたいな部屋の中を自律的に走行するロボット(ただし掃除はしない)を作ってみました。

 

材料

  • Arduino
  • モーターとギアボックス
  • タイヤ
  • モータードライバ(TA7291P)
  • 距離センサー(HC-SR04)
  • ブレッドボード
  • 9V電池
  • 3Dプリンタとフィラメント

筐体の作成

今回は作りながら構成を考えるということで、まずはこういうユニバーサルボードを作ってみます。

穴の大きさは11mm。僕がよく作るねじの直径が10mmなので、そのねじがうまく入るような大きさにしました。

f:id:inajob:20180331224926p:plain

ギアボックスと3mmのボルトとナットでで固定します。この時点でかなりそれっぽい!

f:id:inajob:20180331224834p:plain

OpenSCADでねじを作ります。

f:id:inajob:20180331225129p:plain

Fusion360でこういうのを作ります。

Arduino固定用パーツ

f:id:inajob:20180331225232p:plain

ブレッドボード固定用パーツ

f:id:inajob:20180331225321p:plain

9V電池固定用パーツ

f:id:inajob:20180331225703p:plain

 

で、こんな風に上にArduinoを固定します。

f:id:inajob:20180331225421p:plain

9V電池はこんな風に固定する

f:id:inajob:20180331225752p:plain

回路

下の記事を参考にして、モータードライバを制御しました。今回は両輪別々に制御したかったので、同じ回路を2つ並べました。

shangtian.hatenablog.com

 

超音波センサーについては下記記事を参考に。

deviceplus.jp

 

(この後紹介するスピーカーも含めた回路図です)

f:id:inajob:20180401225137p:plain

 

 

プログラムの作成

ほとんどプログラムも前述の記事の真似です。

自立制御の部分はテキトーに作りました。

50cm先に障害物を発見すると、バックして左に少し回転してまた前進します。

const int motor1A = 2;
const int motor1B = 3;
const int motor2A = 4;
const int motor2B = 5;
const int motorPwm = 6;

const int echoPin = 8;
const int trigPin = 7;

enum Mode{modeLeft, modeRight, modeForward, modeBackward};
Mode mode = modeForward;

void back(){
  digitalWrite(motor1A, HIGH);
  digitalWrite(motor1B, LOW);

  digitalWrite(motor2A, LOW);
  digitalWrite(motor2B, HIGH);
}
void backward(){
  digitalWrite(motor1A, HIGH);
  digitalWrite(motor1B, LOW);

  digitalWrite(motor2A, LOW);
  digitalWrite(motor2B, HIGH);
}
void forward(){
  digitalWrite(motor1A, LOW);
  digitalWrite(motor1B, HIGH);

  digitalWrite(motor2A, HIGH);
  digitalWrite(motor2B, LOW);
}
void turnLeft(){
  digitalWrite(motor1A, HIGH);
  digitalWrite(motor1B, LOW);

  digitalWrite(motor2A, HIGH);
  digitalWrite(motor2B, LOW);
}
void turnRight(){
  digitalWrite(motor1A, LOW);
  digitalWrite(motor1B, HIGH);

  digitalWrite(motor2A, LOW);
  digitalWrite(motor2B, HIGH);
}

double getDistance(){
  double duration = 0;
  double distance = 0;

  digitalWrite(trigPin, LOW); 
  delayMicroseconds(2); 
  digitalWrite( trigPin, HIGH );
  delayMicroseconds( 10 );
  digitalWrite( trigPin, LOW );
  duration = pulseIn( echoPin, HIGH );
  if (duration > 0) {
    duration = duration/2;
    distance = duration*340*100/1000000;
    if(distance > 100){
      distance = 100;
    }
  }
  return distance;
}

void setup() {
  Serial.begin(9600);
  pinMode(motor1A, OUTPUT);
  pinMode(motor2A, OUTPUT);
  pinMode(motor1B, OUTPUT);
  pinMode(motor2B, OUTPUT);
  pinMode(motorPwm, OUTPUT);

  digitalWrite(motorPwm, LOW);

  pinMode( echoPin, INPUT );
  pinMode( trigPin, OUTPUT );

  forward();
}

int sp = 255;
int nearCount = 0;
int forceTurnCount = 0;
int forceBackCount = 0;

void loop() {
  switch(mode){
    case modeLeft: turnLeft(); break;
    case modeRight: turnRight(); break;
    case modeForward: forward(); break;
    case modeBackward: backward(); break;
  }

  // speed
  analogWrite(motorPwm,sp);

  double distance = getDistance();
  if(distance < 50){
    nearCount ++;
  }else{
    nearCount = 0;
  }

  if(nearCount > 10 && mode == modeForward){
    mode = modeBackward;
    forceBackCount = 100;
    Serial.println("detect object. back");
  }else if(mode == modeBackward){
    forceBackCount --;
    if(forceBackCount == 0){
      forceTurnCount = 30;
      mode = modeLeft;
    }
    Serial.println("back");
  }else if(mode == modeLeft){
    forceTurnCount --;
    if(forceTurnCount == 0){
      mode = modeForward;
    }
    Serial.println("left");
  }else{
    mode = modeForward;
    Serial.println("forward");
  }
}

 動画

感想 

3Dプリンタがあると、こういう物理的な工作が簡単にできました。

また今回は初めからすべてを設計するのではなく、ユニバーサルボード的なものを作って、後付けパーツで組み立てていくという方式にしたため、簡単に設計を試行錯誤することができました。

 

そして自律的に動くものをプログラムするのは難しいと感じました。今回はとても単純な仕組みで作りましたが、これ以上賢く作るためには、このようなルールをどんどん入れていくか、ロボットが空間を把握するような仕組みをプログラムするか、、 いずれにしても大変そうです。

また気が向いたら改良していこうと思います。

 

追記:話せるようにしてみる

ちょうど家にあったDFPlayerMiniを使って状況に応じてmp3を再生させるようにしてみました。

 おきまりのセリフをしゃべるだけだけど、これだけでかなり愛着がわくものになった気がします。

 

音声は下記サイトのものを利用しました。

soundeffect-lab.info

 

参考部品リンク

Arduino

つかったのはこちら

www.aitendo.com

 普通のならこれかな?

【永久保証付き】Arduino Uno

【永久保証付き】Arduino Uno

 

 

モーターとギアボックス

つかったのはこれ。もう欠品のようだ

www.aitendo.com

 これが同じもののように見える。タイヤも付いている。

 モータードライバ

つかったのはこれ。ディスコンらしい。

akizukidenshi.com

 Amazonにもあるが別のモータードライバを使うのがよさそう。

 距離センサー

akizukidenshi.com

 Amazonならこれかな?

HC-SR04 超音波距離センサーモジュール For Arduino

HC-SR04 超音波距離センサーモジュール For Arduino

 
DFPlayerMini

 

 

ブレッドボード 

akizukidenshi.com

 Amazonならこれかな?

月10ドルのKubernetesクラスタに無料で取得したドメインでアクセスできるようにする

概要

前回は$10/月のKubernetesクラスタを作成しました。

inajob.hatenablog.jp

ここに自分用のWebサービスなどをデプロイすれば個人サーバとして活用できるわけですが、いつまでもIPアドレスでアクセスするというのはダサいですね。 今回は無料でドメインを取得しKubernetes上のサービスをその名前でアクセスできるようにしてみます。

f:id:inajob:20180228185422p:plain

ドメインをとるのはちょっと・・・という人はcurlのオプションでヘッダをつけてアクセスすることでその挙動を確認することができます。(が今回の記事では触れません)

TKドメインの取得

トラケウのドメインである.tkは無料で取得できます。

.tk - Wikipedia

もちろんすでにドメインを持っている人はこのような怪しいドメインを取得する必要はありません

https://my.freenom.com

とれるかな、、 f:id:inajob:20180228175551p:plain いけそう! f:id:inajob:20180228175618p:plain kubernetesのmasterのIPアドレスを入れる(あとで変えるので何でもよいのですが、、) f:id:inajob:20180228175858p:plain いい感じに入力する f:id:inajob:20180228175946p:plain 下記からアクセスすると所有しているドメインの一覧が確認できる。 https://my.freenom.com/clientarea.php?action=domains

CloudFlareでワイルドカードDNSの登録

FreenomでもDNSサービスは提供しているのですが、CloudFlareのほうが操作性がよくワイルドカードDNSが利用できるのでこちらを使うことにします。

www.cloudflare.com 右上のAdd Siteからドメインを追加画面に遷移 f:id:inajob:20180228180457p:plain

一番左の無料のやつを選択 f:id:inajob:20180228180524p:plain

いったんそのままContinue f:id:inajob:20180228180615p:plain

nameserverを変えろと言われる。 f:id:inajob:20180228180653p:plain

freenomに行って指示通りにNameserverを変更する f:id:inajob:20180228180847p:plain

Cloudflareにて反映されるのを待つ f:id:inajob:20180228180930p:plain

ページを行ったり来たりしていると数分で反映された f:id:inajob:20180228181223p:plain

いまこのドメインにアクセスすると下記のようになる。 これは名前からサーバまではいけるがそのサーバの80番が開いていないということのようだ。 登録したIPが間違っていたかな? f:id:inajob:20180228181250p:plain

nghttpx-ingress-controllerがデプロイされているホストをを調べる f:id:inajob:20180228181410p:plain

worker01にデプロイされているので、そのIPに設定する(masterのIPを指定していました;) (通常のPodは10.x.xのようなクラスタ内IPが付与されますがnghttpx-ingress-controllerは.spec.hostNetwork=trueとなっているためホストのIPとなっています my-vps-kubernetes/rc-default.yaml at master · inajob/my-vps-kubernetes · GitHubf:id:inajob:20180228181657p:plain

アクセスできた! この"default backend -404" というのはKubernetes上のサービスから返却しているものです。

f:id:inajob:20180228181819p:plain

図にするとこんな感じ f:id:inajob:20180228182434p:plain

$ kubectl get pods -n kube-system
NAME                                         READY     STATUS    RESTARTS   AGE
default-http-backend-55c6c69b88-vlqcx        1/1       Running   0          1h <--- これです
etcd-kubernetes                              1/1       Running   0          1h
kube-apiserver-kubernetes                    1/1       Running   0          1h
kube-controller-manager-kubernetes           1/1       Running   0          1h
kube-dns-6f4fd4bdf-c2995                     3/3       Running   0          1h
kube-proxy-622sq                             1/1       Running   0          1h
kube-proxy-j5754                             1/1       Running   0          1h
kube-scheduler-kubernetes                    1/1       Running   0          1h
monitoring-grafana-8dd9cb57f-4ms82           1/1       Running   0          1h
nghttpx-ingress-controller-78749dc54-8wlxr   1/1       Running   0          29m
node-exporter-5294m                          1/1       Running   0          1h
prometheus-8694d8cdd8-vmw7h                  1/1       Running   0          1h
weave-net-r5jft                              2/2       Running   0          1h
weave-net-wm7sn                              2/2       Running   0          1h

Ingressを試す

ドメインの取得、DNSサーバへの登録が終わり、無事取得したドメインでKubernetesクラスタまで到達できることが確認できました。 次はIngressを試します。

Ingressを利用するとVirtualHostのようにHostヘッダの値で違うServiceにアクセスさせることができます。

nginxをデプロイして、Serviceも作成します。

$ kubectl run nginx --image=nginx
deployment "nginx" created
$ kubectl expose deployment nginx --port=80
service "nginx" exposed

Ingressはコマンドでは作れないので下記のようなテキストファイルを用意します

nginx-ingress.yaml

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: nginx-ingress
spec:
  rules:
  - host: nginx.inajob-test.tk
    http:
      paths:
      - path: /
        backend:
          serviceName: nginx
          servicePort: 80

上記をKubernetesにデプロイします。

$ kubectl apply -f nginx-ingress.yaml
ingress "nginx-ingress" created

cloudflareにAレコードを追加します f:id:inajob:20180228183320p:plain

やった!アクセスできました。 f:id:inajob:20180228183400p:plain

新しいサブドメインIngressを用意するたびにcloudflareにAレコードを追加するのが面倒なのでワイルドカードドメインを追加してみます。

丸で囲ったところに注意です。CloudFlareではCloudFlareを経由しキャッシュを返したり、SSL化したりするような機能がありますが、ワイルドカードドメインの場合はそれが使えないので明示的にOFFにしています。

f:id:inajob:20180228183707p:plain

f:id:inajob:20180228183635p:plain

これであとはIngressを用意するだけでサブドメインが増えていくようになります。

まとめ

ここまでできると月$10のクラスタでもそこそこつかえるKubernetesが構築できたのではないかと思います。

ということで、vultrでサーバを作りましょう! (この記事が役立った!という人はぜひこちらのリンクからお願いします。)

www.vultr.com

月10ドルで海外VPSでKubernetesを試してみる(kubernetes v1.9版)

VultrでのKubernetesセットアップ方法

以前書いた記事

inajob.hatenablog.jp

で反響があったのですが、今試すとずいぶんやり方が変わっているところがあるので、Kubernetes1.9のやり方をまとめました。

例によって下記からVultrに申し込んでもらうと僕に$10入ります。 (以前の記事で$10ゲットしたので、やる気が出てこの記事を書く気になりました! この記事読んで試してみたいと思った人はぜひ下記リンクからお願いします。僕へのご褒美になります)

www.vultr.com

以前の記事では1台のマシンで構築したのですが、さすがにつらかったので、今回は奮発して$10/monthで2台構成のクラスタを作成します。 この手順で作るkubernetesはアルファの機能を使っていたり、セキュリティ上まずい設定を指定しているところもあるので、このままインターネットにさらし続けるのは危険です。

今回はコマンドやmanifestをgit上にまとめました。 github.com

準備

f:id:inajob:20180228170636p:plain

サーバは上記のプランで2つ用意する。 OSはUbuntu 17.10 x64

master, worker共通のセットアップ手順

事前にパッケージは最新化しておく

# apt-get update && apt-get upgrade
#必要に応じてリブートする
# reboot
# git clone https://github.com/inajob/my-vps-kubernetes.git
# cd my-vps-kubernetes/
# sh init-scripts/setup.sh

init-scripts/setup.shの解説

#!/bin/sh

# dockerをインストール
apt-get install -y docker.io

# kubeadm kubelet kubectlのインストール
apt-get update && apt-get install -y apt-transport-https
curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add -
cat <<EOF >/etc/apt/sources.list.d/kubernetes.list
deb http://apt.kubernetes.io/ kubernetes-xenial main
EOF
apt-get update
apt-get install -y kubelet kubeadm kubectl

# swapの設定(非推奨だが非力なマシンなので設定します)
dd if=/dev/zero of=/swapfile count=2048 bs=1M
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
echo "/swapfile   none    swap    sw    0   0" >> /etc/fstab

#kubeletの設定ファイルを作るためにkubeadm initを実行(実際は失敗する)
kubeadm init

# kubeletの設定を変更しswapがonの状態でも起動できるようにする
sed -i 's/\/usr\/bin\/kubelet/\/usr\/bin\/kubelet --fail-swap-on=false /' /etc/systemd/system/kubelet.service.d/10-kubeadm.conf

# kubeadmのお片づけをする(これをしないとこのあとのkubeadm initが実行できない)
kubeadm reset

# kubeletの設定変更を反映する
systemctl daemon-reload
systemctl restart kubelet

masterのセットアップ手順

# kubeadm init --ignore-preflight-errors=Swap

--ignore-preflight-errors=Swapは本来Swapが有効だとこのコマンドは失敗するのだが、それを無視して進めるようするためのオプションです。

kubeadm join .... といったworker接続用の文字列が出るのでメモ帳などにコピーしておきます。

workerのセットアップ手順

さきほどコピーしたkubeadm join .... の末尾に同様に--ignore-preflight-errors=Swapをつけて実行します。

# kubeadm join ....--ignore-preflight-errors=Swap

手元のラップトップ

kubectlをインストールしておきます。

SCPでmasterサーバのadmin.confを持ってきます。

$ scp root@masterサーバのアドレス:/etc/kubernetes/admin.conf ./admin.conf

ラップトップ内にmasterサーバへのプロキシを作成します。

$ kubectl --kubeconfig=:admin.conf proxy  --port=8080 --disable-filter=true --accept-hosts='^*$' --reject-paths='^$'

nodeを確認します

$ kubectl get nodes
NAME         STATUS     ROLES     AGE       VERSION
kubernetes   NotReady   master    4m        v1.9.3
worker01     NotReady   <none>    3m        v1.9.3

CNIネットワークを指定していないのでNotReadyのままですが2台のサーバでクラスタが組めていることが確認できます。

Weaveのデプロイ

CNIネットワークプラグインであるweaveををインストールします。 ほかのネットワークプラグインを使うこともできるので気になる人は公式ページから探してみてください

Using kubeadm to Create a Cluster | Kubernetes

$ kubectl apply -f "https://cloud.weave.works/k8s/net?k8s-version=1.9"

Readyになりました。

$ kubectl get nodes
NAME         STATUS    ROLES     AGE       VERSION
kubernetes   Ready     master    6m        v1.9.3
worker01     Ready     <none>    5m        v1.9.3

kube-dnsカスタム設定のデプロイ

$ git clone https://github.com/inajob/my-vps-kubernetes.git
$ cd my-vps-kubernetes/
$ kubectl apply -f manifests/dns/
configmap "kube-dns" created

これの中身は下記です

apiVersion: v1
kind: ConfigMap
metadata:
  name: kube-dns
  namespace: kube-system
data:
  upstreamNameservers: |
    ["8.8.8.8", "8.8.4.4"]

ubuntu固有の問題らしく kube-dns copies 127.0.0.35 from host's /etc/resolv.conf, doesn't work · Issue #45828 · kubernetes/kubernetes · GitHub これを入れないとpodから外への名前解決が失敗します。

どうもUbuntuは内部にDNSサーバを持っているようで、127.0.0.53が/etc/resolv.confに書かれています。しかしPodの中からはこのIPに到達できないので、kube-dnsから外の名前を引くことができなくなるようです。 このConfigMapを入れることで外のDNSを明示的に指定しています。

ingress-controllerのデプロイ

$ kubectl apply -f manifests/ingress-controller/
deployment "default-http-backend" created
service "default-http-backend" created
serviceaccount "ingress" created
clusterrole "ingress-clusterrole" created
role "ingress-role" created
rolebinding "ingress-role-binding" created
clusterrolebinding "ingress-clusterrole-binding" created
deployment "nghttpx-ingress-controller" created
service "nginhttpx-health" created

今後のためにingress-controllerをデプロイします。

prometheus, node-exporter, grafanaのデプロイ

$ kubectl apply -f manifests/monitoring/
deployment "monitoring-grafana" created
service "monitoring-grafana" created
daemonset "node-exporter" created
configmap "prometheus-config" created
clusterrole "prometheus" created
serviceaccount "prometheus" created
clusterrolebinding "prometheus" created
deployment "prometheus" created
service "prometheus" created

負荷状況のメトリクスを取得、可視化するために追加します。

動作確認

Pod一覧

$ kubectl get pods -n kube-system
NAME                                         READY     STATUS    RESTARTS   AGE
default-http-backend-55c6c69b88-vlqcx        1/1       Running   0          1m
etcd-kubernetes                              1/1       Running   0          10m
kube-apiserver-kubernetes                    1/1       Running   0          9m
kube-controller-manager-kubernetes           1/1       Running   0          9m
kube-dns-6f4fd4bdf-c2995                     3/3       Running   0          10m
kube-proxy-622sq                             1/1       Running   0          9m
kube-proxy-j5754                             1/1       Running   0          10m
kube-scheduler-kubernetes                    1/1       Running   0          9m
monitoring-grafana-8dd9cb57f-4ms82           1/1       Running   0          43s
nghttpx-ingress-controller-78749dc54-jl4nv   1/1       Running   0          1m
node-exporter-5294m                          1/1       Running   0          43s
prometheus-8694d8cdd8-vmw7h                  1/1       Running   0          43s
weave-net-r5jft                              2/2       Running   0          4m
weave-net-wm7sn                              2/2       Running   0          4m

PrometheusのUIを確認

Prometheusは固定のポートでexposeしてあります。

http://masterまたはworkerのIPアドレス:30090/

GraphanaのUIを確認

Graphanaも固定のポートでexposeしてあります。

http://masterまたはworkerのIPアドレス:30091/

grafanaの設定方法は 月5ドルの海外VPSでKubernetesを試してみる - inajob's blog の記事のままです。参考にしてください。

f:id:inajob:20180228174920p:plain

next

このリポジトリではingress-controllerもデプロイしています。 ドメインを持っている人は、DNSの設定とIngressリソースのデプロイを行うことで、オリジナルドメインのサービスを提供することができます。

それについては次の記事に記載しています。

inajob.hatenablog.jp

ESP-WROOM-02(ESP8266)で和音を鳴らしてみた

ESP8266 Arduinoで和音を鳴らしてみた。
まだ不可解な現象がありますが、ソフトウェアPWMの特性だということにして、これ以上深堀するのはやめます。

回路

ESP8266の4番ピンにトランジスタとスピーカをつなげています。
試していないけど、イヤホンジャックなどをコンデンサ通してつなげても動くと思います。

ソフトウェア

ESP8266のArduinoを使って和音を鳴らしてみようと思います。
矩形波の場合のみうまく動くことを確認しました。(ノコギリ波にするとうまく動かないことも確認しました)

PWMを32kHz周期に設定し、timer0の割り込みを10000クロック後(160MHzの場合は16kHz周期)に設定します。
ndという変数にタイマ割り込みのたびにdd足していきます。これがオシレータです。
矩形波を作りたいのでndの16ビット目が0か1かで0か256を返すような計算式を作ります。


((nd>>15)&1 == 1)?0:256

これをAnalogWriteすることで音が鳴ります。
音階を作るには少し計算をする必要があります。
ndの16ビット目をみているのでddが1の場合は16K/2^16 Hzの音が鳴るはず(0.24414..という音にならない周波数だけど)
で、ddを変数として意図した周波数を鳴らすための数式は

f:id:inajob:20180225210000p:plain

となる。
式変形して

f:id:inajob:20180225210059p:plain

これを音階ごとに計算すればよい。こうやって計算した値をtonesという変数に用意しています。

和音が鳴らしたいので上記のような変数を4つばかり配列として作成して、足し算します。
AnalogWriteは0-1024までなのでうまくその範囲に収まるように調整します。

以上で和音が作成できます。
ソースコードはこんな感じ。

 

#include <ESP8266WiFi.h>
#include <SoftwareSerial.h>
SoftwareSerial mySerial(2, 3); // RX, TX


const unsigned int tones[] = {
0,
1802  , // A  1
1909  , // A+ 2
2023  , // B  3
2143  , // C  4
2271  , // C+ 5
2406  , // D  6
2549  , // D+ 7
2700  , // E  8
2861  , // F  9
3031  , // F+ 10
3211  , // G 11
3402  , // G+12
3604  , // A 13
3819  , // A+14
4046  , // B 15
4286  , // C 16
4541  , // C+17
4811  , // D 18
5098  , // D+19
5401  , // E 20
5722  , // F 21
6062  , // F+22
6422  , // G 23
6804  , // G+24
7209  , // A 25
7638  , // A+26
8092  , // B 27
8573  , // B+28
};

const unsigned int melody[3][16] = {
{ 4, 6, 8, 9,11,13,15,16,16,15,13,11, 9, 8, 6, 4 },
{ 8, 0,11, 0,15, 0,18, 0,13, 0, 9, 8, 0, 4, 9, 8 },
{16, 0,20, 0,23, 0, 0,28, 0,27, 0,23, 0,20, 0, 0 },
};
int pos = 0;

volatile unsigned int nd[4] = {0,0,0,0};
volatile unsigned int dd[4] = {0, 0, 0, 0};
volatile unsigned int out;

void timer0_ISR (void) {
  timer0_write(ESP.getCycleCount() + 10000L ); // 160M/16K
  nd[0] += dd[0];
  nd[1] += dd[1];
  nd[2] += dd[2];
  nd[3] += dd[3];

  out = (((nd[0] >> 16)&1 == 1)?0:256) +
        (((nd[1] >> 16)&1 == 1)?0:256) +
        (((nd[2] >> 16)&1 == 1)?0:256) +
        (((nd[3] >> 16)&1 == 1)?0:256);
  analogWrite(4, out);
}

void setup() {
  WiFi.mode(WIFI_OFF);

  analogWriteFreq(32000); // pwm 32kHz

  noInterrupts();
  timer0_isr_init();
  timer0_attachInterrupt(timer0_ISR);
  timer0_write(ESP.getCycleCount() + 10000L ); // 160M/16K
  interrupts();

  Serial.begin(115200);
}

unsigned int counter;

void loop() {
  counter ++;
  if(counter%100000 == 0){
    dd[0] = tones[melody[0][pos]];
    dd[1] = tones[melody[1][pos]];
    dd[2] = tones[melody[2][pos]];
    pos ++;
    if(pos >= 16){
      pos = 0;
    }
    Serial.println("tick");
  }

}

 

動画

 

何かおかしい

矩形波はこれでよい感じに鳴ったのだが、ノコギリ波や、ほかの波形を鳴らそうとするとなぜか音にならない。
おそらくこれはESP8266のPWMがソフトウェアで実装されていることにより、頻繁に値が変わるような波形では速度が追い付いていないのではないか?と思われる。

何か気づいたことがある人は教えてほしいです。

3Dプリンタでフリスク太鼓たたき装置を作った

これは何?

手回しオルゴールのように、ハンドルをくるくる回すとリズムよくフリスクをたたいてくれる装置です

工夫したところ

毎度のことですが設計はFusino360を使いました。

f:id:inajob:20180204112359p:plain

床と壁は、別々の部品で作り、ずぼっとはめ込むスタイルにしました。

3DプリンタはZ方向にでかいものを作るのが遅い、ということと積層方向に弱いということから、壁をXY方向に作るのがよいと考えたからです。

穴の大きさと、差し込む部品の大きさについては、今回は全く同じ寸法で作りましたが、3Dプリントすると部品は少し大きめになり、穴は少し小さめに出力されるため、ヤスリで削らないとはまりませんでした。少し大きさを変えて出してもよいのですが、あまりガバガバになってしまうと固定できないので、ヤスリで調整するくらいがちょうどよいのかなと思っています。

f:id:inajob:20180204112719p:plain

ドラムのツメは、このように斜めの面取りをしてあります。これは3Dプリンタで造形する際に、うまく積層できるようにする工夫です。これをしないと空中にプラスチックを射出してしまいうまく造形ができないと思います。

f:id:inajob:20180204112941p:plain

シャフトと回転部分はこのように四角柱のシャフトを通しています。回転する軸受けの部分は四角い穴の開いた丸いリングをはめています。四角いシャフトにすることで連動して回転することができます。

f:id:inajob:20180204114008p:plain

3Dプリンタで出力するときは、いくつかの部品をまとめて出力しました。

プリント時間はかかるけれども、待ってるだけで一気に部品が作れるので作業効率が上がります。

f:id:inajob:20180204114821p:plain

完成動画

もともとフリスクを置く予定はなかったのですが、プラスチックをたたいてもしょぼい音しか出なかったので、「よく響く缶」ということで、手元にあったフリスクのケースを使いました。

 

 

 

感想

3Dプリンタでギアを使ったものが作りたい!と思って作り始めたので、その欲求は満たすことができました。3Dプリンタで作るとき特有の工夫もある程度わかったので、次やるときはもう少しスムーズにできるかなと思います。

こういう試行錯誤をするのにFusion360は非常に使いやすかったです。

タッチパネルと3Dプリンタで激安自作キーボードを作ってみた。

これは何?

Arduinoを使ったゲーム機などを作っているときにキーボードを搭載したくなることがあります。

PS/2接続のキーボードをつなげるというのもよいのですが、できれば筐体に収まるようなかわいらしいキーボードが欲しいなと思っていました。

もちろんタクトスイッチをたくさん並べてオリジナルの小型キーボードを作るのもよいのですが、、、部品もたくさん必要だし、大量のはんだ付けが必要です。制御も面倒です。

 

そこで今回はもっと手軽に作れるキーボードを考えてみました。

 

タッチパネル

今回使ったのはこれ。

www.aitendo.com

aitendoで数百円で買えます。

これはいわゆるNintendo DSとかのタッチパネル部分などに使われているような感圧式のタッチパネルです。ふつうはディスプレイと重ねて使用するものですが、今回はこのタッチパネルだけを利用します。

 

このタッチパネルは専用ICでX,Y座標を取り出すこともできますが、中身は単純な可変抵抗なので、ArduinoだけでもX,Y座標を取り出すことができます。

参考:タッチスクリーンをArduinoから使う – SF Bay Music Tech

 

 この記事とはあまり関係ないですが、ここまでの知識で、このような楽器のようなものが作れます。

 

キーパッドプラスチック

なんといったものか、、

こういうプラスチックです。

f:id:inajob:20180117232914p:plain

裏返すとこんな感じ

f:id:inajob:20180117233026p:plain

3Dプリンタの制約上、上下両側に凸になっている形状は作れないため、2つの部品を貼り合わせるように設計します。

これをタッチパネルの上に固定することで、キーボードのボタンを作成します。

ボタンを押し込むと、裏側の出っ張りが下がり、タッチパネルの特定の個所をタッチするわけです。

f:id:inajob:20180117234023p:plain

特定の位置、と言っても多少はぶれるのでArduinoで値をうまいこと丸め込んで、どのキーが押されたのかを特定します。

 

f:id:inajob:20180201004036p:plain

部品の4隅に穴をあけておいて、ねじとナットで締め付けることで固定します。

完成

 こんな感じ。

文字の表示はこれを使いました。

www.aitendo.com

 

ディスプレイの制御は下記ライブラリを使いました(Arduino IDEのライブラリマネージャからインストールできました)

github.com

 

仕組み上、まれに隣のキーが押されるような挙動をすることがあります。閾値やタイミングの調整で減らせるかなと思っていますが、ゼロにするのは難しそうです。

 

ソースコード

キーボードのタッチ位置の測定部分は現物合わせで調整した値なので、あまり参考にならないです。

 

#include <LCD_ST7032.h>
LCD_ST7032 lcd;

#define Y1 A0
#define X1 A1
#define Y2 A2
#define X2 A3
#define SPEAKER 9

int buttonTrig;
int xavg, yavg;

char charMap[3][10] = {
    {'\n', ' ', ',', 'M', 'N', 'B', 'V', 'C', 'X', 'Z'},
    {';', 'L', 'K', 'J', 'H', 'G', 'F', 'D', 'S', 'A'},
    {'P', 'O', 'I', 'U', 'Y', 'T', 'R', 'E', 'W', 'Q'}
  };

void reset(){
  pinMode(A0, OUTPUT);
  pinMode(A1, OUTPUT);
  pinMode(A2, OUTPUT);
  pinMode(A3, OUTPUT);

  digitalWrite(A0, LOW);
  digitalWrite(A1, LOW);
  digitalWrite(A2, LOW);
  digitalWrite(A3, LOW);
}

void setup() {
  buttonTrig = 0;
  xavg = yavg = 0;
  reset();
  pinMode(SPEAKER, OUTPUT);
  
  lcd.begin();
  lcd.setcontrast(24);

  lcd.setCursor(0, 0);  //LINE 1, ADDRESS 0
  lcd.cursor();
  lcd.print("Hello World");
}


void loop() {
  int x, y;
  reset();
  // x sensor
  digitalWrite(X1, HIGH);
  pinMode(Y1, INPUT);
  pinMode(Y2, INPUT);
  delay(1);
  x = analogRead(Y1);

  reset();
  
  // y sensor
  digitalWrite(Y1, HIGH);
  pinMode(X1, INPUT);
  pinMode(X2, INPUT);
  delay(1);
  y = analogRead(X1);

    
  if(x != 0 && y != 0){
    if(buttonTrig > 4){  // delay 4 frame to wait stable 
      xavg = ((xavg + x) >> 1);
      yavg = ((yavg + y) >> 1);
    }else{
      xavg = x;
      yavg = y;
    }

    if(buttonTrig == 8){
      tone(SPEAKER, 440 + x);
      
      byte yrow = 0;
      if(yavg > 100) yrow = 1;
      if(yavg > 300) yrow = 2;
      if(yavg > 500) yrow = 3;

      byte xrow = 0; // 
      if(xavg > 100) xrow = 1;
      if(xavg > 150) xrow = 2;
      if(xavg > 220) xrow = 3;
      if(xavg > 300) xrow = 4;
      if(xavg > 380) xrow = 5;
      if(xavg > 430) xrow = 6;
      if(xavg > 480) xrow = 7;
      if(xavg > 550) xrow = 8;
      if(xavg > 630) xrow = 9;
      if(xavg > 690) xrow = 10;

      if(charMap[yrow - 1][xrow - 1] == '\n'){
        lcd.clear();
        lcd.setCursor(0, 0);
      }else{
        lcd.write(charMap[yrow-1][xrow-1]);
      }
    }

    buttonTrig ++;
    
  }else{
    noTone(SPEAKER);
    buttonTrig = 0;
    xavg = 0;
    yavg = 0;
  }
}

Arduboy Writerを作ってみた

これは何?

Arduboy用の書き込み装置です。Arduboy( https://arduboy.com/ )とはパソコンを通じてゲームを書き換え、書き換えたのちは1種類のゲームだけが遊べるというArduinoベースの携帯ゲーム機です。

しかし、出先でもArduboyのゲームを切り替えたいと思い作ったのがこれです。

(ちなみにGamebuino ( Getting started - Gamebuino Wiki) という同じくArduinoベースの携帯ゲーム機はSDカードスロットを搭載しており、そこからゲームを切り替えることができます。)

 

f:id:inajob:20180105194749p:plain

構成

仕組みも何もRaspberryPi Zeroです。

去年スイッチサイエンスで購入したものですが、なんかもったいなくて道具箱にしまいっぱなしにしていました。このままだと積み基板になってしまうということで使いました。

そしてOLEDとタクトスイッチを3つ。最後に3Dプリンタで作ったケースにより構成されています。

f:id:inajob:20180106185903p:plain

回路

とても簡単ですが回路図です。スイッチのプルアップはRaspberryPi Zero内部で行うので直結です。

f:id:inajob:20180105195658p:plain

f:id:inajob:20180105195117p:plain

ソフトウェア

RaspberryPi Zeroといっても中身は普通のLinuxです。Pythonを使ってI2C経由でOLEDを操作します。また、ブート時に自動的に起動させるためにsystemdのserviceファイルを作ります。

この記事が参考になりました。

qiita.com

雑ですがコードはこんな感じ。

import RPi.GPIO as GPIO

import time
import os
import subprocess

import Adafruit_GPIO.SPI as SPI
import Adafruit_SSD1306

from PIL import Image
from PIL import ImageDraw
from PIL import ImageFont

# Input pins:
A_pin = 17
B_pin = 27
C_pin = 22

GPIO.setmode(GPIO.BCM)

GPIO.setup(A_pin, GPIO.IN, pull_up_down=GPIO.PUD_UP) # Input with pull-up
GPIO.setup(B_pin, GPIO.IN, pull_up_down=GPIO.PUD_UP) # Input with pull-up
GPIO.setup(C_pin, GPIO.IN, pull_up_down=GPIO.PUD_UP) # Input with pull-up

# Raspberry Pi pin configuration:
RST = 24
# Note the following are only used with SPI:
DC = 23
SPI_PORT = 0
SPI_DEVICE = 0

# 128x64 display with hardware I2C:
disp = Adafruit_SSD1306.SSD1306_128_64(rst=RST)

# Initialize library.
disp.begin()

# Clear display.
disp.clear()
disp.display()

# Load default font.
font = ImageFont.load_default()

# Create blank image for drawing.
# Make sure to create image with mode '1' for 1-bit color.
width = disp.width
height = disp.height
image = Image.new('1', (width, height))

# Get drawing object to draw on image.
draw = ImageDraw.Draw(image)

# Draw a black filled box to clear the image.
draw.rectangle((0,0,width,height), outline=0, fill=0)

buttons = [0,0,0];

hexes = os.listdir('./data')
position = 0;

def flashWrite():
  draw.rectangle((0,0,width,height), outline=0, fill=0)
  draw.text((0,30), 'writing...', font=font, fill=255);

  disp.image(image)
  disp.display()
  result = subprocess.call(['./leonardoUploader', '/dev/ttyACM0', './data/'+hexes[position]]);
  if result == 0:
    draw.text((0,30), 'success', font=font, fill=255);
  else:
    draw.text((0,30), 'error', font=font, fill=255);

  disp.image(image)
  disp.display()
  time.sleep(1)

def scan():
  global position
  global mode

  if not GPIO.input(A_pin):
    if buttons[0] == 0:
      position = position + 1
      if position >= len(hexes):
        position = 0;
    buttons[0] = 1
  else:
    buttons[0] = 0
  if not GPIO.input(B_pin):
    if buttons[1] == 0:
      position = position - 1
      if position < 0:
        position = len(hexes) - 1;
    buttons[1] = 1
  else:
    buttons[1] = 0

  if not GPIO.input(C_pin):
    if buttons[2] == 0:
      flashWrite();
    buttons[2] = 1
  else:
    buttons[2] = 0

def repaint():
  draw.rectangle((0,0,width,height), outline=0, fill=0)

  if buttons[0]:
    draw.rectangle((0,0,10,10), outline=255, fill=1);
  else:
    draw.rectangle((0,0,10,10), outline=255, fill=0);

  if buttons[1]:
    draw.rectangle((10,0,20,10), outline=255, fill=1);
  else:
    draw.rectangle((10,0,20,10), outline=255, fill=0);

  if buttons[2]:
    draw.rectangle((20,0,30,10), outline=255, fill=1);
  else:
    draw.rectangle((20,0,30,10), outline=255, fill=0);

  draw.text((0,30), hexes[position], font=font, fill=255);

  disp.image(image)
  disp.display()

scan()
repaint()

dirty = False
def sig(n):
  global dirty
  scan()
  dirty = True

GPIO.add_event_detect(A_pin, GPIO.BOTH, callback=sig, bouncetime=20);
GPIO.add_event_detect(B_pin, GPIO.BOTH, callback=sig, bouncetime=20);
GPIO.add_event_detect(C_pin, GPIO.BOTH, callback=sig, bouncetime=20);

try:
    while 1:
        if dirty:
          repaint()
          dirty = False
        #time.sleep(.01)

except KeyboardInterrupt:
    GPIO.cleanup()

肝心のArduboyにhexをアップロードする部分は

github.com

このソフトウェアを使いました。

起動時にプログラムを実行するために、systemdのservice定義を書きました。

[Unit]
Description=arduboyWriter
After=syslog.target

[Service]
Type=simple
WorkingDirectory=/home/pi/arduboyWriter
ExecStart=/usr/bin/python main.py
TimeoutStopSec=5
StandardOutput=null

[Install]
WantedBy = multi-user.target

 

これを/eyc/systemd/systemにコピーして systemctl enable arduboy-writerなどとして有効化できます。

ケース

Fusion360モデリングして、3Dプリンタで出力しました。3層の構造を4本のねじとナットでサンドイッチすることで固定しています。

Raspberry Pi ZeroのGPIOにはメスのピンヘッダをつけたので、ちょうどこの2層目がかっちりはまって、安定します。

f:id:inajob:20180105194916p:plain

f:id:inajob:20180105194949p:plain

まとめ

RaspberryPi ZeroでArduboyのゲームを書き込むのは、冷静に考えるとちぐはぐな解決策です。RaspberryPi ZeroのほうがArduboyの何倍も高スペックだからです。しかし、Arduboyは軽量で、ゲーム機として非常によくできているため、多少ちぐはぐであってもかまわないかな、ということで作ってみました。

世の中にはUSBホストになれるマイコンもあるようなので、そういったもので同じようなものが作れればよりスマートだと思います。(PIC32でできる気がするが、まだ僕にはそのスキルがありません・・勉強せねば・・)

RaspberryPi Zeroを使ったプロダクトは初めて作りましたが、この小ささはとても魅力的だと実感しました。小さいと3Dプリンタでケースを作るのも楽だし、成果物も小さく収められます。

ただ、Linuxをブートするため、起動時間が数十秒かかるというのがネックです。これも前述したUSBホストになれるマイコンを使うことで解決できそうです。