해당 포스팅은 windows10 64bit / python 3.6 / IDE: pycharm / anaconda3에서 진행하였습니다.

귀여운 pyinstaller 아이콘..ㅋㅋ

pyinstaller란?

pyinstaller는 python으로 만든 프로그램을 python이 없는 환경에서 실행시켜주기 위해 실행파일로 만들어주는 프로그램입니다~

 

어떻게 설치하나?

설치 방법은 매우 간단합니다!

$ pip install pyinstaller

끝!

 

사용방법은?

사용방법은.....간단하지만 간단하지 않습니다. 하지만 하나씩 차근차근 해본다면 쉽게 만들 수 있다는 것!

제가 만든 프로젝트를 실행파일로 만들면서 겪은 여러 시행착오들을 하나씩 적어보려고 합니다.

 

일단 먼저 최초 실행

$ pyinstaller -F main.py

pyinstaller 명령어를 사용하여 본인이 만든 프로젝트의 main.py를 실행파일로 만들어주게 됩니다.

 

-F : 이 놈은 --onefile 옵션입니다. 즉, 모든 것들(리소스, 라이브러리 등등)이 실행파일 내부로 들어가기 때문에 하나의 실행파일로 내가 만든 기능을 수행할 수 있습니다. 

해당 옵션을 사용하지 않을 경우 여러가지 dll(=Dynimic Link Library)과 함께 생성되기 때문에 실행파일 크기면으로는 이득이지만, 배포하는데 문제가 생길 소지가 있어, 크지 않은 프로그램같은 경우는 단일 실행파일로 배포하는게 훨씬 깔끔합니다.

 

이렇게 명령어를 실행하면 'build' 폴더와 'dist' 폴더, 그리고 'main.spec' 파일이 생성이 됩니다.

 

만든 실행파일을 실행시켜보자!

 

자, 그럼 dist 폴더로 들어가서 main.exe 파일을 실행시켜보면?

아니 무슨 cmd창이 뜨고 바로 사라져버리는 것이 아닌가....ㅋㅋ

만약 바로 원하던 프로그램이 실행이 되면 그냥 그대로 쓰시면 됩니다.

 

하지만 조금이라도 큰 프로젝트를 만들었다면 분명 바로 실행이 될 일이 없으실 겁니다. 

 

자.....순식간에 사라진 cmd 창을 자세히 보셨으면 어떤 글씨들이 썻다가 지워지는 것을 볼 수 있으셨을 겁니다.

 

어떤 메세지가 떴나 확인을 하기 위해서는 실행파일을 cmd창에서 실행시키면 됩니다.

 

저는 git bash를 이용하여 실행시켰습니다.

$ ./main.exe

흠..... KeyError가 떳었구나.....

저의 경우는 KeyError가 떴군요...?

 

해당 에러 이외에도 모듈을 찾을 수 없는 에러라던가, 해당 파일이 존재하지 않는다던가 하는 에러들이 뜹니다.

 

문제를 해결하기 위해서는! 처음 pyinstaller를 실행했을 당시 생성된 'main.spec' 파일을 수정해야 합니다.

 

main.spec 파일을 알아보자

# -*- mode: python ; coding: utf-8 -*-

block_cipher = None


a = Analysis(['main.py'],
             pathex=['C:\\Users\\PC\\PycharmProjects\\plcMonitoring_2'],
             binaries=[],
             datas=[],
             hiddenimports=[],
             hookspath=[],
             runtime_hooks=[],
             excludes=[],
             win_no_prefer_redirects=False,
             win_private_assemblies=False,
             cipher=block_cipher,
             noarchive=False)
pyz = PYZ(a.pure, a.zipped_data,
             cipher=block_cipher)
exe = EXE(pyz,
          a.scripts,
          a.binaries,
          a.zipfiles,
          a.datas,
          [],
          name='main',
          debug=False,
          bootloader_ignore_signals=False,
          strip=False,
          upx=True,
          upx_exclude=[],
          runtime_tmpdir=None,
          console=True )

※ 최초에는 pyinstaller -F main.py로 실행파일을 만들었지만, main.spec을 수정한 후에는 pyinstaller -F main.spec을 실행시켜줄 겁니다.

$ pyinstaller -F main.py
$ pyinstaller -F main.spec

 

여기서 우리가 주목해야 할 부분은 datashiddenimports 값들입니다.

 

datas : 리소스 파일의 경로를 입력하게 됩니다. 입력방법은 tuple 또는 list 형태로 들어가게 되구요.

저는 튜플 방식을 사용했고, 만약 './img/image.png' 파일을 실행파일에 포함시키고 싶다?

튜플 형식 : (src, dest)

datas = [('./img/image.png', './img')]

 

이렇게 넣어주면 됩니다.

 

여러개의 파일을 추가할 때는 

datas = [('./img/image.png', './img'),

('./img/image2.png', './img'),

('./img/image3.png', './img')

]

콤마를 이용하여 작성하면 되구요.

 

폴더 내에있는 모든 리소스를 추가하고 싶으면 

datas = [('./img/*', './img')]

*표를 사용해주면 됩니다.

 

다음으로는 hiddenimports 속성을 한 번 보겠습니다.

 

hiddenimports : 여러가지 패키지, 모듈을 연결할 때 사용합니다.

모듈을 찾지 못했다는 에러가 나왔을 경우 해당 값을 수정해주시면 됩니다.

