본문 바로가기
임베디드 개발/STM32 (ARM Cortex-M)

STM32 + MFC ] 델타 로봇 티칭 시스템 구현, 파일입출력 기능 사용 티칭 데이터 관리, 쓰레드 활용 반복작업 수행

by eteo 2022. 9. 19.

 

 

 

 

 

 

 

 

 

 

 

 

 

로봇의 티칭 방법

로봇에게 가르칠 수 있는 행동의 종류는 MOVE, WAIT, PUMP 세가지가 있고 각각 속성값을 지정할 수 있다.

 

1. MOVE : TORQUE OFF 버튼을 누른 뒤 사람 손으로 원하는 위치로 엔드 이펙터를 이동시킨 후에 READ 버튼을 클릭하면 현재 좌표값이 입력된다. 물론 좌표값을 직접 입력하는 것도 가능하다.

2. WAIT : 직전행동 후 대기시간을 설정한다. ms 단위로 0-5000 사이의 값을 입력할 수 있다.

3. PUMP : ON 또는 OFF를 선택한다.

 

원하는 행동의 라디오버튼을 클릭하고 속성값이 지정되었으면 ">" 버튼을 눌러 해당 행동을 기억시킬 수 있다. 기억된 행동은 순서대로 번호가 부여되며 중간에 행동을 끼워넣거나 "<" 버튼을 눌러 행동을 삭제하는 것도 가능하다.

 

 

 

 

 

 

 

">" 버튼 눌렀을 때의 이벤트 처리 함수

 

void CdeltaControlDlg::OnBnClickedButtonToRight()
{
	UpdateData(TRUE);
	int idx = m_list.GetSelectionMark();
	vector<DataRow*>::iterator iter;
	vector<DataRow*>& refList = DC.getList();

	if (idx == -1)
	{
		iter = refList.end();
	}
	else
	{
		iter = refList.begin() + idx + 1;
	}

	switch (m_radio) {
	case 0:
		if (m_readX == _T("") || m_readY == _T("") || m_readZ == _T(""))
		{
			AfxMessageBox(_T("Please input coordinates."));
		}
		else
		{
			refList.insert(iter, (new DataRow(_T("MOVE"), m_readX + _T("/") + m_readY + _T("/") + m_readZ)));
		}
		break;
	case 1:
		if (m_delay == _T(""))
		{
			AfxMessageBox(_T("Please input wait time."));
		}
		else if (_ttoi(m_delay) < 0 || _ttoi(m_delay) > 5000)
		{
			AfxMessageBox(_T("Please input time within the range."));
		}
		else
		{
			refList.insert(iter, (new DataRow(_T("WAIT"), m_delay)));
		}
		break;
	case 2:
		switch (m_radio2)
		{
		case 0:
			refList.insert(iter, (new DataRow(_T("PUMP"), _T("ON"))));
			break;
		case 1:
			refList.insert(iter, (new DataRow(_T("PUMP"), _T("OFF"))));
			break;
		default:
			break;
		}
	default:
		break;
	}

	renewListControl();
	UpdateData(FALSE);

}

 

코드가 너무 길어지는 것을 방지하고자 참조자와 iterator를 사용했다. 리스트 컨트롤에서 아무것도 클릭되지 않았을 때는 iterator 가 refList.end(); 를 가리키고 어떤 행이 클릭된 경우에는 refList.begin() + idx + 1; 를 가리킨다.

 

라디오 버튼 그룹의 int type 멤버 변수를 사용한 switch case 문을 통해 벡터에 액션 타입과 속성값을 문자열로 기억시킨다. 잘못 입력했을 때의 예외처리도 해줬다.

 

 

 

 

 

 

 

 

 

 

 

"<" 버튼 눌렀을 때의 이벤트 처리 함수

void CdeltaControlDlg::OnBnClickedButtonToLeft()
{
	int idx = m_list.GetSelectionMark();
	vector<DataRow*>::iterator iter;

	if (idx == -1) return;

	DC.getList().erase(DC.getList().begin() + idx);

	renewListControl();
	UpdateData(FALSE);
}

 

