机上の空論主義者

-♰- 有言不実行の自身をブログ名で戒めろ -♰-

「人造先生はチョークを投げるのか」システムを作った

こんにちは!

今回は、「身の回りの困りごとを楽しく解決!【PR】Works Human Intelligence Advent Calendar 2021」の19日目の記事です。このAdvent Calenderでは、「実際に身の周りに起きた困りごとを技術で「楽しく」解決した話」について募集しており、僕もアイデアを解消するいい機会だと思い予約しました。とはいっても、当初予定していた社内雑談用ツールを実装するのはやめて、もう少し面白そうなものを作ってみました。


概要

今回対象とした「身の回りに起きた困りごと」は、オンラインの勉強動画で集中力が全然持たない問題です。昨年、リモート授業onlyだった大学院生活を送りましたが、先生の目が光っていない状況だとものすごく眠たくなるんですよねぇ。集中力も持たないし。

それは既に社会人の皆さんでも、Udemyなどのオンライン学習ツールを用いている方は共感していただけるのではないでしょうか。Udemyなどでも、多少は教師の顔表情が見えたりはします。ただ突然、「ここテストに出るよ」と重要な点を述べたり、悪い学生に対して癇癪を起したりはしないので低刺激なわけですね。

ということで今回は、家にも物理的な先生を用意しよう!というアプローチで解決を図ってみたいと思います!



実装内容

①人造先生の錬成

まずは物理的な先生がいなければ話にもなりません。適当な人形があればそれでいいのですが、手短に存在しなかったので針金で人造先生を錬成しました。人生初の針金工作であり、バランスよく形状を生成することは困難を極めましたが、何重かにワイヤを張り巡らせてネジネジすることで、十分な強度でそれっぽいものが作れました。

手には綿棒を無理やりくっつけており、これが指示棒となって我々への指導をわかりやすくしてくれているわけですね。学生が効率よく学べるためのたゆまぬ工夫、素晴らしい指導者です。

f:id:ume-boshi:20211219033751j:plain:w450
私が針金人造先生だ


②教員のランダムな移動

我々学生は、教鞭のために熱意がある姿勢を持つ先生に惹かれる傾向があるはず。ということで次に、積極的に重要箇所を指すように,先生には教壇の上で移動してもらいます。

そのために、ただのラジコン的な移動体に先生を乗せ、その左右移動を制御していきます。移動速度や制御タイミングは、ある特定の値から一様乱数を用いて適当にランダムにしています。移動用ロボットは、ESP32を用いた自作基板のを利用しています。開発環境はArduino IDEです。ただ、それだけでは先生が授業をボイコットして、どこか遠くまでダッシュで逃げてしまうため、移動の限界を設けました。

f:id:ume-boshi:20211219035529p:plain
左右に移動する先生のシステム

この ↑ 写真の、オレンジで囲ったものが左右に移動するロボットであり、緑の銀色マーカに囲われた範囲のみを移動できるようになっています。

移動ロボットは、↓ 過去に紹介したものを利用しています。

ume-boshi.hatenablog.jp


左右の限界位置の検出には、ライントレーサなどにも使われるようなフォトリフレクタを用いるために、高反射性の素材が望まれます。そのために、今回は人造先生一押し!うまいチュウのイチゴ味の包装紙裏側を用いました。(我が家にはアルミホイルが無いのです)

f:id:ume-boshi:20211219041205j:plain:w450
うまいチュウおいしいです。



そして,左右に移動するだけの動画は ↓ になります。もう少し移動量が少なくてもよかったかもね。

youtu.be


③1/f ゆらぎを用いた振り向きの実装

さて、ただ左右に移動するだけの先生は、ただ説明に焦っているだけの先生でしかありません。これからはもっと生徒と向き合ってもらわなければなりません。生徒の理解度も配慮できてこそ、本当にすぐれた教師といえるでしょう。ということで、サーボを用いて先生をYaw角方向に回転させていきます。

サーボを動作させるための回路は、下図の通りです。サーボ(SG92)の定格は4.8Vなので、ESP32の駆動電圧が3.3Vと異なるため、25番ピンからPWM信号をトランジスタのベースに流して制御してあげます。

f:id:ume-boshi:20211219051837p:plain:w250
サーボ用回路

より人間らしい動きとするために、自然界に存在する1/f ゆらぎに基づくランダム性を付加しました。1/f ゆらぎの実装には、間欠カオス法というものを利用すれば簡易的にできるみたいです。これについては、↓ の記事を参考にしました。

satotoshio.net



さて、この1/f ゆらぎを間欠カオス法で求める場合、0 ~ 1の浮動小数点で導出されます。これを0 ~ 90に適応して、サーボに適用してあげただけです。そしてそのサーボをいい感じに設置してあげれば完了です。

youtu.be

一気に人間らしさが出てきましたよ。針金ならではの振動がいい味を出していますな。




完成品

youtu.be