pyinstaller의 경우 사용자 모듈을 추가해서 작성해주어야 합니다.

 

예를 들어, 내가 a.py에서 'from 공통.통신 import 시리얼통신'이라는 모듈을 못찾았다는 에러를 확인할 경우

 

hiddenimports = ['공통.통신.시리얼통신']

와 같이 작성해주시면 됩니다.

 

여러 개의 모듈을 등록할 경우

 

hiddenimports = ['공통.통신.시리얼통신',

'공통.통신.시리얼통신2'

]

콤마(,)를 이용하여 나열해주시면 되구요.

 

hiddenimports = ['공통.통신.*']

이와 같이 별표(*)를 이용하여 패키지 안의 모든 모듈들을 연결해줄 수도 있습니다.

 

어..? 그런데도 리소스가 안찾아지는데요?

이건 제가 겪은 상황입니다ㅋㅋ.

 

문제의 원인은 실행 위치.....ㅋ

 

저는 프로젝트를 만들 때 상대경로를 이용하여 리소스를 불러왔습니다.

 

하지만 실행파일을 이용하여 프로그램을 실행시키려고 하니.... 

 

리소스들의 위치가 실행파일위치 기준이 아닌 AppData/local 내부에 있는 것이었습니다.

 

문제 해결을 위해서

try:
    os.chdir(sys._MEIPASS)
    print(sys._MEIPASS)
except:
    os.chdir(os.getcwd())

main문 최초 실행 당시 위와같이 작업 경로 변경을 통해 문제를 해결해주었습니다.

 

위와 같이 해주면 실행파일에 포함된 리소스들이 잘 해결될 겁니다.

 

혹시 이렇게 해도 리소스를 못찾을 경우 댓글 남겨주세요!

 

콘솔창 안뜨게 하기

이건 정말 간단합니다.

 

main.spec 파일을 보면 console=True 되어있는데 False로 바꿔주면 됩니다.

 

프로그램 이름이랑 아이콘 생성하기

이것도 간단합니다.

 

main.spec 파일을 보면 name='main'으로 되어있을 텐데 원하시는 것으로 바꾸면 됩니다.

icon의 경우 

EXE( ... ) 내부에

icon = 'icon.ico'

를 추가해주면 됩니다.

exe = EXE(pyz,
          a.scripts,
          a.binaries,
          a.zipfiles,
          a.datas,
          [],
          name='실행파일이름',
          debug=False,
          bootloader_ignore_signals=False,
          strip=False,
          upx=True,
          upx_exclude=[],
          runtime_tmpdir=None,
          console=True,
          icon='아이콘.ico')

 

이상 pyinstaller를 이용하여 실행파일 만들기였습니다!

진짜 겁나 간단하다.

 

https://wiki.python.org/moin/PyQt/Making%20non-clickable%20widgets%20clickable

 

PyQt/Making non-clickable widgets clickable - Python Wiki

On the #pyqt channel on Freenode, xh asked if it was possible to make QLabel objects clickable without subclassing. There are two ways to do this: Use event filters. Assign new methods to the labels. These are shown below. Event filters The following examp

wiki.python.org

 

관련 문서는 바로 위 링크에 있다.

 

def clickable(widget):

    class Filter(QObject):
    
        clicked = pyqtSignal()	#pyside2 사용자는 pyqtSignal() -> Signal()로 변경
        
        def eventFilter(self, obj, event):
        
            if obj == widget:
                if event.type() == QEvent.MouseButtonRelease:
                    if obj.rect().contains(event.pos()):
                        self.clicked.emit()
                        # The developer can opt for .emit(obj) to get the object within the slot.
                        return True
            
            return False
    
    filter = Filter(widget)
    widget.installEventFilter(filter)
    return filter.clicked

위의 코드를 내가 사용하고 싶은 곳에 복붙을 한다.

 

clickable(label1).connect(self.showText1)

clickable('내가 이벤트를 발생시키고 싶은 객체').connect('발생시킬 이벤트 함수')

 

이런 식으로 작성해주면 아주 잘 작동한다.

-> 단순히 라벨뿐만 아니라 프레임, widget 등 QObject 객체라면 모두 사용이 가능하다.

 

connect 함수에서 파라미터를 보내고 싶으면 lambda 함수를 사용하면 되고 ㅎㅎ

 

위의 코드를 보면 clicked라는 이름의 시그널을 생성하고, 이벤트 중 마우스 클릭 이벤트가 발생하면, 해당 이벤트 발생 위치를 분석해서 그 위치가 오브젝트의 사각형 안에 들어갈 경우 click signal을 emit해주는 것.

 

한마디로 QEvent.mousebuttonRelease() 함수만 다른걸로 바꿔주면 여러가지 이벤트를 다양하게 생성할 수 있다는 의미이다.

 

개 꿀

 

 

원본

더보기

서론

pyside2를 이용하여 windows에서 Gui 환경을 적용한 plc 모니터링 시스템을 만드는 중이다.

 

그런데 객체 지향 프로그래밍에 대한 기초가 잘 안잡혀 있어서 구조를 짜고 구현하는데 너무 애를 많이 먹었다...

 

하지만 결국 해결했다 ㅎ


문제

내가 가지고 있었던 문제는 상속 관계에 있었다.

 

프로젝트의 구조는 이러하다.

 

main.py -> monitor.py -> gui.py

                              -> communication.py