벡터에서 해당 데이터를 지우고 리스트 컨트롤을 갱신해 다시 그린다.

 

 

 

 

 

 

리스트 컨트롤 지우고 벡터 데이터로 다시 그리는 함수

 

void CdeltaControlDlg::renewListControl()
{
	m_list.DeleteAllItems();
	for (int i = 0; i < (int)DC.getList().size(); i++)
	{
		CString temp;
		temp.Format(_T("%d"), i + 1);
		m_list.InsertItem(i, temp);
		m_list.SetItem(i, 1, LVIF_TEXT, DC.getList().at(i)->getActionType(), NULL, NULL, NULL, NULL);
		m_list.SetItem(i, 2, LVIF_TEXT, DC.getList().at(i)->getAttributes(), NULL, NULL, NULL, NULL);
		
	}
}

 

 

 

 

 

 

 

 

 

 

 

파일입출력 기능을 사용한 티칭 데이터 관리

 

 

 

데이터 관리를 위해 DataRow 클래스와 DataController 클래스를 추가하였다.

 

 

 

 

DataRow.h

#pragma once
class DataRow
{
private:
	CString actionType;
	CString Attributes;

public:
	DataRow(CString _actionType, CString _Attributes);
	~DataRow();

	CString getActionType() const;
	void SetActionType(const CString& _actionType);
	CString getAttributes() const;
	void setAttributes(const CString& _Attributes);

};

 

 

 

 

 

 

DataRow.cpp

#include "pch.h"
#include "DataRow.h"

DataRow::DataRow(CString _actionType, CString _Attributes)
	:actionType(_actionType), Attributes(_Attributes)
{
	
}

DataRow::~DataRow()
{
	
}

CString DataRow::getActionType() const
{
	return actionType;
}

void DataRow::SetActionType(const CString& _actionType)
{
	this->actionType = _actionType;
}

CString DataRow::getAttributes() const
{
	return Attributes;
}

void DataRow::setAttributes(const CString& _Attributes)
{
	this->Attributes = _Attributes;
}

 

 

 

 

 

 

 

 

 

DataController.h

#pragma once

#include"DataRow.h"
#include <vector>
#include <string>

using namespace std;

class DataController
{

private:
	vector<DataRow*> list;

public:
	vector<DataRow*>& getList();
	void loadListFromCSVFile();
	void SaveListToFile();
	string MakeRowToOneStr(int i);
};

 

 

 

 

 

 

 

 

 

DataController.cpp

#include "pch.h"
#include "DataController.h"

#include <sstream>
#include <fstream>

#include <iostream>

#define COLUMN_NUM 2

void DataController::loadListFromCSVFile()
{
	ifstream data("actionData.csv");
	
	if (!data.is_open()) 
	{
		cerr << "file open error!" << endl;
		return;
	}

	string line;
		
	while (getline(data, line))
	{
		string comp;
		if (!line.compare("")) break;
		stringstream lineStream(line);

		string cell[COLUMN_NUM];

		int i = 0;

		for (int i = 0; i < COLUMN_NUM; i++)
		{
			getline(lineStream, cell[i], ',');
		}

		CString CCell[COLUMN_NUM];
		CCell[0] = cell[0].c_str();
		CCell[1] = cell[1].c_str();

		list.push_back(new DataRow(CCell[0], CCell[1]));

	}
}

vector<DataRow*>& DataController::getList()
{
	return this->list;
}

void DataController::SaveListToFile()
{

	ofstream myfile;

	myfile.open("actionData.csv");

	for (int i = 0; i < list.size() ; i++) 
	{
		myfile << MakeRowToOneStr(i);
	}

	myfile.close();
}

string DataController::MakeRowToOneStr(int i)
{
	string temp;
	temp = list.at(i)->getActionType() + "," + list.at(i)->getAttributes() + "\n";

	return temp;
}

 

 

OnInitDialog() 함수에서 .csv 파일로 부터 데이터를 Load 한다. OnDestroy() 함수에서는 .csv 파일로 데이터를 Save 한다.

 

 

 

 

 

그리고 -Dlg.h 에는 DataController 객체를 멤버 변수로 포함시킨다.

