본문 바로가기
DSP, MCU/STM32 (ARM Cortex-M)

STM32 ] ADC + MFC + MySQL, 시리얼 통신 및 DB연동, 검색기능, 실시간 그래프 구현 (쓰레드 사용)

by eteo 2022. 7. 17.

 

 

깃허브 주소 :

https://github.com/joeteo/MfcDbAdc

 

GitHub - joeteo/MfcDbAdc

Contribute to joeteo/MfcDbAdc development by creating an account on GitHub.

github.com

https://github.com/joeteo/AdcMfcDb.git

 

GitHub - joeteo/AdcMfcDb

Contribute to joeteo/AdcMfcDb development by creating an account on GitHub.

github.com

 

 

 

 

핀설정

 

가변저항의 VCC, GND는 보드의 +3.3v, GND 에 연결

OUT핀은 아래 ADC 핀에 연결

 

 

ADC 설정. DMA 모드를 사용하였다.

 

 

 

 

 

 

 

 

 

 

STM32 while문 내

// 전역변수로 DMA 방식으로 값을 담을 WORD 크기의 길이가 2인 배열을 만든다.
/* USER CODE BEGIN PV */
uint32_t potentiometer[2];
/* USER CODE END PV */

...

/* Initialize all configured peripherals */
MX_GPIO_Init();
MX_RTC_Init();
MX_DMA_Init();
MX_USART3_UART_Init();
MX_ADC1_Init();
/* USER CODE BEGIN 2 */
// HAL_ADC_Start_DMA 함수를 쓰면서 위에서 선언해준 배열 주소와 크기 1를 매개변수로 넘긴다.
HAL_ADC_Start_DMA(&hadc1, potentiometer, 2);
/* USER CODE END 2 */

...

/* USER CODE BEGIN WHILE */
while (1)
{

  // 10ms 간격으로 PC에 송신한다.
  
  memset(uart_buf, 0, sizeof(uart_buf));
  sprintf(uart_buf, "PA%04d\n", potentiometer[0]);
  HAL_UART_Transmit(&huart3, (uint8_t*)uart_buf, sizeof(uart_buf), 10);

  memset(uart_buf, 0, sizeof(uart_buf));
  sprintf(uart_buf, "PB%04d\n", potentiometer[1]);
  HAL_UART_Transmit(&huart3, (uint8_t*)uart_buf, sizeof(uart_buf), 10);

  HAL_Delay(10);

/* USER CODE END WHILE */

/* USER CODE BEGIN 3 */
}

 

예를들어

송신하는 데이터 형식은

1번 가변저항이라면 PA0057\n

2번 가변저항이라면 PB4095\n

이런식이다.

 

매우 빠른 속도인 10ms 간격으로 송신하기 때문에 MFC에서 이걸 다 처리하려면 타이머와 쓰레드를 결합한 방식으로 해야한다.

 

 

 

 

 

HeidiSQL 화면

 

 

 

MySQL에는 adc_db를 하나 만들어 두고 그 안에는 tb_adc1, tb_adc2 라는 2개의 테이블이 있다.

테이블은 Primary Key인 ID와 DATETIME 형식의 datetime 그리고 int 형식의 adc 이렇게 3개의 컬럼이 있다.

 

 

 

 

 

MFC

 

CMysqlController.h

#define DB_HOST "127.0.0.1"	// 서버 아이피
#define DB_USER "root"	// DB 접속계정
#define DB_PASS "1234"	// DB 계정암호
#define DB_NAME "adc_db"	// DB 이름

class CMysqlController
{
public:
	CMysqlController();
	~CMysqlController();
	bool SelectQuery(char* sql, vector<DataRow*>& row);
	bool SelectCountQuery(char* sql, unsigned long long& count);
	bool InsertQuery(char* sql);
	bool InsertQuerys(vector<CString>& querys);
};

 

기존에 있던 함수 외에 SelectCountQuery() 함수와 InsertQuerys() 라는 함수를 추가하였다.

 

bool SelectCountQuery(char* sql, unsigned long long& count); 함수는 SelectQuery 함수와 거의 동일하고 SELECT COUNT(*)라는 쿼리문을 보냈을 때 결과값이 1행1열로 나오는데 그걸 unsigned long long 참조자에 대입하는 함수이다.

 

bool InsertQuerys(vector<CString>& querys); 함수는 여러 쿼리문을 한꺼번에 보내야 할 때 MySQL에 접속하는데 부하가 걸릴 수 있으므로 CString 타입의 벡터를 참조자형태로 받아서 한번 DB에 Connect 한 후 쿼리문을 모두 보내고 벡터를 .clear() 하는 함수이다.

 

 

 

 

 

MfcDbAdcDlg.h

// MfcDbAdcDlg.h: 헤더 파일
//

#pragma once

// * 추가 부분 시작 *
#include "SerialCom.h"
#include <vector>
using namespace std;
#include "CMysqlController.h"
// * 추가 부분 끝 *

// * 그래프용 추가 시작
#include "OScopeCtrl.h"
// * 그래프용 추가 끝

#define MYMSG WM_USER+3
#define MYTERMINATEFLAG WM_USER+4


// CMfcDbAdcDlg 대화 상자
class CMfcDbAdcDlg : public CDialogEx
{
// 생성입니다.
public:
	CMfcDbAdcDlg(CWnd* pParent = nullptr);	// 표준 생성자입니다.

// 대화 상자 데이터입니다.
#ifdef AFX_DESIGN_TIME
	enum { IDD = IDD_MFCDBADC_DIALOG };
#endif

