2014年12月13日土曜日

【Processing】平面二軸アームを動かす

こんにちは。前回の記事では、平面二軸アームの設計ということで、フリーソフトである3DCADのDesignSpark Mechanicalを使ってサーボモータ同士を繋ぐアーム部分の設計をしました。今回の記事では、その3DCADで作製した設計の通りにアクリル板を切り出して、アクリル曲げヒータで曲げ加工をし、サーボと合わせてロボットアームを作成し、ProcessingとArduinoを使って動かします。

 今回購入したのは厚さ3 mmの透明アクリル板910 mm × 600 mmです。近所のホームセンターで4800円でした。この板から今回使用するアームで使用する分(250 mm × 50 mm)を切り出します。切り出すためには、プラスチックカッターを使用します。プラスチックカッターによる加工は、安価であるというメリットがあります。しかし、切る部分に溝を彫り、ある程度のところまで彫れたら力を加えて割る必要があるため細かい加工が難しく、時間がかかるという欠点があります。

 
▲アクリルカッター

初めての加工ということもあり、細かい部分が割れてしまうというアクシデントがありましたが、組み立てには支障なかったのでこのまま使用します。(卓上糸鋸盤がほしい…。)
▲赤丸部分が欠けてしまっている

 切り出すことができたら、アクリル曲げヒータを使って設計どおりに曲げます。アクリル曲げヒータはアクリルに170℃付近まで熱を加える事で熱可塑性により加工を可能としています。アクリル曲げヒータは市販のものを購入すると結構なお値段(\7500~)になりますが、自分で材料を集めれば安く作ることもできます。僕はファブクラウドさんが販売しているアクリルヒータキットを購入しました。(送料込み3000円程度)

 曲げ加工まで済んだらサーボモータをくっつけて(横着して両面テープでとめた)ロボットアームの形にします。
▲横着ぼろっと

最後に、Processingで逆運動学により算出された各関節の角度をArduinoにシリアル通信で送信するプログラムを書いて動かします。

_kiitaniさん(@monitorgazer)が投稿した動画 -

 加工が適当すぎて、ボールペンの接触が不安定ですね。高専時代にもっと機械加工を実践しておくべきだったな〜と思います。


 うーん。アーティスティック…。

以下プログラムです。
Processing(データ送信側)
import controlP5.*;
import processing.serial.*;

ControlP5 cp5;
PGraphics pg;
Slider2D ui;
Serial myPort;

int[] cmd = {255,0,3,0,90,90};
boolean init = false;

float x1,y1,x2,y2,xe,ye;
float oldX2,oldY2,oldXe,oldYe;
float angle1;
float angle2;
float angle1_;
float angle2_;
float the1;
float the2;
final float l1 = 100;
final float l2 = 100;
final int r = 20;
int dir = 1;

void setup(){
  size(640,640);
  stroke(255,255,255);
  fill(255,255,255);
  //set to origin.
  x1 = 300;
  y1 = 350;
  pg = createGraphics(width,height,JAVA2D);
  cp5 = new ControlP5(this);
  
  ui = cp5.addSlider2D("Position")
    .setSize(200,200)
    .setArrayValue(new float[] {79, 1})
    .setMaxX(150)
    .setMaxY(132)
    .setMinX(-90)
    .setMinY(-50)
    .setPosition(30,30)
    ;
  String portName = Serial.list()[5];
  println(Serial.list());
  myPort = new Serial(this,portName,9600);
  init = true;
}

