手の不自由な人のための食事介助ロボット1昨年投稿した食事介助ロボット「マイアーム」の課題点を踏まえ、改良してみました。正直、前作より性能が落ちたかもしれません。が、今後に繋がる新たな発見も多かったです。Add Annotation Order
コンテスト記載必要事項0For whom0Aさん(筋ジストロフィー)Why0障害者施設に勤務していた私が、そこで出会ったAさんに自助具スプーンを作ってほしいと頼まれました。試行錯誤の末、5年の期間を経て昨年ロボットアームが完成しました。「ラーメンを食べよう」という新たな目標のための、箸を使えるロボットの制作に取り組むこととなりました。How06つのモーターを1つのボタンのみでコントロール。ボタンを押すタイミングによって動作が変わるように作成した。Outcome0前回作よりも自ら食べ物を食べているという操作感が上がった。Add Annotation Order
Why0障害者施設に勤務していた私が、そこで出会ったAさんに自助具スプーンを作ってほしいと頼まれました。試行錯誤の末、5年の期間を経て昨年ロボットアームが完成しました。「ラーメンを食べよう」という新たな目標のための、箸を使えるロボットの制作に取り組むこととなりました。
前回のマイアームの問題点01. コントローラー0・サイズが大きく、置く位置を決めるのに時間がかかる(押す、引くの2動作を行うには、身体の正面に置きたいが、ロボットアームがあるため調整が必要)・Aさんの車椅子の座り姿勢の崩れによって、力が出しづらくなる2. 操作感0・お皿を回して食べ物をすくうので、自分自身でキャッチしているという実感が薄いように感じる3. Aさんの様子0・麺類がすくいにくく、ラーメンを食べることができたら嬉しいAdd Annotation Order
1. コントローラー0・サイズが大きく、置く位置を決めるのに時間がかかる(押す、引くの2動作を行うには、身体の正面に置きたいが、ロボットアームがあるため調整が必要)・Aさんの車椅子の座り姿勢の崩れによって、力が出しづらくなる
目標0 1.コントローラーの小型化 2.コントローラーがどの位置であっても操作できる 3.操作感の向上のため、自らキャッチしにいく動作を作る 4.ラーメンを食べるため、箸への変更Add Annotation Order
試作品③0箸を断念!フォークとスプーンで挟む形へ変更0やり方を変え、フォークとスプーンで挟むことにした結果0恐怖感は消えたが、フォークとスプーンで挟んだまま口に入れると、フォークが開く際に口の中であたってしまい、食べ物を口の中に運びにくい(フォークが滑るように口から出るように改善が必要である)Add Annotation Order
試作品②0箸とスプーン2つのアームを持つロボットアームを制作問題点箸とスプーンのアームが動作中ぶつかることがある口を開ける時に反射的にどうしても手に力が入ってしまう。その際にボタンを押してしまい、箸が開いて口にあたってしまうやはり箸に対する恐怖感が取れないスプーンを動かしている間に箸が突然動くのではないかという恐怖感があるロボットアーム自体が大きくなりすぎたため、コントローラーを置く位置がよりテーブルの端になり、押す動作ができなくなった結論箸をやめてみることにコントローラーの改善が必要Add Annotation Order
完成0コントローラーの小型化01ボタン操作に変えたコントローラーにBB弾を使用し動作をスムーズにコントローラーに傾斜をつけ、ボタンを押した後、腕の重さで元の位置に戻るようにしたBB弾の使用0・滑る部分にBB弾を入れ、スムーズに動くようにした・BB弾の厚みによって、部品同士が外れないようにした操作の改善0食べ物がスプーンの上に乗るのを待つのではなく、自分から掴みにいくようにした皿の中に7か所の目標位置を設定し、3秒以内にボタンを押すと次の目標位置にスプーンが移動する1ボタンで全てを操作するために、ボタンを離している間の時間差によって動作が変わるよう変更した3秒以内 次の目標位置(全部で皿内の7か所の目標位置)3秒経過 LEDが点灯し口に移動箸の動きを滑りながら口から出るように0フォークの動きを回転運動から、複雑な動きができるように構造を変えた(箸リンク機構)アームの関節にかかる負担の回避0関節面を斜めにして、重さの負担を軽減した。(人間の各関節の傾斜面に参考にした)Add Annotation Order
操作の改善0食べ物がスプーンの上に乗るのを待つのではなく、自分から掴みにいくようにした皿の中に7か所の目標位置を設定し、3秒以内にボタンを押すと次の目標位置にスプーンが移動する1ボタンで全てを操作するために、ボタンを離している間の時間差によって動作が変わるよう変更した3秒以内 次の目標位置(全部で皿内の7か所の目標位置)3秒経過 LEDが点灯し口に移動
結果0良い点自分からつかみに行く動作が可能になったため、より操作している実感があるフォークが口から滑り出るようになったので、口元の違和感が少なくなった問題点肩の部分のモーターに負担がかかり、たまにアームが落ちてしまう。(関節の傾斜面の角度に改善が必要かも)皿の中の全ての位置の食べ物をつかむことができず、更にプログラムの改善が必要皿の回転をサーボモーターにしたため、独立した動きができず、ボタンを押すたびに初期位置に戻ってしまう。皿の回転の意味がなくなってしまった。(モーターをマイアーム1で使用したモーターに戻すか、プログラムの変更で改善できるかも)Add Annotation Order
Aさんの感想0より操作感が出て、楽しめる物になった。食べる分には以前のロボットアーム1の方が食べやすいかも。(スプーンが一番口当たりが良いため)ボタンを押してからアームが動くまでの少しのタイムラグが気になる。(複雑なプログラムを入れたための影響)Add Annotation Order
まとめ0操作感が出たことは良かった。2つのボタンを押すという手の動きで操作する方法(空間的側面)から、1つのボタンでタイミングによって操作する方法(時間的側面)に変えても操作感が落ちることはなかった。BB弾を使用することによって、3Dプリンターで作れる物の幅が広がった。Aさんの辛口の感想にショックは受けたが、「タイムラグを感じる」という、私自身は気にならなかった部分を指摘され、Aさんの感覚の鋭さを知り、新たな気づきになった。今後はモールス信号のように、よりタイミングによって制御できる幅を広げてみたい。電圧の問題0スプーンを口元に持っていく際に関節に電圧がかかる。マイアーム1は振り子の原理で負担を軽減できていたが、今回はそれができず口元に持っていく際に肩部分のモーターに過度な負担がかかっている。人の身体は口に物を入れる際、肩甲骨自体が下がる(下制)事で肩の負担を減らしているが、マイアーム2の肩甲骨部の動きは軸回転であるため、その動きが再現できていないための考える。Add Annotation Order
電圧の問題0スプーンを口元に持っていく際に関節に電圧がかかる。マイアーム1は振り子の原理で負担を軽減できていたが、今回はそれができず口元に持っていく際に肩部分のモーターに過度な負担がかかっている。人の身体は口に物を入れる際、肩甲骨自体が下がる(下制)事で肩の負担を減らしているが、マイアーム2の肩甲骨部の動きは軸回転であるため、その動きが再現できていないための考える。
使用する道具03Dプリンターで作成した部品0ELEGOO 高速PLAプラス RAPID PLA+フィラメント 黒色1.75mm使用購入した部品0・Arduino・PCA9685 PWMサーボモーター ・ハウジングコネクター・マイクロスイッチ・ジャンパーワイヤー・MG996R・MG996R付属のサーボホーン・丸形サーボホーン・サーボコネクター延長ケーブル・BB弾・LEDライト・コントローラー用USBケーブル・Arduino用USBケーブル・ACDCアダプター・直径2mm幅ケーブル・直径18cm皿・輪ゴム(直径25mm)・箸 スプーン フォーク・工具類Add Annotation Order
購入した部品0・Arduino・PCA9685 PWMサーボモーター ・ハウジングコネクター・マイクロスイッチ・ジャンパーワイヤー・MG996R・MG996R付属のサーボホーン・丸形サーボホーン・サーボコネクター延長ケーブル・BB弾・LEDライト・コントローラー用USBケーブル・Arduino用USBケーブル・ACDCアダプター・直径2mm幅ケーブル・直径18cm皿・輪ゴム(直径25mm)・箸 スプーン フォーク・工具類
STLファイル01.台2.ターンテーブル3.台のふた4.からだ5.肩6.上腕7.前腕8.手9.コントローラー①10.コントローラー②11.コントローラー③12.コントローラー④13.コントローラー⑤14.ベアリング外15.ベアリング内16.前腕ふた17.上腕ふた18.指19.指上部20.指回転部21.指先22.指アーム23.指スライド上24.指スライド下25.スライドふた大26.スライドふた小27.ねじ28.器カバーAdd Annotation Order
組み立て方0手0①ベアリング制作 (4つ) ベアリング内とベアリング外のへこみ部分を合わせ、そこからBB弾を挿入 (動画参照) ②モーターとサーボホーンをつける前に、ベアリング部分を各パーツ(肩×2、前腕×1、手部×1)に ねじでとりつける③MG996Rを取り付けねじで固定する④延長ケーブルを、各関節を通りながらからだ部分を通し台の中に入れ、PCA996⑤番から⑩番につける コントローラー0①マイクロスイッチに半分に切断したコードをはんだ付けする(中央と右側の2か所)②コードとソケットに金具を取り付ける③コードを穴から通し、スイッチをコントローラーにはめ込む④ソケットメス部分をコントローラーふたに押し込む⑤100円USBを切断し、コントローラーケーブルを制作⑥コントローラーのレール部分にBB弾を挿入(動画参照)⑦両側のコントローラー壁部分を取り付けBB弾が外れないようにする⑧完成 ※コントローラーのスイッチ穴は2つあるが1つしか使わない台 / 配線0①回転部にサーボモーターと、サーボホーンを取り付け、レール部分のくぼみからBB弾を挿入②配線(配線図参照)指0①各関節をワッシャーを入れねじでとめる②スライド部レールにBB弾を入れ、ふたをねじどめする③輪ゴム(ナンバー12、直径25㎜)を3か所(スライド部分×2 アーム部分×1)取り付ける④フォーク、スプーン(もしくは箸)は10円玉でねじ固定するベアリング制作0コントローラーレール部分0配線図0Add Annotation Order
手0①ベアリング制作 (4つ) ベアリング内とベアリング外のへこみ部分を合わせ、そこからBB弾を挿入 (動画参照) ②モーターとサーボホーンをつける前に、ベアリング部分を各パーツ(肩×2、前腕×1、手部×1)に ねじでとりつける③MG996Rを取り付けねじで固定する④延長ケーブルを、各関節を通りながらからだ部分を通し台の中に入れ、PCA996⑤番から⑩番につける
コントローラー0①マイクロスイッチに半分に切断したコードをはんだ付けする(中央と右側の2か所)②コードとソケットに金具を取り付ける③コードを穴から通し、スイッチをコントローラーにはめ込む④ソケットメス部分をコントローラーふたに押し込む⑤100円USBを切断し、コントローラーケーブルを制作⑥コントローラーのレール部分にBB弾を挿入(動画参照)⑦両側のコントローラー壁部分を取り付けBB弾が外れないようにする⑧完成 ※コントローラーのスイッチ穴は2つあるが1つしか使わない
指0①各関節をワッシャーを入れねじでとめる②スライド部レールにBB弾を入れ、ふたをねじどめする③輪ゴム(ナンバー12、直径25㎜)を3か所(スライド部分×2 アーム部分×1)取り付ける④フォーク、スプーン(もしくは箸)は10円玉でねじ固定する
プログラミングコード010#include <Wire.h>#include <Adafruit_PWMServoDriver.h>Adafruit_PWMServoDriver pwm = Adafruit_PWMServoDriver();#define SERVO_MIN 150#define SERVO_MAX 600#define LOGICAL_SERVO_COUNT 6 // 論理サーボ数 (0..5 → PCA9685 の 5..10)#define BUTTON_PIN 2#define LED_PIN 1320#define WAIT_TIME 1000// サーボ状態float servoPositions[LOGICAL_SERVO_COUNT];float servoTargetAngles[LOGICAL_SERVO_COUNT];float servoStartAngles[LOGICAL_SERVO_COUNT];unsigned long servoStartTimes[LOGICAL_SERVO_COUNT] = {0};unsigned long servoMoveDurations[LOGICAL_SERVO_COUNT] = {0};30bool servoIsMoving[LOGICAL_SERVO_COUNT] = {false};// --- ポジション ---int positionInitial[LOGICAL_SERVO_COUNT] = {170, 20, 100, 70, 160, 90};int positionMouth[LOGICAL_SERVO_COUNT] = {10, 40, 45, 90, 50, 90};40int positionMiddles[7][LOGICAL_SERVO_COUNT] = { {12, 80, 120, 60, 70, 80}, {12, 40, 160, 60, 70, 60}, {12, 20, 150, 70, 60, 100}, {12, 100, 120, 55, 130, 80}, {12, 100, 120, 65, 130, 80}, {12, 80, 150, 40, 100, 180}, {12, 30, 150, 50, 100, 80}};50int positionGoals[7][LOGICAL_SERVO_COUNT] = { {12, 70, 160, 60, 60, 100}, {12, 70, 110, 60, 70, 60}, {12, 70, 140, 60, 70, 20}, {12, 70, 140, 60, 70, 30}, {12, 40, 130, 70, 130, 30}, {12, 80, 150, 40, 100, 0}, {12, 30, 150, 80, 100, 30}};60// --- スピード / ディレイ ---unsigned long speedToMiddle[7][LOGICAL_SERVO_COUNT] = { {500, 2000, 2000, 2000, 2000, 2000}, {500, 2000, 2000, 2000, 2000, 2000}, {500, 2000, 2000, 2000, 2000, 2000}, {500, 2100, 1900, 2100, 2000, 2000}, {500, 2200, 2000, 2000, 2000, 2000},70 {500, 2100, 1800, 2000, 1900, 2000}, {500, 2000, 2000, 2000, 2000, 2000}};unsigned long delayToMiddle[7][LOGICAL_SERVO_COUNT] = { {0, 0, 500, 0, 500, 500}, {0, 0, 0, 0, 500, 500}, {0, 0, 500, 0, 500, 500}, {0, 0, 500, 0, 500, 500},80 {0, 0, 500, 0, 500, 500}, {700, 700, 700, 700, 700, 0}, {0, 0, 500, 0, 500, 500}};90unsigned long speedToGoal[7][LOGICAL_SERVO_COUNT] = { {0, 2000, 4000, 4000, 4000, 2000}, {0, 2000, 4000, 4000, 4000, 2000}, {0, 4000, 4000, 4000, 4000, 2000}, {0, 1000, 4000, 4000, 4000, 2000}, {0, 1000, 4000, 4000, 4000, 2000},100{0, 1000, 4000, 4000, 4000, 2000}, {0, 1000, 4000, 4000, 4000, 2000}};unsigned long delayToGoal[7][LOGICAL_SERVO_COUNT] = { {0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0},110 {0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0}, {0, 0, 0, 5, 0, 0}};// 戻りパラメータ120unsigned long delayFromGoal[LOGICAL_SERVO_COUNT] = {0, 700, 700, 1200, 700, 500};unsigned long speedFromGoal[LOGICAL_SERVO_COUNT] = {1000, 1500, 2000, 3000, 3000, 2000};unsigned long delayToMouth[LOGICAL_SERVO_COUNT] = {4000, 50, 100, 150, 200, 0};130unsigned long speedToMouth[LOGICAL_SERVO_COUNT] = {4000, 3000, 3000, 3000, 4000, 1500};unsigned long delayFromMouth[LOGICAL_SERVO_COUNT] = {1000, 50, 100, 150, 200, 0};unsigned long speedFromMouth[LOGICAL_SERVO_COUNT] = {4000, 3000, 3000, 3000, 3000, 1500};140// 状態機械enum State { IDLE, MOVING_TO_MIDDLE, MOVING_TO_GOAL, MOVING_TO_INITIAL, MOVING_TO_MOUTH, AT_GOAL, AT_MOUTH, RETURNING_FROM_MOUTH};State state = IDLE;150int currentGoal = -1;bool buttonPreviouslyPressed = false;unsigned long lastReturnTime = 0;unsigned long lastButtonPressTime = 0;const unsigned long debounceDelay = 50;160// プロトタイプvoid moveToPosition(int targetAngles[LOGICAL_SERVO_COUNT], unsigned long speeds[LOGICAL_SERVO_COUNT], unsigned long delays[LOGICAL_SERVO_COUNT]);void setServoTarget(int i, float target, unsigned long duration, unsigned long delayTime);170void updateServo(int i);bool anyServoMoving();void silentInitialize();void setup() { Serial.begin(115200);180Wire.begin(); pwm.begin(); pwm.setPWMFreq(60); pinMode(BUTTON_PIN, INPUT_PULLUP); pinMode(LED_PIN, OUTPUT); digitalWrite(LED_PIN, LOW); silentInitialize(); moveToPosition(positionInitial, speedFromGoal, delayFromGoal); state = MOVING_TO_INITIAL;}190void loop() { unsigned long now = millis(); bool isPressed = digitalRead(BUTTON_PIN) == LOW; bool isMouthReady = (state == IDLE && (now - lastReturnTime >= WAIT_TIME)); digitalWrite(LED_PIN, isMouthReady ? HIGH : LOW);200if (isPressed && !buttonPreviouslyPressed && (now - lastButtonPressTime > debounceDelay)) { lastButtonPressTime = now; buttonPreviouslyPressed = true;210// --- ボタン押下処理 --- if (state == MOVING_TO_INITIAL) { currentGoal = (currentGoal + 1) % 7; moveToPosition(positionMiddles[currentGoal], speedToMiddle[currentGoal], delayToMiddle[currentGoal]);220state = MOVING_TO_MIDDLE; return; } if (state == MOVING_TO_MOUTH) { // Mouth 未到達キャンセル → FromMouth moveToPosition(positionInitial, speedFromMouth, delayFromMouth);230 state = MOVING_TO_INITIAL; return; } if (state == MOVING_TO_GOAL || state == MOVING_TO_MIDDLE) { // Goal 未到達キャンセル → FromGoal moveToPosition(positionInitial, speedFromGoal, delayFromGoal);240state = MOVING_TO_INITIAL; return; } if (state == AT_GOAL) { moveToPosition(positionInitial, speedFromGoal, delayFromGoal); state = MOVING_TO_INITIAL; return; }250if (state == AT_MOUTH) { moveToPosition(positionInitial, speedFromMouth, delayFromMouth); state = RETURNING_FROM_MOUTH; return; }260 if (isMouthReady) { moveToPosition(positionMouth, speedToMouth, delayToMouth); state = MOVING_TO_MOUTH; } else if (state == IDLE) { currentGoal = (currentGoal + 1) % 7;270 moveToPosition(positionMiddles[currentGoal], speedToMiddle[currentGoal], delayToMiddle[currentGoal]); state = MOVING_TO_MIDDLE; } }280if (!isPressed) buttonPreviouslyPressed = false; for (int i = 0; i < LOGICAL_SERVO_COUNT; i++) updateServo(i); if (!anyServoMoving()) { switch (state) { case MOVING_TO_INITIAL:290 case RETURNING_FROM_MOUTH: state = IDLE; lastReturnTime = millis(); break; case MOVING_TO_MIDDLE:300moveToPosition(positionGoals[currentGoal], speedToGoal[currentGoal], delayToGoal[currentGoal]); state = MOVING_TO_GOAL; break;310case MOVING_TO_GOAL: state = AT_GOAL; lastReturnTime = millis(); break; case MOVING_TO_MOUTH:320state = AT_MOUTH; break; default: break; } }}330void moveToPosition(int targetAngles[LOGICAL_SERVO_COUNT], unsigned long speeds[LOGICAL_SERVO_COUNT], unsigned long delays[LOGICAL_SERVO_COUNT]) { for (int i = 0; i < LOGICAL_SERVO_COUNT; i++) {340setServoTarget(i, targetAngles[i], speeds[i], delays[i]); }}void setServoTarget(int i, float target, unsigned long duration, unsigned long delayTime) {350 servoStartAngles[i] = servoPositions[i]; servoTargetAngles[i] = constrain(target, 0.0f, 180.0f); servoMoveDurations[i] = duration; servoStartTimes[i] = millis() + delayTime; servoIsMoving[i] = true;}360void updateServo(int i) { if (!servoIsMoving[i]) return; unsigned long now = millis(); if (now < servoStartTimes[i]) return;370unsigned long elapsed = now - servoStartTimes[i]; if (elapsed >= servoMoveDurations[i]) { servoPositions[i] = servoTargetAngles[i]; servoIsMoving[i] = false; } else {380 float t = (float)elapsed / servoMoveDurations[i]; float easedT = t * t * (3 - 2 * t); servoPositions[i] = servoStartAngles[i] + (servoTargetAngles[i] - servoStartAngles[i]) * easedT; }390 int pwmVal = constrain(map(servoPositions[i], 0, 180, SERVO_MIN, SERVO_MAX), SERVO_MIN, SERVO_MAX); pwm.setPWM(i + 5, 0, pwmVal);}400bool anyServoMoving() { for (int i = 0; i < LOGICAL_SERVO_COUNT; i++) { if (servoIsMoving[i]) return true; } return false;}410void silentInitialize() { for (int i = 0; i < LOGICAL_SERVO_COUNT; i++) { int angle = positionInitial[i]; servoPositions[i] = angle; servoTargetAngles[i] = angle; servoIsMoving[i] = false;420 int pwmVal = constrain(map(angle, 0, 180, SERVO_MIN, SERVO_MAX), SERVO_MIN, SERVO_MAX); pwm.setPWM(i + 5, 0, pwmVal); }}Add Annotation Order
10#include <Wire.h>#include <Adafruit_PWMServoDriver.h>Adafruit_PWMServoDriver pwm = Adafruit_PWMServoDriver();#define SERVO_MIN 150#define SERVO_MAX 600#define LOGICAL_SERVO_COUNT 6 // 論理サーボ数 (0..5 → PCA9685 の 5..10)#define BUTTON_PIN 2#define LED_PIN 13
20#define WAIT_TIME 1000// サーボ状態float servoPositions[LOGICAL_SERVO_COUNT];float servoTargetAngles[LOGICAL_SERVO_COUNT];float servoStartAngles[LOGICAL_SERVO_COUNT];unsigned long servoStartTimes[LOGICAL_SERVO_COUNT] = {0};unsigned long servoMoveDurations[LOGICAL_SERVO_COUNT] = {0};
30bool servoIsMoving[LOGICAL_SERVO_COUNT] = {false};// --- ポジション ---int positionInitial[LOGICAL_SERVO_COUNT] = {170, 20, 100, 70, 160, 90};int positionMouth[LOGICAL_SERVO_COUNT] = {10, 40, 45, 90, 50, 90};
40int positionMiddles[7][LOGICAL_SERVO_COUNT] = { {12, 80, 120, 60, 70, 80}, {12, 40, 160, 60, 70, 60}, {12, 20, 150, 70, 60, 100}, {12, 100, 120, 55, 130, 80}, {12, 100, 120, 65, 130, 80}, {12, 80, 150, 40, 100, 180}, {12, 30, 150, 50, 100, 80}};
50int positionGoals[7][LOGICAL_SERVO_COUNT] = { {12, 70, 160, 60, 60, 100}, {12, 70, 110, 60, 70, 60}, {12, 70, 140, 60, 70, 20}, {12, 70, 140, 60, 70, 30}, {12, 40, 130, 70, 130, 30}, {12, 80, 150, 40, 100, 0}, {12, 30, 150, 80, 100, 30}};
60// --- スピード / ディレイ ---unsigned long speedToMiddle[7][LOGICAL_SERVO_COUNT] = { {500, 2000, 2000, 2000, 2000, 2000}, {500, 2000, 2000, 2000, 2000, 2000}, {500, 2000, 2000, 2000, 2000, 2000}, {500, 2100, 1900, 2100, 2000, 2000}, {500, 2200, 2000, 2000, 2000, 2000},
70 {500, 2100, 1800, 2000, 1900, 2000}, {500, 2000, 2000, 2000, 2000, 2000}};unsigned long delayToMiddle[7][LOGICAL_SERVO_COUNT] = { {0, 0, 500, 0, 500, 500}, {0, 0, 0, 0, 500, 500}, {0, 0, 500, 0, 500, 500}, {0, 0, 500, 0, 500, 500},
90unsigned long speedToGoal[7][LOGICAL_SERVO_COUNT] = { {0, 2000, 4000, 4000, 4000, 2000}, {0, 2000, 4000, 4000, 4000, 2000}, {0, 4000, 4000, 4000, 4000, 2000}, {0, 1000, 4000, 4000, 4000, 2000}, {0, 1000, 4000, 4000, 4000, 2000},
100{0, 1000, 4000, 4000, 4000, 2000}, {0, 1000, 4000, 4000, 4000, 2000}};unsigned long delayToGoal[7][LOGICAL_SERVO_COUNT] = { {0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0}, {0, 0, 0, 0, 0, 0},
120unsigned long delayFromGoal[LOGICAL_SERVO_COUNT] = {0, 700, 700, 1200, 700, 500};unsigned long speedFromGoal[LOGICAL_SERVO_COUNT] = {1000, 1500, 2000, 3000, 3000, 2000};unsigned long delayToMouth[LOGICAL_SERVO_COUNT] = {4000, 50, 100, 150, 200, 0};
130unsigned long speedToMouth[LOGICAL_SERVO_COUNT] = {4000, 3000, 3000, 3000, 4000, 1500};unsigned long delayFromMouth[LOGICAL_SERVO_COUNT] = {1000, 50, 100, 150, 200, 0};unsigned long speedFromMouth[LOGICAL_SERVO_COUNT] = {4000, 3000, 3000, 3000, 3000, 1500};
140// 状態機械enum State { IDLE, MOVING_TO_MIDDLE, MOVING_TO_GOAL, MOVING_TO_INITIAL, MOVING_TO_MOUTH, AT_GOAL, AT_MOUTH, RETURNING_FROM_MOUTH};State state = IDLE;
150int currentGoal = -1;bool buttonPreviouslyPressed = false;unsigned long lastReturnTime = 0;unsigned long lastButtonPressTime = 0;const unsigned long debounceDelay = 50;
160// プロトタイプvoid moveToPosition(int targetAngles[LOGICAL_SERVO_COUNT], unsigned long speeds[LOGICAL_SERVO_COUNT], unsigned long delays[LOGICAL_SERVO_COUNT]);void setServoTarget(int i, float target, unsigned long duration, unsigned long delayTime);
170void updateServo(int i);bool anyServoMoving();void silentInitialize();void setup() { Serial.begin(115200);
180Wire.begin(); pwm.begin(); pwm.setPWMFreq(60); pinMode(BUTTON_PIN, INPUT_PULLUP); pinMode(LED_PIN, OUTPUT); digitalWrite(LED_PIN, LOW); silentInitialize(); moveToPosition(positionInitial, speedFromGoal, delayFromGoal); state = MOVING_TO_INITIAL;}
190void loop() { unsigned long now = millis(); bool isPressed = digitalRead(BUTTON_PIN) == LOW; bool isMouthReady = (state == IDLE && (now - lastReturnTime >= WAIT_TIME)); digitalWrite(LED_PIN, isMouthReady ? HIGH : LOW);
200if (isPressed && !buttonPreviouslyPressed && (now - lastButtonPressTime > debounceDelay)) { lastButtonPressTime = now; buttonPreviouslyPressed = true;
210// --- ボタン押下処理 --- if (state == MOVING_TO_INITIAL) { currentGoal = (currentGoal + 1) % 7; moveToPosition(positionMiddles[currentGoal], speedToMiddle[currentGoal], delayToMiddle[currentGoal]);
220state = MOVING_TO_MIDDLE; return; } if (state == MOVING_TO_MOUTH) { // Mouth 未到達キャンセル → FromMouth moveToPosition(positionInitial, speedFromMouth, delayFromMouth);
230 state = MOVING_TO_INITIAL; return; } if (state == MOVING_TO_GOAL || state == MOVING_TO_MIDDLE) { // Goal 未到達キャンセル → FromGoal moveToPosition(positionInitial, speedFromGoal, delayFromGoal);
240state = MOVING_TO_INITIAL; return; } if (state == AT_GOAL) { moveToPosition(positionInitial, speedFromGoal, delayFromGoal); state = MOVING_TO_INITIAL; return; }
250if (state == AT_MOUTH) { moveToPosition(positionInitial, speedFromMouth, delayFromMouth); state = RETURNING_FROM_MOUTH; return; }
260 if (isMouthReady) { moveToPosition(positionMouth, speedToMouth, delayToMouth); state = MOVING_TO_MOUTH; } else if (state == IDLE) { currentGoal = (currentGoal + 1) % 7;
270 moveToPosition(positionMiddles[currentGoal], speedToMiddle[currentGoal], delayToMiddle[currentGoal]); state = MOVING_TO_MIDDLE; } }
280if (!isPressed) buttonPreviouslyPressed = false; for (int i = 0; i < LOGICAL_SERVO_COUNT; i++) updateServo(i); if (!anyServoMoving()) { switch (state) { case MOVING_TO_INITIAL:
290 case RETURNING_FROM_MOUTH: state = IDLE; lastReturnTime = millis(); break; case MOVING_TO_MIDDLE:
300moveToPosition(positionGoals[currentGoal], speedToGoal[currentGoal], delayToGoal[currentGoal]); state = MOVING_TO_GOAL; break;
330void moveToPosition(int targetAngles[LOGICAL_SERVO_COUNT], unsigned long speeds[LOGICAL_SERVO_COUNT], unsigned long delays[LOGICAL_SERVO_COUNT]) { for (int i = 0; i < LOGICAL_SERVO_COUNT; i++) {
340setServoTarget(i, targetAngles[i], speeds[i], delays[i]); }}void setServoTarget(int i, float target, unsigned long duration, unsigned long delayTime) {
350 servoStartAngles[i] = servoPositions[i]; servoTargetAngles[i] = constrain(target, 0.0f, 180.0f); servoMoveDurations[i] = duration; servoStartTimes[i] = millis() + delayTime; servoIsMoving[i] = true;}
360void updateServo(int i) { if (!servoIsMoving[i]) return; unsigned long now = millis(); if (now < servoStartTimes[i]) return;
370unsigned long elapsed = now - servoStartTimes[i]; if (elapsed >= servoMoveDurations[i]) { servoPositions[i] = servoTargetAngles[i]; servoIsMoving[i] = false; } else {
380 float t = (float)elapsed / servoMoveDurations[i]; float easedT = t * t * (3 - 2 * t); servoPositions[i] = servoStartAngles[i] + (servoTargetAngles[i] - servoStartAngles[i]) * easedT; }
390 int pwmVal = constrain(map(servoPositions[i], 0, 180, SERVO_MIN, SERVO_MAX), SERVO_MIN, SERVO_MAX); pwm.setPWM(i + 5, 0, pwmVal);}
400bool anyServoMoving() { for (int i = 0; i < LOGICAL_SERVO_COUNT; i++) { if (servoIsMoving[i]) return true; } return false;}
410void silentInitialize() { for (int i = 0; i < LOGICAL_SERVO_COUNT; i++) { int angle = positionInitial[i]; servoPositions[i] = angle; servoTargetAngles[i] = angle; servoIsMoving[i] = false;
420 int pwmVal = constrain(map(angle, 0, 180, SERVO_MIN, SERVO_MAX), SERVO_MIN, SERVO_MAX); pwm.setPWM(i + 5, 0, pwmVal); }}
Comments