	protected:
	virtual void DoDataExchange(CDataExchange* pDX);	// DDX/DDV 지원입니다.


// 구현입니다.
protected:
	HICON m_hIcon;

	// 생성된 메시지 맵 함수
	virtual BOOL OnInitDialog();
	afx_msg void OnSysCommand(UINT nID, LPARAM lParam);
	afx_msg void OnPaint();
	afx_msg HCURSOR OnQueryDragIcon();
	DECLARE_MESSAGE_MAP()

public:
	/* 추가 부분 시작 */
	CSerialComm* m_comm;
	BOOL comport_state;



protected:
	CComboBox m_combo_comport_list;
	CComboBox m_combo_baudrate_list;
	CString m_str_comport;
	CString m_combo_baudrate;

	afx_msg LRESULT CMfcDbAdcDlg::OnReceive(WPARAM length, LPARAM lParam);
	afx_msg LRESULT CMfcDbAdcDlg::OnThreadClosed(WPARAM wParam, LPARAM lParam);
	
public:

	afx_msg void OnCbnSelchangeComboComport();
	afx_msg void OnCbnSelchangeComboBaudrate();
	afx_msg void OnBnClickedBtConnect();
	vector<char> rx;
	/* 추가 부분 끝 */
	CListCtrl m_list;
	CListCtrl m_list2;
	void CMfcDbAdcDlg::RenewListControl(int tbNum);
	afx_msg void OnBnClickedButton2();
	afx_msg void OnBnClickedButton3();
	afx_msg void OnDestroy();
	afx_msg void OnDtnDatetimechangeDatetimepicker4(NMHDR* pNMHDR, LRESULT* pResult);

	COleDateTime m_Date;
	afx_msg void OnDtnDatetimechangeDatetimepicker7(NMHDR* pNMHDR, LRESULT* pResult);
	afx_msg void OnDtnDatetimechangeDatetimepicker8(NMHDR* pNMHDR, LRESULT* pResult);
	COleDateTime m_StartTime;
	COleDateTime m_EndTime;

	afx_msg void OnBnClickedButton1();
	
	CDateTimeCtrl m_CtrlDate;
	CDateTimeCtrl m_CtrlStartTime;
	CDateTimeCtrl m_CtrlEndTime;
	afx_msg void OnClose();
	afx_msg void OnTimer(UINT_PTR nIDEvent);
	vector<CString> querys;

	// * 그래프용 추가 시작
	// COScopeCtrl 컨트롤의 객체 포인터를 선언
	COScopeCtrl* _rtGraph;
	double adcValue1;
	double adcValue2;
	// * 그래프용 추가 끝

protected:
	afx_msg LRESULT OnMymsg(WPARAM wParam, LPARAM lParam);
	afx_msg LRESULT OnMyterminateflag(WPARAM wParam, LPARAM lParam);
};

 

 

 

 

MfcDbAdcDlg.cpp

// MfcDbAdcDlg.cpp: 구현 파일
//

#include "pch.h"
#include "framework.h"
#include "MfcDbAdc.h"
#include "MfcDbAdcDlg.h"
#include "afxdialogex.h"

#ifdef _DEBUG
#define new DEBUG_NEW
#endif

// 그래프용 추가 시작
#include "stdafx.h"
// 그래프용 추가 끝

// 응용 프로그램 정보에 사용되는 CAboutDlg 대화 상자입니다.

class CAboutDlg : public CDialogEx
{
public:
	CAboutDlg();

// 대화 상자 데이터입니다.
#ifdef AFX_DESIGN_TIME
	enum { IDD = IDD_ABOUTBOX };
#endif

	protected:
	virtual void DoDataExchange(CDataExchange* pDX);    // DDX/DDV 지원입니다.

// 구현입니다.
protected:
	DECLARE_MESSAGE_MAP()
public:
};

CAboutDlg::CAboutDlg() : CDialogEx(IDD_ABOUTBOX)
{
}

void CAboutDlg::DoDataExchange(CDataExchange* pDX)
{
	CDialogEx::DoDataExchange(pDX);
}

BEGIN_MESSAGE_MAP(CAboutDlg, CDialogEx)

END_MESSAGE_MAP()


// CMfcDbAdcDlg 대화 상자



CMfcDbAdcDlg::CMfcDbAdcDlg(CWnd* pParent /*=nullptr*/)
	: CDialogEx(IDD_MFCDBADC_DIALOG, pParent)
	, m_str_comport(_T(""))
	, m_combo_baudrate(_T(""))
	, m_Date(COleDateTime::GetCurrentTime())
	, m_StartTime(COleDateTime::GetCurrentTime())
	, m_EndTime(COleDateTime::GetCurrentTime())
	, adcValue1(0), adcValue2(0)

{
	m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);
}

void CMfcDbAdcDlg::DoDataExchange(CDataExchange* pDX)
{
	CDialogEx::DoDataExchange(pDX);
	DDX_Control(pDX, IDC_COMBO_COMPORT, m_combo_comport_list);
	DDX_Control(pDX, IDC_COMBO_BAUDRATE, m_combo_baudrate_list);
	DDX_CBString(pDX, IDC_COMBO_COMPORT, m_str_comport);
	DDX_CBString(pDX, IDC_COMBO_BAUDRATE, m_combo_baudrate);
	DDX_Control(pDX, IDC_LIST1, m_list);
	DDX_Control(pDX, IDC_LIST2, m_list2);

	DDX_DateTimeCtrl(pDX, IDC_DATETIMEPICKER4, m_Date);
	DDX_DateTimeCtrl(pDX, IDC_DATETIMEPICKER7, m_StartTime);
	DDX_DateTimeCtrl(pDX, IDC_DATETIMEPICKER8, m_EndTime);

	DDX_Control(pDX, IDC_DATETIMEPICKER4, m_CtrlDate);
	DDX_Control(pDX, IDC_DATETIMEPICKER7, m_CtrlStartTime);
	DDX_Control(pDX, IDC_DATETIMEPICKER8, m_CtrlEndTime);
}

