모 커뮤니티에서 본 라즈베리파이용 터치 스크린을 몇달전에 구매했었는데, 설치만 해보고 어디에 쓸까.. 고민하다가 묵혀놓던 중이었다.
https://shop.pimoroni.com/products/hyperpixel-4?variant=12569485443155
HyperPixel 4.0 - Hi-Res Display for Raspberry Pi - Touch
shop.pimoroni.com
최근 주식에 재미를 붙여 시도때도 없이 토스를 들여다 보던 중에, 놀고 있는 파이와 터치 스크린을 활용해 내가 산 주식들을 모아보자는 생각을 하게 됐다.
앱을 만들기 전에 케이스를 뭘 씌울까 찾아봤는데, 영국에서 만든 스크린인데다 많이 팔리는 제품이 아니다보니 케이스를 파는 곳이 별로 없었다. 검색을 며칠 해보니 3D 도면만 수두룩하게 나오던 중, 엣시에서 이 도면을 3D 프린팅해 판매하는 샵을 발견하고 곧 주문.
품질이 썩 맘에 들진 않지만 그럭저럭 나쁘지 않았고, 이제 앱 개발에 착수해본다.
일단 앱은 이전에 장애인용 키오스크앱 만들때 사용한 electron.js로 개발한다.
웹 기반의 크로스 플랫폼 지원 프레임워크로 나같은 웹개발자에겐 손쉽게 데스크톱 앱을 만들 수 있다.
완성된 화면 캡쳐
개인 사용 용도이므로, 서버는 따로 두지 않고 github에 다음과 같이 JSON 형태로 파일을 만들고 GitHup Pages로 등록하여 웹에서 접근이 가능하도록 한다. 변경이 필요한 경우 github에 들어가 직접 편집하면 되니 그리 번거롭진 않다.
[
{
"name": "나무가",
"code": "190510",
"image": "https://www.namuga.co.kr/images/common/logo.png",
"bought": 16060
},
{
"name": "넥스트칩",
"code": "396270",
"image": "https://www.nextchip.com/images/common/footer_logo.png",
"bought": 11521
},
{
"name": "LG엔솔",
"code": "373220",
"image": "https://www.lgensol.com/assets/img/common/logo.svg",
"bought": 582500
},
{
"name": "삼성전자",
"code": "005930",
"image": "https://www.samsung.com/sec/static/_images/common/logo_samsung_black.svg",
"bought": 65416
},
{
"name": "에코프로비엠",
"code": "247540",
"image": "https://www.ecoprobm.co.kr/images/korean/contents/ecoprobm.png",
"bought": 261250
}
]
종목 이름(name), 종목 코드(code), 이미지 (image), 내가 구매한 가격 (bought)을 Array로 저장했다.
다음은 실시간 주식 정보를 어디서 가져오는지 검색해 본다.
대부분의 증권사들이 제공하는 API는 DLL, OCX 등의 윈도우 기반 모듈을 제공해 라즈베리파이에선 사용이 불가능했고, 한국투자증권이 OAuth 인증 기반 REST API를 제공하고 있어 이를 사용하기로 했다.
https://apiportal.koreainvestment.com/apiservice
KIS Developers
WEBSOCKET 실시간 (웹소켓) 접속키 발급[실시간-000] 기본정보 Method POST 실전 Domain https://openapi.koreainvestment.com:9443 모의 Domain https://openapivts.koreainvestment.com:29443 URL /oauth2/Approval Format JSON Content-Type 개
apiportal.koreainvestment.com
사용할 API는 '실시간시세 (국내주식) - 국내주식 실시간 체결가'로 웹소켓으로 데이터를 받아와야 한다.
이를 위해 'OAuth 인증 - 실시간 (웹소켓) 접속키 발급'을 먼저 요청한다.
function getApprovalKeyBeforeSocket() {
var params = {
grant_type: "client_credentials",
appkey: gAppKey,
secretkey: gAppSecret,
};
$.ajax({
type: "POST",
url: "https://openapi.koreainvestment.com:9443/oauth2/Approval",
async: true,
dataType: "JSON",
data: JSON.stringify(params),
contentType: "application/json; utf-8",
success: function (data) {
approvalKey = data.approval_key;
createWebsocket();
},
error: function (e) {
console.log(e);
},
});
}
응답은 다음과 같다.
{
"approval_key": "a2585daf-8c09-4587-9fce-8ab893XXXXX"
}
받은 approval_key로 웹소켓을 생성한다.
var approvalKey;
var pingCount = 0;
var shutdownReserved = false;
function createWebsocket() {
var url = "ws://ops.koreainvestment.com:21000";
var w = new WebSocket(url);
w.onopen = function () {
console.log("Connection OK");
// 신규 등록된 종목 코드가 있으면 실시간 체결가 등록 요청
requestCodeRegist(w);
};
w.onclose = function (e) {
console.log("Connection Closed");
};
w.onmessage = function (e) {
var data = e.data;
// 신규 등록된 종목 코드가 있으면 실시간 체결가 등록 요청
requestCodeRegist(w);
// 삭제된 종목 코드가 있으면 실시간 체결가 해제 요청
requestCodeRemove(w);
// 수신 상태를 나타내는 아이콘의 Blink 처리
blink();
if (data.startsWith("0") || data.startsWith("1")) {
// 0, 1로 시작하는 문자열 응답은 실시간 체결가 데이터
const dataArr = data.split("|");
const resultCode = parseInt(dataArr[0]);
const dataType = dataArr[1];
const dataCount = parseInt(dataArr[2]);
if (resultCode == 0) {
// 정상 응답에 대한 결과 화면 반영
stocksPurchase(dataType, dataCount, dataArr[3]);
} else {
console.log("Unknown Response '" + data + "'");
}
pingCount = 0;
} else {
// 그 외 JSON 응답은 등록/해제 요청에 대한 응답이거나 실시간 데이터가 없는 경우에 대한 PINGPONG 응답
const response = JSON.parse(data);
if (response.header.tr_id == "PINGPONG") {
// PINGPONG 응답
w.send(data);
// PINGPONG 횟수 저장
pingCount++;
} else if (response.header.tr_id == "H0STCNT0") {
// 등록/해제 요청에 대한 응답
// 실시간 체결가 등록 오류 시 재시도
if (response.body.msg1.includes("ERROR")) {
var msg =
'{"header":{"approval_key": "' +
approvalKey +
'","custtype":"P","tr_type":"1","content-type":"utf-8"},"body":{"input":{"tr_id":"H0STCNT0","tr_key":"' +
response.header.tr_key +
'"}}}';
w.send(msg);
}
pingCount = 0;
} else {
console.log("Unknown Response '" + data + "'");
}
}
// PINGPONG 휫수가 5 초과인 경우 장 종료로 보고 앱 종료
if (pingCount > 5) {
w.close();
if (!shutdownReserved) {
shutdownReserved = true;
$(".toast-ready-shutdown").show();
setTimer(".toast-ready-shutdown .time", 60);
setTimeout(shutdownSystem, 1000 * 60); // 1분후 종료
}
}
};
w.onerror = function (e) {
console.log(e);
};
}
function requestCodeRegist(ws) {
if (companyCodesRegist.length > 0) {
console.log("### companyCodesRegist", companyCodesRegist);
for (i = 0; i < companyCodesRegist.length; i++) {
var msg =
'{"header":{"approval_key": "' +
approvalKey +
'","custtype":"P","tr_type":"1","content-type":"utf-8"},"body":{"input":{"tr_id":"H0STCNT0","tr_key":"' +
companyCodesRegist[i] +
'"}}}';
ws.send(msg);
}
companyCodesRegist = [];
}
}
function requestCodeRemove(ws) {
if (companyCodesRemove.length > 0) {
console.log("### companyCodesRemove", companyCodesRemove);
for (i = 0; i < companyCodesRemove.length; i++) {
var msg =
'{"header":{"approval_key": "' +
approvalKey +
'","custtype":"P","tr_type":"2","content-type":"utf-8"},"body":{"input":{"tr_id":"H0STCNT0","tr_key":"' +
companyCodesRemove[i] +
'"}}}';
ws.send(msg);
}
companyCodesRemove = [];
}
}
기능 구현을 마치고, 라즈베리파이에서 실행이 가능하도록 package.json에 아래 내용을 추가해 준다.
{
...
"build": {
"linux": {
"target": {
"target": "deb",
"arch": "armv7l"
},
"category": "Utility",
"executableName": "minimon",
"artifactName": "${productName}-${version}.${ext}"
},
"deb": {
"fpm": [
"--architecture",
"armhf"
]
}
}
}
그리고 빌드!
➜ minimon git:(vertical) ✗ npm run build:linux64
> minimon@1.0.0 build:linux64
> electron-builder --linux --x64
• electron-builder version=23.6.0 os=22.4.0
• loaded configuration file=package.json ("build" field)
• description is missed in the package.json appPackageFile=/Users/jeongminkim/git/minimon/package.json
• writing effective config file=dist/builder-effective-config.yaml
• rebuilding native dependencies dependencies=bufferutil@4.0.7, utf-8-validate@5.0.10 platform=linux arch=x64
• packaging platform=linux arch=x64 electron=18.3.15 appOutDir=dist/linux-unpacked
• downloading url=https://github.com/electron/electron/releases/download/v18.3.15/electron-v18.3.15-linux-x64.zip size=83 MB parts=8
• downloaded url=https://github.com/electron/electron/releases/download/v18.3.15/electron-v18.3.15-linux-x64.zip duration=10.838s
• building target=snap arch=x64 file=dist/minimon-1.0.0.snap
• building target=AppImage arch=x64 file=dist/minimon-1.0.0.AppImage
• default Electron icon is used reason=application icon is not set
• rebuilding native dependencies dependencies=bufferutil@4.0.7, utf-8-validate@5.0.10 platform=linux arch=armv7l
• packaging platform=linux arch=armv7l electron=18.3.15 appOutDir=dist/linux-armv7l-unpacked
• downloading url=https://github.com/electron-userland/electron-builder-binaries/releases/download/appimage-12.0.1/appimage-12.0.1.7z size=1.6 MB parts=1
• downloading url=https://github.com/electron-userland/electron-builder-binaries/releases/download/snap-template-4.0-2/snap-template-electron-4.0-2-amd64.tar.7z size=1.5 MB parts=1
• downloading url=https://github.com/electron/electron/releases/download/v18.3.15/electron-v18.3.15-linux-armv7l.zip size=73 MB parts=8
• downloaded url=https://github.com/electron-userland/electron-builder-binaries/releases/download/appimage-12.0.1/appimage-12.0.1.7z duration=1.366s
• downloaded url=https://github.com/electron-userland/electron-builder-binaries/releases/download/snap-template-4.0-2/snap-template-electron-4.0-2-amd64.tar.7z duration=2.329s
• downloaded url=https://github.com/electron/electron/releases/download/v18.3.15/electron-v18.3.15-linux-armv7l.zip duration=9.99s
• building target=deb arch=armv7l file=dist/minimon-1.0.0.deb
• default Electron icon is used reason=application icon is not set
• downloading url=https://github.com/electron-userland/electron-builder-binaries/releases/download/linux-tools-mac-10.12.3/linux-tools-mac-10.12.3.7z size=520 kB parts=1
• downloaded url=https://github.com/electron-userland/electron-builder-binaries/releases/download/linux-tools-mac-10.12.3/linux-tools-mac-10.12.3.7z duration=1.408s
• downloading url=https://github.com/electron-userland/electron-builder-binaries/releases/download/fpm-1.9.3-20150715-2.2.2-mac/fpm-1.9.3-20150715-2.2.2-mac.7z size=5.4 MB parts=1
• downloaded url=https://github.com/electron-userland/electron-builder-binaries/releases/download/fpm-1.9.3-20150715-2.2.2-mac/fpm-1.9.3-20150715-2.2.2-mac.7z duration=1.939s
dist 디렉토리에 다음과 같이 deb 파일이 생겼다.
➜ dist git:(vertical) ll
total 468360
-rw-r--r-- 1 jeongminkim staff 1.3K 5 12 12:00 builder-debug.yml
-rw-r--r-- 1 jeongminkim staff 279B 5 12 11:59 builder-effective-config.yaml
-rw-r--r-- 1 jeongminkim staff 364B 5 12 12:00 latest-linux.yml
drwxr-xr-x 22 jeongminkim staff 704B 5 12 11:59 linux-armv7l-unpacked
drwxr-xr-x 22 jeongminkim staff 704B 5 12 11:59 linux-unpacked
-rwxr-xr-x 1 jeongminkim staff 82M 5 12 11:59 minimon-1.0.0.AppImage
-rw-r--r-- 1 jeongminkim staff 52M 5 12 12:00 minimon-1.0.0.deb
-rw-r--r-- 1 jeongminkim staff 70M 5 12 11:59 minimon-1.0.0.snap
이제 라즈베리파이에 .deb 파일을 올려주고 설치한다.
pi@raspberrypi:~ $ sudo dpkg -i minimon-1.0.0.deb
(데이터베이스 읽는중 ...현재 107244개의 파일과 디렉터리가 설치되어 있습니다.)
Preparing to unpack minimon-1.0.0.deb ...
Unpacking minimon (1.0.0) over (1.0.0) ...
minimon (1.0.0) 설정하는 중입니다 ...
Processing triggers for hicolor-icon-theme (0.17-2) ...
Processing triggers for gnome-menus (3.36.0-1) ...
Processing triggers for mailcap (3.69) ...
Processing triggers for desktop-file-utils (0.26-1) ...
실행해보면 다음과 같이 화면이 뜬다. 종목별 화면은 slick.js로 10초마다 슬라이드되도록 설정했다.
그리고 다음과 같이 3개의 쉘 스크립트를 작성했다.
on.sh : 스크린 백라이트 켬
#!/bin/sh
echo 1 > /sys/class/backlight/rpi_backlight/brightness
off.sh : 스크린 백라이트 끔
#!/bin/sh
echo 0 > /sys/class/backlight/rpi_backlight/brightness
pkill -ef -9 unclutter
turnOnMinimon.sh : 앱 실행
#!/bin/sh
export XAUTHORITY=/home/pi/.Xauthority
export DISPLAY=:0.0
# 마우스 포인터 비활성화
unclutter -idle 0 &
# 앱 실행
minimon
사무실에 두고 보려고, 출퇴근 및 점심시간에 맞춰 crontab에 설정한다.
pi@raspberrypi:~/scripts $ crontab -l
0 9 * * 1-6 /home/pi/scripts/turnOnMinimon.sh
15 12 * * 1-6 sudo /home/pi/scripts/off.sh
30 15 * * 1-6 sudo /home/pi/scripts/off.sh
10 9 * * 1-6 sudo /home/pi/scripts/on.sh
0 13 * * 1-6 sudo /home/pi/scripts/on.sh
끝!
댓글 영역