t-hom’s diary

主にVBAネタを扱っているブログ…とも言えなくなってきたこの頃。

3Dプリントの品質改善に着手~フィラメントドライヤー・古くなったホットエンド交換・ベッドレベル再調整・CURAパラメーターいじり

前回は3Dプリンターで印刷した造形物の加工について記事にしたが、今回はそもそもの造形自体の品質UPに取り組んでみた。


きっかけはこちら。
f:id:t-hom:20210821214655p:plain

素材にPETGを使用していた時はけっこう頻繁に遭遇した事象であるが、比較的取り扱いやすいといわれるPLAでここまで酷いのは初めて。。
これはちょっと真面目に向き合わないといけないと思い、色々とやってみた。

ベッドレベル調整

まず取り組んだのはベッドレベルの再調整。
これはプリンターのヘッドとベッド(造形台)の距離を調整する作業である。
買ったときに1度やったままずっと使ってきたけど、かなり面倒な作業なのでこれまで避けてきた。

写真撮り忘れたのでとりあえず手書きの絵で説明すると、四隅のネジを回してヘッドとベッドの間が印刷用紙1枚分の厚さになるように調節する。
f:id:t-hom:20210821215557p:plain

紙をスライドさせたとき、わずかに摩擦というか引っかかりを感じるが問題なくスライドできる程度に調整するとのこと。
これが非常に難しい。4隅のうち1つをいじれば、全体のバランスが変わって他の隅でちょうど良い隙間だったのが変化してしまうのだ。
よってあちらを立てればこちらが立たずという文字通りの状況に四苦八苦しつつ、どこかで妥協するという作業になる。

しかし真面目にやってみたところ、脅威の結果に!
なんと、造形物の底面におこげがない!!(もじゃってるのは次の課題なのでお目こぼしを)
f:id:t-hom:20210821220403p:plain

毎回やる必要はないものの、何回かに一回はやったほうが良いなと反省した。

最近ANYCUBICから上位モデルと思われるVyperという3Dプリンターが出ているのを知った。こちらはオートレベリング機能付きなのでネジを締めたり緩めたりという作業が必要ない。

まだまだレビューは少ないが、私が今から購入するとしたら間違いなく上記にする。。
まぁ既に持っている積層式を買い変えるくらいならまずは光造形式を優先すると思うけど。

CURAパラメーターいじり

以前から造形物の壁面と内容の間に隙間が空いてしまう事象に悩まされていたのだが、調べるとプリンターのホットエンドの温度設定を上げると改善することがあるとのこと。
要はより熱を加えることで、よりドロっとさせて接合力を高めるという理屈。また、壁面の印刷スピードを下げることで丁寧に造形するようにした。

温度は200℃から215℃へ、壁面の速度は50mm/sから40mm/sに。
f:id:t-hom:20210821221016p:plain

すると以下のとおり顕著な改善が見られた。
f:id:t-hom:20210821221138p:plain

ただ仕上がりはまだまだ要改善。
f:id:t-hom:20210821221243p:plain
f:id:t-hom:20210821221326p:plain

フィラメントドライヤー

ネットで検索すると綺麗な船模型がごろごろ出てくるので、これは明らかに私の印刷環境の異常だ。
何がまずいのかと色々調べていたところ、「大したことないだろ」と一蹴していた湿気問題が気になり始めた。
フィラメントは吸湿すると品質が落ちて印刷で様々な不具合がでる。

それで色々調べたところフィラメントドライヤーなるものが存在することを知り、Amazonで購入した。

50℃で6時間保管したので、多少は乾いたはず。

ただ印刷してみるとカッスカスでほとんどフィラメントが出てこないか、まともに印刷できない。
ひょっとして水分飛ばしすぎ?そんなはずは。。

ホットエンド交換

もうあとは目詰まりくらいしか考えられない。ひょっとすると今までフィラメント内の水分でなんとか液体度合が上がって出てたのをドライヤーがとどめになったのかもしれない。。
※フィラメントが乾燥すること自体は良いことである。目詰まりとの相互作用で崩れたかな。。というのは単なる私の素人考えである。

ついにこいつと向き合う時が来たのか。
f:id:t-hom:20210821221826p:plain

さっき爆発してきましたみたいなコゲ様であるが、これはこびりついたフィラメントが焦げたものだ。

幸いなことにANYCUBIC MEGA Sには最初からスペアのホットエンドが付属しているので根気があれば交換できる。

取り外しで参考にしたのがこちらの動画。
youtu.be

ただ私はケーブルタイは切らずにホットエンドに繋がった白いチューブごとするっと引き抜いて、新しいものもそのままするっと取り付けることにした。

取り付け完了。
f:id:t-hom:20210821222139p:plain

ここでミスったなと思ったのは作業の前にヘッドを高く上げすぎていたこと。上から六角レンチを回す必要があるけどヘッドが高すぎると上部の金具と干渉してレンチを回すスペースが無い。
交換するので下部のスペースを広くとろうとして失敗した。古いホットエンドのセンサーを外した後に気づいたけど電源を入れても本体がセンサー異常で高さ変更を受け付けてくれず、苦労した。

印刷結果

印刷前にCURAはちょっといじった。最初のレイヤーを遅くしたのとヘッドの温度を5℃下げて、210℃に。
f:id:t-hom:20210821223349p:plain

結果的に、過去1番くらいの仕上がりになった。
f:id:t-hom:20210821223039p:plain
f:id:t-hom:20210821223059p:plain
f:id:t-hom:20210821223121p:plain
f:id:t-hom:20210821223141p:plain

調整次第で綺麗になるもんだなぁ。

よく見かけるその船は何なの?

これは3D Benchyと呼ばれる有名なテスト用のモデルである。
どちらかといえば3Dプリンターが苦手とする形状を寄せ集めることで、これが綺麗に印刷できたら他もきっとうまくいくという指標になるので、印刷テストに最適なモデルだ。
f:id:t-hom:20210821224236p:plain

こちらからダウンロードできる。
www.3dbenchy.com

終わりに

今回は3Dプリンター関連の調整を諸々試してみた。
苦労した甲斐があってひとまず印刷テストはうまくいった。

購入当時はあっけなく印刷できてしまったのでとても驚いたけどあれから1年色々と失敗も重ねてきた。
なかなか一筋縄ではいかなくてもどかしいけれど、これくらい落とし穴というかちょっとした面倒くささがあった方がスキルとして差別化できて良い気もする。
今後も色々トライして工作の幅を広げていきたいと思う。

3Dプリントしたケースを表面処理してラッカースプレーで塗装してみた

今回は3Dプリンターで出力したケースにパテで積層跡を埋めてヤスリがけで表面を整え、ラッカースプレーで塗装してみた。

まずは完成品のご紹介。
f:id:t-hom:20210815101345p:plain
f:id:t-hom:20210815102840p:plain

対象はこのブログで何度か紹介しているDualShockをマクロキーボードとして使うアダプタのケース。
thom.hateblo.jp

前回書いたとおりDIPスイッチは失敗に終わったのでDipスイッチ用の穴が無いバージョンを印刷した。

この手の作業が初めての割には、とりあえず満足できるレベルに仕上がったと思う。

一方で面によっては厚塗りの影響による液だれが目立つ箇所もある。
f:id:t-hom:20210815103038p:plain

これが干渉を主目的とするプラモとか、他人のために作るものであれば大変残念な仕上がりということになるんだろうけど、自分で使う実用品なのでまぁ合格レベル。
少し手作り感がでてしまったかなという程度である。(と、自分に言い聞かせて面倒なやり直しを避けている。)

購入した道具