void draw()
{ 
  //myPort.write("123,456\0");
  background(0);
  angle1_ = acos((pow(l1,2)-pow(l2,2)+(pow(ui.arrayValue()[0],2)+pow(ui.arrayValue()[1],2)))/((2*l1)*sqrt(pow(ui.arrayValue()[0],2)+pow(ui.arrayValue()[1],2))));
  angle1 = atan2(ui.arrayValue()[1],ui.arrayValue()[0])-angle1_;
  angle2_ = acos((pow(l2,2)-pow(l1,2)+(pow(ui.arrayValue()[0],2)+pow(ui.arrayValue()[1],2)))/((2*l2)*sqrt(pow(ui.arrayValue()[0],2)+pow(ui.arrayValue()[1],2))));
  angle2 = angle1_ + angle2_;
  //print(angle1);print(":");println(angle2);
  float degangle1 = angle1 * 180/PI;
  float degangle2 = angle2 * 180/PI;
  angle1 = degangle1 * PI/180;
  angle2 = degangle2 * PI/180;
  x2 = x1+l1*cos(angle1);
  y2 = y1+l1*sin(angle1);
  xe = x2+l2*cos(angle1+angle2);
  ye = y2+l2*sin(angle1+angle2);
  if(degangle1 <= -90){
    xe = oldXe;
    ye = oldYe;
    x2 = oldX2;
    y2 = oldY2;
    degangle1 = -90;
  }
  if(degangle1 >= 90){
    xe = oldXe;
    ye = oldYe;
    x2 = oldX2;
    y2 = oldY2;
    degangle1 = 90;
  }
  if(angle2<=0){
    xe = oldXe;
    ye = oldYe;
    x2 = oldX2;
    y2 = oldY2;
    degangle2 = 0;
  }
  if(angle2>=180){
    xe = oldXe;
    ye = oldYe;
    x2 = oldX2;
    y2 = oldY2;
    degangle2 = 180;
  }
  ellipse(x1,y1,r,r);
  ellipse(x2,y2,r,r);
  fill(255,0,0);
  ellipse(xe,ye,r,r);
  fill(255);
  line(x1,y1,x2,y2);
  line(x2,y2,xe,ye);
  pg.beginDraw();
    pg.noStroke();
    pg.fill(0,100,100);
    //pg.ellipse(xe,ye,r,r);
  pg.endDraw();
  image(pg.get(0,0,width,height),0,0);
  
  //print(degangle1);print(":");println(degangle2);
  cmd[4] = (int)(90-degangle1);
  cmd[5] = (int)(180-degangle2);
  oldXe = xe;
  oldYe = ye;
  oldX2 = x2;
  oldY2 = y2;
  servoCmd();
}

void servoCmd() {
  if(!init) return;
  for(int i = 0; i < 6; i++) {
    myPort.write((byte)cmd[i]);
  }
}

void mouseDragged(){
  int x = (int)(mouseX-x1);
  int y = (int)(mouseY-y1);
  
  if( x <= -90) x = -90;
  if( x >= 150) x = 150;
  if( y <= -50) y = -50;
  if( y >= 132) y = 130;
  ui.arrayValue()[0] = x;
  ui.arrayValue()[1] = y; 
  print(ui.arrayValue()[0]);print(":");println(ui.arrayValue()[1]);
}

Arduino(データ受信側)
 #include <Servo.h> 
#include <string.h>
#include <stdlib.h>

Servo myservo1,myservo2,myservo3;
float deg1,deg2;
unsigned char incomingByte = 0;
unsigned char data[20];
int axis1 = 9;
int axis2 = 10;
int axis3 = 11;

int count = 0;
int paramLength = 0;
int paramCount = 0;
 
int pos = 0;    // variable to store the servo position 
 
void setup() 
{
  Serial.begin(9600); 
  myservo1.attach(axis1,800,2200);
  myservo2.attach(axis2,800,2200);
} 
 
 
void loop() 
{ 
  if(Serial.available() > 0){
      incomingByte = Serial.read();
      switch(count){
          case 0:
              paramLength = 0;
              paramCount = 0;
              if(incomingByte == 0xff){ //Header
                  data[count++] = incomingByte;
              }
              break;
          case 1:
              if(incomingByte == 0){ //ID
                  data[count++] = incomingByte;
              }
              break;
          case 2:    //LENGTH
              data[count++] = incomingByte;
              paramLength = incomingByte;
              break;
          default:
              if(paramLength > 0){
                      data[count++] = incomingByte;
                      paramCount++;
                      if(paramCount >= paramLength){
                          ServoCmdExec();
                          count = 0;
                      }
              }else{
                  count = 0;
              }
              break;
      }
  } 
}

void ServoCmdExec(){
    if((data[4] >= 0) && (data[4] <= 180) ) myservo1.write(data[4]);
    if((data[5] >= 0) && (data[5] <= 180) ) myservo2.write(data[5]);
}

複数のサーボをProcessingとArduinoのシリアル通信で制御するためには少し工夫が必要になります。