BEGIN_MESSAGE_MAP(CMfcDbAdcDlg, CDialogEx)
	ON_WM_SYSCOMMAND()
	ON_WM_PAINT()
	ON_WM_QUERYDRAGICON()
	// * 추가 부분 시작 *
	ON_MESSAGE(WM_MYRECEIVE, &CMfcDbAdcDlg::OnReceive)
	ON_MESSAGE(WM_MYCLOSE, &CMfcDbAdcDlg::OnThreadClosed)
	ON_CBN_SELCHANGE(IDC_COMBO_COMPORT, &CMfcDbAdcDlg::OnCbnSelchangeComboComport)
	ON_CBN_SELCHANGE(IDC_COMBO_BAUDRATE, &CMfcDbAdcDlg::OnCbnSelchangeComboBaudrate)
	ON_BN_CLICKED(IDC_BT_CONNECT, &CMfcDbAdcDlg::OnBnClickedBtConnect)
	// * 추가 부분 끝 *
	ON_BN_CLICKED(IDC_BUTTON2, &CMfcDbAdcDlg::OnBnClickedButton2)
	ON_BN_CLICKED(IDC_BUTTON3, &CMfcDbAdcDlg::OnBnClickedButton3)
	ON_WM_DESTROY()
	ON_NOTIFY(DTN_DATETIMECHANGE, IDC_DATETIMEPICKER4, &CMfcDbAdcDlg::OnDtnDatetimechangeDatetimepicker4)
	ON_NOTIFY(DTN_DATETIMECHANGE, IDC_DATETIMEPICKER7, &CMfcDbAdcDlg::OnDtnDatetimechangeDatetimepicker7)
	ON_NOTIFY(DTN_DATETIMECHANGE, IDC_DATETIMEPICKER8, &CMfcDbAdcDlg::OnDtnDatetimechangeDatetimepicker8)
	ON_BN_CLICKED(IDC_BUTTON1, &CMfcDbAdcDlg::OnBnClickedButton1)
	ON_WM_CLOSE()
	ON_WM_TIMER()
	ON_MESSAGE(MYMSG, &CMfcDbAdcDlg::OnMymsg)
	ON_MESSAGE(MYTERMINATEFLAG, &CMfcDbAdcDlg::OnMyterminateflag)
END_MESSAGE_MAP()


// CMfcDbAdcDlg 메시지 처리기

 

 

초기설정 부분

