diff --git a/README.md b/README.md index 96e0701..b221c43 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,552 @@ ![](./images/arch.png) +## 라즈베리 파이 내부 코드 구성 + +### 자동차 데이터 수집 + +자동차에 있는 OBD 단자를 통해 CAN 신호를 전달받는다. 이 때 CAN 신호를 `pican` 이라는 보드를 통하여 라즈베리파이가 +읽을 수 있는 신호로 변환, `python-can`라이브러리를 통하여 자동차와 OBD(can)통신을 연결함. + +![](./study/CAN/images/4.png) + +자가 진단 점검 단자의 3, 4, 11 핀을 활용하여 연결 + +![](./study/CAN/images/5.png) + +OBD-2 케이블의 반대편을 잘라서, 필요한 pin(3, 4, 11)에 대한 선만 `pican` 보드에 연결 + +![](./images/IMG_4837.jpg) + +![](./images/IMG_4836.jpg) + +### 데이터 수집을 위한 CAN 통신(OBD) 코드 작성 + +OBD 프로토콜에 맞춰, 차량의 실시간 데이터를 요청하는(쿼리) 코드를 작성해야함. + +#### 쿼리 +![](./images/obd_query.png) + +#### 응답 + +![](./images/obd_res.png) + +#### 실제 구현한 코드 + +따라서 사용 가능한 OBD PID를 분리하고, 이 데이터를 수집하기 위한 파이썬 코드를 작성하였다. +(/greengrass/canPlugin/can_plugin.py 및 /greengrass/canPlugin/canutil.py) + +- canutil.py + +```python3 +from enum import Enum +import can + +DATA_TYPE_INDEX = 2 + + +class NotSupportedDataTypeException(Exception): + def __init__(self): + super().__init__("지원하지 않는 데이터 타입입니다.") + + +class CanDataType(Enum): + """ + OBD2 PIDS : https://en.wikipedia.org/wiki/OBD-II_PIDs + + 차량 제조사별 확장 PID 및 다른 PID가 존재할 수 있다. 확인 필요 + """ + ENGINE_LOAD = 0x04 + SHORT_FUEL_TRIM_BANK = 0x06 + LONG_FUEL_TRIM_BANK = 0x07 + INTAKE_MANIFOLD_ABSOLUTE_PRESSURE = 0x0b + ENGINE_RPM = 0x0C + VEHICLE_SPEED = 0x0d + INTAKE_AIR_TEMPERATURE = 0x0F + MAF_SENSOR = 0x10 + THROTTLE = 0x11 + ENGINE_RUNTIME = 0x1F + TRAVELED_DISTANCE = 0x31 # since Error code cleared ! + FUEL_TANK_LEVEL = 0x2F + OXYGEN_SENSOR = 0x34 + AMBIENT_AIR_TEMPERATURE = 0x46 + SHORT_TERM_FUEL_EFFICIENCY = 0x00 # this is dummy + AVERAGE_FUEL_EFFICIENCY = 0x00 # this is dummy + + # Request & Response + PID_REQUEST = 0x7DF + PID_REPLY = 0x7E8 + + # PID MODES + SHOW_CURRENT_DATA = 0x01 + SHOW_FREEZE_FRAME_DATA = 0x02 + SHOW_TROUBLE_CODES = 0x03 + + +class CanRequestMessage: + # TODO: change this class as singleton + def __str__(self) -> str: + return "{}".format(self.message) + + def __init__(self, data_type) -> None: + self.data_type = data_type # ENUM + self.message = [0x02, + CanDataType.SHOW_CURRENT_DATA.value, + self.data_type.value, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00] + + def get_type(self): + return self.data_type.name + + +class CanDataConvert: + def __init__(self): + pass + + @staticmethod + def convert(recv_msg) -> int: + data_type = recv_msg.data[DATA_TYPE_INDEX] + try: + handler = getattr(CalculateData, CanDataType(data_type).name.lower()) + return handler(recv_msg.data) + except AttributeError: + raise NotSupportedDataTypeException + except Exception as e: + print('error occur : ', e) + + +# cal fef https://www.sciencedirect.com/science/article/pii/S2352484719308649 +# https://stackoverflow.com/questions/44794181/fuel-consumption-and-mileage-from-obd2-port-parameters +class CalculateData: + maf = 0 + speed = 0 + present_fuel_efficiency = 0 + average_fuel_efficiency = 0 + count = 0 + avg_buf = 0 + + def __init__(self): + pass + + @classmethod + def average_fuel_efficiency(cls, recv_msg) -> float: + cls.avg_buf += cls.present_fuel_efficiency + cls.count += 1 + cls.average_fuel_efficiency = round(avg_buf / count, 2) + return cls.average_fuel_efficiency + + @classmethod + def short_term_fuel_efficiency(cls, recv_msg) -> float: + cls.present_fuel_efficiency = round(cls.speed * (1 / 3600) * (1 / cls.maf) * 14.7 * 710, 2) + return cls.present_fuel_efficiency + + @staticmethod + def oxygen_sensor(recv_msg) -> float: + # we can make two data. first one is ratio, the other is mA + return round((256 * recv_msg[3] + recv_msg[4]) * (2 / 65536), 2) + + @staticmethod + def intake_manifold_absolute_pressure(recv_msg) -> int: + return recv_msg[3] + + @classmethod + def maf_sensor(cls, recv_msg) -> float: + cal = round((recv_msg[3] * 256 + recv_msg[4]) / 100, 2) + cls.maf = cal + return cal + + @staticmethod + def engine_load(recv_msg) -> float: + return round((100 / 255) * recv_msg[3], 2) + + @staticmethod + def engine_rpm(recv_msg) -> float: + return round(((recv_msg[3] * 256) + recv_msg[4]) / 4, 2) + + @classmethod + def vehicle_speed(cls, recv_msg) -> int: + cls.speed = recv_msg[3] + return recv_msg[3] + + @staticmethod + def throttle(recv_msg) -> float: + return round((recv_msg[3] * 100) / 255, 2) + + @staticmethod + def short_fuel_trim_bank(recv_msg) -> float: + return round(((100 / 128) * recv_msg[3]) - 100, 2) + + @staticmethod + def long_fuel_trim_bank(recv_msg) -> float: + return round(((100 / 128) * recv_msg[3]) - 100, 2) + + @staticmethod + def short_fuel_trim_bank(recv_msg) -> float: + return round(((100 / 128) * recv_msg[3]) - 100, 2) + + @staticmethod + def intake_air_temperature(recv_msg) -> int: + return recv_msg[3] - 40 + + @staticmethod + def throttle_position(recv_msg) -> float: + return round((100 / 256) * recv_msg[3], 2) + + @staticmethod + def engine_runtime(recv_msg) -> int: + return 256 * recv_msg[3] + recv_msg[4] + + @staticmethod + def traveled_distance(recv_msg) -> int: + return 256 * recv_msg[3] + recv_msg[4] + + @staticmethod + def fuel_tank_level(recv_msg) -> float: + return round((100 / 255) * recv_msg[3], 2) + + @staticmethod + def ambient_air_temperature(recv_msg) -> int: + return recv_msg[3] - 40 + + +``` + +`CanDataType` 이라는 Enum 클래스를 생성하여 각 PID를 관리하였고, 각 데이터 별로 응답에 대한 변환식이 다르기 때문에 +변환을 담당할 클래스또한 작성하였다. + +`CanDataConvert` 클래스는 각 PID에 맞게 적절한 변환식을 호출하기 위해서 응답에 맞는 `handler`를 생성하고 호출하는 +형태로 구현하였다. + +- can_plugin.py + +```python3 +import can +import os +import subprocess +from libs import util +from libs.plugin import run_plugin_thread +from collections import deque +from time import sleep +from canutil import CanDataType, CanRequestMessage, CanDataConvert +from libs.base_plugin import BasePlugin +from typing import List + +TOPIC = util.get_ipc_topic() + +TEST_FIELDS = [ + 'engine_load', + 'engine_rpm', + 'intake_manifold_absolute_pressure', + 'vehicle_speed', + 'throttle', + 'short_fuel_trim_bank', + 'engine_runtime', + 'traveled_distance', + 'fuel_tank_level', + 'ambient_air_temperature', + 'maf_sensor', + 'oxygen_sensor', + 'short_term_fuel_efficiency', + 'average_fuel_efficiency' +] + +OPTION = { + 'channel': 'can0', + 'busType': 'socketcan_native' +} + +INIT_COMMAND = '/sbin/ip link set can0 up type can bitrate 500000' + + +class SocketCanInitFailedException(Exception): + def __init__(self): + super().__init__('소켓 캔 초기화 패') + + +class CanPlugin(BasePlugin): + # TODO: inherite base class and refactor below methods ! + def __init__(self, fields: List[str], option=None) -> None: + super().__init__(fields, option=option) + self.data_list = [ + x.upper() for x in fields + ] + self.enum_list = [ + getattr(CanDataType, x) for x in self.data_list + ] + self.req_messages_for_data = [ + getattr(CanRequestMessage(x), 'message') for x in self.enum_list + ] + self.data_len = len(self.data_list) + self.recv_buffer = deque() + self.return_buffer = deque() + self._channel = option.get('channel', 'can0') + self._bus_type = option.get('busType', 'socketcan_native') + self._init_can() + self.bus = can.interface.Bus(channel=self._channel, bustype=self._bus_type) + + def _init_can(self) -> None: + os.system(INIT_COMMAND) + print('socket can init complete') + + # TODO: + # 1. set return_buffer & recv_buffer with property + + def _send_request(self): + self.return_buffer.clear() + print("버퍼 초기화") + + def is_valid_reply(message) -> bool: + if message.arbitration_id != CanDataType.PID_REPLY.value: + return False + else: + return True + + for message in self.req_messages_for_data: + msg = can.Message(arbitration_id=CanDataType.PID_REQUEST.value, data=message, extended_id=False) + # print("this is will send can msg " , msg) + self.bus.send(msg) + sleep(0.01) + while True: + recv_data = self.bus.recv() + if is_valid_reply(recv_data): + self.recv_buffer.append(recv_data) + break + sleep(0.01) + continue + while len(self.recv_buffer) < self.data_len: + recv_data = self.bus.recv() + if is_valid_reply(recv_data): + self.recv_buffer.append(recv_data) + while self.recv_buffer: + self.return_buffer.append( + CanDataConvert.convert( + self.recv_buffer.popleft() + ) + ) + + return self.return_buffer + + def collect_data(self) -> None: + try: + self.data = list(self._send_request()) + except Exception as e: + print('error occured when collect data ', e) + + # TODO : Calculate Fuel Efficiency before relay. + def cal_fuel_efficiency(self): + """ + https://stackoverflow.com/questions/44794181/fuel-consumption-and-mileage-from-obd2-port-parameters + """ + maf = self.data[self.data_list.index('MAF_SENSOR')] + speed = self.data[self.data_list.index('VEHICLE_SPEED')] + + fuel_efficiency = speed * (1/3600) * (1/maf) * 14.7 * 710 + + return fuel_efficiency + + +def handler(event, context) -> None: + pass + + +try: + cp = CanPlugin(TEST_FIELDS, OPTION) +except Exception as e: + print('failed to make can plugin :', e) + + +def run(): + run_plugin_thread(cp.entry) + + +if __name__ == '__main__': + pass +else: + run() + +``` + +필요한 데이터를 호출하고, 응답하는 과정에서 우리가 원하는 데이터가 아닌경우 버리는 로직을 구성하였다. + +데이터를 호출하는 코드를 한번 수행하고, 원하는 응답이 올 때 까지 기다리는 로직이 존재하며, 이 데이터들을 `deque`로 관리하여 +데이터를 수집/저장 하는 과정에서의 불필요한 시간복잡도를 줄이고자 노력했다. (큰 의미는 없는 것 같다.) + +### 수집한 데이터 송신 부분(실시간 앱 / mqtt / S3 등) + +이렇게 수집된 데이터들은 모두 `binder`라고 불리는 프로세스로 넘겨지게 되며, binder 의경우 미리 설정되어있는 `dispatcher`들로 +데이터를 전달한다. + +즉 각 데이터들은 `dispatch` 되어서 각자의 용도에 맞는 형태로 변형되고 저장되고 송신된다. + +![](./images/dispatchers.png) + +`storage dispatcher` 의 경우. 라즈베리파이 로컬에 csv파일을 저장할 뿐 아니라, S3에도 csv파일을 저장하는 역할을 한다. + +[https://github.com/kimsehwan96/car-iot-platform-from-kpu/blob/master/greengrass/binder/dispatcher/storage_dispatcher.py](https://github.com/kimsehwan96/car-iot-platform-from-kpu/blob/master/greengrass/binder/dispatcher/storage_dispatcher.py). + +`websocket dispatcher`의 경우 실시간 리액트 앱에 socketio를 통해 데이터를 전달하는 역할을 한다. + +[https://github.com/kimsehwan96/car-iot-platform-from-kpu/blob/master/greengrass/binder/dispatcher/websocket_dispatcher.py](https://github.com/kimsehwan96/car-iot-platform-from-kpu/blob/master/greengrass/binder/dispatcher/websocket_dispatcher.py) + + +## 실시간 리액트 앱 구성 + +실시간 리액트앱은 [https://github.com/kwhong95/kpu_sp_rt_dashboard](https://github.com/kwhong95/kpu_sp_rt_dashboard) 에서 구현중이다. + +### 데이터 수집 / 가공 프론트 코드 + +```jsx +import { createContext, useState, useEffect, useContext } from "react"; +import title from '../libs/dataTitle.json'; +import unit from '../libs/dataUnit.json'; +import io from 'socket.io-client'; + +const URL = 'http://localhost:5000/binder' +const socket = io(URL) + +const RealtimeDataContext = createContext([]); + +const RealtimeDataProvider = ({ children }) => { + const [ payloads, setPayloads ] = useState([]); + + return ( + + {children} + + ); +}; + +function useRealtimeData () { + const context = useContext(RealtimeDataContext); + const [payloads, setPayloads] = context; + const [temp, setTemp] = useState({}); + const [drivingData, setDrivingData] = useState([]); + const [fuel, setFuel] = useState([]); + const [realtimeFuelEfficiency, setRealtimeFuelEfficiency] = useState({}); + const [speed, setSpeed] = useState({}); + const [RPM, setRPM] = useState({}); + const [ResidualFuel, setResidualFuel] = useState({}); + + useEffect(() => { + socket.on('rtdata', data => { + const jsonData = JSON.parse(data); + const result = jsonData.fields.map( + (field, idx) => Object({ + id: field, + title: title[field], + value: jsonData.values[idx], + unit: unit[field], + })) + + + setPayloads(result); + + function findPayload(title) { + for(let i = 0; i { + const { realtimeFuelEfficiency } = useRealtimeData(); + return ( + + +
+ {realtimeFuelEfficiency.title} +
+ + + +
+ +
+ 유류비 +
+ + 계산로직필요! + +
+
+ ); +} + +export default FuelEfficiencyBox; + +const Wrapper = styled.div` + ${commonWrapper} +`; + +const Card = styled.div` + ${commonCard} +`; + +const Header = styled.div` + ${commonHeader} +`; + +const Content = styled.div` + margin-top: 20px; + margin-right: 10px; + display: flex; + float: right; +`; +``` + + + ## 사용 기술 스택 1. AWS diff --git a/images/IMG_4836.jpg b/images/IMG_4836.jpg new file mode 100644 index 0000000..28bbc79 Binary files /dev/null and b/images/IMG_4836.jpg differ diff --git a/images/IMG_4837.jpg b/images/IMG_4837.jpg new file mode 100644 index 0000000..a72d444 Binary files /dev/null and b/images/IMG_4837.jpg differ diff --git a/images/dispatchers.png b/images/dispatchers.png new file mode 100644 index 0000000..dcde1f1 Binary files /dev/null and b/images/dispatchers.png differ diff --git a/images/obd_query.png b/images/obd_query.png new file mode 100644 index 0000000..1d86c10 Binary files /dev/null and b/images/obd_query.png differ diff --git a/images/obd_res.png b/images/obd_res.png new file mode 100644 index 0000000..e65bcca Binary files /dev/null and b/images/obd_res.png differ