参考にしたサイト様

2014年12月6日土曜日

【DesignSpark Mechanical】平面二軸ロボットアームの設計

こんにちは。
 前回の記事では次はロボットを組み立てる。と書いていましたが、今日アクリル板を買ったら予想以上に高価で失敗したくないなと思い、3DCADソフトで設計してから実機を作成することにしました。。。

 使用したソフトはDesignSpark Mechanicalというフリーソフトです。Windowsでのみリリースされています。Mac版が欲しい…。
 
 高専に居た頃、SolidWorksを少しかじったことがあるので、わりとすんなり使うことができました。しかし細かい機能は全然わからない。。。

とりあえず、平面二軸アームということで、構成要素はサーボ二台と、アーム二本です。

サーボのモデルはこんな感じでつくりました。



これをアームと組み合わせて



こんな感じになります。

手先先端の穴にはペン等を保持できるように開けています。

次回こそは実機の制作の記事を書きたいと思います。

【Processing】平面二軸ロボットの逆運動学

こんにちは。
 前回の記事”【Processing】平面二軸ロボットの順運動学”では各関節の角度を指定して手先の位置を決める順運動学をProcessingを使って実装しました。今回の記事では手先位置を指定して各関節の角度を決定する逆運動学をProcessingを使って実装します。

 逆運動学を解くと以下のようになり、手先位置XE,YE及びリンク長L1,L2より、各関節角度θ1、θ2を決定することができます。実際には同じ手先位置を実現するロボットアームの姿勢が2つ以上存在することがあります。その場合は静力学、動力学を考慮して拘束条件を設定し、各関節の負担を小さくしたりする必要があります。


この式にもとづいて、Processingのプログラムを作成すると以下のようになります。

import controlP5.*;

ControlP5 cp5;
PGraphics pg;
Slider2D ui;

float x1,y1,x2,y2,xe,ye;
float angle1;
float angle2;
float angle1_;
float angle2_;
float the1;
float the2;
final float l1 = 100;
final float l2 = 80;
final int r = 20;
int dir = 1;

void setup(){
  size(640,640);
  stroke(255,255,255);
  fill(255,255,255);
  //set to origin.
  x1 = 300;
  y1 = 350;
  pg = createGraphics(width,height,JAVA2D);
  cp5 = new ControlP5(this);
  
  ui = cp5.addSlider2D("Position")
    .setPosition(30,40)
    .setSize(200,200)
    .setArrayValue(new float[] {15, 30})
    ;
}

void draw()
{ 
  background(0);
  angle1_ = acos((pow(l1,2)-pow(l2,2)+(pow(ui.arrayValue()[0],2)+pow(ui.arrayValue()[1],2)))/((2*l1)*sqrt(pow(ui.arrayValue()[0],2)+pow(ui.arrayValue()[1],2))));
  angle1 = atan2(ui.arrayValue()[1],ui.arrayValue()[0])-angle1_;
  angle2_ = acos((pow(l2,2)-pow(l1,2)+(pow(ui.arrayValue()[0],2)+pow(ui.arrayValue()[1],2)))/((2*l2)*sqrt(pow(ui.arrayValue()[0],2)+pow(ui.arrayValue()[1],2))));
  angle2 = angle1_ + angle2_;
  the1 = angle1*PI/180;
  the2 = angle2*PI/180;
  print(angle1);print(":");println(angle2);
  x2 = x1+l1*cos(angle1);
  y2 = y1+l1*sin(angle1);
  xe = x2+l2*cos(angle1+angle2);
  ye = y2+l2*sin(angle1+angle2);
  ellipse(x1,y1,r,r);
  ellipse(x2,y2,r,r);
  fill(255,0,0);
  ellipse(xe,ye,r,r);
  fill(255);
  line(x1,y1,x2,y2);
  line(x2,y2,xe,ye);
  pg.beginDraw();
    pg.noStroke();
    pg.fill(0,100,100);
    pg.ellipse(xe,ye,r,r);
  pg.endDraw();
  image(pg.get(0,0,width,height),0,0);
}

逆運動学を使用すると、手先のX、Y座標を指定してアーム姿勢を決定するため、以下のように手先をX方向のみに移動させることやY方向のみに移動することができます。
_kiitaniさん(@monitorgazer)が投稿した動画 -
また、マウスパッドのようにXY座標を同時に入力するインターフェースを用いると、手先位置をXY軸で自由に動かすことができるので、絵を描いたりすることができます。

_kiitaniさん(@monitorgazer)が投稿した動画 -
次回はこのプログラムを基に、Arduinoとサーボモータを使用して、実際に平面二軸ロボットを作ります。

2014年12月4日木曜日

【Processing】平面二軸ロボットの順運動学

こんにちは。
 最近秋葉原でサーボモータを買いました。サーボで簡単にアームロボットを作りながら、ロボット制御工学の基礎を復習してゆきます。まずは、サーボを2つ使って平面二軸アームを作成し、ロボットアームを作ったお絵かきをしたいと思います。

 まずは基礎の基礎の順運動学を解いていきます。順運動学では各軸の角度を決めた時、エンドエフェクタがどの位置に来るかを計算するものです。平面二軸アームを用いると以下に示すようにXY平面上で(可動域の制限内で)自由に位置決めすることができます。このようにアームの各関節の角度が決まった時に、手先の位置を計算する方法を順運動学といいます。
※図の変更(2014/12/04 19:25)

 では、順運動学の計算を用いて平面二軸アームの可動域を可視化するプログラムを書いてみましょう。開発環境にはビジュアルプログラミングが簡単に行えるProcessingを用います。(今後Arduinoでアームを制御する際にも便利)以下にソースコードを示します。

import controlP5.*;
import java.util.ListIterator;

ControlP5 ui;
PGraphics pg;

float x1,y1,x2,y2,xe,ye;
float angle1 = 90;
float angle2;
float the1;
float the2;
final float l1 = 150;
final float l2 = 100;
final int r = 20;
int dir = 1;

void setup(){
  size(640,640);
  stroke(255,255,255);
  fill(255,255,255);
  ui = new ControlP5(this);
  
  ui.addSlider("angle1")
     .setPosition(10,10)
     .setSize(200,20)
     .setRange(-90,90)
     .setValue(-90);
  ui.addSlider("angle2")
     .setPosition(10,50)
     .setSize(200,20)
     .setRange(0,180)
     .setValue(90);   
  //set to origin.
  x1 = 300;
  y1 = 350;
  pg = createGraphics(width,height,JAVA2D);
  
}

void draw()
{
  //angle1++;
  //if(angle1>=90) angle1 = -90;
  //for(int i=0; i<=180; i++){
  // angle2=i; 
  background(0);
  the1 = angle1*PI/180;
  the2 = angle2*PI/180;
  ellipse(x1,y1,r,r);
  x2 = x1+l1*cos(the1);
  y2 = y1+l1*sin(the1);
  xe = x2+l2*cos(the2+the1);
  ye = y2+l2*sin(the2+the1);
  ellipse(x2,y2,r,r);
  ellipse(xe,ye,r,r);
  line(x1,y1,x2,y2);
  line(x2,y2,xe,ye);
  pg.beginDraw();
    pg.noStroke();
    pg.fill(0,100,100);
    pg.ellipse(xe,ye,r,r);
  pg.endDraw();
  //image(pg.get(0,0,width,height),0,0);
  //}
}

 左上に表示されるスライドバーは各関節の角度に対応しています。また、ソースコードのコメントを外すことで自動的に全ての可動域を描画することができます。以下は全ての可動域を可視化した時の動画です。アームの動作をDrawで描画しつつ、可動域を描画するためのTipsはPGraphicsを使用することです。pg.beginDrawからpg.endDraw()までに書かれた描画処理はDraw関数内のリフレッシュによっても消えない独立されたレイヤーに描画されるため、アームの動作とエンドエフェクタの軌跡を同時に示すことができます。


 これで平面アームの順運動学を解き、各関節の角度によってエンドエフェクタがどう動くかをシミュレーションすることが出来ました。順運動学は理解しやすい反面、自由度の増加や三次元空間への拡張を考えると複雑になりすぎて現実的ではありません。そこで用いられるのが逆運動学です。これは、エンドエフェクタの座標を決定したとき、各関節角度はどのようになるかを解くものです。次回は平面二軸アームの逆運動学に取り組みます。