何せ塗装作業が初めてだったので、色々調べても結局どれを買えば良いか分からず、お試し用に色々買ってきた。
f:id:t-hom:20210815103912p:plain

これに加え、タミヤの塗装ブースとシゲマツの防毒マスクも購入。
トータルで3万円くらいの出費だったけど、結局使わなかったものもあるのであと数千円は安くできる。
今回は実際に使ったものだけAmazonリンクを張っておこうと思う。

表面処理

これは太陽光で1分、至近距離からの蛍光灯やLEDで2分で硬化するパテ。
シーリングライトでは遠すぎて固まらないのでそんなに急いで作業する必要もなく、とても便利。
ただ、私の場合は積層跡を完全に消し切るほど何度も表面処理を繰り返す根気は無かったのでヤスリだけで良かったかもしれない。

汚れ防止の為の使い捨て手袋。
パテを指で延ばす用途でも使うので伸縮性のあるタイプが使いやすいと思う。
ヘラも買ったんだけど今一つ伸ばしにくいので結局今回のケースでは指パテで充分だった。
もちろん、塗装にも使用する。

この神ヤスはかなり使いやすかった。今回の対象物とサイズ感が合ってるのも使いやすさに寄与していると思う。
普通の耐水ペーパーだと全体に均一に当てるのが難しいけどスポンジの弾力で面全体に圧力がかかるのでむらなくヤスリがけができる。。。とどこかで読みかじったんだけど普通の耐水ペーパーを使ってないので現時点では比較評価はしづらい。もしかしたら普通の耐水ペーパーの方が平面出たりして。。なんて考えてしまうので、次は普通ので試してみようと思う。

今回主に使ったのは以下の3つ。ただ塗装に失敗してやり直す際に#600で削ったのでそちらもちょっと使った。
#120/#240/#400


この工作マットは今回買ったのではなく以前から持っていたもの。
実際にカッティングでの出番は少ないもの、机に傷を付けたくない作業全般で重宝する。

塗装

塗装ブースは室内でスプレー塗装する場合は必須。屋外で段ボール箱の中に向けて塗装するなら別に要らないと思う。
高いけど、ちょうどクレカのポイントが貯まっていたのでヨドバシポイントに交換して買った。

この防毒マスクはあると快適に作業ができる。
これをつけてる間、ラッカースプレーに含まれるシンナー特有の甘い匂いが全くしない。吸収缶はついてこないので別売り。
そこまで気にしない人は模型用品コーナーとかホームセンターに置いてある粉塵用マスクでも良いと思う。

これはプライマーとサーフェイサーの2役をこなすグレー色のスプレー。プラサフと呼ばれる。
プライマーは下地と塗料の密着性を上げて剥がれを防止するもので、サーフェイサーは表面のキメを整えて仕上がりを美しくする為のもの。
材への塗料の浸透を抑えて、塗料をしっかり上に乗せる役割もあるようだ。

コバルトグリーンのツヤあり。

これはペイントベース。
私が買ったものはAmazonに無かったので代替品を紹介。
サイズの違いを除けば、機能的には似たようなものかなと思う。


以上が今回使ったもの。
使わなかったけどよりクオリティをアップさせるのに使えそうなのはコンパウンド・コンパウンドクロス・トップコートスプレーあたり。
今後も使わない可能性が高いという意味で完全に無駄になったのは、ポリエステルパテとパテヘラだけかな。

作業

まずは印刷。3Dプリンターの低面で発生したおこげが目立つ。
f:id:t-hom:20210815103646p:plain
1個印刷するのに約1時間かかるけど、色々失敗するだろうからということで隙間時間に印刷を繰り返した。

次にヤスリがけ。とりあえず一番荒い #120でおこげを落としていく。
f:id:t-hom:20210815114418p:plain
どうせ塗装するので落ち切らなくても良いけど、あまりに汚いままだと臭いものにフタ感がして気になるので。。

次に#240でサラサラっと磨いて、#400でもサラサラっと磨く。
頑張るのは#120だけで、#240と#400は順番に使ってけばそんなに時間かからなかった。

ここで光硬化パテを使用し、また#400で磨く。写真だと分かりづらいけど少し黄ばんでいる。
f:id:t-hom:20210815115206p:plain

パテと磨きを繰り返すとかなり積層跡を消せるらしいんだけど、面倒なので1回で済ませた。

次に塗装。プラサフを吹いたあと30分ほど置いて色を乗せていく。
f:id:t-hom:20210815115438p:plain

この時点でかなり失敗した。
まずプラサフが厚塗り過ぎて液だれ状態。さらに30分乾燥させることなくそのまま塗装に入ってしまった。

液だれするほど塗料がのってるので乾燥にも時間がかかり、乾いたあともこのとおり液体感が出てしまってる。
f:id:t-hom:20210815115644p:plain

薄塗りを重ねると綺麗に塗装できるという知識はあったけど、どうやったら薄塗りになるのか分かってなかった。。

これを#600番で下地が一部見えるほど削ってテイク2。9割は塗料乗ったままなのでサフ無しで色を重ねていく。

今度は良い感じ。(この面は)
f:id:t-hom:20210815120003p:plain


2度やってみて少しコツがつかめた。まず対象物から20cm離すという注意書きを舐めてはいけない。スプレーは霧で塗装するので近すぎると十分拡散されずに液で塗装している感じになる。逆に離れすぎると付着する前に乾燥してしまい、粒感がでて表面がザラつくらしい。
また、スプレーをシュっと移動させながら吹き付けることで一個所に付着する量を減らして薄塗りする。これを繰り返すことで綺麗な塗装になるようだ。

また、角に塗料が不足しがちなので、面よりも角から順に攻めた方が良いらしい。
面が十分塗れたあとに角を塗ると、余分な塗料が面について厚塗りになってしまうためだとか。

シンナー臭について

塗装が終わっても3時間くらいは室内に甘ったるいシンナー臭が残るので、家族がいる場合は屋内での作業は厳しいかもしれない。
3時間も臭うなら塗装中にマスクしても意味なくない?と思うかもしれないけど、メインは噴霧の吸い込みを防止するためなのでマスクはあったほうが良い。
また、ダストセンサーで計測したところ、塗装後30分は12μg/㎥と高値が出ていたので一応私はその間防毒マスクを着けていた。しっかり換気してて30分も経てば臭いはだいぶマイルドになってると思う。

健康被害については、業者が車の塗装等で長期間大量に吸い続けた場合に発生する程度のなので模型塗装くらいでそんなに神経質にならなくても良いという意見がある。

まぁ子供でも取り扱うものなので、そうだろうなと思う。

おわりに

今回塗装をしようと思ったきっかけは3つかある。

  • 3Dプリンターでどうしてもおこげが発生してしまうのでこれをカバーしたい。
  • 3Dプリンターで形状は自由に作れるようになったので、あとは色を自由にしたい。
  • 塗装の技術を身に着けることで、工作の幅を広げたい。

とにかく面倒くさそうなイメージがあったけど、やってみたら意外に楽しかった。
道具をしっかり揃えて、下調べをしておけばそれほど難しくはないと思うので、皆さんも興味があれば是非トライしてみて欲しいと思う。

以上

USBシリアル通信でArduinoから赤外線LEDを操作する ~ コーディングからケース作成まで

今回は以下の記事の続き。
thom.hateblo.jp

Arduino Unoから赤外線を操作するテストが完了したので、今回はこれをより小さなArduino Pro Microに移植し、ケースに収めて机の天板裏に固定する。
また、USBシリアル通信で送られたコマンドを元に赤外線信号を発信できるようにコードを改良する。

