Ubuntu環境 Raspberry Pi のpythonアプリ用にpythonをソースコードからビルドする

#### Raspberry Pi 用のpython仮想環境をUbuntu環境に構築するためにpythonのソースコードをビルドする方法を紹介します。

ソースコードをビルドすることにした発端は **Ubuntu 18.04 (2023年5月31日 EOL) から Ubutu 22.04にアップグレードした時に python 3.7.x の仮想環境が全滅したことでした。**

以下、**ラズパイ４は [Raspbery Pi 4 ModelB]**、**ラズパイゼロは [Raspberry Pi Zero WH(Legacy ヘッドレスOS) ]** として話をすすめます。

現在私が使っている開発PCは**Ubuntu 22.04** ですが、開発PCのpythonバージョンとラズパイの pythonバーションは異なります。

更に、ラズパイの中でも **Raspberry Pi OS(Legacy)** と **Raspberri Pi OS** では python のバーションが異なります。

**ラズパイをターゲットとするPythonアプリ(特にGPIOを制御する)の開発には、ターゲット機で開発するのが望ましいですが、ラズパイゼロのヘッドレスOS環境では実機上でvimエディタなどで開発するのは現実的ではないでしょう。**

**ラズパイ４ではモニターとキーボードをつないで開発するということも可能ですが、ラズパイ４も２年以上品不足で入手がほぼ困難の状況では、本番機を使って開発するのも望ましくないのは言うまでもありません。**
※ 2023年7月頃からようやくラズパイ４の品不足が解消され購入できるようになりました。  


下記は2023年11月現在の全てのラズパイの運用機と試験機です。
![Raspi_all_machines.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3583120/29bd920d-45b6-d2a8-76ea-82d6ed46a230.jpeg)

### 各環境のOSバージョンとシステムのPythonバージョン確認

#### 開発PC (Ubuntu 22.04): python 3.10.x
```bash
$ lsb_release -a
No LSB modules are available.
Distributor ID:	Ubuntu
Description:	Ubuntu 22.04.3 LTS
Release:	22.04
Codename:	jammy
# システムの python バーション
$ python3 -V
Python 3.10.12
```

#### ラズパイ４ (Desktop): python 3.9.2
```bash
pi@raspi-4:~ $ lsb_release -a
No LSB modules are available.
Distributor ID:	Debian
Description:	Debian GNU/Linux 11 (bullseye)
Release:	11
Codename:	bullseye
# システムの python バーション
pi@raspi-4:~ $ python3 -V
Python 3.9.2
```

#### ラズパイゼロ (Legacy ヘッドレスOS): python 3.7.3
```bash
pi@raspi-zero:~ $ lsb_release -a
No LSB modules are available.
Distributor ID:	Raspbian
Description:	Raspbian GNU/Linux 10 (buster)
Release:	10
Codename:	buster
# システムの python バーション
pi@raspi-zero:~ $ python3 -V
Python 3.7.3
```


### 参考URL (ビルド方法)

