勝手な電子工作・・

勝手なオリジナル電子工作に関する記事を書きます

オリジナル・レーザープロッター その5 極座標の簡単なプログラム紹介

前回書いた「極座標レーザーエングレーバー」の全プログラムを紹介します。

f:id:a-tomi:20190927133619j:plain

プログラムは、JPGファイルからプロッター用データを作るPCのプログラムと、それを刻印するArudinoスケッチの2本だけです。それぞれ、200ステップ強と300ステップ弱しかないとても単純なものです。

上の図は左のjpgファイルから、輝度が一定値以下の部分を白で右に取り出した画面です。PCのプログラムでこれを行いプロットする点を取り出します。そして刻印時間が速く、それでも回転メカの向きが1方向で収まるように並べなおします。

そのためのPCのプログラム言語は何でもできますが、ここではマイクロソフトフリーソフトであるSmall Basicを使っています。これについてはこの連載の初めの方でもご紹介しましたが下の方に再掲しておきます。

刻印までの処理全体の構成は次の図(DFD-GS表記)のようにしています。

f:id:a-tomi:20190927134312j:plain

 jpgの画像ファイルはPICxxxという名前にして、プログラムを入れるフォルダー直下の特定フォルダー(ここではDat5という名前にしています)に格納しておきます。データ化プログラムへのキーイン操作で3桁の番号xxxを指定すると、そのjpgファイルがロードされ画面表示されます。また、そのフォルダー内にプロット用のテキストファイルPICTxxx.txtが出力されますので、マイクロSDへコピーし名前を単なるPICT.txtに変更しておけばArudinoの刻印プログラムはそれを読みエングレーブします。

データ化プログラムではまずプロットする点を表示し、次に刻印しやすいように並べ変えたベクトルのような形で出力ファイルを作ります。次の動画のようになります。処理速度はPCの能力に応じてそれなりの時間がかかります。処理が終わると刻印画像は黄色に表示され、最後にベルがチーンとなります。

youtu.be

このプログラムではテキスト画面とグラフィック画面を出します。テキスト画面はスクリーン上の場所を移動して構いませんが、グラフィック画面は各点の輝度を取り出して計算をしているため、スクリーン上で場所を移動してはいけません。

また、データ収集密度はプログラム内で指定しています。使う画像によりますが、おおざっぱで良いものはこれを疎に(たとえば2.0とか3.5とかに)しますが、一番上に示したように点が抜けない方がよい図は密(たとえば1.0とか1.5とかに)にします。この図の場合は元々粗い図なので、疎(例2.0)に指定して処理すると次の図のようにさらに粗くなります。

f:id:a-tomi:20190927145447j:plain

後はプログラムをごらんいただけばわかるかと思います。

実験に使ったデータ化プログラム(Smallbasic)とArduinoスケッチの順に以下にソースプログラムを掲載します。

 

1)データ化プログラム

 

PgmName="190923-JpgToPolarCoordinatesTxt-V00"
CopyRight="(c)2019 Akira Tominaga, All rights reserved"
' Function
'   Get brightness data from jpg and make txt file for plotter
' Major revisions
'   V00 Sept. 23, 2019

TextWindow.Title=PgmName +"   "+CopyRight
TextWindow.BackgroundColor="Black"

' Set data path  ***** PgmName must be correct to get data path *****
myPath=File.GetSettingsFilePath()       'set current path name all
Tlen1=Text.GetLength(mypath)            ' get current path name length
Tlen2=Text.GetLength(PgmName+".Settings") ' get unneeded length
myPath=Text.GetSubText(myPath,1,Tlen1-Tlen2) 'set folder name only
myPath=myPath+"Dat5\"                   ' and add the name of data folder

' Load picture PICnnn.jpg specified by keyed-in nnn
TextWindow.WriteLine("") ' skip one line For ease of key-in
KeyIn:
TextWindow.Write("     Please type-in PIC#. -> ")
keyinN=TextWindow.Read()
myPict=myPath+"PIC"+keyinN+".jpg"
PsizeX=ImageList.GetWidthOfImage(myPict)
If PsizeX>18 Then   ' exists with enough size to plot ?
  Goto ImageOK