コード

今回は、普段使いのブルーグリーン・ステータス通知用の赤・LED_OFF・LED_ONの4種類をそれぞれ g・ r・ f・n という1文字コードで制御することにした。
これでPCからシリアルモニターで文字を送ることでLEDを制御できる。また、Raspberry PiにUSB接続すればPythonコードからLEDを制御できるようになる。

#include <IRremote.h>

void setup() {
    IrSender.begin(3, false);
    Serial.begin(9600);
}

void loop() {
    uint16_t led_bluegreen[67] = {8930,4420, 580,570, 530,570, 580,570, 530,570, 580,570, 530,570, 530,620, 530,570, 530,1720, 530,1670, 530,1670, 530,1720, 530,570, 530,1720, 480,1720, 480,1720, 530,1720, 480,620, 480,620, 530,1720, 480,620, 530,620, 480,620, 530,620, 480,620, 480,1720, 530,1720, 480,620, 530,1720, 480,1720, 480,1720, 530,1670, 580};
    uint16_t led_red[67] = {8880,4470, 580,570, 530,570, 530,570, 580,570, 530,570, 580,570, 530,570, 580,570, 530,1670, 530,1670, 580,1670, 530,1670, 530,570, 580,1670, 530,1670, 530,1670, 580,570, 530,570, 580,1670, 530,570, 530,620, 530,570, 530,570, 580,570, 530,1670, 580,1670, 530,570, 530,1720, 530,1670, 530,1670, 530,1720, 530,1670, 530};
    uint16_t turn_off[67] = {8880,4470, 530,570, 580,570, 530,570, 580,520, 580,570, 530,570, 580,570, 530,570, 580,1670, 530,1670, 530,1670, 580,1670, 530,570, 530,1670, 580,1670, 530,1670, 580,520, 580,1670, 530,570, 580,570, 530,570, 580,570, 530,570, 530,570, 580,1670, 530,570, 580,1620, 580,1670, 530,1670, 580,1670, 530,1670, 530,1670, 580};
    uint16_t turn_on[67] = {8880,4470, 530,570, 580,570, 530,570, 580,570, 530,570, 530,570, 580,570, 530,570, 580,1670, 530,1670, 530,1670, 580,1670, 530,570, 580,1620, 580,1670, 530,1670, 580,1620, 580,1670, 530,570, 580,570, 530,570, 580,570, 530,570, 530,570, 580,570, 530,570, 580,1670, 530,1670, 530,1670, 580,1670, 530,1670, 530,1670, 580};  // Protocol=NEC Address=0xEF00 Command=0x3 Raw-Data=0xFC03EF00 32 bits LSB first
    
    char key;
    if(Serial.available()){
    key = Serial.read();
      switch(key){
        case 'g':
          IrSender.sendRaw(led_bluegreen, sizeof(led_bluegreen) / sizeof(led_bluegreen[0]), 38);
          break;
        case 'r':
          IrSender.sendRaw(led_red, sizeof(led_red) / sizeof(led_red[0]), 38);
          break;
        case 'f':
          IrSender.sendRaw(turn_off, sizeof(turn_off) / sizeof(turn_off[0]), 38);
          break;
        case 'n':
          IrSender.sendRaw(turn_on, sizeof(turn_on) / sizeof(turn_on[0]), 38);
          break;
      }
    }
}

Pro Microへの移植

今回は以下の5V16MHzタイプを使用した。

私が購入したときは5つで3,300円だったけど今見たら5,300円。昨今の半導体不足の影響か、2,000円も値上がりしている。
まぁそうはいっても1つで3,000円近くする純正品に比べると1個1,000円はお手頃価格と言っても良いだろう。

Pro Microの5V16MHzはArduino Leonardoとある程度互換性がある。ボードに書き込む時の選択もLeonardoで良い。
注意点は3つ。

  • 一部のソフトと相性が悪いようで、私の場合はNZXTのCAMを落としてからでないと書き込みできない。
  • リセットボタンが無いので、バグったときのタイミングがシビア。私の場合は固めのワイヤーの先を輪にしてRSTとGNDをショートさせる道具にしている。デフォルトのブランクスケッチを開いて、書き込み開始した瞬間にRSTとGNDを2回ショートさせると、コンパイル完了した頃にリセットが完了してちゃんと書き込みできるようになるけど、リセットが早すぎると書き込み開始前にプログラムが開始してうまくいかないし、遅いと書き込みが始まったタイミングでまだリセット処理中で書き込み失敗する。ブランクスケッチで正常化させる理由はコンパイル時間が短くてタイミングをはかりやすいから。正常化した後は目的のコードを書きこむ。
  • 5Vきっちり出ない。4.6Vとかそれくらいだった。

テストワイヤ―を使用して動作確認。
f:id:t-hom:20210814083813p:plain

ちなみにテストワイヤ―は下図のように基盤のスルーホールに引っ掛けることができるワイヤ。
f:id:t-hom:20210814084431p:plain

はんだ付けする前に動作チェックできるので持っておくと超便利。

ケース設計

まずはどういう風に収めるか、並べてみる。
f:id:t-hom:20210814085111p:plain

そしてFusion360で設計。
裏が平らな基盤でもはんだ付けでもっこりするので、その部分だけ溝を作って対策。
f:id:t-hom:20210814085156p:plain

設計するときの計測はデジタルノギスがあると便利。Amazonで2~3,000円なのでなんでも良いから持っておくと重宝すると思う。
完全に寸法通りに作ると実際印刷したときにハマらないので、両側合わせて0.5ミリくらいのマージンを持たせると良いかと思う。1ミリだとグラつく。

さて、印刷してみたところ、綺麗に収まった。
f:id:t-hom:20210814085419p:plain

一発で成功するはずがないと思っていたのもあって、とりあえずテスト印刷して再設計するときにフタのことを考えるつもりだったが、面倒くさくなったのでケースはこれで完成ということにしてしまおう。ショート対策としては先日買ったポリイミドテープでカバーしとけば十分だろう。

そしてはんだ付け作業を済ませる。
f:id:t-hom:20210814090213p:plain

もともとLEDモジュールについてた足を外すのにかなり苦労して、さらにその跡に残った半田を一旦除去する作業で2時間くらいかかってしまった。。
はんだ吸い取り線でなかなか穴に残ったはんだが吸い取れず、かといって空気圧で吸い出すやつはもっと苦手なので電動のはんだ吸い取り器が欲しくなる。

配線はGND→GND、VCC→VCC、DAT→3となる。
f:id:t-hom:20210814090803p:plain

完成品

配線まで終わったらケース底にホットボンドを塗って基盤を押し付けて固定し、最後にポリイミドテープでカバーしておしまい。
f:id:t-hom:20210814090936p:plain

このテープは耐熱性があるので結構いろんなところの絶縁に使えて便利だし、この飴色がまた「専用の道具をつかった玄人の手抜き感」が出て満足度が高い。まあ素人なんだけど。

以上

PS4コントローラーをマクロキーボード化 / マクロ切替の別解

つい先日、DIPスイッチを使ってマクロキーボードのモード切替実装の記事を書いたのだが、結論から言ってこの方法は失敗だった。
thom.hateblo.jp

f:id:t-hom:20210810215813p:plain

計算外だったのはDIPスイッチの耐久性だ。
まだ多くても20回程度しか切り替えていないはずだけど、読み取り時に少し手で押さえてやらないと認識しないようになった。恐らく内部で接点が浮いてしまったのだろうと思う。
はんだ付けの熱が悪影響を及ぼしたのか、それともそもそもスイッチの耐久性が低いのかは分からないが、再度付け直す気にはならないので別のモード切り替えを試してみることにした。

今回はソフトウェア的な解決である。
あっさり出来てしまったのであっさり紹介しようと思う。
最初からこうしておけばよかったかもと思わなくもないが、当初はArduinoのメモリにデータを持たせるよりは物理スイッチで物理的に状態を持たせる方が手堅いようなイメージを持ってしまっていたのだ。つまりこれはこれで、必要な失敗だったと思う。

PS4コントローラーにはLEDがついていて、それをArduinoから制御できるのでこれをモード表示に使用することにした。
モードの切り替えはPSボタンと通常ボタンを組み合わせて行う用に設定。モード切り替え時にコントローラーを振動させることで、万が一操作ミスで切り替えてしまっても気づけるようにした。
あえて何もしないモードを設けることで誤操作防止もバッチリなのでこれでコントローラーはPCに繋ぎっぱなしで良くなる。

f:id:t-hom:20210810221643p:plain


コードは次のとおり。
モードごとの切り替えをどうしようか迷っていたが、関数をばっさり分けてしまえばなんてことはなかった。

#include <Keyboard.h>
#include <Mouse.h>
#include <PS4USB.h>

// Satisfy the IDE, which needs to see the include statment in the ino too.
#ifdef dobogusinclude
#include <spi4teensy3.h>
#endif
#include <SPI.h>

// #include "Arduino.h"
// #include "DFRobotDFPlayerMini.h"
// DFRobotDFPlayerMini myDFPlayer;

USB Usb;
PS4USB PS4(&Usb);

bool printAngle, printTouch;
uint8_t oldL2Value, oldR2Value;
uint8_t oldRightHatX, oldRightHatY;
uint8_t redundant;
uint8_t mode;

void setup() {
  if (Usb.Init() == -1) {
    while (1); // Halt
  }
  Mouse.begin();
  redundant = 1;
  mode = 0;
  /* Serial1.begin(9600);
  if (!myDFPlayer.begin(Serial1)) {  //Use softwareSerial to communicate with mp3.
    while(true);
  }
  myDFPlayer.volume(30);  //Set volume value. From 0 to 30
  */
}

void loop() {
  Usb.Task();
  if (PS4.connected()) {
    if (PS4.getButtonPress(PS)){
      if (PS4.getButtonClick(TRIANGLE)) {
        PS4.setRumbleOn(RumbleHigh);
        PS4.setLed(0,255,100);
        mode = 1;
      }
      if (PS4.getButtonClick(CROSS)) {
        PS4.setRumbleOn(RumbleHigh);
        PS4.setLed(Blue);
        mode = 0;
      }
      if (PS4.getButtonClick(CIRCLE)) {
        PS4.setRumbleOn(RumbleHigh);
        PS4.setLed(Red);
        mode = 2;
      }
    }

    switch(mode){
      case 1:
        mode1();
        break;
      case 2:
        mode2();
        break;
    }
  }
}

void mode2(){
  if (PS4.getButtonPress(CROSS)) {
    Keyboard.press('z');
  } else {
    Keyboard.release('z');
  }
  
  if (PS4.getButtonClick(SQUARE)) {
  }

  if (PS4.getButtonPress(UP)) {
    Keyboard.press(KEY_UP_ARROW);
  } else {
    Keyboard.release(KEY_UP_ARROW);
  }
  if (PS4.getButtonPress(RIGHT)) {
    Keyboard.press(KEY_RIGHT_ARROW);
  } else {
    Keyboard.release(KEY_RIGHT_ARROW);
  }
  if (PS4.getButtonPress(DOWN)) {
    Keyboard.press(KEY_DOWN_ARROW);
  } else {
    Keyboard.release(KEY_DOWN_ARROW);
  }
  if (PS4.getButtonPress(LEFT)) {
    Keyboard.press(KEY_LEFT_ARROW);
  } else {
    Keyboard.release(KEY_LEFT_ARROW);
  }

  if (PS4.getButtonPress(R1)) {
    Keyboard.press(KEY_LEFT_SHIFT);
  } else {
    Keyboard.release(KEY_LEFT_SHIFT);
  }
  
  if (PS4.getButtonClick(L1)) {
    Keyboard.press('x');
    delay(40);
    Keyboard.releaseAll();
  }

  if (PS4.getButtonClick(OPTIONS)) {
    Keyboard.press(KEY_ESC);
    delay(40);
    Keyboard.releaseAll();
  }
}
void mode1(){
  if (PS4.getButtonPress(PS)&& ((abs(128 - PS4.getAnalogHat(RightHatX)) + abs(128 - PS4.getAnalogHat(RightHatX))) >= 127)) {
    //TopRight
    if (PS4.getAnalogHat(RightHatX) >= 128 && PS4.getAnalogHat(RightHatY) < 128 ) {
      if (oldRightHatX + redundant < PS4.getAnalogHat(RightHatX) && oldRightHatY + redundant < PS4.getAnalogHat(RightHatY)) {
        //RightRotate
        Keyboard.press(KEY_LEFT_CTRL);
        Mouse.move(0, 0, 1);
        Keyboard.releaseAll();
      }
      if (oldRightHatX - redundant > PS4.getAnalogHat(RightHatX) && oldRightHatY - redundant > PS4.getAnalogHat(RightHatY)) {
        //LeftRotate
        Keyboard.press(KEY_LEFT_CTRL);
        Mouse.move(0, 0, -1);
        Keyboard.releaseAll();
      }
    }
    
    //BottomRight
    if (PS4.getAnalogHat(RightHatX) >= 128 && PS4.getAnalogHat(RightHatY) >= 128 ) {
      if (oldRightHatX - redundant > PS4.getAnalogHat(RightHatX) && oldRightHatY + redundant < PS4.getAnalogHat(RightHatY)) {
        //RightRotate
        Keyboard.press(KEY_LEFT_CTRL);
        Mouse.move(0, 0, 1);
        Keyboard.releaseAll();
      }
      if (oldRightHatX + redundant < PS4.getAnalogHat(RightHatX) && oldRightHatY - redundant > PS4.getAnalogHat(RightHatY)) {
        //LeftRotate
        Keyboard.press(KEY_LEFT_CTRL);
        Mouse.move(0, 0, -1);
        Keyboard.releaseAll();
      }
    }
    
    //BottomLeft
    if (PS4.getAnalogHat(RightHatX) < 128 && PS4.getAnalogHat(RightHatY) >= 128 ) {
      if (oldRightHatX - redundant > PS4.getAnalogHat(RightHatX) && oldRightHatY - redundant > PS4.getAnalogHat(RightHatY)) {
        //RightRotate
        Keyboard.press(KEY_LEFT_CTRL);
        Mouse.move(0, 0, 1);
        Keyboard.releaseAll();
      }
      if (oldRightHatX + redundant < PS4.getAnalogHat(RightHatX) && oldRightHatY + redundant < PS4.getAnalogHat(RightHatY)) {
        //LeftRotate
        Keyboard.press(KEY_LEFT_CTRL);
        Mouse.move(0, 0, -1);
        Keyboard.releaseAll();
      }
    }
    
    //TopLeft
    if (PS4.getAnalogHat(RightHatX) < 128 && PS4.getAnalogHat(RightHatY) < 128 ) {
      if (oldRightHatX + redundant < PS4.getAnalogHat(RightHatX) && oldRightHatY - redundant > PS4.getAnalogHat(RightHatY)) {
        //RightRotate
        Keyboard.press(KEY_LEFT_CTRL);
        Mouse.move(0, 0, 1);
        Keyboard.releaseAll();
      }
      if (oldRightHatX - redundant > PS4.getAnalogHat(RightHatX) && oldRightHatY + redundant < PS4.getAnalogHat(RightHatY)) {
        //LeftRotate
        Keyboard.press(KEY_LEFT_CTRL);
        Mouse.move(0, 0, -1);
        Keyboard.releaseAll();
      }
    }
  }
  oldRightHatX = PS4.getAnalogHat(RightHatX);
  oldRightHatY = PS4.getAnalogHat(RightHatY);

  int r2 = PS4.getAnalogButton(R2);
  int l2 = PS4.getAnalogButton(L2);
  if (r2) {
    Keyboard.press(KEY_DOWN_ARROW);
    if (r2 < 100) {
      delay(80);
    } else if (r2 < 255) {
      delay(24);
    } else {
      delay(12);
    }
    Keyboard.releaseAll();
  }
  if (l2) {
    Keyboard.press(KEY_UP_ARROW);
    if (l2 < 100) {
      delay(80);
    } else if (l2 < 255) {
      delay(24);
    } else {
      delay(12);
    }
    Keyboard.releaseAll();
  }
  /*
  if (PS4.getAnalogButton(R2)) {
    Keyboard.press(KEY_DOWN_ARROW);
    delay(256-PS4.getAnalogButton(R2));
    Keyboard.releaseAll();
  }
  
  if (PS4.getAnalogButton(L2)) {
    Keyboard.press(KEY_UP_ARROW);
    delay(256-PS4.getAnalogButton(L2));
    Keyboard.releaseAll();
  }
  */

  if (PS4.getButtonClick(PS)) {
    
  }
  
  if (PS4.getButtonClick(TRIANGLE)) {
    Keyboard.press('a');
    delay(100);
    Keyboard.releaseAll();
  }
  if (PS4.getButtonClick(CIRCLE)) {
    Keyboard.press(KEY_LEFT_CTRL);
    Keyboard.press(KEY_LEFT_SHIFT);
    Keyboard.press('1');
    delay(100);
    Keyboard.releaseAll();
  }
  if (PS4.getButtonClick(CROSS)) {
    Keyboard.press(KEY_LEFT_CTRL);
    Keyboard.press('d');
    delay(100);
    Keyboard.releaseAll();
  }
  if (PS4.getButtonClick(SQUARE)) {
  }

  if (PS4.getButtonPress(UP)) {
    Keyboard.press(KEY_PAGE_UP);
    delay(40);
    Keyboard.releaseAll();
  } if (PS4.getButtonClick(RIGHT)) {
    Keyboard.press(KEY_LEFT_CTRL);
    Keyboard.press('.');
    delay(100);
    Keyboard.releaseAll();
  } if (PS4.getButtonPress(DOWN)) {
    Keyboard.press(KEY_PAGE_DOWN);
    delay(40);
    Keyboard.releaseAll();
  } if (PS4.getButtonClick(LEFT)) {
    Keyboard.press(KEY_LEFT_CTRL);
    Keyboard.press(',');
    delay(100);
    Keyboard.releaseAll();
  }

  if (PS4.getButtonClick(L1)) {
    Keyboard.press(KEY_LEFT_CTRL);
    Keyboard.press(',');
    delay(100);
    Keyboard.releaseAll();
    //myDFPlayer.play(1);
  }
  if (PS4.getButtonClick(L3)) {
    
  }
  if (PS4.getButtonClick(R1)) {
    Keyboard.press(KEY_LEFT_CTRL);
    Keyboard.press('.');
    delay(100);
    Keyboard.releaseAll();
    //myDFPlayer.play(2);
  }
  if (PS4.getButtonClick(R3)) {
  }

  if (PS4.getButtonClick(SHARE)) {
  }
  if (PS4.getButtonClick(OPTIONS)) {
  }
  if (PS4.getButtonClick(TOUCHPAD)) {
  }
}