BOOL CMfcDbAdcDlg::OnInitDialog()
{
	CDialogEx::OnInitDialog();

	// 시스템 메뉴에 "정보..." 메뉴 항목을 추가합니다.

	// IDM_ABOUTBOX는 시스템 명령 범위에 있어야 합니다.
	ASSERT((IDM_ABOUTBOX & 0xFFF0) == IDM_ABOUTBOX);
	ASSERT(IDM_ABOUTBOX < 0xF000);

	CMenu* pSysMenu = GetSystemMenu(FALSE);
	if (pSysMenu != nullptr)
	{
		BOOL bNameValid;
		CString strAboutMenu;
		bNameValid = strAboutMenu.LoadString(IDS_ABOUTBOX);
		ASSERT(bNameValid);
		if (!strAboutMenu.IsEmpty())
		{
			pSysMenu->AppendMenu(MF_SEPARATOR);
			pSysMenu->AppendMenu(MF_STRING, IDM_ABOUTBOX, strAboutMenu);
		}
	}

	// 이 대화 상자의 아이콘을 설정합니다.  응용 프로그램의 주 창이 대화 상자가 아닐 경우에는
	//  프레임워크가 이 작업을 자동으로 수행합니다.
	SetIcon(m_hIcon, TRUE);			// 큰 아이콘을 설정합니다.
	SetIcon(m_hIcon, FALSE);		// 작은 아이콘을 설정합니다.

	// TODO: 여기에 추가 초기화 작업을 추가합니다.

	// * 추가 부분 시작 *
	m_combo_comport_list.AddString(_T("COM1"));
	m_combo_comport_list.AddString(_T("COM2"));
	m_combo_comport_list.AddString(_T("COM3"));
	m_combo_comport_list.AddString(_T("COM4"));
	m_combo_comport_list.AddString(_T("COM5"));
	m_combo_comport_list.AddString(_T("COM6"));
	m_combo_comport_list.AddString(_T("COM7"));
	m_combo_comport_list.AddString(_T("COM8"));
	m_combo_comport_list.AddString(_T("COM9"));
	m_combo_comport_list.AddString(_T("COM10"));

	m_combo_baudrate_list.AddString(_T("9600"));
	m_combo_baudrate_list.AddString(_T("19200"));
	m_combo_baudrate_list.AddString(_T("115200"));

	comport_state = false;
	GetDlgItem(IDC_BT_CONNECT)->SetWindowText(_T("OPEN"));
	m_str_comport = _T("COM9");
	m_combo_baudrate = _T("115200");
	UpdateData(FALSE);
	
	// * 추가 부분 끝 *
	m_list.SetExtendedStyle(
		LVS_EX_GRIDLINES | LVS_EX_FULLROWSELECT);

	m_list.InsertColumn(0, _T("ID"), LVCFMT_CENTER, 80);
	m_list.InsertColumn(1, _T("시간"), LVCFMT_CENTER, 200);
	m_list.InsertColumn(2, _T("ADC값"), LVCFMT_CENTER, 100);


	m_list2.SetExtendedStyle(
		LVS_EX_GRIDLINES | LVS_EX_FULLROWSELECT);

	m_list2.InsertColumn(0, _T("ID"), LVCFMT_CENTER, 80);
	m_list2.InsertColumn(1, _T("시간"), LVCFMT_CENTER, 200);
	m_list2.InsertColumn(2, _T("ADC값"), LVCFMT_CENTER, 100);


	UpdateData(true);

	CString str = m_Date.Format(_T("%Y-%m-%d  "));
	str += m_StartTime.Format(_T("%H:%M:%S ~ "));
	str += m_EndTime.Format(_T("%H:%M:%S"));
	GetDlgItem(IDC_EDIT1)->SetWindowText(str);

	
	CString formatStyle = _T("HH:mm:ss");
	m_CtrlStartTime.SetFormat(formatStyle);
	m_CtrlEndTime.SetFormat(formatStyle);

	SetTimer(1, 5000, NULL);

	//* 그래프용 추가 시작
	// 오실로스코프 컨트롤이 위치할 영역 가져오기
	CRect rtGraph;
	GetDlgItem(IDC_STATIC_RT_GRAPH)->GetWindowRect(rtGraph);

	ScreenToClient(rtGraph);

	// 오실로스코프 컨트롤을 생성하고 설정한다.
	_rtGraph = new COScopeCtrl(2);      // 2개의 그래프 예약
	_rtGraph->Create(WS_VISIBLE | WS_CHILD, rtGraph, this, IDC_STATIC_RT_GRAPH);
	_rtGraph->SetRanges(0., 4096.);
	_rtGraph->autofitYscale = true;
	_rtGraph->SetYUnits("ADC Value");
	_rtGraph->SetXUnits("Time");
	_rtGraph->SetLegendLabel("Potentiometer(1)", 0);
	_rtGraph->SetLegendLabel("Potentiometer(2)", 1);
	//_rtGraph->SetLegendLabel("tan(t)", 2);
	_rtGraph->SetPlotColor(RGB(255, 0, 0), 0);
	_rtGraph->SetPlotColor(RGB(0, 255, 0), 1);
	//_rtGraph->SetPlotColor(RGB(0, 0, 255), 2);
	_rtGraph->InvalidateCtrl();

	// 오실로스코프 컨트롤을 그리기 위한 타이머 이벤트 활성화
	SetTimer(1000, 10, NULL);


	//* 그래프용 추가 끝

	return TRUE;  // 포커스를 컨트롤에 설정하지 않으면 TRUE를 반환합니다.
}

 

OnInitDialog() 부분에선 시리얼 통신 부분과 리스트 컨트롤, 검색부분, 그래프를 위한 초기 설정을 해주고 타이머를 시작한다.

 

타이머 중 적어도 그래프를 그리기 위한 타이머는 시리얼 포트가 열린 후에 SetTimer()해주고 시리얼 포트가 닫히기 직전에 KillTimer() 해주는 것이 효율적일텐데 일단 위 코드는 그렇게 짜진 않았다. 만약 다음에 한다면 오히려 고속으로 새로 그려지는 부분인 그래프는 다른 다이얼로그로 따로 빼는 것이 낫지 않을까 하는 생각이 든다.

 

 

Com포트와 보드레이트, 시리얼 통신 연결/해제 버튼 처리 부분

void CMfcDbAdcDlg::OnSysCommand(UINT nID, LPARAM lParam)
{
	if ((nID & 0xFFF0) == IDM_ABOUTBOX)
	{
		CAboutDlg dlgAbout;
		dlgAbout.DoModal();
	}
	else
	{
		CDialogEx::OnSysCommand(nID, lParam);
	}
}

// 대화 상자에 최소화 단추를 추가할 경우 아이콘을 그리려면
//  아래 코드가 필요합니다.  문서/뷰 모델을 사용하는 MFC 애플리케이션의 경우에는
//  프레임워크에서 이 작업을 자동으로 수행합니다.

void CMfcDbAdcDlg::OnPaint()
{
	if (IsIconic())
	{
		CPaintDC dc(this); // 그리기를 위한 디바이스 컨텍스트입니다.

		SendMessage(WM_ICONERASEBKGND, reinterpret_cast<WPARAM>(dc.GetSafeHdc()), 0);

		// 클라이언트 사각형에서 아이콘을 가운데에 맞춥니다.
		int cxIcon = GetSystemMetrics(SM_CXICON);
		int cyIcon = GetSystemMetrics(SM_CYICON);
		CRect rect;
		GetClientRect(&rect);
		int x = (rect.Width() - cxIcon + 1) / 2;
		int y = (rect.Height() - cyIcon + 1) / 2;

		// 아이콘을 그립니다.
		dc.DrawIcon(x, y, m_hIcon);
	}
	else
	{
		CDialogEx::OnPaint();
	}
}

// 사용자가 최소화된 창을 끄는 동안에 커서가 표시되도록 시스템에서
//  이 함수를 호출합니다.
HCURSOR CMfcDbAdcDlg::OnQueryDragIcon()
{
	return static_cast<HCURSOR>(m_hIcon);
}





