手の不自由な人のための食事介助ロボット

昨年投稿した食事介助ロボット「マイアーム」の課題点を踏まえ、改良してみました。
正直、前作より性能が落ちたかもしれません。が、今後に繋がる新たな発見も多かったです。

コンテスト記載必要事項


For whom

Aさん(筋ジストロフィー)

Why

障害者施設に勤務していた私が、そこで出会ったAさんに自助具スプーンを作ってほしいと頼まれました。試行錯誤の末、5年の期間を経て昨年ロボットアームが完成しました。
「ラーメンを食べよう」という新たな目標のための、箸を使えるロボットの制作に取り組むこととなりました。

How

6つのモーターを1つのボタンのみでコントロール。ボタンを押すタイミングによって動作が変わるように作成した。

Outcome

前回作よりも自ら食べ物を食べているという操作感が上がった。

前回のマイアームの問題点


1. コントローラー

・サイズが大きく、置く位置を決めるのに時間がかかる
(押す、引くの2動作を行うには、身体の正面に置きたいが、ロボットアームがあるため調整が必要)

・Aさんの車椅子の座り姿勢の崩れによって、力が出しづらくなる

2. 操作感

・お皿を回して食べ物をすくうので、自分自身でキャッチしているという実感が薄いように感じる

3. Aさんの様子

・麺類がすくいにくく、ラーメンを食べることができたら嬉しい

目標

   1.コントローラーの小型化

   2.コントローラーがどの位置であっても操作できる

   3.操作感の向上のため、自らキャッチしにいく動作を作る

   4.ラーメンを食べるため、箸への変更

試作品①


箸でつかむアームを作りました

Aさんの反応 「箸先が顔に向かってくる恐怖感が強い」

箸だけではつかめない物があるので、スプーンも必要

試作品③


箸を断念!フォークとスプーンで挟む形へ変更

やり方を変え、フォークとスプーンで挟むことにした

結果

恐怖感は消えたが、フォークとスプーンで挟んだまま口に入れると、フォークが開く際に口の中であたってしまい、食べ物を口の中に運びにくい
(フォークが滑るように口から出るように改善が必要である)

試作品②

箸とスプーン2つのアームを持つロボットアームを制作



問題点
箸とスプーンのアームが動作中ぶつかることがある
口を開ける時に反射的にどうしても手に力が入ってしまう。その際にボタンを押してしまい、箸が開いて口にあたってしまう
やはり箸に対する恐怖感が取れない
スプーンを動かしている間に箸が突然動くのではないかという恐怖感がある

ロボットアーム自体が大きくなりすぎたため、コントローラーを置く位置がよりテーブルの端になり、押す動作ができなくなった

結論
箸をやめてみることに
コントローラーの改善が必要


結果

良い点
自分からつかみに行く動作が可能になったため、より操作している実感がある
フォークが口から滑り出るようになったので、口元の違和感が少なくなった

問題点
肩の部分のモーターに負担がかかり、たまにアームが落ちてしまう。(関節の傾斜面の角度に改善が必要かも)
皿の中の全ての位置の食べ物をつかむことができず、更にプログラムの改善が必要
皿の回転をサーボモーターにしたため、独立した動きができず、ボタンを押すたびに初期位置に戻ってしまう。皿の回転の意味がなくなってしまった。(モーターをマイアーム1で使用したモーターに戻すか、プログラムの変更で改善できるかも)




Aさんの感想

より操作感が出て、楽しめる物になった。食べる分には以前のロボットアーム1の方が食べやすいかも。(スプーンが一番口当たりが良いため)

ボタンを押してからアームが動くまでの少しのタイムラグが気になる。(複雑なプログラムを入れたための影響)




まとめ

操作感が出たことは良かった。2つのボタンを押すという手の動きで操作する方法(空間的側面)から、1つのボタンでタイミングによって操作する方法(時間的側面)に変えても操作感が落ちることはなかった。

BB弾を使用することによって、3Dプリンターで作れる物の幅が広がった。

Aさんの辛口の感想にショックは受けたが、「タイムラグを感じる」という、私自身は気にならなかった部分を指摘され、Aさんの感覚の鋭さを知り、新たな気づきになった。

今後はモールス信号のように、よりタイミングによって制御できる幅を広げてみたい。

電圧の問題

スプーンを口元に持っていく際に関節に電圧がかかる。マイアーム1は振り子の原理で負担を軽減できていたが、今回はそれができず口元に持っていく際に肩部分のモーターに過度な負担がかかっている。人の身体は口に物を入れる際、肩甲骨自体が下がる(下制)事で肩の負担を減らしているが、マイアーム2の肩甲骨部の動きは軸回転であるため、その動きが再現できていないための考える。