以上

Arduinoから赤外線を発信して机裏のLEDテープの色を変更する。

今回はArduinoを赤外線リモコンとして使用することでリモコン式LEDテープの色を変更してみた。

以下が作成した試作機。Arduino Uno入りのブレッドボードに赤外線送信モジュールと受信モジュールを指してジャンパーワイヤで繋いだだけの代物だ。
f:id:t-hom:20210809191429p:plain

作成の動機

私の部屋ではムードライトとして机裏にLEDテープを張り付けているのだが、これにステータス通知用LEDとしての機能を持たせたい。
もともとRaspberry PiからArduino Microを操作して小型の信号機でステータスを通知させる機能があるが、こんな感じでお世辞にも格好良いとは言えない。
f:id:t-hom:20210809193330p:plain

LEDテープを活用すれば卓上がスッキリするし、もともとただのムードライトに機能性を持たせることで機能美を堪能できる。

部品

Arduino

今回試作に使ったのはstemteraというブレッドボードにArduino Uno互換機が内蔵されたタイプ。
ただ日本だと共立電子でしか買えないうえ今みたらほとんど終売間際で7,000円近くもするのでお勧めはしない。
stemtera.com

私が買ったときはセールか何かだったのか、かなり安かった覚えがある。まぁ便利といえば便利かな。。

Arduino Unoは正規品が3,000円なので初めての方にはそちらの方がおススメ。
慣れてる人の2台目以降はお好みの互換機で良いと思う。

赤外線送受信モジュール

元々はずいぶん前に赤外線LEDそのまま買って試したのだが、色々試してうまくいかなかったので配線が悪いのが壊してしまったのかそれともコードが悪いのか分からずに挫折したので今回はモジュールを買うことにした。これなら原因はコードに絞ることができる。

配線

f:id:t-hom:20210809211256p:plain

GNDとVCCはそれぞれGNDと5Vに繋ぐ。
データピンは受信機が2番(黒い受光パーツ)、送信機(透明LED)が3番となる。

なお、今回は赤外線リモコンコードの解析と送信プログラムで配線をし直すのが面倒なので一緒に繋いでいるが、解析中は送信機つけなくても良いし、解析が終わったら逆に受信機は不要になる。

コード

ライブラリの準備

ツール→ライブラリの管理からIRremoteを検索してインストールしておく。
f:id:t-hom:20210809203952p:plain

受信と解析

ファイル→スケッチ例→IRremote→ReceiveDumpを開く。
Arduinoに書き込み、シリアルモニターを開く。
通信速度は115200bpsに設定。
f:id:t-hom:20210809204226p:plain


そのままリモコンを受信機に向けて操作すると以下のように内容が出力される。
(注)エアコンのリモコンはコードが長いためデフォルトでは失敗するらしく、バッファを増やしてやる必要があるとのこと。

Protocol=NEC Address=0xEF00 Command=0x4 Raw-Data=0xFB04EF00 32 bits LSB first

Raw result in internal ticks (50 us) - with leading gap
rawData[68]: 
     -65535
     + 178,-  89     +  11,-  12     +  10,-  12     +  10,-  13
     +  10,-  12     +  10,-  12     +  11,-  11     +  11,-  12
     +  11,-  12     +  10,-  34     +  10,-  34     +  11,-  34
     +  10,-  34     +  10,-  12     +  11,-  34     +  10,-  34
     +  11,-  34     +  10,-  12     +  11,-  11     +  11,-  34
     +  11,-  11     +  11,-  11     +  12,-  11     +  11,-  11
     +  11,-  12     +  11,-  33     +  11,-  34     +  11,-  11
     +  11,-  34     +  10,-  34     +  11,-  33     +  11,-  34
     +  10,-  34     +  11