public:
	DataController DC;

 

 

 

 

 

 

 

 

 

 

쓰레드를 활용한 반복작업 수행

 

 

 

 

 

 

쓰레드 객체의 포인터를 담아 둘 변수를 만들고 쓰레드의 상태를 나타내게 할 enum 타입 변수를 생성한다.

 

typedef enum {
	THREAD_STOP,
	THREAD_RUNNING,
	THREAD_PAUSE
}_ThreadStatus;

//...
public:
	CWinThread* m_pThread;
	_ThreadStatus m_threadStatus;

 

 

 

 

 

RUN 버튼 클릭시 쓰레드 생성 및 시작 또는 일시중단된 쓰레드 재개

 

void CdeltaControlDlg::OnBnClickedButtonRun()
{

	if (m_pThread == NULL) 
	{		

		m_pThread = AfxBeginThread(ThreadRepeat, (LPVOID)this);
		if (m_pThread == NULL)
		{
			AfxMessageBox(_T("ERROR : Fail to begin thread."));
		}
		else 
		{
			m_pThread->m_bAutoDelete = FALSE;
			m_threadStatus = THREAD_RUNNING;
		}
	}
	else if (m_threadStatus == THREAD_PAUSE) 
	{
		m_pThread->ResumeThread();
		m_threadStatus = THREAD_RUNNING;
	}

}

 

쓰레드 함수를 전역으로 만들었는데 다이얼로그 클래스에 접근 가능케 하기 위해 파라미터로 현재 다이얼로그의 포인터인 this를 넘긴다.

 

 

 

 

 

SUSPEND 버튼을 클릭 시 쓰레드 일시 중단

 

void CdeltaControlDlg::OnBnClickedButtonSuspend()
{
	if (m_pThread != NULL) 
	{
		m_pThread->SuspendThread();
		m_threadStatus = THREAD_PAUSE;
	}
	
}

 

 

 

 

 

 

STOP 버튼 클릭 시 / 시리얼 통신 끊길 시 / 다이얼로그 종료 시에 쓰레드 종료

 

void CdeltaControlDlg::OnBnClickedButtonStop()
{
	terminateThread();
}

void CdeltaControlDlg::terminateThread()
{
	if (m_pThread != NULL)
	{
		pump(0);
		currentRow = -1;
		m_pThread->SuspendThread();
		DWORD dwResult;
		GetExitCodeThread(m_pThread->m_hThread, &dwResult);

		delete m_pThread;
		m_pThread = NULL;
		m_threadStatus = THREAD_STOP;
		renewListControl();
	}
}

 

pump(0)으로 꺼주고 현재 작업중이던 행을 뜻하는 멤버변수 currentRow는 -1로 바꾼다. 종료 후 리스트 컨트롤을 한번 갱신해준다.

 

 

 

 

 

 

쓰레드 전역 함수

 

UINT ThreadRepeat(LPVOID LpData)
{

	CdeltaControlDlg* pDlg = (CdeltaControlDlg*)(LpData);
	vector<DataRow*>& refList = pDlg->DC.getList();

	while (pDlg->m_threadStatus == THREAD_RUNNING)
	{
		for (int i = 0; i < refList.size(); i++)
		{
			pDlg->currentRow = i;
			pDlg->renewListControl();

			if (!refList.at(i)->getActionType().Compare(_T("MOVE")))
			{
				CString temp = refList.at(i)->getAttributes();
				CString strX, strY, strZ;
				AfxExtractSubString(strX, temp, 0, '/');
				AfxExtractSubString(strY, temp, 1, '/');
				AfxExtractSubString(strZ, temp, 2, '/');
				pDlg->move(_ttoi(strX),_ttoi(strY),_ttoi(strZ));
				Sleep(200);
			}
			else if (!refList.at(i)->getActionType().Compare(_T("WAIT")))
			{
				int time = _ttoi(refList.at(i)->getAttributes());
				pDlg->wait(time);
				Sleep(time);
			}
			else if (!refList.at(i)->getActionType().Compare(_T("PUMP"))) 
			{
				if (!refList.at(i)->getAttributes().Compare(_T("ON")))
				{
					pDlg->pump(1);
				}
				else if (!refList.at(i)->getAttributes().Compare(_T("OFF")))
				{
					pDlg->pump(0);
				}
				Sleep(200);
			}			
		}
	}

	return 0;
}

 