Else
  TextWindow.WriteLine("*** No such PCL#.jpg ***")
  Goto Keyin
EndIf
ImageOK:
PsizeY=ImageList.GetHeightOfImage(myPict)

'Graphics window
GraphicsWindow.BackgroundColor = "Black"
GraphicsWindow.Width=PsizeX*2
GraphicsWindow.Height=PsizeY
GraphicsWindow.Title=PgmName
inPic=ImageList.LoadImage(myPict)
GraphicsWindow.DrawImage(inPic,0,0)

'Setup Polar Coordinates values for tested rotator 
thetaPi=1211 ' device specific number to rotate pi (half circle)
rMax=Math.SquareRoot(PsizeX*PsizeX/4+PsizeY*PsizeY)+1

'Setup work file name and create header record
wkFile=myPath+"Work"+KeyinN+".txt"
lnum=1 'PsizeX,PsizeY characters with left zero
szX=PsizeX+1000 ' to secure 4 digits, add 1000
szY=PsizeY+1000 ' to secure 4 digits, add 1000
rMax4=Text.GetSubText(rMax+1000,1,4) 'delete value under 1
thetaPi4=thetaPi+1000 ' to secure 4 digits, add 1000
header=szX+","+szY+","+rMax4+","+thetaPi4
File.WriteLine(wkFile,lnum,header)

'Get RGB data and convert them to Brightness data
Density=2.0 ' Set data density *********** Specify density*********
For Ptheta=0 To thetaPi step Density
  For Pr=0 To rMax Step Density
    'Convert polar-coordinates to XY
    CalcXY()
    xX=XX+1000 'force 4digits
    YY=YY+1000 'force 4digits
    X=Text.GetSubText(XX,1,4)-1000
    Y=Text.GetSubText(YY,1,4)-1000
    If X<=0 or X>=PsizeX or Y<=0 or Y>=PsizeY then
      Goto Ignore
    EndIF  
    'Get color and convert it to brightness
    color=GraphicsWindow.GetPixel(X,Y)
    getkido() ' convert RGB to Brightness
    gcolor="#000000"
    txdata=""
    'If dark then make SD record and  plot picture
    If kido<128 Then
      gcolor="#FFFFFF"
      'Set text data
      X4=X+1000
      Y4=Y+1000 
      rp4=Pr+1000
      thetap4=ptheta+1000
      txdata=X4+","+Y4+","+rp4+","+thetap4
      lnum=lnum+1      
      File.WriteLine(wkFile,lnum,txdata)
    EndIf
    GraphicsWindow.SetPixel(X+PsizeX,Y,gcolor)
    Ignore:
  EndFor
EndFor  
' When all data done, write EOF
lnum=lnum+1
eof=9999
File.WriteLine(wkFile,lnum,eof)

TextWindow.Write("Work")
TextWindow.Write(KeyinN)
TextWindow.WriteLine(".txt has been created as a work file")
TextWindow.Write("  with ")
TextWindow.Write(lnum-2)
TextWindow.WriteLine(" points to plot")

' Optimization for quick plotting

'Prepare output file
OutFile=myPath+"PICT"+KeyinN+".txt"

' Transfer header (Xsize,Ysize,rMax,thetaPi)
InLnum=1
InText=File.ReadLine(wkFile,InLnum)
OutLnum=1
OutText=InText
File.WriteLine(OutFile,OutLnum,OutText)

' Convert wkFile to OutFile
OutReverse=0
InLnum=InLnum+1
InText=File.ReadLine(wkFile,InLnum)
OutText=InText
OutRec=1
Tsave=Text.GetSubText(InText,16,4)

OutLoop:
InLnum=InLnum+1
InText=File.ReadLine(wkFile,InLnum)
XX=Text.GetSubText(InText,1,4)
If XX=9999 Then
  OutLnum=OutLnum+1
  File.WriteLine(OutFile,OutLnum,OutText)
  OutText=InText
  OutLnum=OutLnum+1
  File.WriteLine(OutFile,OutLnum,OutText) 
  Goto Eof
