機械にくすぐられたい

どうもこんにちは。
久しぶりの色物記事です。

やはりぱわぱわとは気が合いますね。
もはや非対称性性市場は苛烈を極めているので(中略)
unmannedな性サービス、いやもっと広げて“欲求・心を満たすコト”は発展すると思うんですよ。
壁打ちを極めるというか。
生物系の人、アミューズメント用触手の開発してくださいよ。

さてはて、工学を学ぶ端くれとして想像の補完が効くor実現性を考えることができるということで似たもので機械姦がありますが、やはり前から倒錯はしていたようでpixivのふぁぼ欄は酷いことになっております。

で、中学高校大学と思い立ったタイミングで電子工作をやってるんですが、
あんまり完成したものってなくて、そもそも「いずれ使うだろう」って買ったまま放置してるものもあるんですよね。
そこでおもむろに素材箱漁ってたら天啓を得ました。

今ならできるかも、機械姦。
羽根なら多少の機械力の誤差を吸収するし、くすぐりなら行けるんじゃね?

バカ

ハードの作成

まずは一般の家庭にもあると思うんですが、Arduino mini上位互換のSoC:ATMEGA328pのボードを用意します。

AE-ATMEGA328-MINI

そしてサーボといったらこれ、SG90です。片持ちのホーン無くしちゃいました。

サーボモーターのデファクトスタンダード

ということでうろ覚えですがブレッドボードに実装しました。

今回は軽負荷なのでサーボVccはボード5Vにつけてます。
3つのタクトスイッチを33kΩのプルダウンでDinにつなげて、
シリアルioはusb変換ボードとつなげて電力源・モニタリング・書き込みに使います。

白 モード切り替え 長押しで逆順
黒 値+/- 長押しで大移動

肝心のくすぐるための羽根は適当に用意しました。

確か100円 もうボロくなってる

ソフトの作成

VScodeでいままでやったのはNode.js関連(ts、jsx、html)くらいなので、
C++のスーパーセットであるArduinoは慣れてません。
昔ながらのArduino IED IDEも使えるのですが、どうせならgitつかいたいし環境統一したいのでVScodeのpathにArduino CLIを指定します。

C++のコンパイラもたくさんあるんですね。gccしか知りませんでした。
Cのコンパイラのconfigをarduino関連の拡張機能が書き換え続けるせいで面倒なことになってましたが、なんやかんやで解決しました。(いろんなことあってもう覚えてない)

IDEだとデフォで入ってるservoライブラリもcliの素だと入ってないのでコマンド叩かないといけないんですね。AIに聞いたら一発でした。すごい。

ということで色々あって()書いたのですが、めんどくさいしコード全部垂れ流しちゃいますね。

コード

#include <string.h>
#include <Servo.h>

const int DREAD1=2, DREAD2=3, DREAD3=4;//入力用ボタン端子(プルダウン)
const int sv_pin1=6;  //サーボピン
const int RAND_ANALOG_PIN=0;  //乱数シード用オープン端子

const char* modes[9]={"do","min_ang","max_ang","min_uni","max_uni","min_tim","max_tim","exp_mal","std_mal"};//モード名称
int mode=0;//モード番号
int featuredNum=0;//モード別メイン数値
int ang=90;     //サーボ角度
float expM=0,stdM=0;
int angMin=60,angMax=120,angUnitMin=5,angUnitMax=40,delayMin=1,delayMax=10;//各種乱数オプション

int pushed1=0, pushed2=0, pushed3=0;   //ボタン長押し判定
int timeCount=0;//次移動までの秒数カウント
int nextDelay=10;//次移動までの秒数
int timeMin=400,timeMax=5000;//次移動までの秒数 最大・最小
bool anyPushed=false;//ボタン押下?→シリアル出力


Servo sv1;