使用する道具


3Dプリンターで作成した部品

ELEGOO 高速PLAプラス RAPID PLA+フィラメント 黒色1.75mm使用

購入した部品

・Arduino
・PCA9685 PWMサーボモーター 
・ハウジングコネクター
・マイクロスイッチ
・ジャンパーワイヤー
・MG996R
・MG996R付属のサーボホーン
・丸形サーボホーン
・サーボコネクター延長ケーブル
・BB弾
・LEDライト
・コントローラー用USBケーブル
・Arduino用USBケーブル
・ACDCアダプター
・直径2mm幅ケーブル
・直径18cm皿
・輪ゴム(直径25mm)
・箸 スプーン フォーク
・工具類

STLファイル

1.台
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.器カバー

組み立て方


手

①ベアリング制作 (4つ)
 ベアリング内とベアリング外のへこみ部分を合わせ、そこからBB弾を挿入 
 (動画参照) 
②モーターとサーボホーンをつける前に、ベアリング部分を各パーツ(肩×2、前腕×1、手部×1)に
 ねじでとりつける
③MG996Rを取り付けねじで固定する
④延長ケーブルを、各関節を通りながらからだ部分を通し台の中に入れ、PCA996⑤番から⑩番につける

 

 

コントローラー

①マイクロスイッチに半分に切断したコードをはんだ付けする(中央と右側の2か所)
②コードとソケットに金具を取り付ける
③コードを穴から通し、スイッチをコントローラーにはめ込む
④ソケットメス部分をコントローラーふたに押し込む
⑤100円USBを切断し、コントローラーケーブルを制作
⑥コントローラーのレール部分にBB弾を挿入(動画参照)
⑦両側のコントローラー壁部分を取り付けBB弾が外れないようにする
⑧完成
 ※コントローラーのスイッチ穴は2つあるが1つしか使わない

台 / 配線

①回転部にサーボモーターと、サーボホーンを取り付け、レール部分のくぼみからBB弾を挿入
②配線(配線図参照)

指

①各関節をワッシャーを入れねじでとめる
②スライド部レールにBB弾を入れ、ふたをねじどめする
③輪ゴム(ナンバー12、直径25㎜)を3か所(スライド部分×2 アーム部分×1)取り付ける
④フォーク、スプーン(もしくは箸)は10円玉でねじ固定する

ベアリング制作


コントローラーレール部分


配線図


プログラミングコード


1

#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

2

#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};

3

bool 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};

4

int 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}
};

5

int 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}
};

6

// --- スピード / ディレイ ---
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},

7

 {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},

8

 {0, 0, 500, 0, 500, 500},
  {700, 700, 700, 700, 700, 0},
  {0, 0, 500, 0, 500, 500}
};

9

unsigned 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},

10

{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},

11

 {0, 0, 0, 0, 0, 0},
  {0, 0, 0, 0, 0, 0},
  {0, 0, 0, 0, 0, 0},
  {0, 0, 0, 5, 0, 0}
};

// 戻りパラメータ

12

unsigned 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};

13

unsigned 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};

14

// 状態機械
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;

15

int currentGoal = -1;
bool buttonPreviouslyPressed = false;
unsigned long lastReturnTime = 0;
unsigned long lastButtonPressTime = 0;
const unsigned long debounceDelay = 50;

16

// プロトタイプ
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);

17

void updateServo(int i);
bool anyServoMoving();
void silentInitialize();

void setup() {
  Serial.begin(115200);

18

Wire.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;
}

19

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

20

if (isPressed && !buttonPreviouslyPressed && (now - lastButtonPressTime > debounceDelay)) {
    lastButtonPressTime = now;
    buttonPreviouslyPressed = true;

21

// --- ボタン押下処理 ---
    if (state == MOVING_TO_INITIAL) {
      currentGoal = (currentGoal + 1) % 7;
      moveToPosition(positionMiddles[currentGoal], speedToMiddle[currentGoal], delayToMiddle[currentGoal]);

22

state = MOVING_TO_MIDDLE;
      return;
    }

    if (state == MOVING_TO_MOUTH) {
      // Mouth 未到達キャンセル → FromMouth
      moveToPosition(positionInitial, speedFromMouth, delayFromMouth);

23

 state = MOVING_TO_INITIAL;
      return;
    }

    if (state == MOVING_TO_GOAL || state == MOVING_TO_MIDDLE) {
      // Goal 未到達キャンセル → FromGoal
      moveToPosition(positionInitial, speedFromGoal, delayFromGoal);

24

state = MOVING_TO_INITIAL;
      return;
    }

    if (state == AT_GOAL) {
      moveToPosition(positionInitial, speedFromGoal, delayFromGoal);
      state = MOVING_TO_INITIAL;
      return;
    }

25

if (state == AT_MOUTH) {
      moveToPosition(positionInitial, speedFromMouth, delayFromMouth);
      state = RETURNING_FROM_MOUTH;
      return;
    }

26

 if (isMouthReady) {
      moveToPosition(positionMouth, speedToMouth, delayToMouth);
      state = MOVING_TO_MOUTH;
    } else if (state == IDLE) {
      currentGoal = (currentGoal + 1) % 7;

27

 moveToPosition(positionMiddles[currentGoal], speedToMiddle[currentGoal], delayToMiddle[currentGoal]);
      state = MOVING_TO_MIDDLE;
    }
  }