EndIf

' Change color of process point to yellow
X=XX-1000
YY=Text.GetSubText(InText,6,4)
Y=YY-1000
gcolor="#FFFF00"
GraphicsWindow.SetPixel(X+PsizeX,Y,gcolor)

' Go on to check next input
Tchk=Text.GetSubText(InText,16,4)
If Tchk=Tsave Then  ' if same theta then 
  Goto accumText    '   accumulate data without output
Else                ' else output accumulated data
  Tsave=Tchk
  Goto accumOut
EndIf

' Accumulate data records with opposite sequences alternatively
accumText:
If OutReverse=0 Then
   OutText=Text.GetSubText(OutText,1,22*OutRec-2)+"##"+InText 
Else
  OutText=Text.GetSubText(InText,1,20)+"##"+OutText
EndIf
OutRec=OutRec+1
Tsave=Tchk
Goto OutLoop

accumOut:
OutLnum=OutLnum+1
File.WriteLine(OutFile,OutLnum,OutText)
OutText=InText
If OutReverse=0 Then
  OutReverse=1
Else
  OutReverse=0
EndIf
Goto OutLoop

' End of file process
Eof:
TextWindow.Write("Edited ")
TextWindow.Write(InLnum-2)
TextWindow.Write(" input to ")
TextWindow.Write(OutLnum)
TextWindow.WriteLine(" output text-lines")
Sound.PlayBellRing()

Sub getkido   ' from color code #rrggbb  to brightness
  HexV=0
  Hexc=Text.GetSubText(color,2,1)
  Hex2V()
  R10=HexV
  Hexc=Text.GetSubText(color,2,1)
  Hex2V()
  R01=HexV
  Rv=R10*16+R01
  Hexv=Text.GetSubText(color,3,1)
  Hex2V()
  G10=HexV
  Hexc=Text.GetSubText(color,4,1)  
  Hex2V()
  G01=HexV
  Gv=G10*16+G01
  Hexc=Text.GetSubText(color,5,1)
  Hex2V()
  B10=HexV
  Hexc=Text.GetSubText(color,6,1)
  Hex2V()
  B01=HexV
  Bv=B10*16+B01
  Kido=0.299*Rv+0.587*Gv+0.114*Bv
EndSub

Sub Hex2V   ' from hexa-decimal one digit to value 
  If Hexc < "9" Then
    Hexv=Hexc
  EndIf
  If Hexc="A" Then
    HexV=10
  EndIf
  If Hexc="B" Then
    HexV=11
  EndIf  
  If Hexc="C" Then
    HexV=12
  EndIf
  If Hexc="D" Then
    HexV=13
  EndIf   
  If Hexc="E" Then
    HexV=14
  EndIf
  If Hexc="F" Then
    HexV=15
  EndIf     
EndSub

Sub CalcXY ' Calculate XX and YY from Pr and Ptheta
  xx=Pr*Math.Cos(Math.Pi*Ptheta/thetaPi)
  XX=xx+PsizeX/2
  yy=Pr*Math.Sin(Math.Pi*Ptheta/thetaPi)
  YY=PsizeY-yy
EndSub

次にArduinoスケッチです。

/* ******************************************************
*    Polar-coordinates Laser Engraver 			*
*    (c)2019 Akira Tominaga, All rights reserved.	*
*    Version 00 Sept. 19, 2019				*
*    Major revisions:					*
*							*
******************************************************* */
#include     // <SPI.h>
#include      // <SD.h>
// pin assignment and values
// *** A4988 stepper driver *** X for Radius, Y for Theta
#define NsleepX 14  // !sleepX
#define StepX 9     // StepX
#define DirX 8      // DirectionX
#define NsleepY 7   // !sleepY
#define StepY 6     // StepY
#define DirY 4      // DirectionY
// ***for laser
#define Pwm 5       // PWM 980Hz for Laser
#define DutyCtl 2   // Duty rate control pin (A2=D16)
// *** for micro SD (SPI)
// D13=SCK,D12=MISO,D11=MOSI
# define ChipSel 10
File mySD;          // symbol for SD file
// *** for LED and sitches
#define Led 2       // LED for operations
#define Sw 3        // Tact switch for operation processes
#define Rotate  3   // Fwd/bkwd sw for initial pos(A3=D17)
#define Stoppers 15  // OR for limitters (A0=D15)
// constants and values
#define tmStpX 700   // stepperX phase time in μS
#define tmStpY 700   // stepperY phase time in μS
#define enlargeX 2   // enlarge-multiplier for X
#define enlargeY 1   // enlarge-multiplier for Y (must be 1)
int dutyOn;          // duty ratio when laser On
#define dutyOff 0    // duty ratio when laser Off
#define dutyPos 1    // duty ratio for positioning
#define burnTime 14  // burning time (milliseconds)
int Rmax;            // Radius max-value
int ThetaPi;         // Theta value for Pi radian
int curR = 0;        // current Radius = 0
int curTheta = 0;    // current Theta =0
int newR = 0;        // new Radius position
int newTheta = 0;    // new Theta position Y
int Delta;           // amount to move for both R & Theta
byte Byte;           // byte work area for SD data
int Value;           // value work area
int k;               // common loop counter
int loopNum = 0;     // data sequence number of SD file

void setup() { // Arduino Setup *******************
  analogWrite(Pwm, dutyOff);  // laser off
  pinMode(NsleepX, OUTPUT);
  digitalWrite(NsleepX, LOW); // sleep X = Radius
  pinMode(NsleepY, OUTPUT);
  digitalWrite(NsleepY, LOW); // sleep Y = Theta
  pinMode(DirX, OUTPUT);
  pinMode(DirY, OUTPUT);
  pinMode(StepX, OUTPUT);
  digitalWrite(StepX, LOW);
  pinMode(StepY, OUTPUT);
  digitalWrite(StepY, LOW);
  pinMode(Sw, INPUT_PULLUP);
  pinMode(Led, OUTPUT);
  digitalWrite(Led, LOW);
  Serial.begin(9600);
  delay(1000);        // wait for all devices stabilized

  // ** Process 1) 12V power-on for laser and motors
  Serial.println("Pwr 12V on");
  while (digitalRead(Sw) == HIGH) {  // wait for sw pushed
    bLED(1, 50, 10);           // repeat LED on while waiting
    delay(20);
  }
  delay(500);     //avoid chattering and long-push detections
  while (digitalRead(Sw) == LOW) { // wait for Sw released
  }
 
  // ** Process 2) adjust center position
  Serial.println("Adj Center");
  while (digitalRead(Sw) == HIGH) {  // loop while Sw not pushed
    analogWrite(Pwm, dutyPos); // blink weak laser-beam
    digitalWrite(Led, HIGH);   // LED on while waiting
    // Fwd and Bkwd by tact switches
    int RotPos = analogRead(Rotate);
    if (RotPos > 767) {
      digitalWrite(DirY, HIGH);   // Theta fwd
      Delta=1;
      RotateY();
    }
    if (RotPos < 255) {
      digitalWrite(DirY, LOW);   // Theta bkwd
      Delta=1;
      RotateY();
    }
    delay(5);
    analogWrite(Pwm, dutyOff); // blink laser-beam
    digitalWrite(Led, LOW);    // blink LED, too
    delay(5);
  }
  // Process-Sw pushed
    while (digitalRead(Sw) == LOW) { // wait for Sw released
  }
  analogWrite(Pwm, dutyOff); // laser off while (digitalRead(Sw) == HIGH)
  delay(500); //avoid chattering
  
  // ** Process 3) prompt to wear protection glasses
  Serial.println("Protect eyes!");
  delay(500);
  while (digitalRead(Sw) == HIGH) {
    bLED(1, 15, 30);         // blinkking LED
  }
  while (digitalRead(Sw) == LOW) { // wait for Sw released
  }

  // check if SD card exists
  if (!SD.begin(ChipSel)) {
    Serial.println("Chk SD");  // if error, show error
    while (1) {                // and stop,
      bLED(20, 20, 30);        // blinking LED forever
    }
  }
  // read PICT.txt header
  mySD = SD.open("PICT.txt");
  Rdskip(10);           // skip header for XY sizes
  Rd4();                // read 4bytes to Value
  Rmax = Value - 1000;  // get Rmax length
  Rdskip(1);            //skip comma
  Rd4();                // read 4bytes to Value
  ThetaPi = Value - 1000; // get Theta value for Pi radian
  Rdskip(2);           // skip CR+LF

  digitalWrite(NsleepX, HIGH); // wake-up X
  delay(2);             // delay for A4988 charge-pump
}