void setup() {
  Serial.begin( 9600 );
  pinMode(DREAD1,INPUT);//ピンinput
  pinMode(DREAD2,INPUT);
  pinMode(DREAD3,INPUT);
  sv1.attach(sv_pin1,500,2400);//サーボ定義
}

void loop() {
  randomSeed(analogRead(RAND_ANALOG_PIN));//ランダムシード
  ang=min(ang, 180);
  ang=max(ang, 0);
  sv1.write(ang);
  
  if(digitalRead(DREAD1)==HIGH){pushed1++;}else{pushed1=0;}
  if(digitalRead(DREAD2)==HIGH){pushed2++;}else{pushed2=0;}
  if(digitalRead(DREAD3)==HIGH){pushed3++;}else{pushed3=0;}

  if(hasShortPushed(pushed1)){mode>=8?mode=0:mode++;anyPushed=true;}
  if(hasLongPushed(pushed1)){mode==1?mode=8:(mode<=0?mode=7:mode-=2);anyPushed=true;}

//const char* modes[9]={"do","min_ang","max_ang","min_uni","max_uni","min_tim","max_tim","exp_mal","std_mal"};//モード名称

  if(anyPushed==true){
    char str[64];
    sprintf(str,"%s %d",modes[mode],featuredNum);
    Serial.println(str);
    anyPushed=false;
  }
  


  switch (mode)
  {
  case 0: //do
    if(timeCount>nextDelay){
      timeCount=0;
      //アンダーフロー対策 ここだけang+100
      //if(ang+angUnitMax>angMax){ang-=angUnitMax;}
      //if(ang-angUnitMin<angMin){ang+=angUnitMax;}
      ang+=100;
      int oldAng=ang;
      ang=ang+(angUnitMin+(float(random(0,__INT16_MAX__))/__INT16_MAX__)*(angUnitMax-angUnitMin))-((angUnitMax+angUnitMin)/2);
      while (ang-100>angMax||ang-100<angMin||abs(ang-oldAng)<angUnitMin)//上下範囲外、最小移動以下だとやり直し
      {
        ang=ang+(angUnitMin+(float(random(0,__INT16_MAX__))/__INT16_MAX__)*(angUnitMax-angUnitMin))-((angUnitMax+angUnitMin)/2);
      }
      
      
      

      ang=min(ang, angMax+100);
      ang=max(ang, angMin+100);
      ang-=100;
      nextDelay=makeRandomUES(expM,stdM,timeMin,timeMax);
      char str[12];sprintf(str,"next:%d(ms) angle:%d(deg)",nextDelay,ang);Serial.println(str);//動作中、時間と角度表示
    }
    featuredNum=-1;
    break;
  
    case 1: //min_ang
      if(hasShortPushed(pushed2)){angMin+=1;anyPushed=true;}
      if(hasLongPushed(pushed2)){angMin+=9;anyPushed=true;}
      if(hasShortPushed(pushed3)){angMin-=1;anyPushed=true;}
      if(hasLongPushed(pushed3)){angMin-=9;anyPushed=true;}
      angMin=min(angMin, 180); angMin=max(0,angMin);
      ang=angMin;
      featuredNum=angMin;
    break;

      case 2: //max_ang
      if(hasShortPushed(pushed2)){angMax+=1;anyPushed=true;}
      if(hasLongPushed(pushed2)){angMax+=9;anyPushed=true;}
      if(hasShortPushed(pushed3)){angMax-=1;anyPushed=true;}
      if(hasLongPushed(pushed3)){angMax-=9;anyPushed=true;}
      angMax=min(angMax, 180); angMax=max(angMax, 0);
      ang=angMax;
      featuredNum=angMax;
    break;
      case 3: //min_uni
          if(hasShortPushed(pushed2)) { angUnitMin += 1; anyPushed=true; }
          if(hasLongPushed(pushed2)) { angUnitMin += 9; anyPushed=true; }
          if(hasShortPushed(pushed3)) { angUnitMin -= 1; anyPushed=true; }
          if(hasLongPushed(pushed3)) { angUnitMin -= 9; anyPushed=true; }
          angUnitMin=min(angUnitMin, 180); 
          angUnitMin=max(angUnitMin, 0); 
          if(timeCount%1000==0){ang = angUnitMin;}
          if(timeCount%1000==500){ang = 0;}
          featuredNum=angUnitMin; 
    break;
          case 4: //max_uni
          if(hasShortPushed(pushed2)) { angUnitMax += 1; anyPushed=true; }
          if(hasLongPushed(pushed2)) { angUnitMax += 9; anyPushed=true; }
          if(hasShortPushed(pushed3)) { angUnitMax -= 1; anyPushed=true; }
          if(hasLongPushed(pushed3)) { angUnitMax -= 9; anyPushed=true; }
          angUnitMax=min(angUnitMax, 180); 
          angUnitMax=max(angUnitMax, 0); 
          if(timeCount%1000==0){ang = 0;}
          if(timeCount%1000==500){ang = angUnitMax;}
          featuredNum=angUnitMax; 
    break;
            case 5: //min_time
          if(hasShortPushed(pushed2)) { timeMin += 100; anyPushed=true; }
          if(hasLongPushed(pushed2)) { timeMin += 900; anyPushed=true; }
          if(hasShortPushed(pushed3)) { timeMin -= 100; anyPushed=true; }
          if(hasLongPushed(pushed3)) { timeMin -= 900; anyPushed=true; }
          timeMin=min(timeMin, 3000); 
          timeMin=max(timeMin, 0);
          featuredNum=timeMin;
          
    break;
          case 6: //max_time
          if(hasShortPushed(pushed2)) { timeMax += 100; anyPushed=true; }
          if(hasLongPushed(pushed2)) { timeMax += 900; anyPushed=true; }
          if(hasShortPushed(pushed3)) { timeMax -= 100; anyPushed=true; }
          if(hasLongPushed(pushed3)) { timeMax -= 900; anyPushed=true; }
          timeMax=min(timeMax, 30000); 
          timeMax=max(timeMax, 500);
          featuredNum=timeMax;
    break;
          case 7: //exp_mal
          if(hasShortPushed(pushed2)) { expM += 0.10;anyPushed=true; }
          if(hasLongPushed(pushed2)) { expM += 0.20; anyPushed=true;}
          if(hasShortPushed(pushed3)) { expM -= 0.10;anyPushed=true; }
          if(hasLongPushed(pushed3)) { expM -= 0.20; anyPushed=true;}
          expM=min(expM, 1.0); 
          expM=max(expM, 0.0); 
          featuredNum=expM*100;
    break;
          case 8: //std_mal
          if(hasShortPushed(pushed2)) { stdM += 0.10; anyPushed=true; }
          if(hasLongPushed(pushed2)) { stdM += 0.20; anyPushed=true; }
          if(hasShortPushed(pushed3)) { stdM -= 0.10; anyPushed=true; }
          if(hasLongPushed(pushed3)) { stdM -= 0.20;anyPushed=true;  }
          stdM=min(stdM, 1.0); 
          stdM=max(stdM, 0.0); 
          featuredNum=stdM*100; 
    break;
  default:
    break;
  }


  delay(1);
  timeCount++;
}