void CMfcDbAdcDlg::OnCbnSelchangeComboComport()	// * 추가 부분 *
{
	UpdateData();
}


void CMfcDbAdcDlg::OnCbnSelchangeComboBaudrate()	// * 추가 부분 *
{
	UpdateData();
}


void CMfcDbAdcDlg::OnBnClickedBtConnect()	// * 추가 부분 *
{
	// TODO: Add your control notification handler code here
		// TODO: Add your control notification handler code here
	if (comport_state)
	{
		if (m_comm)        //컴포트가존재하면
		{
			m_comm->Close();
			m_comm = NULL;
			AfxMessageBox(_T("COM 포트닫힘"));
			comport_state = false;
			GetDlgItem(IDC_BT_CONNECT)->SetWindowText(_T("OPEN"));
			//GetDlgItem(IDC_BT_SEND)->EnableWindow(false); //버튼 비활성화
		}
	}
	else
	{
		m_comm = new CSerialComm(_T("\\\\.\\") + m_str_comport, m_combo_baudrate, _T("None"), _T("8 Bit"), _T("1 Bit"));         // initial Comm port
		if (m_comm->Create(GetSafeHwnd()) != 0) //통신포트를열고윈도우의핸들을넘긴다.
		{
			AfxMessageBox(_T("COM 포트열림"));
			comport_state = true;
			GetDlgItem(IDC_BT_CONNECT)->SetWindowText(_T("CLOSE"));
			//GetDlgItem(IDC_BT_SEND)->EnableWindow(true); //버튼 클릭할 수 있는 상태
		}
		else
		{
			AfxMessageBox(_T("ERROR!"));
		}

	}
}

 

 

 

수신 부분

afx_msg LRESULT CMfcDbAdcDlg::OnReceive(WPARAM length, LPARAM lParam)	// * 추가 부분 *
{
	CString str;
	char* data = new char[length + 1];

	if (m_comm)
	{
		m_comm->Receive(data, length);	// Length 길이만큼 데이터 받음.
		

		for (int i = 0; i < length; i++)
		{
			rx.push_back(data[i]);
		}
		while (rx.size() >= 7)
		{
			if (rx.at(0) == 'P' && rx.at(6) == '\n')
			{
				if(rx.at(1)=='A')
				{
					int tempValue = 0;
					tempValue = (rx.at(2) - '0') * 1000 + (rx.at(3) - '0') * 100 + (rx.at(4) - '0') * 10
						+ (rx.at(5) - '0') * 1;
					adcValue1 = (double)tempValue;
										
					CString temp;
					CString strTempValue;
					CString strTempNow;

					temp = _T("INSERT INTO tb_adc1(datetime, adc) VALUES('");
					COleDateTime now = COleDateTime::GetCurrentTime();
					strTempNow = now.Format(_T("%Y-%m-%d %H:%M:%S', "));
					temp += strTempNow;
					strTempValue.Format(_T("%d"), tempValue);
					temp += strTempValue;
					temp += _T("); ");

					querys.push_back(temp);
										
				}else if(rx.at(1)=='B')
				{
					int tempValue = 0;
					tempValue = (rx.at(2) - '0') * 1000 + (rx.at(3) - '0') * 100 + (rx.at(4) - '0') * 10
						+ (rx.at(5) - '0') * 1;
					adcValue2 = (double)tempValue;
				
					CString temp;
					CString strTempValue;
					CString strTempNow;

					temp = _T("INSERT INTO tb_adc2(datetime, adc) VALUES('");
					COleDateTime now = COleDateTime::GetCurrentTime();
					strTempNow = now.Format(_T("%Y-%m-%d %H:%M:%S', "));
					temp += strTempNow;
					strTempValue.Format(_T("%d"), tempValue);
					temp += strTempValue;
					temp += _T("); ");

					querys.push_back(temp);
					
				}

				rx.erase(rx.begin(), rx.begin() + 6);

			}
			else if (rx.at(0) != 'P')
			{
				rx.erase(rx.begin());

			}
			else if (rx.at(6) != '\n')
			{
				rx.erase(rx.begin(), rx.begin() + 6);
			}

		}
		
		UpdateData(false);

		str = "";
	}
	delete data;


	return 0;
}

 

커맨드의 형식과 일치하는 경우 4바이트 문자열을 4자리의 정수 형태로 바꾼 뒤 그래프를 그리기 위한 변수에 대입하고

GetCurrentTime() 함수로 확인한 현재시간과 ADC 값을 포함한 INSERT 쿼리문을 완성하고 querys 라는 함수에 차곡차곡 쌓는다.

COleDateTime now = COleDateTime::GetCurrentTime();
strTempNow = now.Format(_T("%Y-%m-%d %H:%M:%S', "));

 

커맨드 처리 후 rx.erase() 함수로 수신 데이터를 계속 지우고 잘못된 형식의 데이터가 들어왔을 때도 계속 지우면서 rx 벡터 사이즈가 커맨드 길이인 7미만이 될 때까지 반복하고 빠져나간다.

 

 

새로고침 부분

afx_msg LRESULT CMfcDbAdcDlg::OnThreadClosed(WPARAM length, LPARAM lParam)	// * 추가 부분 *
{
	((CSerialComm*)lParam)->HandleClose();
	delete ((CSerialComm*)lParam);

	return 0;
}