[Python.jp プログラミング言語 Python情報サイト: Python環境構築ガイド 〉 Ubuntu環境のPython](https://www.python.jp/install/ubuntu/index.html)

### ソースコードの取得

下記サイトから必要なバージョンのソースを取得します。  
私は念の為もう一つ古い安定版 3.7.15を選びました。
ソースコードは ```Gzipped source tarball``` 形式のほうが解凍が簡単です。 

[Python Source Releases](https://www.python.org/downloads/source/)

### 1. ビルド

基本的に「Ubuntu環境のPython」に記載されている手順に従い実行します。

#### 1-1. ビルドツール・ライブラリのインストール

```bash
$ sudo apt install build-essential libbz2-dev libdb-dev \
> libreadline-dev libffi-dev libgdbm-dev liblzma-dev libncursesw5-dev \
> libsqlite3-dev libssl-dev zlib1g-dev uuid-dev tk-dev
```

#### 1-2. ソースコードの解凍

```bash
$ tar zxf Python-3.7.15.tgz
$ cd Python-3.7.15
```

#### 1-3. コンフィグレーション

何も指定しなければ /usr/local にインストールされます。  

**参考サイトでは「ディレクトリを指定してインストール」も紹介しています。**  

※1 **私はシステムのPython3に影響を与えないよう /opt にインストールしました**  
※2 **エラーなく creating Makefile** と出力されたらOKです

```bash
$ ./configure --enable-shared --prefix=/opt/python3.7.15
# ...省略...
creating Makefile
```

#### 1-4. make

結構時間かかります。CPUがたくさん積んでいるなら **-j8** 位指定すると早く終了します。  

```bash
$ make -j8
```

#### 1-5. インストール

**インストールには root権限が必要なので ```sudo``` を指定します。**  
※1 デフォルト以外のパスにインストールした場合、下記の様にWARNINGが出力されます。
※2 **この状態ではpythonのライブラリへのパスがシステムに設定されていないので 1-6 の追加の処理が必要になります**

```bash
$ sudo make install
# ...省略...
Installing collected packages: setuptools, pip
  WARNING: The script easy_install-3.7 is installed in '/opt/python3.7.15/bin' which is not on PATH.
  Consider adding this directory to PATH or, if you prefer to suppress this warning, use --no-warn-script-location.
  WARNING: The scripts pip3 and pip3.7 are installed in '/opt/python3.7.15/bin' which is not on PATH.
  Consider adding this directory to PATH or, if you prefer to suppress this warning, use --no-warn-script-location.
Successfully installed pip-22.0.4 setuptools-47.1.0
WARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager. 
It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv
```

#### 1-6. 共有ライブラリをシステムに認識させる

**デフォルトでインストールした場合はこの処理は不要です。** 

**ldconfig** の参考URL
+ [日経クロステック トップ> ITシステム> Linuxコマンド集【ldconfig】共有ライブラリの依存関係情報を更新する](https://xtech.nikkei.com/it/article/COLUMN/20140127/532504/)
+ [Ubuntu manuals 22.04LTS (日本語) ldconfig - 動的リンカーによる実行時の結合関係を設定する](https://manpages.ubuntu.com/manpages/jammy/ja/man8/ldconfig.8.html)

```bash
$ sudo ldconfig /opt/python3.7.15/lib
```

下記は ldconfig を設定していなかったとき Pycharmのプロジェクトインタープリターの設定で出力されたエラー

```bash
/usr/local/bin/python3.7.15: error while loading shared libraries: libpython3.10.so.1.0: cannot open shared object file: No such file or directory
```

#### 1-7. /usr/local/bin にシンボリックリンクを作成する

※ /opt/python3.7.15/bin/python3 と直接入力するのも大変です

```bash
$ sudo ln -s /opt/python3.7.15/bin/python3 /usr/local/bin/python3.7
```

### 2. ラズパイ用のpython仮想環境作成

仮想環境名の先頭に **py37_** をつけるようにしておくと python3.7.xで作成したものと区別しやすくすなります。

```bash
$ python3.7 -m venv py37_pigpio
$ . py37_pigpio/bin/activate
```

#### 2-1. GPIO関連ライブラリをインストール

(1) 基本ライブラリ: **pigpio, pyserial, spidev(SPI), smbus2(I2C)**

```bash
(py37_pigpio) $ pip install pigpio pyserial spidev smbus2
```

(2) 液晶OLED出力に必要な lumaライブラリ

```bash
(py37_pigpio) $ pip install luma.core luma.lcd luma.oled
```

(3) インストールライブラリ確認

```bash
(py37_pigpio) $ pip freeze
cbor2==5.4.3
luma.core==2.4.0
luma.lcd==2.10.0
luma.oled==3.9.0
pigpio==1.78
Pillow==9.3.0
pyftdi==0.54.0
pyserial==3.5
pyusb==1.2.1
RPi.GPIO==0.7.1
smbus2==0.4.2
spidev==3.6
```

※1 現在ラズパイゼロ(本番機)から取得したrequrement.txt (pip freeze) では下記のエラーが出て復元できませんでした。
※2 ネットを調べるといろいろ方策が有るようですが、上記でイントールしたライブラリで開発したソースは本番機でエラーなく動作するので今回は pip install xxxx で復元しています。

```bash
(py37_pigpio) $ pip install -r ~/Desktop/work/raspi-zero/requirements_raspi-zero.txt 
WARNING: pip is configured with locations that require TLS/SSL, however the ssl module in Python is not available.
WARNING: Retrying (Retry(total=4, connect=None, read=None, redirect=None, status=None)) after connection broken by 'SSLError("Can't connect to HTTPS URL because the SSL module is not available.")': /simple/cbor2/
WARNING: Retrying (Retry(total=3, connect=None, read=None, redirect=None, status=None)) after connection broken by 'SSLError("Can't connect to HTTPS URL because the SSL module is not available.")': /simple/cbor2/
WARNING: Retrying (Retry(total=2, connect=None, read=None, redirect=None, status=None)) after connection broken by 'SSLError("Can't connect to HTTPS URL because the SSL module is not available.")': /simple/cbor2/
WARNING: Retrying (Retry(total=1, connect=None, read=None, redirect=None, status=None)) after connection broken by 'SSLError("Can't connect to HTTPS URL because the SSL module is not available.")': /simple/cbor2/
WARNING: Retrying (Retry(total=0, connect=None, read=None, redirect=None, status=None)) after connection broken by 'SSLError("Can't connect to HTTPS URL because the SSL module is not available.")': /simple/cbor2/
Could not fetch URL https://pypi.org/simple/cbor2/: There was a problem confirming the ssl certificate: HTTPSConnectionPool(host='pypi.org', port=443): Max retries exceeded with url: /simple/cbor2/ (Caused by SSLError("Can't connect to HTTPS URL because the SSL module is not available.")) - skipping
ERROR: Could not find a version that satisfies the requirement cbor2==5.4.2 (from versions: none)
ERROR: No matching distribution found for cbor2==5.4.2
```



### 3. 開発PCのpython開発環境

**Pycharm Community Edition for Linux** を使っています。 

#### 3-1. ラズパイゼロ用のプロジェクトの pythonインタープリター設定

ライブラリはほとんど開発PC上では実行できません。 

![pycharm_python_interpretter_pigpio.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3583120/6edfea77-b229-3018-e1a0-d66af17306b4.jpeg)

#### 3-2. pythonアプリケーション開発画面

作成したpythonスクリプトを開発PC上で直接実行することはできませんが、**IDE上ではソースコードを見るとかができ、かつ型を指定すればエディタのコード補完が効くのでそれほど不便は感じていません。**  

![pycharm_raspi_develop_pigpio.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3583120/c456c353-81fc-fc5b-fcd6-865b5a4c92d9.jpeg)


### 4. どうやって開発しているか?

#### 4-1. ネットで一番簡単なサンプルを実行

下記は非常に簡単な Lチカのサンプル

[Scientific Specialties: Blink LED with Python and pigpio Library](https://scispec.dev/2020/04/04/blink-led-with-python-and-pigpio-library-2/)
※クリーンアップしていないのでバッドサンプルです。

```led_blink.py```

```python
import pigpio
import time

pi = pigpio.pi()
pi.set_mode(5, pigpio.OUTPUT)
while (True):
  pi.write(5, True)
  time.sleep(0.5)
  pi.write(5, False)
  time.sleep(0.5)
```

さすがにクリーンアップしないとね。キーボードのCTRL+Cでクリーンアップするように修正
サンプルをラズパイの実機のコピーし、コマンドラインから実行します。

```python
import pigpio
import time

pi = pigpio.pi()
pi.set_mode(5, pigpio.OUTPUT)
try:
    while (True):
        pi.write(5, True)
        time.sleep(0.5)
        pi.write(5, False)
        time.sleep(0.5)
except KeyboardInterrupt:
    pass
finally:
    pi.write(5, False)
    pi.stop()
```

ここまでで動作が確認できたら、もうPycharm IDE で一気にプログラムを仕上げちゃいます。

#### 4-2. Pycharm IDE上でサンプルを発展させる

上記サンプルを元に、LEDピンとスリープ秒を指定できるように拡張します。

```led_blink_extention.py```

```python
import argparse
from collections import namedtuple
import pigpio as gpio
import time

PiParam = namedtuple("PiParam", ["use_pin", "sleep_sec"])

global pi
global param

def setup():
    # 出力モードに設定
    pi.set_mode(param.use_pin, gpio.OUTPUT)
    pi.write(param.use_pin, gpio.LOW)
    print("Using pin:{}".format(param.use_pin))


def cleanup():
    pi.write(param.use_pin, gpio.LOW)
    # Stop connection
    pi.stop()


def loop():
    while True:
        pi.write(param.use_pin, gpio.HIGH)
        time.sleep(param.sleep_sec)
        pi.write(param.use_pin, gpio.LOW)
        time.sleep(param.sleep_sec)


if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument("--use-pin", type=int, required=True)
    parser.add_argument("--sleep", type=float, default=1.0)
    args = parser.parse_args()
    print(args)

    # init pigpio connection
    pi = gpio.pi()
    param = PiParam(use_pin=args.use_pin, sleep_sec=args.sleep)
    setup()
    try:
        loop()
    except KeyboardInterrupt:
        cleanup()
```

参考までにラズパイゼロ(本番機)のUDPパケット受信プログラムはこちらになります。
**Pycharm IDE で開発するメリットは計り知れませんね。**


```python
import argparse
import logging
import os
import pigpio
import signal
import socket
import subprocess
import threading
import time
from datetime import datetime
from urllib.parse import quote_plus
import db.weatherdb as wdb
import util.file_util as FU
from mail.gmail_sender import GMailer
from lib.ht16k33 import Brightness, BUS_NUM
from lib.led4digit7seg import LED4digit7Seg, LEDCommon, LEDNumber
from lib.timeled7seg import LEDTime
from log import logsetting

"""
UDP packet monitor from ESP Weather sensors module
[UDP port] --port 2222
[GPIO Pin] --brightness-pin 17
Execute on system service
  sudo systemctl enable udp-weather-mon.service
  (user) pi
[connect]
  RaspberryPi: Raspi
  Bidirectional level conversion module for I2C bus: BusLvl
  LED matrix driver module: HT16k33
  [Raspi - HT16k33]
    VDD: 5V                           --|   
    VDD: 3v3    -> BusLvl.VREF1 -> BusLvl.VREF2 -> HT16k33.VDD
    GPIO2 (SDA) -> BusLvl.SDA1  -> BusLvl.SDA2  -> HT16k33.SDA
    GPIO3 (SCL) -> BusLvl.SCL1  -> BusLvl.SCL2  -> HT16k33.SCL
                                -> BusLvl.GND   -> HT16k33.GND
    GND                               --|
  [Tact button for brightness adjustment] Active HIGH (SW-ON: HIGH, SW-OFF: LOW) 
    GPIO17  <-> SW [1 (3)]
    VDD:3v3 <-> SW [2 (4)] 

  * If HT16k33 is not connected, only DB recording
"""

logger = logsetting.create_logger("service_weather")
base_dir = os.path.abspath(os.path.dirname(__file__))

# args option default
BRIGHTNESS_PIN = 17
WEATHER_UDP_PORT = 2222
DATA_I2C_ADDRESS = 0x71
TIME_I2C_ADDRESS = 0x70

BUFF_SIZE = 1024
# UDP packet receive timeout 12 minutes
RECV_TIMEOUT = 12. * 60
FMT_MAIL_CONTENT = "[{}] Weather sensors UDP packet is delayed, so the battery may be dead."
MAIL_SUBJECT = "Weather sensor UDP packet is delayed."

# Global instance
pi = None
# データ表示用LEDのHT16K33ドライバー
led_driver = None
# 時刻表示用LEDのHT16K33ドライバー
time_led_driver = None
udp_client = None
# Global flag
led_available = False
has_led_i2c_error =False
i2c_error_count = 0
# Callback brightness switch
cb_brightness = None
curr_brightness = None
# Prevent chattering
prev_tick = 0
THRESHOLD_DIFF_TICK = 500000
# for delayed notificaton
mailer = None
delayed_mail_sent = False
isLogLevelDebug = False
# Subprocess configuration
conf_subprocess = None

# Dict for next brightness: HIGH > MID > LOW > DIM
NextBrightness = {Brightness.HIGH: Brightness.MID,
                  Brightness.MID: Brightness.LOW,
                  Brightness.LOW: Brightness.DIM,
                  Brightness.DIM: Brightness.HIGH}


def mail_config():
    """
    Mail configuration
    :return: [is Sendmail: True or False], [subject], [content-template], [recipients]
    """
    conf_path = os.path.join(base_dir, "conf", "conf_sendmail.json")
    is_sendmail=True
    mail_conf = None
    if os.path.exists(conf_path):
        mail_conf = FU.read_json(conf_path)
        if isLogLevelDebug:
            logger.debug(mail_conf)
        is_sendmail = mail_conf["enable"]

    if is_sendmail:
        if mail_conf is not None:
            # Custom configuration
            if len(mail_conf["subject"]) > 0:
                subject = mail_conf["subject"]
            else:
                subject = MAIL_SUBJECT
            content_template = mail_conf["content-template"]
            recipients = mail_conf["recipients"]
        else:
            # Default configuration
            subject = MAIL_SUBJECT
            content_template = FMT_MAIL_CONTENT
            recipients = None

    if isLogLevelDebug:
        logger.debug("is_sendmail: {}\nsubject: {}\ncontent-template: {}\nrecipients: {}".format(
            is_sendmail, subject, content_template, recipients
        ))
    return is_sendmail, subject, content_template, recipients


def detect_signal(signum, frame):
    """
    Detect shutdown, and execute cleanup.
    :param signum: Signal number
    :param frame: frame
    """
    logger.info("signum: {}, frame: {}".format(signum, frame))
    if signum == signal.SIGTERM:
        # signal shutdown
        cleanup()
        # Current process terminate
        exit(0)


def has_i2cdevice(slave_addr):
    """
    Check connection with i2c device.
    :param slave_addr: i2c device address
    :return: if available True, not False.
    """
    handle = pi.i2c_open(BUS_NUM, slave_addr)
    if isLogLevelDebug:
        logger.debug("i2c_handle: {}".format(handle))
    is_available = False
    if handle >= 0:
        try:
            b = pi.i2c_read_byte(handle)
            if isLogLevelDebug:
                logger.debug("read_byte: {}".format(b))
            is_available = True
        except Exception as e:
            logger.warning("{}".format(e))
        finally:
            pi.i2c_close(handle)
    else:
        logger.warning("I2C[{}] is not available.".format(slave_addr))
    return is_available


def change_brightness(gpio_pin, level, tick):
    """
    Change LED brightness.
    :param gpio_pin: Brightness GPIO pin number
    :param level: level
    :param tick: tick time.
    """
    global curr_brightness, prev_tick
    if isLogLevelDebug:
        logger.debug("pin:{}, level:{}, tick: {}".format(gpio_pin, level, tick))
    if prev_tick != 0:
        tick_diff = pigpio.tickDiff(prev_tick, tick)
        if isLogLevelDebug:
            logger.debug("tick_diff:{}".format(tick_diff))
        if tick_diff < THRESHOLD_DIFF_TICK:
            if isLogLevelDebug:
                logger.debug("tick_diff:{} < {} return".format(tick_diff, THRESHOLD_DIFF_TICK))
            return

    prev_tick = tick
    next_brightness = NextBrightness[curr_brightness]
    if curr_brightness != next_brightness:
        curr_brightness = next_brightness
        # データ表示LEDs
        led_driver.set_brightness(next_brightness)
        # 時刻表示LED
        time_led_driver.set_brightness(next_brightness)


def setup_gpio():
    """ Tact button for brightness adjustment. """
    global cb_brightness
    pi.set_mode(BRIGHTNESS_PIN, pigpio.INPUT)
    pi.write(BRIGHTNESS_PIN, pigpio.LOW)
    # Set internal pull-down register.
    pi.set_pull_up_down(BRIGHTNESS_PIN, pigpio.PUD_DOWN)
    # SW OFF(LOW) -> ON(HIGH)
    cb_brightness = pi.callback(BRIGHTNESS_PIN, pigpio.RISING_EDGE, change_brightness)


def subproc_displaymessage(reason_key, at_datetime):
    """
    Display delayed message on Subprocess Display message script.
    :param reason_key: happen reason key
    :param at_datetime: happen at datetime
    """
    msg = conf_subprocess["displayMessage"]["msgfmt"][reason_key].format(at_datetime)
    encoded = quote_plus(msg)
    if isLogLevelDebug:
        logger.debug("encoded: {}".format(encoded))
    script = conf_subprocess["displayMessage"]["script"]
    exec_status = subprocess.run([script, "--encoded-message", encoded])
    logger.warning("Subprocess DisplayMessage terminated: {}".format(exec_status))


def subproc_playmelody():
    """
    Play the melody as an alert on Subprocess Play melody script.
    """
    script = conf_subprocess["playMelody"]["script"]
    exec_status = subprocess.run([script])
    logger.warning("Subprocess PlayMelody terminated: {}".format(exec_status))


def send_mail(subject, content, recipients):
    """
    Send notification to recipients
    :param subject:
    :param content:
    :param recipients:
    """
    try:
        # Use application passwd (for 2 factor authenticate)
        mailer.sendmail(subject, content, recipients)
        logger.warning("Mail sent to -> {}".format(recipients))
    except Exception as err:
        logger.warning(err)


def cleanup():
    """ GPIO cleanup, and UDP client close. """
    if cb_brightness is not None:
        cb_brightness.cancel()
    if led_driver is not None:
        # Check i2c connect.
        if not has_led_i2c_error:
            led_driver.cleanup()
    if time_led_driver is not None:
        if not has_led_i2c_error:
            time_led_driver.cleanup()
    pi.stop()
    udp_client.close()


def data_led_standby():
    """ Initialize all LEDs to '----' """
    led_driver.printOutOfRange(led_num=LEDNumber.N1)
    led_driver.printOutOfRange(led_num=LEDNumber.N2)
    led_driver.printOutOfRange(led_num=LEDNumber.N3)
    led_driver.printOutOfRange(led_num=LEDNumber.N4)


def led_show_i2cerror():
    """ Raise exception to 'EEEE' """
    led_driver.printError(led_num=LEDNumber.N1)
    led_driver.printError(led_num=LEDNumber.N2)
    led_driver.printError(led_num=LEDNumber.N3)
    led_driver.printError(led_num=LEDNumber.N4)


def isfloat(val):
    """
    Check float value.
    :param val: float value
    :return: True if float else False.
    """
    try:
        float(val)
    except ValueError:
        return False
    else:
        return True


def valid_temp(val):
    """
    Check temperature value.
    Numerial and valid range: -40.0 〜 50.0
    :param val: temperature value
    :return: True if valid temperature range else False.
    """
    if val < -40.0 or val > 50.0:
        return False
    return True


def valid_humid(val):
    """
    Check humidity value.
    Numerial and valid range: 0.0 〜 100.0 (%)
    :param val: humidity value
    :return: True if valid humidity range else False.
    """
    if val < 0.0 or val > 100.0:
        return False
    return True


def valid_pressure(val):
    """
    Check pressure value.
    Numerial and valid range: 900 〜 1100 hpa
    :param val: pressure value
    :return: True if valid pressure range else False.
    """
    if val < 900 or val > 1100:
        return False
    return True


def output_data_leds(temp_out, temp_in, humid, pressure):
    """
    Output measurement values to LED.
    :param temp_out: Outer temperature value
    :param temp_in: Inner temperature value
    :param humid: Inner humidity value
    :param pressure: Inner pressure value
    """
    if not isfloat(temp_out):
        led_driver.printError(led_num=LEDNumber.N1)
    else:
        val = float(temp_out)
        if not valid_temp(val):
            led_driver.printOutOfRange(led_num=LEDNumber.N1)
        else:
            led_driver.printFloat(val, led_num=LEDNumber.N1)
    # LED2: Inner temperature
    if not isfloat(temp_in):
        led_driver.printError(led_num=LEDNumber.N2)
    else:
        val = float(temp_in)
        if not valid_temp(val):
            led_driver.printOutOfRange(led_num=LEDNumber.N2)
        else:
            led_driver.printFloat(val, led_num=LEDNumber.N2)
    # LED3: humidity
    if not isfloat(humid):
        led_driver.printError(led_num=LEDNumber.N3)
    else:
        val = float(humid)
        if not valid_humid(val):
            led_driver.printOutOfRange(led_num=LEDNumber.N3)
        else:
            led_driver.printFloat(val, led_num=LEDNumber.N3)
    # LED4: pressure(interger)
    if not isfloat(pressure):
        led_driver.printError(led_num=LEDNumber.N4)
    else:
        val = float(pressure)
        val = int(round(val, 0))
        if not valid_pressure(val):
            led_driver.printOutOfRange(led_num=LEDNumber.N4)
        else:
            led_driver.printInt(val, led_num=LEDNumber.N4)


def loop(client):
    """
    Infinit UDP packet monitor loop.
    :param client: UDP socket
    """
    global delayed_mail_sent, has_led_i2c_error, i2c_error_count
    # Rest I2C Error flag.
    has_led_i2c_error = False
    server_ip = ''
    # Timeout setting
    client.settimeout(RECV_TIMEOUT)
    while True:
        try:
            data, addr = client.recvfrom(BUFF_SIZE)
        except socket.timeout:
            logger.warning("Socket timeout!")
            now = datetime.now()
            if delayed_mail_sent is not None:
                if conf_subprocess["playMelody"]["enable"]:
                    melody_thread = threading.Thread(target=subproc_playmelody)
                    logger.warning("Subprocess PlayMelody thread start.")
                    melody_thread.start()
                if conf_subprocess["displayMessage"]["enable"]:
                    # Generate short message for Raspberry Pi zero (not scroll)
                    time_now = now.strftime('%H:%M')
                    display_thread = threading.Thread(target=subproc_displaymessage, args=("udp-delay", time_now,))
                    logger.warning("Subprocess DisplayMessage thread start.")
                    display_thread.start()

                # Send Gmail: Read every occurence
                (is_sendmail, subject, content_template, recipients) = mail_config()
                if is_sendmail:
                    delayed_now = now.strftime('%Y-%m-%d %H:%M')
                    content = content_template.format(delayed_now)
                    mail_thread = threading.Thread(target=send_mail, args=(subject, content, recipients,))
                    logger.warning("Mail Thread start.")
                    mail_thread.start()
                delayed_mail_sent = True
            continue

        if server_ip != addr:
            server_ip = addr
            logger.info("server ip: {}".format(server_ip))

        # Reset flag.
        if delayed_mail_sent:
            delayed_mail_sent = False
        # From ESP output: device_name, temp_out, temp_in, humid, pressure
        line = data.decode("utf-8")
        if isLogLevelDebug:
            logger.debug(line)
        record = line.split(",")
        # Insert weather DB with local time
        # unix_tmstmp = int(time.time())   # time.time() is UTC unix epoch
        local_time = time.localtime()
        unix_tmstmp = time.mktime(local_time)
        wdb.insert(*record, measurement_time=unix_tmstmp, logger=logger)

        # output 4Digit7SegLED
        if led_available:
            try:
                # 測定時刻LED: Unix Timestampから時刻部分を表示
                time_led_driver.printTime(unix_tmstmp)
                # データ表示LED(4個)
                output_data_leds(record[1], record[2], record[3], record[4])
            except Exception as i2c_err:
                has_led_i2c_error = True
                logger.warning(i2c_err)
                # Display at onece.
                if i2c_error_count == 0:
                    # Show I2C Error at once.
                    try:
                        led_show_i2cerror()
                    except Exception as e:
                        pass
                    # Output OLED display
                    time_now = now.strftime('%H:%M')
                    display_thread = threading.Thread(target=subproc_displaymessage, args=("i2c-error", time_now,))
                    logger.warning("Subprocess DisplayMessage thread start.")
                    display_thread.start()
                i2c_error_count += 1
                logger.warning("I2C error count: {} at {}".format(i2c_error_count, time_now))


if __name__ == '__main__':
    pi = pigpio.pi()
    if not pi.connected:
        logger.warning("pigpiod not stated!")
        exit(1)

    isLogLevelDebug = logger.getEffectiveLevel() <= logging.DEBUG
    parser = argparse.ArgumentParser()
    parser.add_argument("--udp-port", type=int, default=WEATHER_UDP_PORT, help="Port from UDP Server[ESPxx].")
    parser.add_argument("--brightness-pin", type=int, default=BRIGHTNESS_PIN, help="Brightness change SW Pin.")
    args = parser.parse_args()
    logger.info(args)

    signal.signal(signal.SIGTERM, detect_signal)
    BRIGHTNESS_PIN = args.brightness_pin

    # Configuration
    app_conf = FU.read_json(os.path.join(base_dir, "conf", "conf_udpmon.json"))
    if isLogLevelDebug:
        logger.debug(app_conf)
    # UDP receive timeout.
    RECV_TIMEOUT = float(app_conf["monitor"]["recv_timeout_seconds"])
    # Subprocess configration
    conf_subprocess = app_conf["subprocess"]

    # GMailer
    mailer = GMailer(logger=logger)

    hostname = socket.gethostname()
    # Receive broadcast
    broad_address = ("", args.udp_port)
    logger.info("{}: {}".format(hostname, broad_address))
    # UDP client
    udp_client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    udp_client.bind(broad_address)
    # HT16k33 connection check
    led_available = has_i2cdevice(DATA_I2C_ADDRESS)
    # LED for displaying measurement data
    if led_available:
        curr_brightness = Brightness.HIGH
        # 測定値表示LED
        led_driver = LED4digit7Seg(
            pi,
            DATA_I2C_ADDRESS,
            common=LEDCommon.CATHODE,
            brightness=curr_brightness,
            logger=None
        )
        led_driver.clear_memory()
        # 測定時刻(パケット到着時刻)表示LED
        time_led_driver = LEDTime(
            pi,
            TIME_I2C_ADDRESS,
            brightness=curr_brightness,
            logger=None
        )
        time_led_driver.clear_memory()
        # スタンバイ表示
        # 測定値表示LEDs
        data_led_standby()
        # 時刻表示LDE
        time_led_driver.printStandby()
        # GPIO pin setting for brightness adjustment
        setup_gpio()
    try:
        loop(udp_client)
    except KeyboardInterrupt:
        pass
    except Exception as err:
        logger.error(err)
    finally:
        cleanup()
```

開発PCで開発したラズパイゼロ (本番機) 向けのソースコードは下記GitHubリポジトリで公開しております。
[GitHub(pipito-yukio) home_wather_sensers/raspi_zero](https://github.com/pipito-yukio/home_weather_sensors/tree/master/raspi_zero)