void loop() {   // Arduino Loop ******************
  /* ********************************************
      1.Read SD file and get new(newR,newTheta) data
   *  ***************************************** */
  // read PICT.text
  Rd4();                // read 4bytes to Value
  if (Value >= 9999) {
    EndProc();
  }
  Rdskip(6);            // skip comma, Y, and comma
  Rd4();                // read 4 bytes to Value
  newR = Value - 1000;
  Rdskip(1);            // skip comma
  Rd4();                // read 4bytes to Value
  newTheta = Value - 1000;  // get newTheta
  Rdskip(2);           // skip CRLF (2bytes)

  loopNum++;
  Serial.print("#");
  Serial.print(loopNum);     // show data number

  /* ********************************************
      2.Set laser duty-ratio on potentiometer
   *  ***************************************** */
  dutyOn = 250.0 * analogRead(DutyCtl) / 1023;
  if (dutyOn < 5) {         // if small value
    dutyOn = 1;             // then regard it as min.
  }

  /* ********************************************
      3.Drive steppers and irradiate laser beam
   *  ***************************************** */
  // move R and Theta
  Serial.print (" ");
  Serial.print (curR);
  Serial.print (",");
  Serial.print (curTheta);
  Serial.print (" to ");
  Serial.print (newR);
  Serial.print (",");
  Serial.println (newTheta);
  movY();         // move Y=Theta first
  movX();         // move X=Radius next
  laserBeam();    // beam on for the burning time
  curR = newR;    // set newR to curR
  curTheta = newTheta; // set newTheta to curTheta

}                 // last line of main loop ***

/***********************************
     User-defined functions
* **********************************/
// *** Read SD 4 bytes into integer Value ***
void Rd4(void) {
  Value = 0;
  for (k = 0; k < 4; k++) {
    Byte = mySD.read();
    Byte = Byte & B00001111;
    Value = Value * 10 + Byte;
  }
}

// *** Read SD and skip given bytes ***
void Rdskip(int bn) {
  for (k = 0; k < bn; k++) {
    Byte = mySD.read();
  }
}

// *** move stepperX (no sleep actions) ***
void movX(void) {
  if (newR == curR) {
    return;
  }
  if (newR > curR) {
    digitalWrite(DirX, HIGH);   // X fwd
    Delta = newR - curR;
  }
  if (newR < curR) {
    digitalWrite(DirX, LOW);   // X bkwd
    Delta = curR - newR;         // set absolute value
  }
  for (int l = 0; l < Delta * enlargeX; l++) {
    for (int m = 0; m < 8; m++) { // 1 cycle for MicroStep1
      digitalWrite(StepX, HIGH);
      delayMicroseconds(tmStpX);
      digitalWrite(StepX, LOW);
      delayMicroseconds(tmStpX);
    }
  }
  delay(2);
}

// *** move stepperY (sleep actions inside) ***
void movY(void) {
  if (newTheta == curTheta) {
    return;
  }
  if (newTheta > curTheta) {
    digitalWrite(DirY, HIGH);   // Y fwd
    Delta = newTheta - curTheta;
  }
  if (newTheta < curTheta) {
    digitalWrite(DirY, LOW);   // Y bkwd
    Delta = curTheta - newTheta;     // and change sign
  }
  RotateY();   // rotate Y with sleep actions
}