int readFromSerial_int(){
  if (Serial.available() > 0) {
    delay(1);
    int val = Serial.parseInt();    //文字列データを数値に変換

    while (Serial.available() > 0) {//受信バッファクリア
      char t = Serial.read();
    }
  return val;
  }
}

bool hasLongPushed(int pushed){//長押し判定
  return pushed==300?true:false;
}

bool hasShortPushed(int pushed){//長押し判定
  return pushed==10?true:false;
}

char readFromSerial_char(){
  char reading_str;
    if(Serial.available() > 0) {
    reading_str = Serial.read();

    if(10 == reading_str) {
      reading_str = '¥n';
    }
  }
  return reading_str;
}

/** 間隔乱数生成 
 * @param expM 指数分布項係数 0-1
 * @param stdM 正規分布項係数 0-1
 * 1-expM-stdM = 一様分布項係数*/
float makeRandomUES(float expM,float stdM,float t_min,float t_max){
  stdM=min(stdM,1);stdM=max(stdM,0);//例外処理
  expM=min(expM,1);expM=max(expM,0);
  if(expM+stdM>1){stdM=0.5;expM=0.5;}

  const float uni=float(random(0,__INT16_MAX__))/__INT16_MAX__; //一様乱数

  float exp=-0.2*log(float(random(0,__INT16_MAX__))/__INT16_MAX__) ;      //指数乱数
exp=min(exp, 1);exp=max(exp, 0); //上限下限

  float std=0;                    //正規分布
  for (size_t i = 0; i < 10; i++) //大数の法則
  {std+=float(random(0,__INT16_MAX__))/__INT16_MAX__;}                  
  std=std/10;
  std=min(std,1);std=max(std,0);                     //上限下限
  
  return t_min+(t_max-t_min)* //多項式
    (uni*(1.0-expM-stdM)
    +exp*expM
    +std*stdM);

}
ライブラリインクルード、ピン定数、変数定義
一回だけの呼び出し
ここからループ
オープンにした端子の電圧値を読み出してそれをシードとして乱数生成
角度保護+サーボ駆動命令
ボタンが押されているか判定
白ボタン短押し:モード順繰り 長押し:逆順
ボタン操作があるときにモニタリングに返す
モード0「実行」
時が来たなら司令角度を変える
桁溢れ・キャスト対策←超めんどい
次に変更するまでの時間を指定する
モード1-4「角度指定」
短押し+/-1 長押し+/-10
最大・最小 角度・単位駆動角度を指定
指定値に実際サーボを動かす
モード5-6「時間指定」
ランダム発生時間の最大・最小を指定
100ms / 1000ms単位
モード7-8「アルゴリズム係数」
指数分布、正規分布の影響度を指定
10% / 20%単位
どちらも0なら一様分布
シリアルモニタリング用処理
長押し短押し判定
今回のキモ、乱数生成。
(Mstd×std)+(Mexp×exp)+((1-Mstd-Mexp)×uni)

