티스토리 뷰

반응형



파이썬 웹소켓을 사용하여 웹 컨트롤러를 만들텐데 파이썬용 socketio 샘플을 찾아보니 두가지 웹 프레임워크를 선택 할 수 있게 되어있다.

aiohttp 와 wsgi 이다.  ( 자료 참고 : https://pypi.python.org/pypi/python-socketio )


무엇을 선택해야 할까 고민을 하다가 벤치마크 자료를 찾아보았다.

자료 출처 : http://klen.github.io/py-frameworks-bench/


json 데이터 응답시간이 wsgi 가 가장 빨랐지만 그외에 나머지 모든 부분은 aiohttp 가 모두 좋은 성과를 보여주고있었다.


어차피 wsgi 도 flask 와 함께 사용하기 때문에 aiohttp 와 크게 차이가 나지 않을것으로 보고 aiohttp 를 이용하기로 했다.


파이썬 aiohttp with asyncio 를 가지고 채팅 베이스를 아주 잘 만들어 놓은 자료를 찾았다. 

https://github.com/miguelgrinberg/python-socketio/tree/master/examples/aiohttp

샘플 소스를 다운받아서 살펴본다. 


테스트시에 필요한 파일은 app.py (서버) 와 app.html (클라이언트) 두가지 이다.

# app.router.add_static('/static', 'static')
app.router.add_get('/', index)

static 경로 설정해주는 부분 필요없으니 주석처리한다.  


python3 를 기본으로 할것이며 pip 가 설치 되어있어야한다.


먼저 필요한 라이브러리들을 다운받는다. 

sudo apt-get install python-socketio mysql-client python-mysql* libmysqlclient-dev

pip3 install aiohttp asyncio python-socketio mysqlclient


여기까지 준비되었으면 웹서버를 구동시켜본다.

python3 app.py


jason@jason-desktop:~/py$ python3 app.py

======== Running on http://0.0.0.0:8080 ========

(Press CTRL+C to quit)


위와같이 나온다면 성공이다. 

방화벽에 별다른 룰이 설정되어있지 않다면 서버 ip:8080 포트를 웹 주소에 입력하면 아래와같은 화면을 볼수 있다.


정말 채팅 프로그램을 만드는데 필요한 기본기는 모두 갖추어져 있는 완벽한 샘플 소스이다.

별다른 노력없이 너무 쉽게 샘플을 얻어서 무언가 아쉽지만, 아직은 파이썬에 익숙하지 않고 빨리 완성해야 하기에 감사한 마음과 함께 다음 과정으로 넘어가도록 한다. 


다른 socketio 와 개념은 동일하다.

on 이벤트와 emit 이벤트로 서로 이벤트를 주고 받는다.


원격 컨트롤러를 만드는데 기본은 각방 분배기를 ON / OFF 하는 단순 동작이다.

더 나아가서는 각방 전원 상태, 타이머, 운전모드, 온도 설정등을 할수 있을 것이다.

또한, 들어오는 온수와 퇴수로 사이에 온도를 체크해서 로그에 쌓고, 추후 로그데이터를 분석해보면 벨브를 조절해 열손실이 발생하지 않는 최적의 조건도 쉽게 찾을수 있을것이다.


이 모든것을 하기 위해서는 정보를 담을 디비를 먼저 만들어야 한다.

익숙한 Mysql 을 사용하기로 하며, 우선 간단히 만들어두도록 한다.



room_info 에는 방정보가 담기며 room_mode 는 운전 모드이다. 

room_temp_hist 는 온도 로그이며 방온도,  온수 유입,퇴수 온도까지 모두 기록하기로 한다. 

(날짜가 왜 없지?  == hist_no 키값을 201803031220 처럼 사용할것이기 때문) 


본격적으로 코드를 짜보자. 


app.py

from aiohttp import web
import socketio
import json

import data_room
import control_room

import time

data = data_room.Data_room()

sio = socketio.AsyncServer(async_mode='aiohttp')
app = web.Application()
sio.attach(app)

async def index(request):
with open('app.html') as f:
return web.Response(text=f.read(), content_type='text/html')


@sio.on('room_control', namespace='/test')
async def room_control(sid, message):
await sio.emit('my response', {'data': 'room : '+message['room_id']+' '+message['room_set']}, room=sid, namespace='/test')

if(message['room_set'] == 'on'):
control_room.roomON(int(message['room_id']))
data.set_room_power(1, message['room_id'])

if (message['room_set'] == 'off'):
control_room.roomOFF(int(message['room_id']))
data.set_room_power(0, message['room_id'])

@sio.on('room_mode', namespace='/test')
async def room_mode(sid, message):
await sio.emit('my response', {'data': 'room : '+message['room_id']+' '+message['room_mode']}, room=sid, namespace='/test')
data.set_room_mode(message['room_mode'], message['room_id'])


@sio.on('disconnect request', namespace='/test')
async def disconnect_request(sid):
await sio.disconnect(sid, namespace='/test')


@sio.on('connect', namespace='/test')
async def connect(sid, environ):
#await sio.emit('my response', {'data': 'Connected', 'count': 0}, room=sid, namespace='/test')

await boradcast_roominfo()


@sio.on('disconnect', namespace='/test')
def disconnect(sid):
print('Client disconnected')

async def boradcast_roominfo():
list = data.get_room_info()
for row in list:
#print(row)
if (row['room_power'] == 1):
control_room.roomON(row['room_no'])
else:
control_room.roomOFF(row['room_no'])

await sio.emit('room_info', {'data': json.dumps(list)}, namespace='/test')




#app.router.add_static('/static', 'static')
app.router.add_get('/jason_home', index)



if __name__ == '__main__':

print("NEW START");

try:
web.run_app(app, host="0.0.0.0", port=8080)
except KeyboardInterrupt:

control_room.roomOFF(1)
control_room.roomOFF(2)
control_room.roomOFF(3)
control_room.roomOFF(4)

print("keyboard interrupt")
finally:
control_room.roomOFF(1)
control_room.roomOFF(2)
control_room.roomOFF(3)
control_room.roomOFF(4)

print("finally")


data_room.py 

import MySQLdb
import json
from io import StringIO

class Data_room:
def __init__(self):
self.db = None
self.room_data = {}
self.room_timer = {}

def connect(self):
self.db = MySQLdb.connect(host="127.0.0.1", user="", passwd="", db="", charset="utf8",
init_command="SET NAMES UTF8")

def close(self):
self.db.close()

# 방 정보 가져오기
def get_room_info(self):
self.connect()

c = self.db.cursor(MySQLdb.cursors.DictCursor)
c.execute("""SELECT * FROM jason.room_info ORDER BY room_no""")
room_data = c.fetchall()

self.close()

for row in room_data:
self.room_data[row['room_no']] = row


return room_data

# 방 전원 정보 업데이트
def set_room_power(self, power, room_id):
self.connect()

c = self.db.cursor()
c.execute("""UPDATE jason.room_info SET room_power = %s WHERE room_no = %s""", (power, room_id))

self.close()

# 방 운전 모드 설정
def set_room_mode(self, mode, room_id):
self.connect()

c = self.db.cursor()
c.execute("""UPDATE jason.room_info SET room_mode = %s WHERE room_no = %s""", (mode, room_id))

self.close()

#방 정보 업데이트
self.get_room_info()

control_room.py

import RPi.GPIO as GPIO
import time

# room info
ROOM1_PIN = 21 #거실
ROOM2_PIN = 20 #침실
ROOM3_PIN = 19 #놀이방
ROOM4_PIN = 26 #작업실

CHARGE_PIN = 15 #전류 sender

INPUT_NTC_PIN =18 #유입 온수 온도
OUPUT_NTC_ROOM1 = 7 #거실 퇴수 온도
OUPUT_NTC_ROOM2 = 11 #침실 퇴수 온도
OUPUT_NTC_ROOM3 = 13 #놀이방 퇴수 온도
OUPUT_NTC_ROOM4 = 15 #작업실 퇴수 온도

ROOM_INFO = {1:ROOM1_PIN, 2:ROOM2_PIN, 3:ROOM3_PIN, 4:ROOM4_PIN}
NTC_INFO = {1:OUPUT_NTC_ROOM1, 2:OUPUT_NTC_ROOM2, 3:OUPUT_NTC_ROOM3, 4:OUPUT_NTC_ROOM1, 5:INPUT_NTC_PIN}

def GPIOsetup():
GPIO.setwarnings(False)
GPIO.setmode(GPIO.BCM)

GPIO.setup(ROOM1_PIN, GPIO.OUT)
GPIO.setup(ROOM2_PIN, GPIO.OUT)
GPIO.setup(ROOM3_PIN, GPIO.OUT)
GPIO.setup(ROOM4_PIN, GPIO.OUT)

GPIO.setup(INPUT_NTC_PIN, GPIO.IN)

def roomON(room_id):
GPIOsetup()
GPIO.output(ROOM_INFO[room_id], 0)
return()

def roomOFF(room_id):
GPIOsetup()
GPIO.output(ROOM_INFO[room_id], 1)
return()

def readTemp(pin):
GPIOsetup()
return GPIO.input(NTC_INFO[pin])


# create discharge function for reading capacitor data
def discharge():
GPIO.setup(INPUT_NTC_PIN, GPIO.IN)
GPIO.setup(CHARGE_PIN, GPIO.OUT)
GPIO.output(CHARGE_PIN, False)
time.sleep(0.005)

# create time function for capturing analog count value
def charge_time():
GPIO.setup(INPUT_NTC_PIN, GPIO.IN)
GPIO.setup(CHARGE_PIN, GPIO.OUT)
count = 0
GPIO.output(CHARGE_PIN, True)
while not GPIO.input(INPUT_NTC_PIN):
count = count +1
return count

# create analog read function for reading charging and discharging data
def analog_read():
discharge()
return charge_time()


app.html

<!DOCTYPE HTML>
<html>

<head>
<title>Jason's Home Controller</title>
<meta name='viewport' content='width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no'>
<style>
input[type=button]{
padding-top:10px;
padding-bottom:10px; }
thead th{text-align: center;}
</style>

<script type="text/javascript" src="//code.jquery.com/jquery-2.1.4.min.js"></script>
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/socket.io/2.0.4/socket.io.slim.js"></script>
<script type="text/javascript" charset="utf-8">
$(document).ready(function(){
namespace = '/test';
var socket = io.connect('http://' + document.domain + ':' + location.port + namespace);
socket.on('connect', function() {
$('#log').prepend('<br>Connected');
$("#room_list").html('')
//socket.emit('get_room_info');

});

socket.on('disconnect', function() {
$('#log').prepend('<br>Disconnected');
});

socket.on('my response', function(msg) {
$('#log').prepend('<br>Received: ' + msg.data);
});

socket.on('room_info', function(msg) {
$("#room_list").html('')
data = JSON.parse(msg.data);


for(i = 0; i<data.length; i++){

var room_id = data[i]['room_no'];
var room_name = data[i]['room_name'];
var room_power = (data[i]['room_power'] == 1) ? "on" : "off";

var room_mode = new Array();
room_mode[data[i]['room_mode']] = 'checked';

$("#room_list").append("<tr>" +
"<td><input type='button' class='room_switch' room_id='"+room_id+"' room_set='"+room_power+"' room_name='"+room_name+"' value='"+room_name+" "+room_power+"'></td>" +
"<td>" +
"<label><input type='radio' name='room_mode"+room_id+"' value='0' room_id='"+room_id+"' class='room_mode' "+room_mode[0]+">온도</label>" +
"<label><input type='radio' name='room_mode"+room_id+"' value='1' room_id='"+room_id+"' class='room_mode' "+room_mode[1]+">타이머</label></td>" +
"<td></td>" +
"</tr>"
);
}

$('.room_switch').on('click', function() {

var room_power = ($(this).attr('room_set') == 'on') ? 'off' : 'on';

$(this).attr('room_set', room_power);
$(this).val( $(this).attr('room_name')+' '+room_power);

socket.emit('room_control', {
room_id: $(this).attr('room_id'),
room_set: room_power
});
return false;
});

$(".room_mode").on('change', function () {

socket.emit('room_mode', {
room_id: $(this).attr('room_id'),
room_mode: $(this).val()
});
return false;
});


});

socket.on('room_state', function(msg) {
$('#log').prepend('<br>room:' + msg.room_id+' state:'+msg.room_state);
});




});

</script>
</head>
<body>

<table cellpadding="10" >
<thead>
<tr>
<th></th>
<th>운전모드</th>
<th>타이머</th>
</tr>
</thead>
<tbody id="room_list">

</tbody>

</table>

<h2>Receive:</h2>
<div id="log" style="height: 100px; overflow: auto"></div>
</body>
</html>


다시 프로그램을 실행해보자. 

위와같이 아주 심플한 방 컨트롤러가 완성이 되었다.

채팅 방 개념이기 때문에 접속한 사용자들에게는 모두 즉시 변경된 데이터가 boradcast 되어 보인다.


릴레이도 모두 따각따각 소리를 내며 잘 움직인다.


이제 분배기에 심어야하니 납땜질만 남았다.

집에 불나지 않게 쇼트가 나지 않도록 릴레이와 라즈베리 파이 보드를 분리시키고 글루건으로 잘 고정해준다.
( 라즈베리파이는 유지보수가 필요할수 있으니 글루건을 쏘지 않음 ) 


라즈베리파이2 보드에는 무선랜 어댑터가 없어서 와이파이 중계기를 AP 모드로 바꾸고 외부에서도 언제든 접속이 가능하도록 라즈베리파이를 고정 아이피로 할당되게 해두었다. 

보안책으로는 외부에서 접속 가능한 포트를 제외하고 나머지는 모두 집 내부에서만 접속 가능토록 하였다. 


220V 전원까지 연결하고 최종 테스트를 해보았다.

끝! 보일러를 이제 외부에서도 컨트롤이 가능하게 되었다.


마무리는 예쁜 구급상자통으로 안전하게! 

( 딸아이 병원놀이용 상자를 몰래 쓰다가 들켜버렸다. 사진을 찍는 이순간 옆에서 옷붙잡고 울고있었다 ) 


보일러 원격 컨트롤러 시공 비용은

항목 

비용 

 4채널 릴레이 1개

 약 3,000원 

 라즈베리파이2 보드

 약 40,000원

 무선 중계기 사은품 

 노동비

 무료 


재료비로는 5만원이 들지 않았다. 



1달여 시간동안 틈날때마다 모든 기능들을 붙여 지금의 최종판이 나왔다.

운전모드와 타이머 기능이 들어가고 디자인을 이쁘게 붙였다.


아, 각방 온도와 온수 온도 체크는 미완성이다.

라즈베리파이에 온도 센서를 gpio 핀에 바로 물릴수가 없다. 

아두이노처럼 아날로그 센서가 없기 때문이다. 


복잡하게 하지 않고, 아두이노와 함꼐 사용하기로 하고 

다음 프로젝트는 아두이노와 무선통신을 이용하여 각방 온습도와 온수 체크를 할수 있도록 한다. 

반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함