2014年12月2日火曜日

C言語でArduinoとシリアル通信してアナログ電圧値をグラフ表示するWin32なC/C++のサンプルプログラムをつくる(1)

 こんにちは。タイトル長い。所用でとあるアナログデータをAD変換をしてPCに取り込み、グラフ表示するプログラムが必要になったので、”こんな感じでできます”サンプルとして、VisualStudioでWin32のプログラムを書いてゆきます。開発環境はWindows 7 32bit , Visual Studio 2012です。

 AD変換器として部屋に転がっていたArduinoUNOを使用しました。ArduinoUNOは10bitのADCを持っていますので、0-5Vのアナログ入力値を0-1023段階、4.9mV単位で量子化します。本当はもっと深いAD変換が必要だと思いますが、とりあえず簡易なデータロギング・表示のサンプルです。

 これまでC言語でGUIを持つプログラムはFormしか作ってこなかったので、かなり調べながらやりました。要素の配置を頭で想像しながらやるのはかなり大変でした。全ての処理を把握したいという要望があるみたいなので、Cで作れば文句ないと思います。

 簡単に処理の流れを書くと。。。
  1. ウィンドウ作成
  2. シリアルポート確立
  3. グラフグリッド描画
  4. グラフのリアルタイム描画用副スレッドの立ち上げ
  5. 副スレッドにてシリアルデータを読みながらプロットの描画
 と、こんな感じになっています。

プログラムを見ていく前にまず、プロジェクトの作成方法を確認します。

Win32GUIプログラムのプロジェクトの作成方法
 起動したらまず、新しいプロジェクトをクリックします。



 そうすると次のようなウィンドウが表示されますので、Visual C++ -> Win32 -> Win32プロジェクトの順で選択肢、適当な名前を入力してOKをクリックします。


すると、以下のようなウィザードが現れますが、特に変更するべきことはありませんので、完了をクリックしてプロジェクトの作成を完了します。


 プロジェクトの作成が完了しましたら、ビルドして実行をしてみます。ショートカットキーはF5です。できていれば何もない白いウィンドウが表示されるはずです。これが、Win32プログラムのスタートポイントです。


プログラム作成
 ではここからは作成したひな形に自分のプログラムを加えていきます。まずは、ウィンドウの大きさや名前を変更してみます。
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
   HWND hWnd;
   DWORD dwID;
   
   hInst = hInstance; // グローバル変数にインスタンス処理を格納します。
   
   //Windowの大きさと名前の変更
   hWnd = CreateWindow(szWindowClass, L"GRAPHS", WS_OVERLAPPEDWINDOW,
      CW_USEDEFAULT, 0, MW_W, MW_H, NULL, NULL, hInstance, NULL);

   if (!hWnd)
   {
      return FALSE;
   }
   //Serialポートの確立
   setupSerial();
   ShowWindow(hWnd, nCmdShow);
   UpdateWindow(hWnd);

   //描画スレッドの作成
   CreateThread(NULL,0,threadfunc,(LPVOID)hWnd,0,&dwID);

   //ボタンの作成
   hButton1 = CreateWindow(L"BUTTON",L"計測開始",WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON,
    600,10,100,30,hWnd,(HMENU)ID_BUTTON1,hInstance,NULL);
   
   return TRUE;
}

 CreateWindow関数の2番目の引数を変更することで、ウィンドウの名前を変更することができます。また、6番、7番目の変数はそれぞれ、ウィンドウの横幅、縦幅となっています。
 その他の部分では、CreateThread関数を使って、描画用の副スレッドを作成しています。また、setupSerial関数はシリアルポートを確立し、Arduinoとシリアル通信を繋ぐ自作関数です。
 
