前回書いた「極座標レーザーエングレーバー」の全プログラムを紹介します。
プログラムは、JPGファイルからプロッター用データを作るPCのプログラムと、それを刻印するArudinoスケッチの2本だけです。それぞれ、200ステップ強と300ステップ弱しかないとても単純なものです。
上の図は左のjpgファイルから、輝度が一定値以下の部分を白で右に取り出した画面です。PCのプログラムでこれを行いプロットする点を取り出します。そして刻印時間が速く、それでも回転メカの向きが1方向で収まるように並べなおします。
そのためのPCのプログラム言語は何でもできますが、ここではマイクロソフト社フリーソフトであるSmall Basicを使っています。これについてはこの連載の初めの方でもご紹介しましたが下の方に再掲しておきます。
刻印までの処理全体の構成は次の図(DFD-GS表記)のようにしています。
jpgの画像ファイルはPICxxxという名前にして、プログラムを入れるフォルダー直下の特定フォルダー(ここではDat5という名前にしています)に格納しておきます。データ化プログラムへのキーイン操作で3桁の番号xxxを指定すると、そのjpgファイルがロードされ画面表示されます。また、そのフォルダー内にプロット用のテキストファイルPICTxxx.txtが出力されますので、マイクロSDへコピーし名前を単なるPICT.txtに変更しておけばArudinoの刻印プログラムはそれを読みエングレーブします。
データ化プログラムではまずプロットする点を表示し、次に刻印しやすいように並べ変えたベクトルのような形で出力ファイルを作ります。次の動画のようになります。処理速度はPCの能力に応じてそれなりの時間がかかります。処理が終わると刻印画像は黄色に表示され、最後にベルがチーンとなります。
このプログラムではテキスト画面とグラフィック画面を出します。テキスト画面はスクリーン上の場所を移動して構いませんが、グラフィック画面は各点の輝度を取り出して計算をしているため、スクリーン上で場所を移動してはいけません。
また、データ収集密度はプログラム内で指定しています。使う画像によりますが、おおざっぱで良いものはこれを疎に(たとえば2.0とか3.5とかに)しますが、一番上に示したように点が抜けない方がよい図は密(たとえば1.0とか1.5とかに)にします。この図の場合は元々粗い図なので、疎(例2.0)に指定して処理すると次の図のようにさらに粗くなります。
後はプログラムをごらんいただけばわかるかと思います。
実験に使ったデータ化プログラム(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へ自動変換する機能も備えています。
もちろん、pythonやgccなど便利な言語環境はたくさんあるわけで、筆者もそれらを使っていますが、関連コンポネントとの相性など整合性確保に手間がかかることがあります。仕事や複雑なものを作るとき以外は、つまり趣味にはこの単純なSmallBasicを使うことが多く、時間もエネルギーも最小でたいへん助かっています。
この言語には型とか変換とかの概念がなく、数値は常に最大桁数の高精度であるなど、超単細胞(!)なのは筆者から見ると却って魅力的です。それどころか文字と数値との区別すらありません!
よって機能は限られますが、使う側の工夫次第。例えば多次元の配列がないため代わりに1行のベクトル内に自分で作るとか。その代わり、わけのわからない環境設定問題に遭遇しないのがたいへん気楽です。つまり全ては自分の手の内でできるというわけ。
限られた構造化文だけでなく、タブーのgoto(ジャンプ)も許容されtているので却って書きやすいこともあります。逆にswitch case (Select when)などの構造化文がないので、If文を並べなければならないことがあります。
なにはともあれマイクロソフトからダウンロードしてみてください。ネットでの情報は整っていて、手元にマニュアルが要らない簡単さです。
さて、以上のようなしかけはレーザー装置でなく、CNCルーターや単なるプロッターでも同じように手軽に作れます。(なお、線が主体の場合には線を引く方式にする方がよい場合があります。そのような汎用ソフトはXY用しかない感じですから工夫して自作がいいですね。)
最後にレーザーを使う場合の大事な注意点です。
レーザーは失明、傷害、火災等の危険を伴い、周囲にも危険がおよびます。取扱う場合、必ず関連法規と基準(http://kikakurui.com/c6/C6802-2011-01.html)などに従って、自己責任にて正しく管理をしてください。もしこの記事をみて実験や自作をされる場合でも、一切の責任を免除させていただきます。
ではこの辺で。
©2019 Akira Tominaga, All rights reserved.