28

if (!isPressed) buttonPreviouslyPressed = false;

  for (int i = 0; i < LOGICAL_SERVO_COUNT; i++) updateServo(i);

  if (!anyServoMoving()) {
    switch (state) {
      case MOVING_TO_INITIAL:

29

 case RETURNING_FROM_MOUTH:
        state = IDLE;
        lastReturnTime = millis();
        break;
      case MOVING_TO_MIDDLE:

30

moveToPosition(positionGoals[currentGoal], speedToGoal[currentGoal], delayToGoal[currentGoal]);
        state = MOVING_TO_GOAL;
        break;

31

case MOVING_TO_GOAL:
        state = AT_GOAL;
        lastReturnTime = millis();
        break;
      case MOVING_TO_MOUTH:

32

state = AT_MOUTH;
        break;
      default:
        break;
    }
  }
}

33

void 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++) {

34

setServoTarget(i, targetAngles[i], speeds[i], delays[i]);
  }
}

void setServoTarget(int i, float target, unsigned long duration, unsigned long delayTime) {

35

 servoStartAngles[i] = servoPositions[i];
  servoTargetAngles[i] = constrain(target, 0.0f, 180.0f);
  servoMoveDurations[i] = duration;
  servoStartTimes[i] = millis() + delayTime;
  servoIsMoving[i] = true;
}

36

void updateServo(int i) {
  if (!servoIsMoving[i]) return;
  unsigned long now = millis();
  if (now < servoStartTimes[i]) return;

37

unsigned long elapsed = now - servoStartTimes[i];
  if (elapsed >= servoMoveDurations[i]) {
    servoPositions[i] = servoTargetAngles[i];
    servoIsMoving[i] = false;
  } else {

38

 float t = (float)elapsed / servoMoveDurations[i];
    float easedT = t * t * (3 - 2 * t);
    servoPositions[i] = servoStartAngles[i] + (servoTargetAngles[i] - servoStartAngles[i]) * easedT;
  }

39

 int pwmVal = constrain(map(servoPositions[i], 0, 180, SERVO_MIN, SERVO_MAX), SERVO_MIN, SERVO_MAX);
  pwm.setPWM(i + 5, 0, pwmVal);
}

40

bool anyServoMoving() {
  for (int i = 0; i < LOGICAL_SERVO_COUNT; i++) {
    if (servoIsMoving[i]) return true;
  }
  return false;
}

41

void silentInitialize() {
  for (int i = 0; i < LOGICAL_SERVO_COUNT; i++) {
    int angle = positionInitial[i];
    servoPositions[i] = angle;
    servoTargetAngles[i] = angle;
    servoIsMoving[i] = false;

42

 int pwmVal = constrain(map(angle, 0, 180, SERVO_MIN, SERVO_MAX), SERVO_MIN, SERVO_MAX);
    pwm.setPWM(i + 5, 0, pwmVal);
  }
}

完成


コントローラーの小型化

1ボタン操作に変えた
コントローラーにBB弾を使用し動作をスムーズに
コントローラーに傾斜をつけ、ボタンを押した後、腕の重さで元の位置に戻るようにした



BB弾の使用

・滑る部分にBB弾を入れ、スムーズに動くようにした
・BB弾の厚みによって、部品同士が外れないようにした

操作の改善

食べ物がスプーンの上に乗るのを待つのではなく、自分から掴みにいくようにした
皿の中に7か所の目標位置を設定し、3秒以内にボタンを押すと次の目標位置にスプーンが移動する

1ボタンで全てを操作するために、ボタンを離している間の時間差によって動作が変わるよう変更した

3秒以内  次の目標位置(全部で皿内の7か所の目標位置)
3秒経過  LEDが点灯し口に移動

箸の動きを滑りながら口から出るように

フォークの動きを回転運動から、複雑な動きができるように構造を変えた
(箸リンク機構)


アームの関節にかかる負担の回避

関節面を斜めにして、重さの負担を軽減した。(人間の各関節の傾斜面に参考にした)