Raw result in microseconds - with leading gap
rawData[68]: 
     -3276750
     +8900,-4450     + 550,- 600     + 500,- 600     + 500,- 650
     + 500,- 600     + 500,- 600     + 550,- 550     + 550,- 600
     + 550,- 600     + 500,-1700     + 500,-1700     + 550,-1700
     + 500,-1700     + 500,- 600     + 550,-1700     + 500,-1700
     + 550,-1700     + 500,- 600     + 550,- 550     + 550,-1700
     + 550,- 550     + 550,- 550     + 600,- 550     + 550,- 550
     + 550,- 600     + 550,-1650     + 550,-1700     + 550,- 550
     + 550,-1700     + 500,-1700     + 550,-1650     + 550,-1700
     + 500,-1700     + 550

Result as internal ticks (50 us) array - compensated with MARK_EXCESS_MICROS
uint8_t rawTicks[67] = {178,89, 11,12, 10,12, 10,13, 10,12, 10,12, 11,11, 11,12, 11,12, 10,34, 10,34, 11,34, 10,34, 10,12, 11,34, 10,34, 11,34, 10,12, 11,11, 11,34, 11,11, 11,11, 12,11, 11,11, 11,12, 11,33, 11,34, 11,11, 11,34, 10,34, 11,33, 11,34, 10,34, 11};  // Protocol=NEC Address=0xEF00 Command=0x4 Raw-Data=0xFB04EF00 32 bits LSB first

Result as microseconds array - compensated with MARK_EXCESS_MICROS
uint16_t rawData[67] = {8880,4470, 530,620, 480,620, 480,670, 480,620, 480,620, 530,570, 530,620, 530,620, 480,1720, 480,1720, 530,1720, 480,1720, 480,620, 530,1720, 480,1720, 530,1720, 480,620, 530,570, 530,1720, 530,570, 530,570, 580,570, 530,570, 530,620, 530,1670, 530,1720, 530,570, 530,1720, 480,1720, 530,1670, 530,1720, 480,1720, 530};  // Protocol=NEC Address=0xEF00 Command=0x4 Raw-Data=0xFB04EF00 32 bits LSB first

uint16_t address = 0xEF00;
uint16_t command = 0x4;
uint32_t data = 0xFB04EF00;

Pronto Hex as string
char ProntoData[] = "0000 006D 0022 0000 0157 00AA 0016 0016 0014 0016 0014 0018 0014 0016 0014 0016 0016 0014 0016 0016 0016 0016 0014 0041 0014 0041 0016 0041 0014 0041 0014 0016 0016 0041 0014 0041 0016 0041 0014 0016 0016 0014 0016 0041 0016 0014 0016 0014 0018 0014 0016 0014 0016 0016 0016 003F 0016 0041 0016 0014 0016 0041 0014 0041 0016 003F 0016 0041 0014 0041 0016 06C3 "

沢山出力されるが、いろんな方法で表示してくれているだけなので、実は必要なのは以下のみ。

uint16_t rawData[67] = {8880,4470, 530,620, 480,620, 480,670, 480,620, 480,620, 530,570, 530,620, 530,620, 480,1720, 480,1720, 530,1720, 480,1720, 480,620, 530,1720, 480,1720, 530,1720, 480,620, 530,570, 530,1720, 530,570, 530,570, 580,570, 530,570, 530,620, 530,1670, 530,1720, 530,570, 530,1720, 480,1720, 530,1670, 530,1720, 480,1720, 530};  // Protocol=NEC Address=0xEF00 Command=0x4 Raw-Data=0xFB04EF00 32 bits LSB first

これはunit16_t型の配列rawData[67]に赤外線のデータを代入するC言語の初期化コードになっている。

送信

送信コードは次のとおり。
先ほどの配列を任意の名前の変数に格納しておき、sendRawメソッドで赤外線送信される。

#include <IRremote.h>

void setup() {
    IrSender.begin(3, false);
}

void loop() {
    uint16_t led_bluegreen[67] = {8930,4420, 580,570, 530,570, 580,570, 530,570, 580,570, 530,570, 530,620, 530,570, 530,1720, 530,1670, 530,1670, 530,1720, 530,570, 530,1720, 480,1720, 480,1720, 530,1720, 480,620, 480,620, 530,1720, 480,620, 530,620, 480,620, 530,620, 480,620, 480,1720, 530,1720, 480,620, 530,1720, 480,1720, 480,1720, 530,1670, 580};
    uint16_t led_red[67] = {8880,4470, 580,570, 530,570, 530,570, 580,570, 530,570, 580,570, 530,570, 580,570, 530,1670, 530,1670, 580,1670, 530,1670, 530,570, 580,1670, 530,1670, 530,1670, 580,570, 530,570, 580,1670, 530,570, 530,620, 530,570, 530,570, 580,570, 530,1670, 580,1670, 530,570, 530,1720, 530,1670, 530,1670, 530,1720, 530,1670, 530};
    uint16_t turn_off[67] = {8880,4470, 530,570, 580,570, 530,570, 580,520, 580,570, 530,570, 580,570, 530,570, 580,1670, 530,1670, 530,1670, 580,1670, 530,570, 530,1670, 580,1670, 530,1670, 580,520, 580,1670, 530,570, 580,570, 530,570, 580,570, 530,570, 530,570, 580,1670, 530,570, 580,1620, 580,1670, 530,1670, 580,1670, 530,1670, 530,1670, 580};
    uint16_t turn_on[67] = {8880,4470, 530,570, 580,570, 530,570, 580,570, 530,570, 530,570, 580,570, 530,570, 580,1670, 530,1670, 530,1670, 580,1670, 530,570, 580,1620, 580,1670, 530,1670, 580,1620, 580,1670, 530,570, 580,570, 530,570, 580,570, 530,570, 530,570, 580,570, 530,570, 580,1670, 530,1670, 530,1670, 580,1670, 530,1670, 530,1670, 580};  // Protocol=NEC Address=0xEF00 Command=0x3 Raw-Data=0xFC03EF00 32 bits LSB first
    
    IrSender.sendRaw(led_bluegreen, sizeof(led_bluegreen) / sizeof(led_bluegreen[0]), 38);
    delay(3000);
    IrSender.sendRaw(led_red, sizeof(led_red) / sizeof(led_red[0]), 38);
    delay(3000);
    IrSender.sendRaw(turn_off, sizeof(turn_off) / sizeof(turn_off[0]), 38);
    delay(3000);
    IrSender.sendRaw(turn_on, sizeof(turn_on) / sizeof(turn_on[0]), 38);
    delay(3000);
}

このコードをArduinoに書き込むと、LEDテープを、青緑・赤・OFF・ONと3秒ごとに切り替える処理を繰り返す赤外線リモコンになる。
注意点として市販リモコンほど効きがよくないので、私の環境だと30センチくらいの距離で正面から当ててやる必要があったので、うまくいかない場合は近づけてみて切り分けすると良いと思う。

今後の展望

ラズパイからシリアル通信で連携して各種通知に使おうと思う。この先は既に経験がある領域なのでそれほど苦労なく実装できるかと思う。
今回はLEDテープの操作だったけど赤外線リモコン全般操作できると思うので寝る前にサーキュレーターの電源を自動で切ったり、色々と応用が効きそうだ。

Python tkinterで ゆっくり霊夢 を瞬きアニメーション

今回は、YouTubeの解説系動画でおなじみの ゆっくり霊夢 をPythonのtkinterを使って瞬きさせてみた。
f:id:t-hom:20210806211627g:plain

作成の動機と経緯