では、setupSerial関数を見てみます。
void setupSerial(){
   //シリアル通信用ポートの確立 ("COM8")
   serialPort = CreateFile(L"COM8",GENERIC_WRITE|GENERIC_READ,0,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);
   if(serialPort == INVALID_HANDLE_VALUE)
   {
    OutputDebugString(L"PORT COULD NOT OPEN");
   }
   bool Ret = SetupComm(serialPort,1024,1024);
   if(!Ret){
    OutputDebugString(L"SET UP FAILED");
    CloseHandle(serialPort);
   }
   Ret = PurgeComm(serialPort,PURGE_TXABORT|PURGE_RXABORT|PURGE_TXCLEAR|PURGE_RXCLEAR);
   if(!Ret){
    OutputDebugString(L"CLEAR FAILED");
    CloseHandle(serialPort);
   }
   //基本通信条件の設定
   DCB dcb;
   GetCommState(serialPort,&dcb);
   dcb.DCBlength = sizeof(DCB);
   dcb.BaudRate = 9600;
   dcb.fBinary = FALSE;
   dcb.ByteSize = 8;
   dcb.fParity =NOPARITY;
   dcb.StopBits = ONESTOPBIT;

   Ret = SetCommState(serialPort,&dcb);

   if(!Ret){
    OutputDebugString(L"SetCommState FAILED");
    CloseHandle(serialPort);
   }
}

この関数は以前に書いたC言語でArduinoとシリアル通信してLEDを制御するの部分とほとんど同じです。

では次に、ウィンドウにグラフのグリッドを描画してみます。
//
//  関数: CreateGrids(HDC)
// 目的: グラフのグリッド描画を行います。
//
int CreateGrids(HDC hdc)
{
 HGDIOBJ hUserPen,hDefPen;

 //方眼の線色の設定
 hUserPen = CreatePen(PS_SOLID,1,RGB(200,200,200));
 hDefPen = SelectObject(hdc,hUserPen);

 //方眼の作成
 //x軸の描画
 for(int i=0; i<= (GW_y2-GW_y1)/grid_interval_x + (GW_x2-GW_y2)/grid_interval_x; i++)
 {
  WCHAR szBuffer[100];
  int current_x_posi = GW_x1+grid_interval_x*i;
  MoveToEx(hdc,current_x_posi,GW_y1,NULL);
  LineTo(hdc,current_x_posi,GW_y2);
  _stprintf_s(szBuffer,L"%d",grid_interval_x*i);
  TextOut(hdc,current_x_posi,GW_y2,szBuffer,wcslen(szBuffer));
 }
 //y軸の描画
 for(int i=0; i<=(GW_y2-GW_y1)/grid_interval_y; i++)
 {
  WCHAR szBuffer[100];
  int current_y_posi = GW_y2-grid_interval_y*i;
  MoveToEx(hdc,GW_x1,current_y_posi,NULL);
  LineTo(hdc,GW_x2,current_y_posi);
  _stprintf_s(szBuffer,L"%dV",i);
  TextOut(hdc,GW_x1-20,current_y_posi,szBuffer,wcslen(szBuffer)); 
   
 }
 
 return 0;
}

 このようにして方眼を作成します。線を引くためにはまず始点をMoveToEx関数で指定して、そこからLineTo関数を使って線を描画します。また、軸に数値を表示するためにはまず、数値を文字列に変換するために、_stprintf_s関数を用いてからTextOut関数で描画します。