파라미터로 넘어온 포인터를 사용해서 다이얼로그의 멤버변수와 함수에 접근할 수 있다. 쓰레드가 Running 상태인 동안은 계속 while문 안을 반복한다.

pDlg->currentRow = i; 는 현재 작업중인 행을 노란색으로 표시해 사용자가 직관적으로 알 수 있게끔 하기 위함이다.

참고로 쓰레드 안에서는 UpdateData(False)를 하면 오류가 난다.

 

 

 

 

 

 

 

리스트 컨트롤 색상을 행 단위로 실시간 변경하기 위해 만든 함수

 

void CdeltaControlDlg::OnCustomdrawMyList(NMHDR* pNMHDR, LRESULT* pResult)
{
	NMLVCUSTOMDRAW* pLVCD = reinterpret_cast<NMLVCUSTOMDRAW*>(pNMHDR);

	// Take the default processing unless we 
	// set this to something else below.
	*pResult = CDRF_DODEFAULT;

	// First thing - check the draw stage. If it's the control's prepaint
	// stage, then tell Windows we want messages for every item.

	if (CDDS_PREPAINT == pLVCD->nmcd.dwDrawStage)
	{
		*pResult = CDRF_NOTIFYITEMDRAW;
	}
	else if (CDDS_ITEMPREPAINT == pLVCD->nmcd.dwDrawStage)
	{
			// This is the prepaint stage for an item. Here's where we set the
			// item's text color. Our return value will tell Windows to draw the
			// item itself, but it will use the new color we set here.
			// We'll cycle the colors through red, green, and light blue.

		COLORREF crText = RGB(0, 0, 0);
		COLORREF crBkgnd = RGB(255, 255, 255);


		if (m_list.GetItemText(pLVCD->nmcd.dwItemSpec, 1) == _T("WAIT")) 
		{
			crText = RGB(200, 0, 0);
		}
		else if (m_list.GetItemText(pLVCD->nmcd.dwItemSpec, 1) == _T("MOVE"))
		{
			crText = RGB(0, 200, 0);
		}
		else if (m_list.GetItemText(pLVCD->nmcd.dwItemSpec, 1) == _T("PUMP"))
		{
			crText = RGB(0, 0, 200);
		}

		if (pLVCD->nmcd.dwItemSpec == currentRow) 
		{
			crBkgnd = RGB(255, 255, 0);
		}

		// Store the color back in the NMLVCUSTOMDRAW struct.
		pLVCD->clrText = crText;
		pLVCD->clrTextBk = crBkgnd;

		// Tell Windows to paint the control itself.
		*pResult = CDRF_DODEFAULT;
	}
}

 

 

 

 

 

 

반복 작업을 위해 만든 함수들

 

void CdeltaControlDlg::pump(bool option) 
{
	CString str = _T("DP");

	if (option)
	{
		str += _T("1");
	}
	else
	{
		str += _T("0");
	}

	str += _T("\n");
	m_comm->Send(str, str.GetLength());

}

void CdeltaControlDlg::wait(int time)
{
	CString str = _T("DW");

	CString temp;
	temp.Format(_T("%04d"), time);

	str += temp;
	str += _T("\n");

	m_comm->Send(str, str.GetLength());

}

void CdeltaControlDlg::move(int x, int y, int z)
{

	CString str = _T("DZ");
	CString temp;
	if (x >= 0) {
		str += _T("+");
	}
	else {
		str += _T("-");
	}
	temp.Format(_T("%03d"), abs(x));
	str += temp;
	if (y >= 0) {
		str += _T("+");
	}
	else {
		str += _T("-");
	}
	temp.Format(_T("%03d"), abs(y));
	str += temp;

	if (z >= 0) {
		str += _T("+");
	}
	else {
		str += _T("-");
	}
	temp.Format(_T("%03d"), abs(z));
	str += temp;

	str += _T("\n");

	m_comm->Send(str, str.GetLength());

}