サーマルカメラ(サーモ AI デバイス TiD) Python Form編

***
1. [紹介編](https://qiita.com/MH-SS/items/c3d12abe871bfd62d1df)
2. [センサ編](https://qiita.com/MH-SS/items/61021e58b3a5a7f81b3d)
3. [センサケース編](https://qiita.com/MH-SS/items/ab142942c702eb6e545f)
4. [Raspberry Pi編](https://qiita.com/MH-SS/items/73f09db1ad1890c90987)
5. Python編  
    5.1 [Form編](https://qiita.com/MH-SS/items/035682853d18dc2074f2)  
    5.2 [オムロン 非接触温度センサ D6T-44L-06編](https://qiita.com/MH-SS/items/f9301bde143de482a17a)  
    5.3 [Pololu 測距センサ VL53L0X編](https://qiita.com/MH-SS/items/df3c33a6256a533d4eb5)  
    5.4 [BOSCH 温湿度・気圧センサ BME280](https://qiita.com/MH-SS/items/aff10e7b4d84c1b03714)  
    5.5 [シャットダウン・再起動スイッチ編](https://qiita.com/MH-SS/items/82db17524553e56d7605)  
    5.6 [OpenCV編](https://qiita.com/MH-SS/items/bcc9092bb9b6977014a1)  
    5.7 [高速化編](https://qiita.com/MH-SS/items/154dd994d73b4c93854f)  

***

# Python
## はじめに
MH ソフトウェア & サービスの製品にはPythonが多用されています。ライブラリが豊富で、管理しやすく、コンパイルが不要の為、スマホでもプログラミングできます。
WebサイトもPythonで書かれています。
Pythonのバージョンは、2020/10/25時点で3.8.6となっています。

## GUI
Pythonで使用できるGUI環境は多数ありますが、標準でインストールされるtkinterを多用しています。

## test_form.py
MH ソフトウェア&サービスのFormクラスを使用するサンプルです。
320x160のウィンドウが(100,50)の位置に表示されます。
<a href="https://mhss.server-on.net/a656d/index/tid3/development/python/form/test_form.zip" target="_blank">test_form.py (ZIPで圧縮済)</a>

```python
#!/usr/bin/env python
# -*- coding:utf-8 -*-
from form import Form


class Root(Form):
    def __init__(self):
        super().__init__(
            #dialog=True,
            border=0,
            relief=None,
            #icon=icon_file,
            #minsize=minimum_size,
            name='main',
            position=['320', '160', '100', '50'],
            title='タイトル')


def main():
    root = Root()
    root.mainloop()


if __name__ == '__main__':
    main()
```
こんな感じのウィンドウです。
![form.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569469/f045a1f3-37bb-676b-c41e-83a06744ece2.jpeg)

## form.py
MH ソフトウェア&サービスでtkinterを使用する際のFormクラスです。
マウスイベント、UDP送受信機能の準備など共通機能を盛り込んでいます。
同じフォルダ内にudp.pyも置いてください。
<a href="https://mhss.server-on.net/a656d/index/tid3/development/python/form/form.zip" target="_blank">form.py (ZIPで圧縮済)</a>

```python
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import socket
import threading
import tkinter as tk
from tkinter import ttk

if __name__ == '__main__':
    from udp import UDP
else:
    from .udp import UDP


class Form(tk.Tk):
    def __init__(self, *args, **kwargs):
        """
        Argument:
            dialog=<class 'bool'>
            icon=full_path
            hasmenu=True or False: Change height.
            master=object
            minsize=(width, height)
            name=<class "str"> 
            position=[width, height, left, top]
            title=<class "str">
            topmost=False
            udp_port=4000
            udp_bufsize=4096
        """
        if kwargs.get('toplevel', False):
            # Use for dialog new window.
            tk.Toplevel.__init__(self)
            self.resizable(0, 0)
            self.grab_set()
            # Focus to me.
            self.focus_set()
            # Not show taskbar.
            self.transient(self.master)
        else:
            super().__init__(
                #border=0,
                #relief=None,
            )

        if len(kwargs.get('minsize', ())) == 2:
            self.minsize(
                kwargs.get('minsize')[0],
                kwargs.get('minsize')[1])

        self.border_style = {'borderwidth': 0, 'relief': 'solid'}

        self._scaling = self._get_dpi(self) / 72
        self.tk.call('tk', 'scaling', self.scaling)
        #self.tk.call('tk', 'scaling', 0.5)

        # 96 is development PC's DPI. This value is default.
        self._screen_ratio = 96 / self._get_dpi(self)

        self.master = kwargs.get('master', None)
        self.title(kwargs.get('title', ''))
        self.name = kwargs.get('name', '')

        self._hasmenu = kwargs.get('hasmenu', False)
        if len(kwargs.get('position', [])) == 4:
            width = int(kwargs.get('position')[0])
            if self.minsize()[0] > width:
                width = self.minsize()[0]

            height = int(kwargs.get('position')[1])
            if self.minsize()[1] > height:
                height = self.minsize()[1]

            x = kwargs.get('position')[2]
            y = kwargs.get('position')[3]

            self.position = [width, height, x, y]

        if kwargs.get('icon', '') != '':
            self.icon = kwargs.get('icon', '')

        if kwargs.get('topmost', False):
            self.grab_set()

        if kwargs.get('dialog', False):
            self.resizable(0,0)

        self._attach_events()

        if kwargs.get('udp_port', '') != '':
            self.udp = UDP(self, *args, **kwargs)
            self.udp.receive.trace('w', self.udp_receive_changed)

        self.resize_event = True

    @property
    def host(self):
        result = socket.gethostbyname_ex(socket.gethostname())
        result = result[2][0]
        return result

    @property
    def icon(self):
        return self._icon
    @icon.setter
    def icon(self, value):
        self._icon = value
        self.tk.call('wm', 'iconphoto', self._w, tk.PhotoImage(file=self._icon))

    @property
    def position(self):
        return [
            self.winfo_width(),
            self.winfo_height(),
            self.winfo_x(),
            self.winfo_y()]
    @position.setter
    def position(self, value):
        """
        geometry=[width, height, left, top]
        """
        if self._hasmenu:
            value[1] += 20
        self.geometry(f'{value[0]}x{value[1]}+{value[2]}+{value[3]}')

    @property
    def resize_event(self):
        return self._resize_event
    @resize_event.setter
    def resize_event(self, value):
        self._resize_event = value
        if self._resize_event:
            self.bind('<Configure>', self._on_resize)
        else:
            self.unbind('<Configure>')

    @property
    def scaling(self):
        return self._scaling

    @property
    def screen_ratio(self):
        return self._screen_ratio

    def _attach_events(self):
        self.protocol('WM_DELETE_WINDOW', self.on_exit)

        self.bind('<Configure>', self._on_resize)
        self.unbind('<Configure>')

        self.bind('<MouseWheel>', self._mouse_wheel)
        self.bind('<Motion>', self._mouse_move)

        self.bind('<ButtonPress-1>', self._button1_press)
        self.bind('<ButtonRelease-1>', self._button1_release)

        self.bind('<ButtonPress-2>', self._button2_press)
        self.bind('<ButtonRelease-2>', self._button2_release)
        self.bind('<B2-Motion>', self._button2_move)

        self.bind('<ButtonPress-3>', self._button3_press)
        self.bind('<ButtonRelease-3>', self._button3_release)

        self.bind('<Expose>', self._expose)

    def _button1_press(self, *args):
        pass

    def _button1_release(self, *args):
        pass

    def _button2(self, *args):
        pass

    def _button2_move(self, event):
        pass

    def _button2_press(self, *args):
        pass

    def _button2_release(self, *args):
        pass

    def _button3(self, *args):
        pass

    def _button3_press(self, *args):
        pass

    def _button3_release(self, *args):
        pass

    def _expose(self, *args):
        pass

    def _get_dpi(self, window):
        MM_TO_IN = 1/25.4
        pxw = window.winfo_screenwidth()
        inw = window.winfo_screenmmwidth() * MM_TO_IN
        return int(pxw/inw)

    def _mouse_move(self, *args):
        pass

    def _mouse_wheel(self, event):
        sf = 1.0
        if event.delta < 0:
            sf = 0.9
        else:
            sf = 1.3
        pass

    def on_exit(self):
        self.destroy()

    def _on_resize(self, event):
        """
        override from child.
        This event is hook <"Configure">
        If size is not changed, through the function.
        """
        #if (self.geometry != self.geometry()):
        #    self.geometry = self.geometry()
        self.on_resize(event)

    def on_resize(self, event):
        # override from child.
        pass

    def udp_receive_changed(self, *args):
        """
        When UDP use, then override this function .
        """
        pass

def main():
    root = Form()
    #root.wm_attributes("-transparentcolor", "white")
    root.mainloop()


if __name__ == '__main__':
    main()
```

## udp.py
UDP通信の共通クラスです。
threadで通信を監視します。
<a href="https://mhss.server-on.net/a656d/index/tid3/development/python/form/udp.zip" target="_blank">udp.py (ZIPで圧縮済)</a>

```
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import os
import socket
import threading
import tkinter as tk


class UDP():
    def __call__(self):
        return self.receive.get()

    def __init__(self, master, *args, **kwargs):
        self.binded = False

        self.error_message_enable = kwargs.get('error_message_enable', True)
        self.error = self.Error(self)

        self._port_changing = False

        self._port = int(kwargs.get('udp_port', '4000'))
        self._bufsize = int(kwargs.get('udp_bufsize', '4096'))

        self.receive = tk.StringVar(value='')

        self._sock = None
        self._start()

    def __new__(cls, master, *args, **kwargs):
        cls.master = master

        if hasattr(cls.master, 'root'):
            cls.root = cls.master.root
            if hasattr(cls.root, 'ini'):
                cls.ini = cls.root.ini

        if hasattr(cls.master, "parent"):
            cls.parent = cls.master.parent

        return super().__new__(cls)

    @property
    def bufsize(self):
        return self._bufsize
    @bufsize.setter
    def bufsize(self, value):
        self._bufsize = value

    class Error():
        def __init__(self, master):
            self.master = master
            self.root = self.master.root

            self.id = tk.IntVar(value=0)
            self.id.trace('w', self._error_changed)
            self.message = ''
            self.title = ''

        def _error_changed(self, *args):
            if self.master.error_message_enable:
                if self.id.get() > 0:
                    tk.messagebox.showwarning(self.title, self.message)

    def _start(self):
        self._sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        self._sock.settimeout(0.1)
        self._socket_bind()

        self.thread = threading.Thread(
            name='udp{}{}'.format(self.port, self.binded),
            target=self._thread,
            )
        self.thread.setDaemon(True)
        self.thread.start()

    def _socket_bind(self):
        try:
            # host is ''.
            self._sock.bind(('', self.port))
        except Exception as e:
            if e.errno == 10048:
                title = (
                    lambda: 
                        'UDPポート異常'.format(self.port)
                    if lang() == 'ja_JP' else 
                        'Failed UDP port'.format(self.port)
                    )()
                msg = (
                    lambda: 
                        'ポート {} は使用されています'.format(self.port)
                    if lang() == 'ja_JP' else
                        'Port {} was used.'.format(self.port)
                    )()
            else:
                title = (
                    lambda: 
                        'その他のエラー'.format(self.port)
                    if lang() == 'ja_JP' else 
                        'Other error'.format(self.port)
                    )()
                msg = (
                    lambda: 
                        'ポート {} は使用されています'.format(self.port)
                    if lang() == 'ja_JP' else
                        'Port {} was used.'.format(self.port)
                    )()
            #tk.messagebox.showwarning(title, msg)
            self.error.message = msg
            self.error.title = title
            self.error.id.set(1)
            self.binded = False
        else:
            self.error.message = ''
            self.error.title = ''
            self.error.id.set(0)
            self.binded = True

    def _thread(self):
        while True:
            data, addr = None, None
            while self.binded:
                try:
                    if self._port_changing:
                        break
                    data, addr = self._sock.recvfrom(self._bufsize)
                    data = data.decode()
                    if (data is not None) & (addr is not None):
                        self.receive.set('{};{};{}'.format(
                            data, addr[0], addr[1]))
                        data, addr = None, None
                except Exception as e:
                    #print(e)
                    pass

            if self._port_changing:
                self._sock.close()
                del(self._sock)
                self._sock = None

                self._sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
                self._sock.settimeout(0.1)
                self._socket_bind()

                self.thread.name = 'udp{}{}'.format(self.port, self.binded)
                self._port_changing = False

    def udp_reset(self):
        if type(self._sock) == socket.socket:
            self._sock.close()
        del(self._sock)
        self._sock = None

    @property
    def port(self):
        return self._port
    @port.setter
    def port(self, value):
        value = int(value)
        if value != self._port:
            self._port = value
            self._port_changing = True

def lang():
    """
    Sometime os don't has os.environ['LANG'].
    This function can not be use...
    """
    result = ""
    if ('LANG' in os.environ):
        result = os.environ.get('LANG')
        result = result.split('.')
        result = result[0]
        if result != 'ja_JP':
            result = 'other'
    else:
    	result = 'other'
    return result
```

***
[YouTube: サーマルカメラ(サーモ AI デバイス TiD) Python編](https://www.youtube.com/watch?v=fgwBsISDtXo)
[web: サーモ AI デバイス TiD Python Form編](https://mhss.server-on.net/?f401c=Index&ae504=tid3/development/python/form) (URLが変更されました)