次に副スレッドでArduinoからデータを取得して、リアルタイムにプロットを描画していきます。
DWORD WINAPI threadfunc(LPVOID vdParam)
{
 HDC hdc;
 unsigned int iCount = 0;
 char data[255]="0"; 
 double ch1Value = 0.0;
 double ch2Value = 0.0;
 char* ch1Data = {0};
 char* ch2Data = {0};
 char* ctx = {0};
 HGDIOBJ hUserPen,hUserBrush,hDefPen,fDefBrush,hNullPen,hNullBrush;

 while(1)
 {
  hdc = GetDC((HWND)vdParam);
  //横軸のポイント数のカウントアップ
  iCount+=1;
  
  //現在取得した点(LineToの終点)
  nPointch1x =  GW_x1+iCount;
  nPointch1y = GW_y2-ch1Value*100;
  nPointch2x =  GW_x1+iCount;
  nPointch2y = GW_y2-ch2Value*100;
  //ch1の線色を指定して描画
  hUserPen = CreatePen(PS_SOLID,1,RGB(255,0,0));
  hDefPen = SelectObject(hdc,hUserPen);
  MoveToEx(hdc,nOldPointch1x,nOldPointch1y,NULL);
  LineTo(hdc,nPointch2x,nPointch1y);
  SelectObject(hdc,hDefPen);
  //ch2の線色を指定して描画
  hUserPen = CreatePen(PS_SOLID,1,RGB(0,0,255));
  hDefPen = SelectObject(hdc,hUserPen);
  MoveToEx(hdc,nOldPointch1x,nOldPointch2y,NULL);
  LineTo(hdc,nPointch2x,nPointch2y);
  SelectObject(hdc,hDefPen);

  //計測点のマーカの描画、marker_sizeを0にすると表示しない。
  if(marker_size != 0)
  {
   Rectangle(hdc,nPointch1x+marker_size,GW_y2-ch1Value*100-marker_size,nPointch1x-marker_size,GW_y2-ch1Value*100+marker_size);
   Rectangle(hdc,nPointch2x+marker_size,GW_y2-ch2Value*100-marker_size,nPointch2x-marker_size,GW_y2-ch2Value*100+marker_size);
  }
  
  //現在取得した点を次のLineTo始点にする。
  nOldPointch1x = nPointch1x;
  nOldPointch1y = nPointch1y;
  nOldPointch2x = nPointch2x;
  nOldPointch2y = nPointch2y;

  DWORD dwRead;
  DWORD dwSendSize;
  BYTE sendflag = 1;
  DWORD dwErrorMask;
  COMSTAT comStat;
  DWORD dwCount;
  TCHAR tdata[255];

  ClearCommError(serialPort,&dwErrorMask,&comStat);
  dwCount = comStat.cbInQue;
  WriteFile(serialPort,&sendflag,sizeof(sendflag),&dwSendSize,NULL);
  bool Ret = ReadFile(serialPort,&data,dwCount,&dwRead,NULL);
  if(!Ret){
   OutputDebugString(L"thread READ FAILED\n");
   CloseHandle(serialPort);
  }
  //シリアルデータが文字列として送信されてくるので区切る
  ch1Data = strtok_s(data,",\r\n",&ctx);
  ch2Data = strtok_s(null_ptr,",\r\n",&ctx);
  //ch2DataのNULLによるエラー防止処理
  if(ch2Data==NULL)ch2Data="9.9";
  //10bitデータを0~5Vに変換する。刻みはおよそ4.9mVとなる。
  ch1Value = map(atoi(ch1Data),0.0,1023.0,0.0,5.0);
  ch2Value = map(atoi(ch2Data),0.0,1023.0,0.0,5.0);
  //取得したデータを電圧に変換して表示
  _stprintf_s(tdata,L"ch1:%1.5lf     ch2:%1.5lf",ch1Value,ch2Value);
  TextOut(hdc,20,20,tdata,wcslen(tdata));
  //サンプリング間隔(ミリ秒)※要改善点※
  Sleep(1000);
 }
}

 副スレッドでデータを取得する部分は無限ループを一定間隔で回すことでリアルタイム描画を実現します。2ch分のデータを取得するのでArduinoからのデータは文字列にて送信し、C言語のプログラム側でカンマで分割処理することで2ch分のデータを同時に取得します。また、Arduinoからデータを送ってもらうタイミングを同期するためにC言語側から適当なデータをArduinoに送信し、そのタイミングでArduinoからデータを送信してもらうようにしています。

Arduino側のプログラムは
//logger 2ch
void setup() {
  // initialize the serial communication:
  Serial.begin(9600);
}

void loop() {
  // send the value of analog input:
  if(Serial.available()>0){
    Serial.print(analogRead(A0));
    Serial.print(",");
    Serial.println(analogRead(A1));
    Serial.read();
  }
}
以上です。

動作している様子は以下のようになっています。

_kiitaniさん(@monitorgazer)が投稿した動画 -

今回のプログラムのVisualStudioのプロジェクトファイルは以下からダウンロード可能です。

https://app.box.com/s/bw4gvmaqd7nlihvjaouj

改善が必要な部分
 現在のプログラムでは、軸の拡大縮小・移動が実装されていません。また、表示可能な部分が限られていますので、グラフ領域の端まできたら表示部位を切り替えてプロットしていくようなアルゴリズムを考える必要があります。また、データを保存する機能なども必要だろうと思います。これから改善してゆきます。