一度、あまりの熱弁が裏目に、教壇を超えた指導に走ってしまいましたが、これも人造先生ならではの良さとはいえるのではないでしょうか。人造先生の熱弁によって新猫論の講義を真剣に聞くことができ、中生代後期白亜紀トリケラトプスも大歓喜、バジル君も驚きのあまり口をあんぐりですね。




おわりに

このシステムを今後発展させるつもりはないですが、寝ている人に叱責をしたり、肩をポンポンたたいてきたり、「○○、ここの答えは何だと思う?(威圧)」と声をかけたりするように、使用者との相互作用を増やしていくことで、より現実の授業のような感覚を各家庭に届けられるかもしれませんね。
(Works Human Intelligence様。プレゼント用にルンバが欲しいです、よろしくお願いします!)


こういう、短期で実装する系の開発記事は久しぶりに書いた気がしますが、結構気楽にネタを含められて楽しかったです。来年度、社会人になってからはこういう突発的な記事が増えるかもしれませんね。

それでは ;)




付録:プログラム

#include <ESP32Servo.h>

/*モータ制御用ピン * 8の定義省略*/
#define Photo 35
#define BACK 0
#define FRONT 1

/* pwm definition 一部省略*/
#define LEDC_CHANNEL_0 0
#define LEDC_TIMER_10_BIT 10
#define LEDC_BASE_FREQ 5000
#define uS_TO_S_FACTOR 1000000  /* Conversion factor for micro seconds to seconds */

float animParam = 0.1;
float mInterval = 1.0;
float animInterval = 1.0;
long startTime = 0;
long startMTime = 0;
float speed;
Servo humanServo;


void setup() {  
  Serial.begin(115200);
  /* speaker pwm init */
  ledcSetup(LEDC_CHANNEL_1, LEDC_BASE_FREQ, LEDC_TIMER_10_BIT);
  ledcAttachPin(BLIN2, LEDC_CHANNEL_1);
  /*その他PWMピンの定義は省略*/

  int minUs = 500;
  int maxUs = 2400;
  humanServo.setPeriodHertz(50);
  humanServo.attach(25,minUs,maxUs);
  pinMode(Photo,INPUT);

  /*初期状態はモータを止める*/
  ledcWrite(LEDC_CHANNEL_0,0);
  ledcWrite(LEDC_CHANNEL_1,0);
  ledcWrite(LEDC_CHANNEL_2,0);
  ledcWrite(LEDC_CHANNEL_3,0);

  animParam = 0.1;
  startTime = millis();
  speed = 0;
}


void loop() {
  /*だいたい1000 ms ごとに先生をサーボで回転*/
  if(millis()-startTime > 1000*animInterval){
    /*間欠カオス法による 1/f ゆらぎ*/
    if(animParam> 0.5) animParam = animParam + 2*animParam*animParam;
    else  animParam = animParam - 2*(1.0-animParam)*(1.0-animParam);
    if(animParam < 0.1 || animParam > 0.9){
      animParam = random(10, 90) / 100.0;
    }

    humanServo.write(int(90 * animParam));
    delay(10);

 animInterval = random(5, 15)/10.0//次にいつ回転させるか 
    startTime = millis();
  }

  /*だいたい1000 ms ごとに先生の移動速度を決定*/
  if(millis()-startMTime > 1000*mInterval){
    speed = random(-10, 10)/10.0;
    Serial.println(200*speed);

    // 前後どっちかのモータしか回転してません
    actMotorFB(FRONT, 200*speed, 200*speed);
    actMotorFB(BACK , 200*speed, 200*speed);
    
    mInterval = random(5, 15)/10.0;
    startMTime = millis();
  }

  /*左右の移動制限に達したときに,移動方向と反対に制御する*/
  int val = analogRead(Photo);
  if(val>3100){ 
    Serial.print(val);
    Serial.print("  ");
    Serial.println(speed);
    if(speed > 0){
      actMotorFB(FRONT, -300, -300); 
      actMotorFB(BACK , -300, -300);
    }else{
      actMotorFB(FRONT, 300, 300);
      actMotorFB(BACK , 300, 300);
    }
    delay(400);
    actMotorFB(FRONT, 0, 0);
    actMotorFB(BACK , 0, 0);
  }
}


//モータの制御用.
void actMotorFB(int ForB,int speedL,int speedR){ 
  int R1,R2,L1,L2;
  if(ForB == FRONT){
    R1=LEDC_CHANNEL_2; R2=LEDC_CHANNEL_3;  
    L1=LEDC_CHANNEL_0; L2=LEDC_CHANNEL_1;  
  }else{
    R1=LEDC_CHANNEL_6; R2=LEDC_CHANNEL_7;  
    L1=LEDC_CHANNEL_4; L2=LEDC_CHANNEL_5;  
  }

  if(speedL >= 0){
    ledcWrite(L1,speedL);
    ledcWrite(L2,0);
  }else{
    ledcWrite(L1,0);
    ledcWrite(L2,-speedL);  
  }

  if(speedR >= 0){
    ledcWrite(R1,speedR);
    ledcWrite(R2,0);
  }else{
    ledcWrite(R1,0);
    ledcWrite(R2,-speedR);    
  }
}