元々は、ラズパイとかArduinoを駆使してデジタル秘書を作りたいというのが発端。
と言ってもそんなに技術はないので、出来上がったのは「〇時〇分です、〇〇をしてください。」と発話するリマインダーボット。
これを作るのは全然大したことではなくて、ラズパイでcronにaplayを仕込んでるだけ。WindowsでいうとTask Schedularで特定の時間に特定のwav再生させてるだけと言えばイメージ湧くだろうか。

このツール、しばらく運用してみて2つの要望が生じた。

  • 折角しゃべるんだから更に愛着が持てるように顔を付けたくなる。
  • wavファイルの更新が面倒なので音声合成にしたい。

長らくこれは願望どまりだったが、先日ラズパイにゆっくりボイス(AquesTalk Pi)をインストールできることを知り、だったら顔もゆっくりで行くか!てな感じで放置されていた改修案件が動き出した。
これが今回の話の発端である。

音声合成から再生までの箇所は検証済なので、今回やりたいのはtkinterでゆっくりキャラのアニメーションだ。

コード

import os
import tkinter
import time
from PIL import Image, ImageTk

script_path = os.path.dirname(os.path.abspath(__file__))
charactor =  "Reimu"

root = tkinter.Tk()
root.title("Yukkuri")
root.geometry("500x380")
root['background']='#800000'

resource_path = script_path + '/' + charactor
body = Image.open(resource_path + '/body/00.png')
skin = Image.open(resource_path + '/skin/00a.png')
mouth = Image.open(resource_path + '/mouth/00.png')
brow = Image.open(resource_path + '/brow/00.png')
eye = [Image.open(resource_path + '/eye/00.png'),
        Image.open(resource_path + '/eye/00a.png'),
        Image.open(resource_path + '/eye/00b.png'),
        Image.open(resource_path + '/eye/00c.png'),
        Image.open(resource_path + '/eye/00d.png'),
        Image.open(resource_path + '/eye/00e.png')]

def createface(n):
    face = Image.alpha_composite(body, eye[n])
    face = Image.alpha_composite(face, skin)
    face = Image.alpha_composite(face, mouth)
    face = Image.alpha_composite(face, brow)
    return face

charactor_image = ImageTk.PhotoImage(image=createface(1))
canvas = tkinter.Canvas(root, width=400, height=320, bd=0, highlightthickness=0, relief='ridge')
canvas['background']=root['background']
imagearea = canvas.create_image(0, 0, image=charactor_image, anchor=tkinter.NW)
canvas.pack()


def animation():
    global charactor_image
    for i in range(6):
        time.sleep(0.05)
        charactor_image = ImageTk.PhotoImage(image=createface(i))
        canvas.itemconfig(imagearea, image=charactor_image)
        canvas.update()
    for i in reversed(range(6)):
        time.sleep(0.05)
        charactor_image = ImageTk.PhotoImage(image=createface(i))
        canvas.itemconfig(imagearea, image=charactor_image)
        canvas.update()
    root.after(3000, animation)


root.after(3000, animation)
root.mainloop()

コーディングでハマったところ

関数内で作成されたImageは関数がおわると崩壊する

最初はanimationが1回終わるとキャラが消失するという事象に悩まされていた。
原因として、関数内で作成されたImageは通常、関数スコープを抜けると消えてしまうようだ。
キャンバス内で保持されるから大丈夫だろうと思っていたんだけど、恐らくキャンバスは画像を受け取って自分で保持しているわけではなく、画像を指定された変数を保持しているんだろう。
だからローカル変数にイメージを格納しているとスコープを抜けたら変数が消えてキャンバスの表示も消える。
animation関数でglobal charactor_imageと書いている部分がその対策として入れたコードである。

after関数に指定する関数名にカッコを付けると即時呼び出しになる

root.after(3000, animation)という記述は、3秒後にanimation関数を呼び出すようスケジュールしなさいという命令だ。
最初、root.after(3000, animation())という風にanimation関数に()を付けていたのだが、なぜか3秒立たずに連続で呼び出されてしまう。しかも途中で再帰上限を超えたというエラーが出た。

この挙動は、pythonでは関数も値として扱うことができるためである。評価せずにそのまま関数値として取り扱う場合は()を付けてはいけない。

今後の展望

記事にするかどうかは別として、次は発話できるようにしたい。
方法としては、subprocessでLinuxのaplayを実行し、ループ中のpollでsubprocessが終了したからどうかををみ取りつつ、終了するまでの間口パクアニメーションを流すということを考えている。

そこまでできればちょっとしたガジェットなのでラズパイに入れて運用しつつ、感情表現を増やしていこうかなと思う。

ゆっくり霊夢って何?

これが分かりやすいかも。
youtu.be

PS4コントローラーをマクロキーボード化するArduinoにDIPスイッチでモード切り替え実装中

凄く長いタイトルになってしまったけど、ようするにコレの続き。
thom.hateblo.jp

そして、こういうことである。
f:id:t-hom:20210802181253p:plain

初見様用の説明

初見の方に説明すると、これはArduinoというマイコンを使ってプレステ4のDual Shockコントローラーをキーボード入力信号に変換するツールである。
長らく在宅勤務なので少しでも楽に効率よく仕事をしようと思い製作した。傍目にはプレステで遊んでるようにしか見えないけど、PCから見ればただのキーボードなので、ショートカットを駆使してメールを閲覧してる人とやってることは同じ。
詳細は上のリンク先にあるリンク先にあるリンク先をたどれば最初の記事が出てくる。

今回の変更点と経緯

もともと仕事で使っているのだが、時折ゲームや別の用途で設定を変更したい場合が出てくる。
そこで、DIPスイッチというパーツを使ってスイッチのON・OFFを読み取り、マクロを切り替えできるようにする。

今回完成したのはハードウェア部分のみ。テストコードでとりあえず動いたのでもうできたも同然ということで記事を書いてしまった。(できてないけど。)

部品の選定と配線プラン

今回つかった部品はこちらのDIPスイッチ。4スイッチ付いているので4ビット=スイッチの組み合わせは16パターン作れる。

実店舗なら1個何十円かで購入できるけど、これだけのために出かけると電車賃の方が高くつくのでいつものAmazon。
結局色々失敗して3~4個壊す羽目になったので多めに買っといてよかった。

5個で十分だったというのは結果論であって、「やべぇ、あと1個しかねぇ」というメンタルではクリエイティブに色々試すことが出来ない。
やっぱパーツの在庫は多いに越したことは無いのだ。

配線はこんな感じで計画。デジタルの5~8番はUSBホストシールドとはんだで接着してるだけで、電気的には特に意味はないので今回活用する。
f:id:t-hom:20210802182949p:plain
※実際は上のGNDを使った。

試行錯誤の履歴

まず手持ちのワイヤーで接続することを考えてみた。
私が普段使いしてるのはこちらで紹介されている単線のワイヤー。ブレッドボードに直挿しできるので便利なんだけど、もはや針金と紹介されているとおり、相当固い。
blog.siliconhouse.jp

なんで単線メインかというと、当時の私は予備はんだというテクニックを知らず、こんな感じに端子に挿すときにイラっとしてたから。
f:id:t-hom:20210802184807p:plain

そんで単線の太いのをこんな細かい部品にはんだ付けできる自信がなくて、ビニール被膜と芯線の間にDipスイッチの足を突っ込む作戦。。
f:id:t-hom:20210802183530p:plain

何度か足を折りつつ、3個めで成功したんだけど。。
f:id:t-hom:20210802185056p:plain

むり。。固すぎて基盤にはんだ付けしたが最後、フタが閉まらなくなる。

ということで作戦変更。なるべく細い線でやってみよう。