1. main.py에서 monitor 객체를 생성한다.

 

2. monitor class __init__()에서 Ui_monitor 객체를 생성하고 Ui_monitor.setupUi()라는 함수를 호출한다.

(pyqt designer를 이용해 짠 Ui파일을 불러오는 작업이다.)

 

이제 gui.py communication.py를 이용하여 Gui 이미지 생성 및 갱신하는 부분과 통신 부분을 구현할 생각이었다.

 

그런데 왠열.... 처음에는 mro Error(찾아보니 다이아몬드 형식으로 상속을 했을 때 발생하는 오류라고 한다.)가 발생하고, 이를 해결하니 Gui 창이 안바뀌고....

 

문제의 원인은 2가지였다.

1. 클래스 객체와 인스턴스 객체

2. super().__init__()

 

 1. 인스턴스 객체는 인스턴스... 즉, class를 이용하여 인스턴스를 생성하면 각 인스턴스마다 가지는 객체이다.

그래서 python3 부터 self.~~라는 변수명을 볼 수 있는데 이게 바로 인스턴스 객체이다.

 

하지만 내 ui객체(ui = Ui_monitor())는 단 하나만 존재해야 한다. 그래서 인스턴스 객체가 아닌 클래스 객체로 선언을 해줘야 한다. 어떤 Monitor 객체가 생성되도 내가 관리할 Gui 창은 하나이기 때문이다.

 

2. 문제는 pyside의 setupUi() 함수이다. setupUi()는 qtdesigner에서 만든 다양한 위젯들을 만들어주는 함수이다.

이놈은 자기 자신을 인자로 받아 처리하기 때문에 생성자에 선언하게 된다.

즉, 한 번 생성이 되고, 다시 생성이 될 경우(ex. 자식 객체가 super().__init__을 한 경우 등) main.py에서 show()를 통해 이미 출력되어 있는 창이 아닌 다른 인스턴스 객체들이 생성이 되어버린다.

 

그렇게 생성된 다른 인스턴스를 이용하여 값을 변경 시키려고 해봤자... 현재 떠있는 창에는 아무런 반응이 없을 수 밖에...ㅎ

 

즉, 클래스 변수로 객체를 선언하고, 자식 클래스에서 __init__을 통한 부모 객체를 추가적으로 생성시키지 않게 하면서 일단 문제를 해결하였다...


그런데 더 좋은 구조가 있는데 아직 부족해서 내가 못찾고 있는 게 아닌가 하는 생각이 든다.

뭐랄까.... 깔끔하지가 않다... 찜찜한 이느낌..ㅋ

 

수정

왠지 너무 찝찝해서 다시 열심히 조사를 하고 드디어 (일단) 가장 만족스러운 구조를 찾았다.

 

기본적인 생각은 동일하다.

 

GUI와 Serial 통신을 나누는 것....

 

class는 크게 2가지이다. monitor classsignalThread class

 

기본적인 상속 구조는 이런식이다.

Ui_monitor, QMainWindow(pyside2 class) -> monitor class

QThread -> signalThread class

 

그리고 내가 최대한 신경쓴 singalThread class에서 serial 통신을 이용하여 값을 받아 왔을 때 그 결과를 GUI에 어떻게 반영할 것인가이다.

 


01. 통신 결과를 GUI에 반영하기

communication.py

5 line : QThread Class를 상속받는다.

6 line : Pyside2.QtCore.Signal() 함수를 이용하여 사용자 Singal을 설정할 수 있다. Signal의 경우 Class 변수로 선언하고, Singal의 인자로 반환할 타입을 입력하면 데이터를 반환할 수 있다.

현재는 dic(dictionary) 타입을 넘겨주기 위해 작성하였다. 

8~14 line : QThread의 run() 함수를 오버라이딩한다. 실제 통신 결과를 data에 넣게 된다. 

10 line : with threading.Lock() Serial 통신이 동기적으로 움직일 수 있도록 구현하였다.

13 line : emit(data) 함수를 이용하여 data를 나중에 사용될 slot에 넘겨주면서 signal을 발생시키도록 하였다. 일종의 callback() 함수와 동일하게 동작한다고 보면 된다. serial 통신이 모두 완료가 되면 signal을 발생시키고, 그 signal과 연결된 slot을 수행하여 GUI를 수정할 것이다.