// *** rotate stepperY (with sleep actions) ***
void RotateY(void) {
  digitalWrite(NsleepY, HIGH); // wake-up Y
  delay(2);                    // for A4988 charge-pump
  for (int l = 0; l < Delta * enlargeY; l++) {
    for  (int m = 0; m < 8; m++) { // 1 cycle MicroStep1
      digitalWrite(StepY, HIGH);
      delayMicroseconds(tmStpY);
      digitalWrite(StepY, LOW);
      delayMicroseconds(tmStpY);
    }
  }
  delay(2);                   // avoid mechanical inertia
  digitalWrite(NsleepY, LOW); // sleep Y
}

// *** Laser beam-on for burnTime ***
void laserBeam(void) {
  analogWrite(Pwm, dutyOn);
  delay(burnTime);
  analogWrite(Pwm, dutyOff);
}

// *** End of File process ***
void EndProc(void) {
  newR = 0;
  newTheta = ThetaPi*2;
  movY();
  movX();
  delay(2);        // avoid mechanical inertia
  digitalWrite(NsleepX, LOW);  // sleep X
  Serial.println("Power-off 12V-DC");
  mySD.close();
  while (1) {}
}

// *** blink LED ***
void bLED(int nTimes, int onmS, int offmS)
{
  for (int m = 0; m < nTimes; m++) {
    digitalWrite(Led, HIGH);
    delay(onmS);
    digitalWrite(Led, LOW);
    delay(offmS);
  }
}

// end of program

 

ちょっとした処理に便利な、Microsoft Small Basicの簡単なご紹介をしておきます。

Windowsの場合に、すぐ使うプログラムを作るのに一番単純で手っ取り早いのはマイクロソフトがフリーで提供している Small Basicかと思います。これですと作成時間が僅かで済むし、Windows自体との相性もいい(それはあたりまえか)。さらに、完成したプログラムをVisual Basicへ自動変換する機能も備えています。

もちろん、pythongccなど便利な言語環境はたくさんあるわけで、筆者もそれらを使っていますが、関連コンポネントとの相性など整合性確保に手間がかかることがあります。仕事や複雑なものを作るとき以外は、つまり趣味にはこの単純なSmallBasicを使うことが多く、時間もエネルギーも最小でたいへん助かっています。

f:id:a-tomi:20190810155827j:plain

この言語には型とか変換とかの概念がなく、数値は常に最大桁数の高精度であるなど、超単細胞(!)なのは筆者から見ると却って魅力的です。それどころか文字と数値との区別すらありません!

よって機能は限られますが、使う側の工夫次第。例えば多次元の配列がないため代わりに1行のベクトル内に自分で作るとか。その代わり、わけのわからない環境設定問題に遭遇しないのがたいへん気楽です。つまり全ては自分の手の内でできるというわけ。

限られた構造化文だけでなく、タブーのgoto(ジャンプ)も許容されtているので却って書きやすいこともあります。逆にswitch case (Select when)などの構造化文がないので、If文を並べなければならないことがあります。

なにはともあれマイクロソフトからダウンロードしてみてください。ネットでの情報は整っていて、手元にマニュアルが要らない簡単さです。

 さて、以上のようなしかけはレーザー装置でなく、CNCルーターや単なるプロッターでも同じように手軽に作れます。(なお、線が主体の場合には線を引く方式にする方がよい場合があります。そのような汎用ソフトはXY用しかない感じですから工夫して自作がいいですね。)

 

最後にレーザーを使う場合の大事な注意点です。

レーザーは失明、傷害、火災等の危険を伴い、周囲にも危険がおよびます。取扱う場合、必ず関連法規と基準(http://kikakurui.com/c6/C6802-2011-01.html)などに従って、自己責任にて正しく管理をしてください。もしこの記事をみて実験や自作をされる場合でも、一切の責任を免除させていただきます。

 

ではこの辺で。

 

©2019 Akira Tominaga, All rights reserved.