void CMfcDbAdcDlg::RenewListControl(int tbNum)
{
	CMysqlController conn;
	vector<DataRow*> row;

	switch(tbNum)
	{
	case 1:
		m_list.DeleteAllItems();
		if (conn.SelectQuery("SELECT * FROM tb_adc1 WHERE id BETWEEN (SELECT MAX(id) from tb_adc1)-1000 and (SELECT MAX(id) from tb_adc1);", row) == true){}
		for (size_t i = 0; i < row.size(); i++)
		{
			m_list.InsertItem(i, row.at(i)->getId());
			m_list.SetItem(i, 1, LVIF_TEXT, row.at(i)->getDateTime(), NULL, NULL, NULL, NULL);
			m_list.SetItem(i, 2, LVIF_TEXT, row.at(i)->getAdcValue(), NULL, NULL, NULL, NULL);
		}
		m_list.SendMessage(WM_VSCROLL, SB_BOTTOM);
		break;
	case 2:
		m_list2.DeleteAllItems();
		if (conn.SelectQuery("SELECT * FROM tb_adc2 WHERE id BETWEEN (SELECT MAX(id) from tb_adc2)-1000 and (SELECT MAX(id) from tb_adc2);", row) == true){}
		for (size_t i = 0; i < row.size(); i++)
		{
			m_list2.InsertItem(i, row.at(i)->getId());
			m_list2.SetItem(i, 1, LVIF_TEXT, row.at(i)->getDateTime(), NULL, NULL, NULL, NULL);
			m_list2.SetItem(i, 2, LVIF_TEXT, row.at(i)->getAdcValue(), NULL, NULL, NULL, NULL);
		}
		m_list2.SendMessage(WM_VSCROLL, SB_BOTTOM);
		break;
	}

	for (size_t i = 0; i < row.size(); i++)
	{
		delete row.at(i);
	}

}

void CMfcDbAdcDlg::OnBnClickedButton2()
{

	RenewListControl(1);
}


void CMfcDbAdcDlg::OnBnClickedButton3()
{
	RenewListControl(2);
}

 

RenewListControl() 함수에서는 테이블번호 1 또는 2를 정수형으로 받아 switch case문으로 처리한다.

 

그냥 SELECT ~ LIMIT 1000; 이라고 하면 최근이 아니라 오래된 데이터 부터 1000개를 보여주고 그냥 ORDER id BY DESC 하면 최신 데이터 부터 나오는데 내림차순이다.

SELECT * FROM tb_adc1 WHERE id BETWEEN (SELECT MAX(id) from tb_adc1)-1000 and (SELECT MAX(id) from tb_adc1);

 

이 쿼리문 끝에 ORDER BY datetime 해도 된다.

 

처음 리스트 컨트롤을 .DeleteAllItems() 해주고 .InsertItem 과 .SetItem 으로 입력한 후 마지막으로 스크롤바를 제일 하단에 위치한다.

 

 

 

검색부분

void CMfcDbAdcDlg::OnDtnDatetimechangeDatetimepicker4(NMHDR* pNMHDR, LRESULT* pResult)
{
	LPNMDATETIMECHANGE pDTChange = reinterpret_cast<LPNMDATETIMECHANGE>(pNMHDR);
	// TODO: 여기에 컨트롤 알림 처리기 코드를 추가합니다.


	UpdateData(TRUE);
	CString str = m_Date.Format(_T("%Y-%m-%d  "));
	str += m_StartTime.Format(_T("%H:%M:%S ~ "));
	str += m_EndTime.Format(_T("%H:%M:%S"));
	GetDlgItem(IDC_EDIT1)->SetWindowText(str);

	*pResult = 0;
}


void CMfcDbAdcDlg::OnDtnDatetimechangeDatetimepicker7(NMHDR* pNMHDR, LRESULT* pResult)
{
	LPNMDATETIMECHANGE pDTChange = reinterpret_cast<LPNMDATETIMECHANGE>(pNMHDR);
	UpdateData(TRUE);
	CString str = m_Date.Format(_T("%Y-%m-%d  "));
	str += m_StartTime.Format(_T("%H:%M:%S ~ "));
	str += m_EndTime.Format(_T("%H:%M:%S"));
	GetDlgItem(IDC_EDIT1)->SetWindowText(str);

	*pResult = 0;
}


void CMfcDbAdcDlg::OnDtnDatetimechangeDatetimepicker8(NMHDR* pNMHDR, LRESULT* pResult)
{
	LPNMDATETIMECHANGE pDTChange = reinterpret_cast<LPNMDATETIMECHANGE>(pNMHDR);

	UpdateData(TRUE);
	CString str = m_Date.Format(_T("%Y-%m-%d  "));
	str += m_StartTime.Format(_T("%H:%M:%S ~ "));
	str += m_EndTime.Format(_T("%H:%M:%S"));
	GetDlgItem(IDC_EDIT1)->SetWindowText(str);

	*pResult = 0;
}

Date Time Picker 에 대한 이벤트 처리기 함수들이다.

 

Dtn Datetimechange 될 때마다 날짜와 시간을 CString 포맷으로 바꿔서 그 밑에 있는 에디트 컨트롤에 GetDlgItem(IDC_EDIT1)->SetWindowText(str) 을 사용해서 표현한다. 에디트 컨트롤의 멤버변수에 단순히 값을 대입해준게 아니라 GetDlgItem 을 사용하는 이 경우에는 UpdateData(false)를 할 필요가 없다.

 

 

 

Date Time Picker 의 형식은 위의 것은 Short Date 이고 아래 두개는 Time 형식이다.

 

Time 형식은 디폴트가 한글로 오전/오후가 출력이 되는데 이를 24시간 표시형식으로 바꾸기 위해 OnInitDialog() 함수에서 아래와 같은 코드를 추가했었다.

CString formatStyle = _T("HH:mm:ss");
m_CtrlStartTime.SetFormat(formatStyle);
m_CtrlEndTime.SetFormat(formatStyle);

 

 

 

검색부분 계속

void CMfcDbAdcDlg::OnBnClickedButton1()
{
	// TODO: 여기에 컨트롤 알림 처리기 코드를 추가합니다.
	COleDateTime currentTime = COleDateTime::GetCurrentTime();
	if(m_Date.m_dt > currentTime.m_dt)
	{
		GetDlgItem(IDC_EDIT1)->SetWindowText(_T("오늘 이전의 날짜를 선택해주세요"));
	}else if(m_StartTime > m_EndTime)
	{
		GetDlgItem(IDC_EDIT1)->SetWindowText(_T("종료시간이 시작시간보다 빠를수 없습니다"));
	}else
	{
		CMysqlController conn;
		vector<DataRow*> row;

		m_list.DeleteAllItems();

		UpdateData(TRUE);

		CString temp = _T("SELECT * FROM tb_adc1 WHERE datetime >= '");
		CString tempDate = m_Date.Format(_T("%Y-%m-%d "));
		temp += tempDate;
		CString tempStartTime = m_StartTime.Format(_T("%H:%M:%S' and datetime <= '"));
		temp += tempStartTime;
		temp += tempDate;
		CString tempEndTime = m_EndTime.Format(_T("%H:%M:%S'"));
		temp += tempEndTime;

		//SELECT* FROM tb_adc1 WHERE datetime >= '2022-07-14 13:17:31' and datetime <= '2022-07-14 13:17:48'

		if (conn.SelectQuery(LPSTR(LPCTSTR(temp)), row) == true) {}
		for (size_t i = 0; i < row.size(); i++)
		{
			m_list.InsertItem(i, row.at(i)->getId());
			m_list.SetItem(i, 1, LVIF_TEXT, row.at(i)->getDateTime(), NULL, NULL, NULL, NULL);
			m_list.SetItem(i, 2, LVIF_TEXT, row.at(i)->getAdcValue(), NULL, NULL, NULL, NULL);
		}
		m_list.SendMessage(WM_VSCROLL, SB_BOTTOM);

		m_list2.DeleteAllItems();

		temp = _T("SELECT * FROM tb_adc2 WHERE datetime >= '");
		tempDate = m_Date.Format(_T("%Y-%m-%d "));
		temp += tempDate;
		tempStartTime = m_StartTime.Format(_T("%H:%M:%S' and datetime <= '"));
		temp += tempStartTime;
		temp += tempDate;
		tempEndTime = m_EndTime.Format(_T("%H:%M:%S'"));
		temp += tempEndTime;

		if (conn.SelectQuery(LPSTR(LPCTSTR(temp)), row) == true) {}
		for (size_t i = 0; i < row.size(); i++)
		{
			m_list2.InsertItem(i, row.at(i)->getId());
			m_list2.SetItem(i, 1, LVIF_TEXT, row.at(i)->getDateTime(), NULL, NULL, NULL, NULL);
			m_list2.SetItem(i, 2, LVIF_TEXT, row.at(i)->getAdcValue(), NULL, NULL, NULL, NULL);
		}
		m_list2.SendMessage(WM_VSCROLL, SB_BOTTOM);


		for (size_t i = 0; i < row.size(); i++)
		{
			delete row.at(i);
		}
	}
}

 

검색하기 버튼을 눌렀을 때의 이벤트 처리 함수이다. 날짜나 검색 시작시간, 종료시간에 유효하지 않은 값을 입력한 경우 에디트 컨트롤에 오류문구를 출력하게끔 했다. 그리고 날짜를 비교하는데 .m_dt를 사용했는데 .getDay() 함수를 썼어도 됐을 것 같다.

 

COleDateTime 클래스 사용법 : https://docs.microsoft.com/ko-kr/cpp/atl-mfc-shared/reference/coledatetime-class?view=msvc-160 

 

한 행의 정보를 온전히 담기 위해 만들어 둔 DataRow 클래스 포인터 타입 벡터를 생성하고 쿼리문을 만든 후 SelectQuery() 함수를 호출하는데 벡터명을 인수로 넘기고 안에선 참조자로 받는다. 리스트 컨트롤에 다 출력한 후에는 만들었던 벡터가 함수의 끝을 만나 사라지기 전에 동적할당 해제해준다.

 

참고로 CMysqlController.cpp 파일 내 SelectQuery(char* sql, vector<DataRow*>& row) 함수에서는 아래와 같이 new 로 동적할당 해 벡터에 데이터를 넣는다.

sql_result = mysql_store_result(connection);
while((sql_row = mysql_fetch_row(sql_result)) != NULL)
{
    row.push_back(new DataRow(sql_row[0], sql_row[1], sql_row[2]));

}
mysql_free_result(sql_result);

 

 

 

 

오래된 데이터 지우는 부분