手持ちのアイテムではコレが一番細い線だった。何のパーツか忘れたけど転がってるってことは切っても大丈夫だろう。上に1本映ってる黄色いのは例の単線ワイヤ。
f:id:t-hom:20210802185239p:plain

ただこれ失敗すると後がないので同じようなケーブルを買っておくことにした。
さて、どうやって同じケーブルを探すか。

ケーブル自体になんか書いてあるようだけど、肉眼では読めない。
とりあえずカメラでズーム撮影してみると。。
f:id:t-hom:20210802185500p:plain

ふむ、28AWGというのが芯線の太さの規格らしい。
ビニール被膜(シースというらしい)も含めた太さはノギスで測って0.9ミリくらいだったのでそれに近いのを探す。
Amazonで電子ワイヤーのベンダーを探して、協和ハーモネットというメーカーのサイトのカタログで欲しいワイヤーを探して、再びAmazonで型番検索。

30AWGのこちらを購入した。

こういうのはAmazonだけだと情報不足でさっぱり分からないのでやっぱメーカーカタログの確認が重要。

色々調べた副産物として、ワイヤーストリッパーのどの穴使えば良いのかを学んだ。今まで適当だったんだけど、AWGってそういうことね。。
f:id:t-hom:20210802190245p:plain

ちなみに前述の太いワイヤーのAWG値も気になって探しまわったけどどこにも乗ってなくてかなり時間をロスした。結論、AWG規格に該当するサイズじゃなかった。。近いサイズはAWG22で単線だから左のゲージで下から3つめの穴を使う。


さて、線を買ったらあとは繋ぐだけ。今回の細線なら直接はんだづけできそうだ。なんならDIPスイッチの足が邪魔だから、足が折れたスイッチにはんだ付けしてみた。

全部繋がって「完成!」と思ってスイッチをスライドしてみたらスイッチがポキっと。。どうやらはんだ付けの熱がプラ部分に伝わって劣化&固着を引き起こしたようだ。。

気を取り直して足がついたままでリトライ。
半田ゴテ側にはんだをたっぷりのせ、スイッチの足と電線を重ねたところに一瞬で擦り付けるという荒業でなんとか接着。
f:id:t-hom:20210802191056p:plain

全く褒められた方法ではないがとりあえずくっついた。。

次の心配は、ショートである。
このまま閉めたらスイッチの足が基盤に触れてぶっ壊れるのではと。

そこで絶縁材を探していたところ、ポリイミドテープというものがあることを知った。
リチウムイオン電池とかに使われてるアレ。。といっても伝わらないかと思うので。。コレ↓

頭についてる飴色のがポリイミドという特殊なプラスチックで、耐熱・絶縁という特性がある。

以下の商品を購入。Amazonには耐熱性が無いただのテープを着色しただけの偽物も出回ってるらしいけどコメント見る感じは大丈夫そう。

まぁ今回は絶縁だけで良いので大丈夫だろう。

最終的にはポリイミドテープで足を適当に覆って、ホットボンドで接着し、更に横から上からホットボンドを塗りたくって強引に接着。
f:id:t-hom:20210802192307p:plain

ケースの加工

試作段階では前回プリントしたケースに穴を空けただけ。
Vesselのドライバーにドリルビットで穴を空けた後、タケノコドリルビットで穴を広げる。
f:id:t-hom:20210802192524p:plain

あとはルーターで雑に加工。。案の定汚いけど試作なのでとりあえずOK。
f:id:t-hom:20210802192703p:plain

その後、配線がうまくいってフタが閉まることもテストできたので、Fusion 360でモデルを編集して再印刷。
f:id:t-hom:20210802192922p:plain

Fusion 360の無料サブスクリプションが切れてて購買ページに飛ばされたので一瞬焦ったけど、ちゃんと個人ライセンス(無償)を更新する方法が見つかったので良かった。。
makerslove.com

テストコード

テストなので興味ないと思うけど一応乗せておく。
東方用の設定に、使ってない△ボタンでスイッチの状態をシリアルモニターに流すコードを足しただけ。

#include <Keyboard.h>
#include <Mouse.h>
#include <PS4USB.h>

// Satisfy the IDE, which needs to see the include statment in the ino too.
#ifdef dobogusinclude
#include <spi4teensy3.h>
#endif
#include <SPI.h>

// #include "Arduino.h"
// #include "DFRobotDFPlayerMini.h"
// DFRobotDFPlayerMini myDFPlayer;

USB Usb;
PS4USB PS4(&Usb);

bool printAngle, printTouch;
uint8_t oldL2Value, oldR2Value;
uint8_t oldRightHatX, oldRightHatY;
uint8_t redundant;

void setup() {
  pinMode(5, INPUT_PULLUP);
  pinMode(6, INPUT_PULLUP);
  pinMode(7, INPUT_PULLUP);
  pinMode(8, INPUT_PULLUP);
  Serial.begin(9600);
  if (Usb.Init() == -1) {
    while (1); // Halt
  }
  Serial.println("Started:");
  Mouse.begin();
  redundant = 1;
}

void loop() {
  Usb.Task();

  if (PS4.connected()) {
    if (PS4.getButtonClick(PS)) {
    }
    if (PS4.getButtonPress(TRIANGLE)) {
      for(int i = 5; i<=8; i++){
        Serial.print(i);
        if (digitalRead(i)) {
          Serial.println(" is connected");
        }
        else {
          Serial.println(" is disconnected");
        }
      }
      
    }
    if (PS4.getButtonClick(CIRCLE)) {
    }
    if (PS4.getButtonPress(CROSS)) {
      Keyboard.press('z');
    } else {
      Keyboard.release('z');
    }
    
    if (PS4.getButtonClick(SQUARE)) {
    }

    if (PS4.getButtonPress(UP)) {
      Keyboard.press(KEY_UP_ARROW);
    } else {
      Keyboard.release(KEY_UP_ARROW);
    }
    if (PS4.getButtonPress(RIGHT)) {
      Keyboard.press(KEY_RIGHT_ARROW);
    } else {
      Keyboard.release(KEY_RIGHT_ARROW);
    }
    if (PS4.getButtonPress(DOWN)) {
      Keyboard.press(KEY_DOWN_ARROW);
    } else {
      Keyboard.release(KEY_DOWN_ARROW);
    }
    if (PS4.getButtonPress(LEFT)) {
      Keyboard.press(KEY_LEFT_ARROW);
    } else {
      Keyboard.release(KEY_LEFT_ARROW);
    }

    if (PS4.getButtonPress(R1)) {
      Keyboard.press(KEY_LEFT_SHIFT);
    } else {
      Keyboard.release(KEY_LEFT_SHIFT);
    }
    
    if (PS4.getButtonClick(L1)) {
      Keyboard.press('x');
      delay(40);
      Keyboard.releaseAll();
    }
 
    if (PS4.getButtonClick(OPTIONS)) {
      Keyboard.press(KEY_ESC);
      delay(40);
      Keyboard.releaseAll();
    }
  }
}

終わりに

今回は狭いケースにスイッチを追加するという結構無茶なことをやったのでワイヤーの選定や絶縁処理など色々と勉強になることがあり面白かった。
もう一度やれと言われたらマジ勘弁だけど。

あとはソースコードを整理するだけだが、こればかりは自分用のセッティングなので他人が真似してもしょうがないし、PS4マクロキーボードシリーズはこの記事で最後である。

当ブログは、amazon.co.jpを宣伝しリンクすることによってサイトが紹介料を獲得できる手段を提供することを目的に設定されたアフィリエイト宣伝プログラムである、 Amazonアソシエイト・プログラムの参加者です。