2018年02月10日

ドリルぶち折るw

bCNCでPCB作ろうと思ってEAGLEからgcode吐き出して読ませた。パターンを切り出すまではよかったが、穴あけになって変なところにトラベルしてドリルを3本ほどブチ折りやがってむかついたので(GCODE確認しても意外と見落とすところがある)気を使わなくてもいいようにツールを工夫してみることにした。

そもそもGRBL自体難しいIFではないので、きちんとUSBでシリアル接続できていればあとはモニタリングして先端がどこに居るのかソフトウェアで解析すればドリルを折ってしまうことも少なくなるはずだw
で、最初Python+pyserial使ってやろうとも思ったのだが、bCNC使ってパターンを切るときにやたら重かったのでそちらは今回見送って、FPCのLazarus+synaser(TBlockSerial)を使ってやることにした。丁度自前のシリアルの制御フローも書き換えたいと思ってたしねw

Ararat Synapse

さて、ひとまずTBlockSerialの構造を知っておく必要がある。Lazarusも当然マルチスレッドプログラムを書くことは出来るのだが、Linux系の場合明示的にプロジェクトファイルに「{$DEFINE UseCThreads}」と加えなければならない。まぁプロジェクトオプションでやることも出来るんだろうけど自分は手で書く。
それもあってかなんかは知らんがTBlockSerialはブロッキング通信になってて、ソースを見ると普通にselect文でお待ちになっておられる。お陰で知らずにやったらいきなりフリーズしたようになってしまうわけだw

前はタイマーのイベントを使って定期的にポーリングするようにしてたのだが、どうもかっこ悪い。というかリアルタイムに処理したいとき最低限タイマーの分解能に左右されてしまう。なので、それを独立したスレッドにお任せしてデータが出てきたら受け取るようにしたというわけだ。

要約すると係員が一人の状態でストップウォッチみて時間になったら受付窓口を見に行く、ではなく係員を二人に増やして受付窓口専用係員を設けるって感じだな。ただ複数雇用するにはプロジェクト側に申告する必要がある、ということでww

lazarusにはTThreadというスレッド管理オブジェクトがある。こいつを継承してそのスレッドの中でTBlockSerialを動かしてやればいい。そうすればどれだけ待っていようがメインスレッドには影響しないのでタイムアウト時間を長めに設定(といっても限度はあるが)しても問題はなさそうだ。

type
TUartThread=class(TThread)
 private
  FRsvBuf,FSndBuf:TMemoryStream;
  FRsvKey,FSndKey:TCriticalSection;
 public
  procedure SndData(Stream:TStream);
  procedure RsvData;
  procedure Execute; override;
 end;

こんな感じでデータ送るメソッドとデータ受け取るメソッドを実装してその部分をSynchronizeで同期させてやればいい。で、データの送受信時にスレッドが被らないようにバッファの読み書き時にロックオブジェクトを使えばトラブルも起きないだろう。

procedure TUartThread.Execute;
var
Buf:TMemoryStream;
 SDev:TBlockSerial;
b:array[0..4095] of byte;
len:integer;
begin
Buf:=TMemoryStream.Create;
SDev:=TBlockSerial.Create;
try
// init blockserial
SDev.Config(FBaud, FBits, FParity, FStopBit, FSoftFlow, FHardFlow);
SDev.RTS:=true;
SDev.Connect(FDevName);

SDev.SendBuffer(startmsg, 4); //grbl用なので\r\n\r\nを送る。
sleep(2000); //でしばし休む。
SDev.Flush;//バッファクリアしておく。

while not Terminated do //TThreadに終了命令が来たかをループ毎にチェック
begin
// send data
FSndKey.Enter; // 送信用バッファ操作をロックする
if FSndData.Size > 0 then
begin
SDev.SendBuffer(FSndData.Memory, FSndData.Size);
FSndData.Clear;
end;
FSndKey.Leave; // 処理が終わったらロック解除
// data send end
// data recieve
while SDev.CanRead(100) do //ココを長くしすぎると Terminate に時間が掛かるw
begin
len:=SDev.RecvBuffer(@b,4096); //読めるときに読むw
if len < 1 then break; // 読めなかったら読み込みループを抜ける
Buf.Write(b,len); // 読み込めてたならそれを書き込む
end;
   // バッファに内容が貯まってたらその分はメインスレッドに通知する。
if Buf.Size > 0 then
begin
Buf.Seek(0,soFromBeginning);
FRsvKey.Enter; // メインスレッドへの通知用バッファをロック 送信と同じ要領
FRtnData.CopyFrom(Buf,Buf.Size);
FRtnData.Seek(0, soFromBeginning);
Buf.Clear;
FRsvKey.Leave;
Synchronize(@RtnData);
end;
// data recieve end
// loop infinite
end;
finally
  // 何故かデバイスのロックファイルが消えないときがあった。手動で消すことにした。
SDev.cpomReleaseComport;
SDev.Free;
Buf.Free;
end;
end;

スレッド内部はこんな感じ。要するにメインスレッドと共用するオブジェクトを操作する場合はロック用のキーを使って触れないようにしてその上で操作をする。ということでメインスレッド側も操作する場合は同じキーを使わなければ動作がぐちゃぐちゃになって意味がない。

procedure TUartThread.RtnData;
begin
FRsvKey.Enter;
FRtnData.Seek(0, soFromBeginning);
//イベントで通知できるようにする為のハンドラ。面倒なのでこれ以上は書かん。
if FOnReturnData <> nil then
begin
FOnReturnData(Self,FRtnData);
end;
FRtnData.Clear;
FRsvKey.Leave;
end;

procedure TUartThread.SndData(Stream:TStream);
begin
FSndKey.Enter;
Stream.Seek(0, soFromBeginning);
FSndData.CopyFrom(Stream,Stream.Size);
FSndData.Seek(0, soFromBeginning);
FSndKey.Leave;
end;

こうしておけば送受信中にバッファを書き換えられることも防げるのだ。
例えばGrblの場合SndDataのStreamオブジェクトに色々コマンドを入れて送れば、対応したレスポンスが上述のイベントへ送られるというわけだ。ただロックしてるのでロックを早く開放するためにコピーしたStreamをイベントから送ってもいいとは思うがw
posted by MCI_Error at 11:00| Comment(0) | cnc
この記事へのコメント
コメントを書く
お名前:

メールアドレス:

ホームページアドレス:

コメント: