/ ET-CETERA

CAN Protocol

CAN 개요

CAN은 Controller Area Network의 약자로 1986년에 독일의 메르세데스 벤츠사의 요구 (자동차내의 3개의 서로다른 ECU간의 데이터 통신)로 자동차 부품회사인 로베르트 보쉬사가 개발해 자동차기술자 협회에서 제안한 네트워크 시스템입니다.

ECU는 Electoric Control Unit의 약자로 자동차의 엔진, 자동변속기, ABS등의 상태를 컴퓨터로 제어하는 전자제어장치를 의미합니다. 이런 ECU들은 각 목적에 따라 차량안에 분산되어 위치하고 있습니다. 여러 센서들로부터 데이터를 ECU가 받아들이고 다른 ECU와 통신해서 차량의 모든 부분을 제어하는 역할을 담당하고 있습니다.

차량의 ECU 연결

현재 현대차 Genesis는 약 70개의 ECU가 탑제되어 있고 벤츠 S클래스와 BMW 7시리즈에는 80여개의 ECU, 렉서스 LS시리즈에는 약 100개의 ECU가 탑제되어 있습니다.

대표적인 ECU를 몇가지 살펴보면,

  • ACU(Airbag Control Unit) : 자동차 충돌 상황의 센서 신호를 받아 Airbag을 제어하는 ECU.
  • BCM(Body Control Module) : 자동차의 각종 경고, 도난방지 기능등을 제어하는 ECU.
  • ECU(Engine Control Unit) : 엔진의 상태를 모니터링하면서 연료량, 점화시기등의 기능을 제어하는 ECU.
  • TCU(Transmission Control Unit) : 자동차의 속도, 바퀴속도등의 값을 이용해 변속기를 제어하는 ECU.
  • ABS(Anti-lock Breaking System) : 자동차가 미끄러지지 않도록 브레이크의 on/off를 짧은시간동안 반복 제어하는 ECU.

시간상으로 보자면, 1986년에 CAN 1.0발표후에 1991년에 CAN 2.0이 발표되고 1992년에 메르세데스 벤츠에서 CAN을 채택한 자동차가 출시되게 됩니다. 그리고 그 다음해인 1993년에 ISO에 의해서 국제표준규격으로 채택됩니다.

자동차내부의 통신은 CAN이 유일할까요? 그렇지는 않습니다. 시간이 지나면서 여러가지 형태의 네트워크가 개발, 발표되고 있습니다. (FlexRay, LIN, CAN FD 등)

그럼 CAN은 ECU간의 통신을 어떤 방식으로 할까요?

CAN은 ECU간의 통신을 위해 직렬(Serial) 네트워크 통신방식을 이용합니다. 하나의 ECU가 시스템 안에 있는 다른 ECU에 대해 각각 입출력 단자를 갖는것이 아니라 CAN BUS에 대한 단일 CAN Interface만을 보유하고 있는 것이 특징입니다.

CAN버스를 이용한 연결

자동차안에는 많은 ECU가 탑제되어 있고 이런 ECU간의 데이터 공유를 통해서 자동차가 제어됩니다. 자동차는 환경적으로 고온, 충격, 진동 노이즈가 많은 환경인데 이런 열악한 환경에서도 CAN은 잘 견디기 때문에 속도가 느림에도 불구하고 주력 차량 네트워크 통신 표준으로 사용되고 있습니다.


CAN 프로토콜의 장점

  • CAN 프로토콜은 Multi Master 통신을 한다. CAN BUS를 공유하고 있는 MCU들은 모두가 Master의 역할을 수행할 수 있으며 BUS가 idle상태이면 언제든지 BUS를 사용하고 싶을 때 사용할 수 있습니다.
  • 노이즈에 매우 강하다. Twisted Pair Wire 2개를 사용하여 전기적 전압차를 이용한 통신을 하기 때문에 노이즈에 강합니다.
  • 표준 프로토콜. CAN은 표준 프로토콜이기 때문에 시장성을 확보할 수 있습니다.
  • 하드웨어적으로 오류보정이 가능. CRC가 하드웨어 적으로 생성되기 때문에 오류 검출이 가능하고 만약 오류가 검출되었을 때 하드웨어적으로 재전송합니다.
  • 다양한 방식의 통신. CAN은 수신필터를 이용하여 필터를 어떻게 설정하느냐에 따라 unicast, multicast, broadcast 통신을 할 수 있습니다.
  • ECU간의 우선순위가 존재. 각각의 ECU는 고유의 ID를 가지고 있는데 이 ID값이 낮을수록 우선순위가 높습니다. 이 우선순위를 이용하면 급한 Message를 먼저 처리할 수 있습니다.
  • CAN BUS 이용. 사용되는 전선의 양을 획기적으로 줄일 수 있습니다.

CAN 통신의 형태

CAN 프로토콜 환경 구성은 CAN ControllerCAN Transceiver로 구현되어있습니다. CAN Controller는 내부 버퍼를 가지며 CAN Transceiver의 수신 메시지에 대해 ID 값을 기반으로 유효 데이터인지를 판별하여 MCU로 전송하는 역할을 수행합니다.

CAN통신은 일반적으로 다음의 두 가지 형태로 사용된다.

  1. MCU(Micro Controller Unit) 내부에 CAN Controller가 존재(통합형)

통합형

  1. MCU(Micro Controller Unit) 외부에 CAN Controller가 존재(독립형)

독립형


CAN 프로토콜의 특징

CAN 통신은 여러 개의 ECU를 CAN BUS에 병렬로 연결하여 데이터를 주고 받는 방식으로 2가닥의 Twisted Pair Wire로 연결되어 있습니다.

또한 CAN BUS는 직렬통신 프로토콜을 사용하는데 직렬(Serial) 통신은 일반적으로 하나의 신호선을 이용하여 데이터를 주고받는 통신을 지칭합니다. 신호선이 하나이기 때문에 데이터를 일정한 시간간격으로 전송하게 되며 따라서 일정한 길이의 데이터를 보내기 위해서는 약간의 시간이 필요합니다.

데이터 전송에 약간의 시간이 필요하지만 적은 수의 신호선을 사용하기 때문에 비용을 절감할 수 있는 장점이 있습니다. 이때문에 많은 통신장비들이 직렬통신으로 데이터를 전송합니다. Serial 통신의 대표적인 예가 USB, PC의 COM port이죠.

이와 반대로 병렬통신은 여러 개의 신호선을 이용하여 데이터를 전송하는 방식이기 때문에 단위시간당 더 많은 데이터를 보낼 수 있지만 통신 거리가 길어질수록 많은 선들을 모두 연장해야 하므로 비용이 증가하게 되고 최신 단말 장치들이 소형화 추세에 있는데 현실적으로 통신을 하기위한 I/O단자의 크기는 크게 줄일 수 없습니다. 따라서 많은 I/O단자를 가지고 있는 병렬통신은 소형화된 기기에서 사용하기에 적합하지 않습니다.

CAN 통신은 주소를 이용한 통신이 아닌 ID를 이용한 통신으로 이 ID의 값에 따라 통신의 우선순위가 결정됩니다.

각 ECU들은 BUS상에서 메시지를 주고 받기 때문에 기존의 ECU들은 새롭게 추가된 ECU에 대한 정보를 Update할 필요가 없습니다.

CAN은 다중 통신망(Multi Master Network)이고 CSMA/CD(Carrier Sense Multiple Access/Collision Detection)방식을 이용합니다. 즉, Ethernet 통신방식과 유사하다고 생각하시면 됩니다.

각 ECU는 메시지를 보내기 이전에 CAN BUS가 사용중인지를 파악(Carrier Sense)합니다. 메시지를 보낼 수 있을 때 메시지를 보내며 수신측에서 CRC를 이용해 충돌이 발생했는지를 감지하고 만약 충돌이 발생하면 재전송하게됩니다.

보낸 메시지는 송신측이나 수신측의 주소를 포함하지 않습니다. 대신 메시지안에는 ID(11bit 혹은 29bit)가 포함되어 있습니다. CAN 메시지는 크게 ID Field와 Data Field를 가지고 있는데 ID Field는 ECU의 ID값이 될 수 있지만 “엔진 회전수”와 같은 것을 나타내는 ID값이 될 수 도 있습니다. 이때 Data Field는 2000 rpm과 같은 값을 가지고 있게 되는 것이죠. 이런 메시지인 경우 여러 ECU에서 해당 message를 수신해야 하는데 각 ECU는 ID MASK(Filter)를 이용하여 자신이 받아들일 수 있는 메시지를 정해서 받습니다. ECU가 데이터를 전달 받으면 Receiver는 Ack를 신호를 생성하여 보내게 됩니다.

CAN 네트워크상의 모든 ECU는 네트워크 상의 메시지를 수신한 후 ID를 이용하여 자신에게 필요한 메시지인지를 식별한 후 자신이 필요한 경우만 메시지를 취하고 그렇지 않으면 해당 메시지는 무시합니다.

CAN BUS상에 흐르는 여러 ECU의 데이터들이 동시에 하나의 ECU로 유입되는 경우 식별자(ID)의 숫자를 비교하여 우선순위가 높은 메시지를 먼저 취합니다.

메시지를 보낼 경우 마찬가지로 우선 순위가 높은 메시지가 CAN BUS의 사용권한을 보장 받으며 낮은 순위의 메시지는 다음 BUS cycle에 전송하게 됩니다.


CAN 프레임 구성

CAN 통신은 프레임이라고 하는 패킷(packet)으로 데이터를 전송합니다. 프레임이란 하나의 메시지를 이루는 필드 또는 bit들의 집합을 말하며 CAN 프레임은 다음과 같은 분할 구역(section)으로 구성되어 있습니다.

CAN Frame 구조

  • SOF(Start Of Frame) : 메시지의 시작을 의미하는 비트로 버스의 노드(node)를 동기화하기 위해 사용됩니다.
  • Identifier(ID) : 식별자로서 메시지의 내용을 식별하고 메시지의 우선순위를 부여합니다. CAN 메시지에 있는 ID의 길이에 따라서 표준 CAN과 확장 CAN 두 가지로 구분됩니다. 표준 CAN은 11 비트 식별자이고, 확장 CAN은 29비트 식별자로 구분됩니다.
  • Control : 데이터의 길이(DLC)를 의미합니다.
  • Data : 특정한 노드에서 다른 노드로 전송하는 데이터 포함하며 8Bytes까지 사용할 수 있습니다.
  • CRC : 프레임의 송신 오류 및 오류 검출에 사용됩니다.
  • ACK : 오류가 없는 메시지가 전송되었다는 것을 의미하는 비트로서, CAN 제어기는 메시지를 정확하게 수신했다면 ACK(Acknowledgement) 비트를 전송합니다. 전송 노드는 버스 상에서 ACK 비트의 유무를 확인하고 만약 ACK 비트가 발견되지 않는다면 재전송을 시도합니다. 임의의 노드에서 올바른 메시지를 수신하게 되면 ACK 필드를 받는 순간 ACK 슬롯의 값을 ‘d’로 설정해 Bus 상에서 계속 전송하게 됩니다.
  • EOF(End of Frame) : 프레임의 끝을 나타내고 종료를 의미합니다.

CAN의 활용

CAN 통신은 자동차 분야뿐만 아니라 자동화 기기, 의료 기기 그리고 로봇 등 다양한 분야에서도 널리 쓰이고 있기 때문에 앞으로도 매우 중요하게 쓰일 통신방법이 될 것입니다.

두 개의 컴퓨터를 CAN 장비를 이용해서 Serial 연결 한 후 CANPro Analyzer를 이용하여 packet을 주고 받는 실습을 진행한 후 Java Program과 Arduino를 결합하여 실습을 진행합니다.

CANPro 장비 연결

  1. Windows 10은 디지털 서명 해제 작업을 먼저 선행해야 한다. 디지털 서명 해제 for Windows10

  2. 드라이버를 설치하지 않고 CANPro 장비의 USB를 PC에 연결하고 제어판 > 하드웨어 및 소리 > 장치관리자를 확인하면 기타장치 > CANPro Analyzer가 노란색 느낌표가 표시된 상태로 등록된것을 볼 수 있다.

  3. 제공된 드라이버 폴더 RealSYS_USB_Device_Driver(20170316)안의 dp-chooser.exe를 관리자 모드로 실행해서 개발툴용 USB Device Driver를 설치한다. 설치가 완료되면 노란색 느낌표가 있던 기타장치는 사라지고 장치관리자 > 포트 부분에 CANPro가 등록된걸 확인할 수 있다. (혹은 USB Serial Port로 등록) 정상적으로 설치가 되었으면 리부팅.

  4. 드라이버에 설치하면 제공된 CANPro Analyzer for Windows10을 설치한다.

  5. 총 3대의 컴퓨터(데스크탑 PC + 노트북 + 라떼판다)에 각각 CAN 장비(Controller + Transceiver)를 연결합니다. CAN BUS가 존재하지 않기 때문에 3대를 연결하기 위해서는 아래 그림처럼 선을 겹쳐서 연결해야 합니다.

CAN 연결

CANPro 장비 설정

  • 노트북을 기준으로 설명하면 CANPro Analyzer를 실행시킨 후 동작 > CANPro 환경 설정 쓰기를 실행합니다.

CAN 환경설정-1

  • 아래 그림과 같이 CANPro 장비 설정을 바꿉니다. PC통신 인터페이스는 Serial 통신으로 설정해야 하고 적절한 Serial 통신 포트를 설정해야 합니다. Serial 통신 속도부분과 프로토콜, CAN BPS부분은 다른 PC의 설정들과 동일하게 설정해야 합니다. 수신 ID는 자신의 수신 ID를 설정하는 부분입니다. 다른 장비과 구분되게 설정합니다. 설정버튼을 누르면 현재 설정대로 CAN장비의 설정을 변경합니다.

CAN 환경설정-2

  • 동작 > CANPro 환경 설정 읽기를 실행해서 설정한 내용을 읽어와서 정상적으로 설정이 됬는지 확인합니다. 데이터를 받기 위해서는 동작 > CAN 데이터 수신 시작을 실행하여 데이터 수신을 시작해야 다른 CAN장비로부터 들어오는 packet을 받을 수 있습니다.

CAN 환경설정-3

  • 데스크탑 PC와 노트북을 CAN장비로 연결한 후 packet을 보내고 받는 영상입니다. 수신 MASK ID를 설정하지 않았기 때문에 모든 packet을 다 받습니다.
  • CAN Message 수신시 특정한 ID 또는 어떤 범위의 ID만 받고자 할때 MASK를 이용합니다. Mask는 Filter의 사용여부(Enable/Disable)를 결정합니다. 즉, 실제 Message ID가 CAN Rx에 수신 되었을 때, Filter의 각 bit값이 비교 되어 같은지 다른지 판단하여 수용하게 됩니다. 이때 각 Filter bit값이 비교 되기 위해서는 이에 대응되는 Mask bit가 Enable,즉 1이 되어야 합니다. 따라서 0일때는 Filter bit값에 상관없이 모두 수용하게 되는 것입니다. 이 부분은 실제 실습을 통해서 확인해야 할 듯 합니다.

Java 프로그램을 이용한 CAN 활용

CANPro Analyzer를 이용하여 CANPro 환경설정과 데이터의 이동, MASK를 이용한 멀티캐스트를 이해했다면 이제 Java 코드를 이용하여 CANPro를 통해 데이터를 주고 받아보겠습니다. 즉, Analyzer프로그램 대신 Java Program을 이용하겠다는 의미입니다.

CANPro 통신 프로토콜 사용자 메뉴얼을 참고하여 프로그램을 작성해야 합니다.

  • DataFrame Sender (Using JavaFX)
public class Exam01_DataFrameSender extends Application{

	TextArea textarea;    // 메시지창 
	Button connBtn, sendBtn;    // 포트 연결 버튼, Data Frame 보내기 버튼
	
	private CommPortIdentifier portIdentifier;
	private CommPort commPort;
	private SerialPort serialPort;
	
	private OutputStream out;
	
	// TextArea에 문자열 출력하기 위한 method
	private void printMSG(String msg) {
		Platform.runLater(()->{
			textarea.appendText(msg + "\n");
		});
	}
	
	private void connectPort(String portName) {
		try {
			portIdentifier = CommPortIdentifier.getPortIdentifier(portName);
			printMSG(portName + "에 연결을 시도합니다!!");
			
			if(portIdentifier.isCurrentlyOwned()) {
				printMSG(portName + "가 현재 다른 프로그램에 의해 사용중입니다. ");				
			} else {
				// 포트를 열고 포트객체를 얻는다.
				// open()의 첫번째 인자 : Name of application making this call.
				// open()의 두번째 인자 : Time in milliseconds to block waiting for port open.
				commPort = portIdentifier.open(this.getClass().getName(), 5000);
				if( commPort instanceof SerialPort ) {
					serialPort = (SerialPort) commPort;
					serialPort.setSerialPortParams(9600, // 통신속도
							SerialPort.DATABITS_8, // 데이터 비트 
							SerialPort.STOPBITS_1, // Stop 비트 
							SerialPort.PARITY_NONE);  // Parity 비트
					// OutputStream 생성
					out = serialPort.getOutputStream(); 
					printMSG("성공적으로 " + portName + "에 접속되었습니다.!!");
				} else {
					printMSG("Serial Port만 사용할 수 있습니다.!!");	
				}
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	private void sendDataFrame(String msg) {
		// String msg = "W" + "28" + "10003B01" + "0000000000005011";
		// 데이터프레임 전송
		// CAN 네트워크상에 특정 CAN Message를 보내고자 할 때
		// 시작문자 => 1문자 => ":" 이용
		// 명령코드 => 1문자 => "W" (전송)
		// 송신데이터 특성코드 => 2문자
		//                   5번째 bit가 0이면 CAN2.0A, 1이면 CAN2.0B를 나타냄
		//                   4번째 bit가 0이면 Data Frame을 지칭, 1이면 Remote Frame
		//                   3번째~0번째는 송신데이터의 길이(0부터 8까지의 값을 가지게 된다.)
		//                   예) 28 => 00101000 => CAN2.0B이면서 데이터 길이가 8인
		//                             Data Frame을 의미.
		// CAN 송신 ID => 4문자(11bit사용시) 혹은 8문자(29bit사용시)
		//               16진수로 표현하고 4bit가 1문자로 표현되기때문에 29bit를 표현하기 위해서는
		//               8개의 16진수가 필요하고 문자로 표현.
		//               예) 041C0800 => 0번부터 28번 비트중에
		//                            => 26번, 20번, 19번, 18번, 11번 bit가 check
		// CAN 송신 Data => 위에서 정의한 송신데이터 특성 코드 중 송신데이터의 길이에 따라
		//              => 0 ~ 16문자를 표현
		//              => 예) 데이터 프레임 개수(송신데이터의 길이)가 8이면 ASCII형식으로는
		//                     8개의 문자로 Hex표현이면 16개의 문자로 표현
		// Checksum => 2문자(수식에 의한 Checksum계산)                       
		// 끝 문자 => 1문자 ( "\r" 사용 )
		
		msg = msg.toUpperCase();
		char c[] = msg.toCharArray();
		int checksumData = 0;
		for (char cc : c) {
			checksumData += cc;
		}
		checksumData = (checksumData & 0xFF);
		String sendMsg = ":" + msg + 
				Integer.toHexString(checksumData).toUpperCase() + "\r";
		
		printMSG("생성된 전송 메시지 : " + sendMsg);
		
		// 전송할 byte배열 생성
		byte[] inputData = sendMsg.getBytes();
		try {
			out.write(inputData);
			//out.flush();
			printMSG("성공적으로 전송되었습니다.!!");
		} catch (IOException e1) {
			e1.printStackTrace();
		}					
	}
	
	@Override
	public void start(Stage primaryStage) throws Exception {

		BorderPane root = new BorderPane();
		root.setPrefSize(700, 500);
		
		textarea = new TextArea();
		root.setCenter(textarea);
			
		connBtn = new Button("COM 포트 접속");
		connBtn.setPrefSize(200, 50);
		connBtn.setPadding(new Insets(10));
		connBtn.setOnAction(e->{
			String portName = "COM16";
			// 포트 접속
			connectPort(portName);
		});
		
		sendBtn = new Button("Data Frame 전송");
		sendBtn.setPrefSize(200, 50);
		sendBtn.setPadding(new Insets(10));
		sendBtn.setOnAction(e->{
			// Data Frame 전송
			String msg = "W" + "28" + "10003B01" + "0000000000005011";
			sendDataFrame(msg);			
		});				
		
		FlowPane flowPane = new FlowPane();
		flowPane.setPrefSize(700, 50);
		flowPane.setHgap(10);
		flowPane.getChildren().add(connBtn);
		flowPane.getChildren().add(sendBtn);
		
		root.setBottom(flowPane);
		
		Scene scene = new Scene(root);
		primaryStage.setScene(scene);
		primaryStage.setTitle("CAN DataFrame Send Test");
		primaryStage.setOnCloseRequest(e->{
			System.exit(0);
		});
		primaryStage.show();
		
	}
	 
	public static void main(String[] args) {
		launch();
	}		
}
  • DataFrame Receiver & 환경설정
public class Exam02_DataFrameReceiver extends Application {

	TextArea textarea;
	// 포트접속버튼, 환경쓰기버튼, 데이터수신가능버튼, 데이터수신가능버튼
	Button connBtn, envBtn, revEnableBtn, revDisableBtn;
		
	private CommPortIdentifier portIdentifier;
	private CommPort commPort;
	private SerialPort serialPort;
	
	private BufferedInputStream bin;
	private OutputStream out;
	
	
	class MyPortListener implements SerialPortEventListener {
		@Override
		public void serialEvent(SerialPortEvent event) {
			if (event.getEventType() == SerialPortEvent.DATA_AVAILABLE) {
				byte[] readBuffer = new byte[128];
				try { 
					while (bin.available() > 0) {
						bin.read(readBuffer);
					}
					String revData = new String(readBuffer);
					printMSG("Receive Low Data:" + revData);	
					if(revData.trim().equals(":G01A8")) {
						printMSG("지금부터 데이터 수신 가능합니다.");
					}
					if(revData.trim().equals(":G00A7")) {
						printMSG("지금부터 데이터 수신이 불가능합니다.");
					}	
					// CAN 수신 데이터 읽기 
					// U2800000001111100000000000044
					// 시작문자 => :
					// 명령코드 => U
					// 수신데이터 특성코드 : 0010 1000 => 28
					// CAN 수신 ID : 00000001
					// CAN 수신 데이터 : 1111000000000000
					// checksum : 44
					if(revData.charAt(1) == 'U') {
						printMSG("데이터를 수신했습니다.");
					}
				} catch (Exception e) {
					e.printStackTrace();
				}				
			}		
		}		
	}
	
	// TextArea에 문자열 출력하기 위한 method
	private void printMSG(String msg) {
		Platform.runLater(()->{
			textarea.appendText(msg + "\n");
		});
	}
	
	private void connectPort(String portName) {
		try {
			portIdentifier = CommPortIdentifier.getPortIdentifier(portName);
			printMSG(portName + "에 연결을 시도합니다!!");
			
			if(portIdentifier.isCurrentlyOwned()) {
				printMSG(portName + "가 현재 다른 프로그램에 의해 사용중입니다. ");				
			} else {
				// 포트를 열고 포트객체를 얻는다.
				// open()의 첫번째 인자 : Name of application making this call.
				// open()의 두번째 인자 : Time in milliseconds to block waiting for port open.
				commPort = portIdentifier.open(this.getClass().getName(), 5000);
				if( commPort instanceof SerialPort ) {
					serialPort = (SerialPort) commPort;
					serialPort.setSerialPortParams(921600, // 통신속도
							SerialPort.DATABITS_8, // 데이터 비트 
							SerialPort.STOPBITS_1, // Stop 비트 
							SerialPort.PARITY_NONE);  // Parity 비트
			        printMSG(portName + "에 이벤트 리스너가 등록되었습니다.");
					// Stream 생성
					bin = new BufferedInputStream(serialPort.getInputStream());
					out = serialPort.getOutputStream(); 
					printMSG("성공적으로 " + portName + "에 접속되었습니다.!!");
				    serialPort.addEventListener(new MyPortListener());
				    serialPort.notifyOnDataAvailable(true);
				} else {
					printMSG("Serial Port만 사용할 수 있습니다.!!");	
				}
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	
	@Override
	public void start(Stage primaryStage) throws Exception {

		BorderPane root = new BorderPane();
		root.setPrefSize(850, 500);
		
		textarea = new TextArea();
		root.setCenter(textarea);
		
		connBtn = new Button("Serial 포트 접속");
		connBtn.setPrefSize(200, 50);
		connBtn.setPadding(new Insets(10));
		connBtn.setOnAction(e->{
			String portName = "COM16";
			connectPort(portName);
		});				
		
		revEnableBtn = new Button("데이터수신가능");
		revEnableBtn.setPrefSize(200, 50);
		revEnableBtn.setPadding(new Insets(10));
		revEnableBtn.setOnAction(e->{
			// CAN 데이터 수신 여부 설정
			// 시작문자 => 1문자 => ":" 이용
			// 명령코드 => 1문자 => "G"
			// 수신 여부 명령 코드 => 2문자
			//                   00 : 현재 CANPro의 CAN 데이터 수신 여부 환경을 읽어온다.
			//                   10 : CANPro의 CAN 데이터 수신 동작을 중지한다.
			//                   11 : CANPro의 CAN 데이터 수신 동작을 시작한다.
			// Check Sum => 2문자
			String str = "G11";
			String tmpStr = str.toUpperCase();
			char c[] = tmpStr.toCharArray();
			int checksumData = 0;
			for (char cc : c) {
				checksumData += cc;
			}
			checksumData = (checksumData & 0xFF);
			String checksumHexString = Integer.toHexString(checksumData).toUpperCase();
			//printMSG("checksum계산값 : " + checksumHexString);
			
			// 끝문자 => 1문자 => "\r" 이용			
			// String msg = ":G11A9\r";
			String msg = ":" + str + checksumHexString + "\r";
			
			try {
				byte[] inputData = msg.getBytes();
				out.write(inputData);
				// 정상응답이 올경우
				// ":G01A8"   => 00의 의미는 현재 CAN 데이터 수신 동작이 중지.
				//            => 01의 의미는 현재 CAN 데이터 수신 동작이 시작.
				//            => A8의 의미는 G01에 대한 checksum         
				
			} catch (IOException e1) {
				e1.printStackTrace();
			}
		});				
		
		revDisableBtn = new Button("데이터수신중지");
		revDisableBtn.setPrefSize(200, 50);
		revDisableBtn.setPadding(new Insets(10));
		revDisableBtn.setOnAction(e->{
			// CAN 데이터 수신 여부 설정
			// 시작문자 => 1문자 => ":" 이용
			// 명령코드 => 1문자 => "G"
			// 수신 여부 명령 코드 => 2문자
			//                   00 : 현재 CANPro의 CAN 데이터 수신 여부 환경을 읽어온다.
			//                   10 : CANPro의 CAN 데이터 수신 동작을 중지한다.
			//                   11 : CANPro의 CAN 데이터 수신 동작을 시작한다.
			// Check Sum => 2문자
			String str = "G10";
			String tmpStr = str.toUpperCase();
			char c[] = tmpStr.toCharArray();
			int checksumData = 0;
			for (char cc : c) {
				checksumData += cc;
			}
			checksumData = (checksumData & 0xFF);
			String checksumHexString = Integer.toHexString(checksumData).toUpperCase();
			//printMSG("checksum계산값 : " + checksumHexString);
			
			// 끝문자 => 1문자 => "\r" 이용			
			// String msg = ":G10A9\r";
			String msg = ":" + str + checksumHexString + "\r";
			
			try {
				byte[] inputData = msg.getBytes();
				out.write(inputData);
				// 정상응답이 올경우
				// ":G00A7"   => 00의 의미는 현재 CAN 데이터 수신 동작이 중지.
				//            => 01의 의미는 현재 CAN 데이터 수신 동작이 시작.
				//            => A7의 의미는 G00에 대한 checksum         
			} catch (IOException e1) {
				e1.printStackTrace();
			}
		});				
		
		envBtn = new Button("환경설정");
		envBtn.setPrefSize(200, 50);
		envBtn.setPadding(new Insets(10));
		envBtn.setOnAction(e->{
			// CAN 데이터 수신 여부 설정
			// 시작문자 => 1문자 => ":" 이용
			// 명령코드 => 1문자 => "Z"
			// 자세한 설정내용은 CANPro User Manual 참조
			// CAN bit time에 대한 내용은 User Manual 3page
			// 250k => 0x0F, 0x34로 지정되어 있음.
			// https://www.mathsisfun.com/binary-decimal-hexadecimal-converter.html
			// 해당사이트를 이용하면 4bit의 값을 hexa로 표현 가능.
			// String str = "Z 1C 0F34 00000001 00000001";
			String str = "Z1C0F340000000100000001";
			String tmpStr = str.toUpperCase();
			char c[] = tmpStr.toCharArray();
			int checksumData = 0;
			for (char cc : c) {
				checksumData += cc;
			}
			checksumData = (checksumData & 0xFF);
			String checksumHexString = Integer.toHexString(checksumData).toUpperCase();
			//printMSG("checksum계산값 : " + checksumHexString);
			
			// 끝문자 => 1문자 => "\r" 이용			
			String msg = ":" + str + checksumHexString + "\r";
			
			try {
				byte[] inputData = msg.getBytes();
				out.write(inputData);
			} catch (IOException e1) {
				e1.printStackTrace();
			}
		});				
		
		FlowPane flowPane = new FlowPane();
		flowPane.setPrefSize(850, 50);
		flowPane.setHgap(10);
		flowPane.getChildren().add(connBtn);
		flowPane.getChildren().add(revEnableBtn);
		flowPane.getChildren().add(revDisableBtn);
		flowPane.getChildren().add(envBtn);
		
		root.setBottom(flowPane);
		
		Scene scene = new Scene(root);
		primaryStage.setScene(scene);
		primaryStage.setTitle("CAN DataFrame Receive Test");
		primaryStage.setOnCloseRequest(e->{
			System.exit(0);
		});
		primaryStage.show();
		
	}
	 
	public static void main(String[] args) {
		launch();
	}	
	
}

End.