UINT deleteOldData(LPVOID LpData)
{
	CMfcDbAdcDlg* target = (CMfcDbAdcDlg*)(LpData);

	CMysqlController conn;

	unsigned long long temp1 = 0;
	unsigned long long temp2 = 0;
	conn.SelectCountQuery("SELECT COUNT(*) FROM tb_adc1 WHERE DATETIME < DATE_SUB(NOW(), INTERVAL 10 MINUTE)", temp1);
	conn.SelectCountQuery("SELECT COUNT(*) FROM tb_adc2 WHERE DATETIME < DATE_SUB(NOW(), INTERVAL 10 MINUTE)", temp2);

	for (int i = 0; i < (temp1 / 1000) + 1; i++)
	{
		if (conn.InsertQuery("DELETE FROM tb_adc1 WHERE DATETIME < DATE_SUB(NOW(), INTERVAL 10 MINUTE) LIMIT 1000") == true) {}
	}

	for (int i = 0; i < (temp2 / 1000) + 1; i++)
	{
		if (conn.InsertQuery("DELETE FROM tb_adc2 WHERE DATETIME < DATE_SUB(NOW(), INTERVAL 10 MINUTE) LIMIT 1000") == true) {}
	}

	SendMessage(target->m_hWnd, MYTERMINATEFLAG, NULL, NULL);

	return 0;
}



void CMfcDbAdcDlg::OnClose()
{
	// TODO: 여기에 메시지 처리기 코드를 추가 및/또는 기본값을 호출합니다.

	AfxBeginThread(deleteOldData, (LPVOID)this);
	
	return;


	CDialogEx::OnClose();
}


void CMfcDbAdcDlg::OnDestroy()
{
	CDialogEx::OnDestroy();
	
	// TODO: 여기에 메시지 처리기 코드를 추가합니다.
    // 그래프용 포인터 동적할당 해제
	delete _rtGraph;

}

afx_msg LRESULT CMfcDbAdcDlg::OnMyterminateflag(WPARAM wParam, LPARAM lParam)
{
	//OnDestroy();
	EndDialog(IDCANCEL);
	return 0;
}

 

사용자가 다이얼로그의 X버튼을 눌렀을 경우 OnClose() 함수로 들어오는 데 이때 AfxBeginThread(deleteOldData, (LPVOID)this) 로 쓰레드를 시작한 후 바로 return; 하면 종료가 되지 않는다.

 

deleteOldData 쓰레드에서는 SelectCountQuery() 에 쿼리문을 보내 일단 10분 이전의 데이터 개수가 몇개인지 세고 for 문으로 반복하면서 1000개 씩 지운다. 예를들어 10000개를 한번 지우는 것보다 1000개를 10번에 걸쳐 지우는 것이 병목현상을 방지하고 속도 면에서도 우수하기 때문이다.

 

다 지우고 나서는 SendMessage 로 MYTERMINATEFLAG 를 호출하고 해당 메시지 안에서는 EndDialog()로 종료한다. 이렇게 하면 OnClose()를 거치지 않기 때문에 오래된 데이터를 지우는 작업을 완료한 후에 종료하게끔 할 수 있다.

 

참고로 OnClose() 나 EndDialog() 처럼 메시지 처리 함수를 재정의 하려면 프로젝트 - 클래스 마법사 에서 하는 방법도 있지만 리소스뷰 - 속성 에서 해당 메시지를 찾아서 선택해줘도 된다.

 

 

 

10ms 마다 그래프를 그리고, 5초마다 INSERT 쿼리문을 보내는 부분

UINT handleQuerys(LPVOID LpData)
{
	CMfcDbAdcDlg* target = (CMfcDbAdcDlg*)(LpData);
	CMysqlController conn;
	conn.InsertQuerys(target->querys);

	SendMessage(target->m_hWnd, MYMSG, NULL, NULL);

	//PostMessage(target->m_hWnd, MYMSG, NULL, NULL);

	return 0;

}


void CMfcDbAdcDlg::OnTimer(UINT_PTR nIDEvent)
{
	// TODO: 여기에 메시지 처리기 코드를 추가 및/또는 기본값을 호출합니다.


	if (nIDEvent == 1)
	{
		AfxBeginThread(handleQuerys, (LPVOID)this);
	}

	if (nIDEvent == 1000) {

		double value[2] = { adcValue1, adcValue2 };

		_rtGraph->AppendPoints(value);
	}
	
	CDialogEx::OnTimer(nIDEvent);
}


afx_msg LRESULT CMfcDbAdcDlg::OnMymsg(WPARAM wParam, LPARAM lParam)
{
	//RenewListControl(1);
	//RenewListControl(2);

	return 0;
}

 

타이머에서는 타이머의 ID가 1일때는 handleQuerys 라는 쓰레드를 시작하고 ID가 1000일 때는 adcValue1, adcValue2 라는 멤버변수의 값을 그래프로 그린다.

 

그래프를 그리는 속도에 맞춰 STM32에서도 ADC 값을 10ms 간격으로 송신하게끔 했는데 그러면 수신 데이터가 너무 많아져서 수신할 때마다 DB에 연결해 INSERT 하기에는 부하가 많이 걸린다. 그래서 수신 부분에서는 쿼리문으로 만들어 벡터에 쌓아두고 5초마다 트리거 되는 타이머를 사용해 그동안 쌓인 쿼리문을 DB와 한번 연결으로 한꺼번에 처리한다.

InsertQuerys() 함수는 CString 타입의 벡터를 참조자 타입으로 받고 아래와 같이 .size()만큼 쿼리문을 처리한 후 .clear()한다.

	for(int i=0; i< querys.size();i++)
	{
		query_stat = mysql_query(connection, querys.at(i));

		if (query_stat != 0)
		{
			fprintf(stderr, "Mysql query error : %s", mysql_error(&conn));
			return false;
		}
	}
	querys.clear();