12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class Monitor(QMainWindow, Ui_monitor):
 
    def __init__(self):
        super(Monitor, self).__init__()
        self.setupUi(self)
 
        self.sig = signalThread()
        self.sig.start()
 
        #통신을 통해 한 스캔을 완료했을 경우 발생하는 signal이 finished이고, 해당 시그널이 발생하면 통신 결과를 Gui에 바로 반영할 수 있도록 update_info()를 호출
        #그리고 update_info()의 인자로 받아온 값들을 통해 gui에 바로 업데이트를 할 수 있다.
        self.sig.finished.connect(self.update_info)
 
        #click을 눌렀을 경우 데이터를 communication.py에 보내기
        data = {}
        data['send'= 'Good'
        self.buttonIn.clicked.connect(lambda: self.sig.sendSignal(data))
 
    @Slot(dict)
    def update_info(self, data):
        try:
            print('Update Info', data)
        except Exception as e:
            print(e)
            pass
cs

18~19 line : signalThread() 객체를 생성한 후 run Thread를 실행시켜주는 start()를 실행한다.

23 line : finished Signal을 self.update_info Slot과 연결시킨다.

31 line : finished 신호가 오면 그 결과를 Gui에 반영하는 함수이다. set~~가 여러개 들어갈 예정이다.

 

이렇게 되면 serial 통신은 signalThread()에서만 수행하게 되고 GUI 결과는 Monitor()에서 처리를 할 수 있게 된다.

 


02. 사용자 입력값(GUI에서 조작한 값)을 serial 통신으로 보내기

 

signalThread class

23
24
25
26
27
28
29
30
 
    @Slot()
    def sendSignal(self, data):
        with threading.Lock():
            print("Send: ", data)
 
 
 
 
 
 
cs

signalThread에 slot을 하나 생성해주었다. 이 slot은 monitor class에서 버튼을 클릭했을 때 호출이 될 예정이다.

 

monitor class

25
26
27
28
29
30
31
32
33
34
35
36
        #click을 눌렀을 경우 데이터를 communication.py에 보내기
        data = {}
        data['send'= 'Good'
        self.buttonIn.clicked.connect(lambda: self.sig.sendSignal(data))
 
    @Slot(dict)
    def update_info(self, data):
        try:
            print('Update Info', data)
        except Exception as e:
            print(e)
            pass
cs

26~28 line : 버튼을 lambda 함수를 이용하여 인자를 넘기면서 호출해주면 연결 끝이다.

configparser로 *.ini 파일이나 *.cfg 파일과 같은 어떤 프로그램의 속성을 저장해놓는 파일을 관리할 수 있다.

 

그런데.... 아니 별것도 아닌게 지금 개발 중인 모니터링 프로그램에서 사용하려고 했더니만 안되는 것이 아닌가..

 

 

그래서 지금 퇴근길... 그것도 지하철 안에서 운좋게 앉아서 test파일을 만들고 사용해봤다..

 

응? 잘 되네??

 

뭐지 ㅋㅋㅋㅋ

 

근데 이놈 좀 웃긴게 있다...

외부 파일을 가져오기 위해 생성한 객체를 이용해 read를 하는데 path를 확인을 안한다......

 

즉, 저 'config.ini'가 잘못된 path라도 저기서 안걸리고 일단 통과된다 ㅋㅋㅋ

맵소사

파일도 없는데 그 다음 코드로 넘어가 key가 없다는 에러를 내뿜는다 ㅋ

 

포스팅을 한 김에 configparser 사용법을 몇가지 적어나 보자

 

● 설치

$ pip install configparser

● 선언

import configparser

config = configparser.ConfigParser()

 

● 파일 읽어오기

config.read([파일명])
config.read('config.ini', encoding='utf-8')	#한글이 들어갈 경우 인코딩 값 역시 설정을 해줘야 오류가 안난다~

※ 파일명이 잘못되도 에러를 반환하지 않는다..ㅋ

 

● 파일 저장

with open('example.ini', 'w') as configfile:
    config.write(configfile)

with ~ as ~는 파일을 열어 처리를 하거나 lock()을 쓸 때 매우 유용하다. 알아서 파일을 닫아주고, release해준다고 한다 ㅎㅎ

 

● 기타

#읽기
>>> config['SECTION']['KEY']

#쓰기
>>> config['SECTION']['KEY'] = value

#section 확인
>>> config.sections()
<Section: SECTION>

오류 확인

왼쪽에 있는 이미지가 현재 프로젝트의 tree구조이고, community.py에서 configparser를 사용하려고 한다.

 

그래서 read의 path를 config.ini로 했는데 sections()함수를 이용하여 key값을 확인해봤더니 텅텅 비어있는 것이 아니겠는가...?

 

왜그런가 봤더니... 가장 최초로 프로그램이 시작되는 main.py쪽에 config.ini 파일이 있는것...ㅋㅋ

 

그랬던 것이다. path의 기준은 처음 python 프로그램을 시작한 부분이 현재 작업 위치인 것이다.

 

즉, 이를 위해서는 절대 경로를 지정해주거나 작업 영역 기준으로 상대 경로를 지정하거나~ 작업 영역을 바꿔주면 된다.

 

 작업 경로를 바꾸면 문제가 발생할 수 있으니 현재 작업 영역을 기준으로 config.ini 작업 영역을 설정해주자~

 

p.s. 하루 자고 일어나면 보이는게 퇴근 시간에는 절대 안보이더라 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ

서론

만들어보자

 

만들자~

일단 대충 이런 폼을 하나 만들었다.

그리고 "여기"에 저것들을 왕왕 넣을 것이다.

이런 식으로 말이다.

 

이렇게 하기 위해서는 main.py에서 여러가지 작업을 해줘야한다.

 

1. class 생성

2. layout생성

3. 반복 생성 함수 만들기

 

 

1번 먼저 해보자

from ui_rightCar import Ui_rightCar

class rightCar(QFrame, Ui_rightCar):
    def __init__(self, parent=None):
        super(rightCar, self).__init__(parent)
        self.setupUi(self)

이 세상 간단함이 아니다.

 

*.ui 파일로 만든 ui_*.py 파일을 불러와 setup해주는 함수를 만든다.

 

2. layout 생성

      self.ui.layoutMonitor = QGridLayout(self.ui.BoxMonitor)
      self.ui.layoutMonitor.setSpacing(0)	#각 객체 사이의 여백을 설정(Horizental, Vertical 따로 설정할 수 있는 함수도 있다)
      self.ui.layoutMonitor.setAlignment(Qt.AlignBottom)
      self.ui.layoutMonitor.setContentsMargins(0,0,0,0)	#객체 하나하나의 여백을 설정

main.py에 layoutMonitor를 QGridLayout 객체를 만들고 그 parent를 BoxMonitor라는 GroupBox의 Layout으로 설정해준다.

더보기
이놈이다.

그리고 

 

    def carMonitoring(self):
        self.CAR_NUM = 30
        self.carLList = []
        self.carRList = []
        self.carCList = []

        for floor in range(0, int(self.CAR_NUM/2)):
            self.carLList.append(leftCar())
            self.carRList.append(rightCar())
            self.carCList.append(centerCar())

            self.ui.layoutMonitor.addWidget(self.carLList[floor], floor, 0, 1, 1)
            self.ui.layoutMonitor.addWidget(self.carCList[floor], floor, 1, 1, 1)
            self.ui.layoutMonitor.addWidget(self.carRList[floor], floor, 2, 1, 1)

이런식으로 함수를 만들어주면 된다.

 

관리를 편하게 하기 위해 만든 객체들은 list에 각각 넣어주고,

for문을 반복하면서 원하는 위치에 layout의 addWidget함수를 이용하여 추가해준다.

 

그럼 끗~!

 

역시 개발은 다하고 나면 별거 아닌데 처음 할 때는 힘든 듯 하다.

 

분명 객체를 이용하는 거라 방법이 있을 거라고 생각하고 이것저것 해봤는데 layout을 생성해서 layout에 추가하는 것을 생각을 못해 시간을 쬐까 잡아먹었다. 아직 갈 길이 멀다 ㅎ

 

 

서론

요는 pyqt에서는 코드 내에서 'designer'를 이용해 만든 .ui파일을 업데이트 하는 라이브러리가 있는 반면, pyside2에서는 해당 자료를 찾을 수가 없어 직접 만들어보았다.

 

목표

$ pyside2-uic monitor.ui > ui_monitor.py

(> : pyside2-uic monitor.ui의 결과를 ui_monitor.py 파일에 저장하라는 명령이다.)

를 python 내에서 바로 실행이 될 수 있게 만들어보자.

코드

import os, sys
from subprocess import Popen, PIPE
import PySide2 as ref_mod
from PySide2.QtWidgets import *


def main():
    #파일 이름
    file_name = "monitor.py"

    dir = os.path.dirname(os.path.realpath(__file__)) #root 폴더 경로 가져오기
    ui_path = os.path.join(dir,"ui" ,file_name.replace(".py", ".ui"))   #*.ui 경로 가져오기
    ui_py_path =  os.path.join(dir,"ui_py" ,"ui_" + file_name)          #ui_*.py 경로 가져오기
    exe = os.path.join(os.path.dirname(ref_mod.__file__), "uic.exe")    #실행 명령어 입력하기
    cmd = [exe] + ['-g', 'python'] + [ui_path]                          

    proc = Popen(cmd, stdout=PIPE, encoding='utf8') #명령어 실행
    out = open(ui_py_path, 'w') #저장할 파일 열기
    print(proc.stdout.read(), file=out) #파일 저장
    out.close() #저장 파일 닫기

설명

디렉토리 구조

main.py에서 실행이 되는 코드이고, ui/monitor.ui를 ui_py/ui_monitor.py로 변경해주는 기능을 구현한 것이다.

 

일단 pyside2-uic의 uic파일의 위치부터 확인해보았다.(우연찮게..ㅋㅋ)

$pyside2-uic -h

를 치면 다양한 옵션이 나오는데 여러가지 시도해보던 중 에러 코드의 실행 위치를 통해 uic.exe 파일의 위치를 확인할 수 있었다.

C:\Users\PC\anaconda3\envs\plcmonitoring\Lib\site-packages\PySide2

plcmonitoring이 프로젝트 명이고 그 뒤로부터는 똑같을 것이다..ㅎㅎ 여기 uic.exe가 있다.

그리고 이를 실행시켜주는 코드는 

C:\Users\PC\anaconda3\envs\plcmonitoring\Lib\site-packages\PySide2\scripts\pyside_tool.py

pyside_tool.py 안에 코드가  구현이 되어있다.

해당 코드의 부분을 잠깐 살펴보면 

from __future__ import print_function

import sys
import os
import subprocess

from subprocess import Popen, PIPE
import PySide2 as ref_mod


def main():
    # This will take care of "pyside2-lupdate" listed as an entrypoint
    # in setup.py are copied to 'scripts/..'
    cmd = os.path.join("..", os.path.basename(sys.argv[0]))
    command = [os.path.join(os.path.dirname(os.path.realpath(__file__)), cmd)]
    command.extend(sys.argv[1:])
    sys.exit(subprocess.call(command))


def qt_tool_wrapper(qt_tool, args):
    # Taking care of pyside2-uic, pyside2-rcc, and pyside2-designer
    # listed as an entrypoint in setup.py
    pyside_dir = os.path.dirname(ref_mod.__file__)
    exe = os.path.join(pyside_dir, qt_tool)

    cmd = [exe] + args
    proc = Popen(cmd, stderr=PIPE)
    out, err = proc.communicate()
    if err:
        msg = err.decode("utf-8")
        print(msg, file=sys.stderr)
    sys.exit(proc.returncode)


def uic():
    qt_tool_wrapper("uic", ['-g', 'python'] + sys.argv[1:])

이런 식으로 구현이 되어있는데, 여기서 모티브를 얻었다.

 

먼저 os.path부터 보자.

 

os.path.dirname() : 파일이 있는 디렉토리 path를 반환한다.
os.path.basename() : 현재 파일의 이름을 반환한다.
os.path.join() : path끼리 합쳐준다.(인자 여러개 가능)

이 외에도 다양하게 많은데, 몇개만 알고 있으면 파일의 위치를 찾아내 절대 경로를 설정하는 것은 전혀 어렵지 않을 듯 하다.

 

살짝 헤맸던 부분은 subprocess의 Popen()이다.

class subprocess.Popen(args, bufsize=-1, excutable=None, 
                       stdin=None, stdout=None, stderr=None, 
                       preexec_fn=None, close_fds=None, shell=False, 
                       cwd=None, env=None, universal_newlines=False, 
                       startupinfo=None, creationflags=0, restoreflags=0, 
                       restore_signals=True, start_new_session=False, pass_fds=()
                      )

Popen은 여러가지 옵션들이 있는데 내가 설정한 것은 크게 2가지이다. stdout과 encoding.

stdout = PIPE로 설정을 했는데 이는 Popen()의 결과가 현재 프로세스로 출력되는 것이 아닌 PIPE를 통해 자식 프로세스에서 출력이 되도록(현재의 출력창에는 보이지 않도록) 하기 위함이고,

 

이 놈은 출력을 binary 형식으로 하는데 우리는 utf-8이 제일 익숙하므로 해당 타입으로 출력하라는 의미이다.

 

출력 결과는  proc.stdout.read()를 통해 읽을 수 있고,

open() 명령어를 통해 file descriptor를 생성해주고 print()의 결과를 file descriptor에 써준 뒤 다시 파일을 닫음으로써 새로운 파일을 생성시킨다.

pytube는 pip에 두가지 종류가 있다. pytube와 pytube3

 

만약 이 두가지를 같이 설치하게 될 경우 충돌이 발생한다.

 

따라서 하나만 설치해야 된다.

 

pytube

 

pytube3

사용 가능한 python 버전을 보면 pytube3의 경우 3.6/3.7/3.8에서 가능하다고 한다.

나는 python3.7을 사용하기 때문에 두 개 다 사용할 수 있겟지만 나중을 생각해서 조금더 버전이 높은 놈을 선택했다.

 

#-*- coding:utf-8 -*-
#Youtube download module is pytube3, pytube.
#pytube3 is available to python 3.6/7/8
#if you download both, happen to abort
from pytube import YouTube

yt = YouTube('http://youtube.com/watch?v=9bZkp7q19f0')

print(yt.streams.filter())

그러고 테스트를 해보니 성공적으로 수행 완료

 

내가 만났던 에러 : ImportError: cannot import name 'quote' from 'pytube.compat'

와... 이거 쳐봐도 진심 1도 안나오더라...ㅋㅋㅋ

 

이렇게 문제가 발생하였을 땐

$ pip uninstall pytube
$ pip uninstall pytube3
$ pip insatll pytube3

모두 지우고 다시 패키지를 설치하면 잘 동작한다.

서론

PLC의 데이터를 이용하여 PC의 동영상을 켜고 끄는 동작을 구현해보려고 한다.

 

참 많은 우여곡절이 있었다...

 

처음엔 라즈베리파이의 omxplayer를 이용하여 구현해보려고 했는데.... omxplayer-wrapper는 2.7버전에서 구동테스트를 하고, modbus는 3.5버전에서 구동 테스트를 하고....ㅋ

 

동영상 플레이어를 어떻게 제어할까 고민하다가 subprocess를 이용하여 다른 프로세스에 명령을 보내는(?) 방법을 찾아보던 중 pytube라는 패키지에 눈이 돌아갔다.

(나도 왜 돌아갔는지는 모르겠지만..ㅋ)

 

그런데 pytube를 아무리 설치해도 안되더라.... 자꾸 quote가 없다고 에러가 뜨는데 인터넷을 아무리 뒤져봐도 이 에러에 대한 원인을 찾을 수 없었다....ㅋㅋㅋ

 

그러던 중 내가 환경 설정을 아주 x같이 했다는 것 역시 깨닫게 되었고.... 오늘 python 가상환경에 대해서 한 걸음 더 나아갈 수 있었다.


본론

각설하고, 결국 최종적으로 결정한 나의 테스트 환경은 이러하다.

  • PC
    • ubuntu 16.04 LTS
    • python 3.7 with anaconda
    • python-vlc
    • pyserial
  • PLC
    • xg5000
    • LS 산전 제품

참 우여곡절이 많았다....

 

오늘 회사에 라즈베리파이를 들고가서 이것저것 테스트 해보려고 하다가 바보같이 micro sd카드를 뽀개먹었다. 그것도 케이스에 끼우다가...ㅋ

 

당연히 동작은 맛이 갔고... 나는 오늘 무엇을 해야하는가 고민하다가....

 

rasbian과 같은 리눅스 계열인 ubuntu를 이용하여 한 번 테스트를 진행해봐야겠다라고 결심을 하고 실행에 옮기기 시작했다.

 

그러던 중 서론에서 이야기 했던 pytube관련 issue를 만나게 되었고 삽질을 오질나게 했다는 것...ㅋ


아나콘다3 가상환경 설정

아나콘다..... python을 이용하여 프로젝트를 진행하다보면 다양한 패키지들을 활용하게 되는데 이러한 패키지 설치 환경을 구분해주는 일종의 프레임워크를 만들 수 있는 도구라고 볼 수 있을 것 같다.

 

나는 pytube라는 패키지 때문에 ubuntu에 아나콘다를 깔게 되었다.

 

처음에는 그냥 내가 프로젝트 해봤자 이 환경에서 얼마나 하겠어... 하고 그냥 초기의 ubuntu 환경에서 작업을 진행하려고 했다.

 

하지만 ubuntu 16.04 LTS에는 python 2.7 버전과 python 3.5 버전이 이미 깔려있더라....

pip으로 패키지를 깔면 2.7에 저장되고, pip3로 패키지를 깔면 3.5에 저장이 되고.... 설상가상으로 pytube는 계속 인터넷에 쳐도 안나오는 오류 메세지를 내보내고..ㅋㅋ

 

그래서 ubuntu에 아나콘다를 깔았다.

 

ubuntu에 아나콘다 까는 법

간단하다.

1. 아나콘다3 홈페이지에서 linux 버전의 아나콘다3를 다운받는다.(~~~~~.sh 파일임)

2. 다운받은 쉘 파일을 설치해준다.

$ bash Anaconda3-2019.03-Linux-x86_64.sh

3. 아나콘다에서 설치한 환경변수를 반영하기 위해서 bashrc를 읽는다(터미널을 껐다 켜도 된다)

$ source ~/.bashrc

4. 가상환경을 만든다.

아래는 conda에서 자주 쓰이는 명령어들이다. 진짜 많이 쓰이더라.

※ 설치
$ conda create -n [가상환경 이름] [파이썬 버전]
$ conda create -n py356 python=3.5.6

※ 제거
$ conda remove -n [가상환경 이름] --all
$ conda remove -n py356 --all
 
※ 확인
$ conda info --envs

※ 실행
$ conda activate [가상환경 이름]
$ conda activate py356

※ 종료
$ conda deactivate

5. 환경 변수를 설정을 하고 나면 다음부 ubuntu의 터미널을 키게 되면 앞에 저런게 붙는다. 아나콘다3의 root 환경이라는 의미다.

 

저기서 conda activate를 수행하면 (base)가 [가상환경 이름]으로 바뀌는 것을 알 수 있다.

 

추가적으로 나는 ubuntu에 pycharm을 직접 깔아서 개발을 진행하려고 생각했다


pycharm-community 설치하는 법

너무 간단하다.

※ 설치
$ sudo apt install snapd snapd-xdg-open

$ sudo snap install pycharm-community --classic

※ 실행
$ pycharm-community

pycharm 가상환경 설정하는 법

1. 아나콘다 가상환경 생성(가상환경 이름 : test, python 버전 = 3.7)

$ conda create -n test python=3.7

 

2. 가상환경 생성 여부 확인 

$  conda info --envs

잘 생성 되었다


3. pycharm-community 실행(시간 조금 걸림... 아 겁나 느리네)

$  pycharm-community


4. Configure -> Settings 클릭

 

5. Project Interpreter -> 톱니바퀴 아이콘 -> Show All

 

6. 아까 만든 가상환경 설정에서 python interpreter 설정

 

7. 가상환경 선택

 

8.  새 프로젝트 생성

 

9. 아까 만든 인터프리터 설정 후 create

9.1 만약 인터프리터를 변경하고 싶다면 생성된 프로젝트-> Setting에서도 변경 가능

10. 생성된 프로젝트에서 pycharm에 존재하는 Terminal을 켜게 되면 프로젝트 생성 가능

 

11. 해당 터미널에서 설치하고 싶은 패지키 설치

 

12. 설치된 pip 패키지 정보들 확인

$ pip list

 

 

12.1 Setting(Ctrl + Alt + S) -> Project Interpreter에서도 확인 가능


다음으로는 vlc를 설치해서 프로젝트를 진행해보려 한다.

python-vlc로 동영상 제어하기

※ vlc player  설치
$ sudo apt-get install vlc

※ python-vlc  설치
$ pip install python-vlc

▼ 예제 코드 입력(파일 위치 수정이 필요할 경우 수정)

#-*- coding:utf-8 -*
#https://www.videolan.org/developers/vlc/doc/doxygen/html/group__libvlc__video.html#gabdbb7230cc3db78e73070ce10e679315

import vlc
import time
class Player():
    def __init__(self):
        self._instance = vlc.Instance(['--video-on-top', '--input-repeat=-1'])  #동영상 재생 반복
        self._player = self._instance.media_player_new()
        self._player.set_fullscreen(True)	#동영상을 전체화면으로 실행

    def play(self, path):
        media = self._instance.media_new(path)
        self._player.set_media(media)
        self._player.play()

    def stop(self):
        self._player.stop()

p=Player()
p.play('./videos/01.mp4')
time.sleep(5)
p.stop()
p.play('./videos/02.mp4')
time.sleep(5)

잘 동작하는 것을 확인할 수 있다.

 

▼ 간단하게 이런 식으로도 가능

import vlc
import time

player = vlc.MediaPlayer("./videos/01.mp4")
vlc.libvlc_set_fullscreen(player, True)
player.play()
time.sleep(5)

가상환경에서 외부 usb Serial 포트 연결

을 하기 위해서는 ubuntu에서 현재 사용자 계정이 Serial Port로 접근을 할 수 있게 권한을 설정해주어야 함.

root 계정으로 로그인
$ sudo su

passwd 입력

※ /dev 폴더로 이동
$ cd /dev


※ tttyUSB0 권한 등록
$ chown [사용자 계정 이름] ttyUSB0
chown mistive ttyUSB0

 

'PLC' 카테고리의 다른 글

[plc&pc 연동] nodejs를 이용한 modbus RTU 구현  (0) 2020.03.17


0: 너 이거 왜하는데?

PLC와 PC 통신을 하려고 그런다.

1: 어떻게 할껀데?

일단 PLC와 PC 통신 하기 전에 usb to rs232 Converter를 이용해서 Tx로 쏜걸 Rx에서 받는 걸 파이썬(pyserial)으로 구현할꺼다.



서론 : 시리얼 통신이 뭔데?



시리얼 통신이 무엇일까?


단어를 이해하기 위해서는 각 단어의 의미를 먼저 알아보는게 좋다.


통신은 지나가는 유치원생한테 물어봐도 대충 무슨 의미인지는 알겠고... 시리얼... 시리얼...


시리얼? 이미지 검색결과


이 시리얼을 의미하는 것은 절대 아닐테고....(죄송합니다)


시리얼은 Serial. 즉 직렬이란 의미이다.


직렬의 반대는 그럼 병렬일텐데...


A컴퓨터와 B컴퓨터가 서로 통신한다고 쳤을 때 병렬은 한 번에 여러개의 데이터를 전송할 수 있다. 왜냐? 병렬이니깐

하지만 병렬로 A컴퓨터와 B컴퓨터를 연결하면 돈이 많이들겠다. 왜냐?? 세상에 공짜는 없으니깐 말이다. 한 번에 여러 개의 데이터를 보낸다는 의미는 말그대로 전선이 많이 들어갔다는 소리다.


반대로 직렬은 그럼... 선이 한 개이고 이 하나의 선만으로 데이터를 전송하는 것이다.


자.... 그럼 하나의 선만으로 어떻게 데이터를 주고 받을 것인가?


컴퓨터는 알다시피 머리가 상당히 나빠 0이랑 1밖에 모른다. 당연히 전선이 하나만 연결이 되어있으니깐 두 컴퓨터간에 약간의 약속이 필요하다. 그게 바로 baud rate(보레이트)라고 하는 놈이다.


만약 A컴퓨터가 0000 1111 0101 1010 이라는 데이터를 보내고 싶어한다. 그런데 B라는 놈은 000 011 110 101 101 0 과 같은 식으로 데이터를 받았다고 치자. 그럼 두 놈이 원활한 의사소통이 되겠는가? 당연히 될 수가 없다.


그래서 일반적으로 serial 통신을 할 때는 baud rate를 통신하려는 기기끼리 서로 맞춰준다.



그리고 또 솔직히 말하면 serial 통신이 전선이 딱 하나만 있는 건 아니다. 송신선과 수신선이 서로 나뉘어져 있어 송신과 수신을 동시가 할 수 있는 것도 있고(full duplex) 송신선과 수신선이 하나로 이루어져 있어 한 번에 송신과 수신을 할 수 없는 놈도 있다.(half duplex)


또한 단순히 전압의 차이만을 이용하여 회로 구성이 간단하지만 노이즈에 좀 취약한 가장 기본적인 시리얼 통신인 RS-232가 있고, 232에서는 송신선을 하나의 전선으로 구성했다면, 2개의 전선을 이용하여 그 2개의 전선의 전위차를 가지고 통신하는 RS-422, 485와 같은 Serial 통신 방법 등 다양하게 있다.


각 시리얼 통신의 차이를 구글에 검색해보면 정말 잘 나와있는 글들이 많으니 시리얼 통신 설명은 여기까지 하겠다.


준비물



1. RS233 to usb converter
2. Jump Cable(female - female)
3. 컴퓨터

만 있으면 테스트 해볼 수 있다.


준비 과정


1. 위의 그림처럼 점프 케이블을 이용해 RS232의 Rx와 Tx를 연결시켜준다.



2. ["Win + X" -> 장치관리자]에서 "USB Serial Converter"가 있는지 확인한다.


3. 만약 연결이 안되었거나, 앞에 느낌표가 있으면 온라인으로 드라이버 찾기 하면 자알 찾아준다.


4. 처음 연결해서 드라이버를 설치하면 컴퓨터를 껐다 켜야 제대로 인식한다.



5. 위의 사진처럼 USB Serial Port의 번호(COM3)를 확인한다.


6. pip install pyserial을 이용하여 pyserial을 설치해주고


7. 아래의 코드를 한 번 집어넣어본다.

import serial

ser = serial.Serial("COM3", 115200, timeout = 1)
while True:
print("insert op :", end=' ')
op = input()
ser.write(op.encode())
print("R: ", ser.readline())

if op is 'q':
ser.close()


8. 참고로 COM 포트 번호는 사람마다 다 다를 수 있다.


9. 출력 결과는 이와 같이 Rs232에서 출력한 값을 받아오는 것이다. 


10. 앞에 b는 byte 표시다. byte 타입의 문자열로 보내는 이유는(op.encode()) 유니코드로 보내면 에러 떠서 못받는다.




























+ Recent posts