一様乱数Uは組み込み関数より、
正規分布は中心極限定理よりU 10回の平均から、
ポアソン過程の到着時間である指数分布は逆関数法(-1/λ)ln(U)で生成。
ただし上限下限時間指定があるので無限大時間に対して再試行。

これらを係数でブレンドすることにより「いい感じの過程」を生成します。

中心極限定理
複数の一様乱数を足し合わせる
生成した指数分布と頭打ちした指数分布
正規分布と指数分布を程よくブレンドした分布
期待できそう
【参考】一定時間で動作可否を判定する二項分布として考えた場合。
今回は次動作する時間を分布とした。
コミットグラフ

高級な環境だと組み込まれてるようなボタン長押し判定とか自分で作るの楽しかったです。

はい。なんかいつの間にかアカデミックに熱中してました。
性欲どこ行ったし。

実際動かす

ほとんど実機を動かさずにここまでプログラム組んでしまいました。
果たしてしっかり動くんでしょうか。

OTGケーブルでスマホにつなげてみましょう。

しっかり機敏に動いてますね。デフォルトは一様乱数なのでそれも確認できます。

角度指定もできてます。

“使った”

はい。なんか拍子抜けです。
既知の性感帯である手のひらと脇でやってみたんですが、
機械的な刺激はあるんですが、ランダム性の「不意打ち」感はないんですよね。
もっと熱意があれば開発続けてたんでしょうけど、もう興味は別のことにいっちゃってますね。
え、"開発"する場所違いだろって?

おやすみ!!

この記事が気に入ったらサポートをしてみませんか?