忘れ物チェックパネルの作り方

[必要なものを買う]

  1. 7インチタッチパネル
    チェックボタンをタッチして忘れ物がないかチェックするようにします。
  2. Raspberry Pi 3 model B
    最近は価格が高騰してしまってます。
  3. USB電源アダプター
    Raspberry Piに電源アダプタがついていない場合、別で購入します。
  4. SDカード
    Raspberry Piを起動するOSを入れるために必要。
  5. ハードディスク
    デフォルトだとSDカードにすべてのデータを保存しますが、ハードディスクを使うようにします。
  6. プッシュボタンスイッチ
    タッチパネルのスリープボタンとして、外部のスイッチが必要です。
  7. ジャンパワイヤー
    外部スイッチの接続などに使います。

[起動SDカードを準備する]

  1. Raspberry Pi Imagerをインストールする
    https://www.raspberrypi.com/software/から、Raspberry Pi Imagerをダウンロードしてインストールします。
  2. OSを選ぶ
    Raspberry Pi OS (32-bit)を選びます。
  3. 設定をする
    Wifiなどの設定をします。Raspberry Pi Imagerを使えば、最初からネットワーク接続可能なOSイメージを作成できます。SSHを有効化しておけば、最初からネットワーク経由でPCからアクセスできます。



  4. microSDカードに書き込む。

[起動する]

  1. SDカードを挿入する
  2. タッチパネルを接続する
    HDMIケーブル、USBケーブルでRaspberry Piとタッチパネルを接続します。有線LANでネット接続する場合はLANケーブルも接続します。USBキーボードも接続します。
  3. 電源を入れる
    電源ON/OFFは注意が必要で、USBケーブルでタッチパネルとつなげている関係で、タッチパネルの電源がRaspberry Piの電源にまわりこんでしまうため、両方の電源をOFFにしないと、Raspberry Piが完全にOFFになりません。ただし、後からタッチパネルをONにしても画面サイズなどの最適化がされないので、Raspberry Piの電源を先に接続し、すぐにタッチパネルの電源を接続します。
  4. Raspberry Pi画面へのアクセスを許可する
    デフォルトでは、リモートでの画面へのアクセスは許可されていません。xhostコマンドで許可します。

    pi@raspberrypi:~ $ xhost +
  5. PCにターミナルソフトをインストールする
    Tera TermなどのSSHログインのできるソフトをインストールします。
  6. UNIX上でのテキストファイル編集方法
    テキストファイルの編集方法は、こちらhttps://atmarkit.itmedia.co.jp/ait/articles/1712/06/news013.htmlなどを参考に。以下なんどもテキストファイルの編集が必要になります。また、sudoコマンドを最初につけることにより、スーパーユーザーの権限が必要なコマンドを実行することができます。sudo vi [ファイル名]、でスーパーユーザー権限でテキストの編集が出来ます。
  7. ターミナルソフトからSSH接続する
    確認したIPアドレスに、PCからSSH接続します。

    ログインIDとパスワードはSDカードイメージを作成したときに設定したものです。

[保存先をSDカードからハードディスクに切り替える]

  1. ハードディスクを接続する
    ハードディスクをUSB接続します。PCのように、接続しただけで使えるようにはなりません。
  2. 接続を確認します
    pi@raspberrypi:~ $ lsusb
    Bus 001 Device 004: ID 0411:02cc BUFFALO INC. (formerly MelCo., Inc.) HD-EDS-A
    Bus 001 Device 003: ID 0424:ec00 Microchip Technology, Inc. (formerly SMSC) SMSC9512/9514 Fast Ethernet Adapter
    Bus 001 Device 002: ID 0424:9514 Microchip Technology, Inc. (formerly SMSC) SMC9514 Hub
    Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
  3. パーティションの確認
    sudo fdisk -lでパーティションを確認できます。(一部省略)
    pi@raspberrypi:~ $ sudo fdisk -l
    
    Disk /dev/mmcblk0: 3.8 GiB, 4008706048 bytes, 7829504 sectors
    
    Device         Boot  Start     End Sectors  Size Id Type
    /dev/mmcblk0p1        8192  532479  524288  256M  c W95 FAT32 (LBA)
    /dev/mmcblk0p2      532480 7829503 7297024  3.5G 83 Linux
    
    Disk /dev/sda: 1.8 TiB, 2000398934016 bytes, 3907029168 sectors
    
    Device     Boot Start        End    Sectors  Size Id Type
    /dev/sda1          64 3907029119 3907029056  1.8T  7 HPFS/NTFS/exFAT
    
  4. パーティションの変更
    sudo fdisk /dev/sda でハードディスクのパーティション変更を開始します。ハードディスクの内容は全て消去されます。
    pi@raspberrypi:~ $ sudo fdisk /dev/sda
    Welcome to fdisk (util-linux 2.33.1).
    Changes will remain in memory only, until you decide to write them.
    Be careful before using the write command.
    
    Command : d で既存のパーティションを消します。
    Command (m for help): d
    Selected partition 1
    Partition 1 has been deleted.
    Command : n で1つ目のパーティションを作ります。
    Command (m for help): n
    Partition type
       p   primary (0 primary, 0 extended, 4 free)
       e   extended (container for logical partitions)
    Select (default p): p
    Partition number (1-4, default 1): 
    First sector (2048-3907029167, default 2048): 
    Last sector, +/-sectors or +/-size{K,M,G,T,P} (2048-3907029167, default 3907029167): +800G
    
    Created a new partition 1 of type 'Linux' and of size 800 GiB.
    
    ファイルサーバ用に、2つ目のパーティションを作ります。2つ目を作らない場合、1つ目のLast sectorの入力をせずEnterでdefaultのままにします。
    
    Command (m for help): n
    Partition type
       p   primary (1 primary, 0 extended, 3 free)
       e   extended (container for logical partitions)
    Select (default p): p
    Partition number (2-4, default 2): 
    First sector (1677723648-3907029167, default 1677723648): 
    Last sector, +/-sectors or +/-size{K,M,G,T,P} (1677723648-3907029167, default 3907029167): 
    
    Created a new partition 2 of type 'Linux' and of size 1 TiB.
    Command : w で変更を確定して書き込みます。これでハードディスクの中身は全部消えますので注意。
    
    Command (m for help): w
    The partition table has been altered.
    Calling ioctl() to re-read partition table.
    Syncing disks.
  5. フォーマット
    Linuxのファイルシステムext4でフォーマットします。(/dev/sda2も同様)
    
    pi@raspberrypi:~ $ sudo mkfs.ext4 /dev/sda1
    mke2fs 1.44.5 (15-Dec-2018)
    Creating filesystem with 209715200 4k blocks and 52428800 inodes
    Filesystem UUID: 2124281e-6fb1-4dda-81a2-7fe990301dce
    Superblock backups stored on blocks: 
    	32768, 98304, 163840, 229376, 294912, 819200, 884736, 1605632, 2654208, 
    	4096000, 7962624, 11239424, 20480000, 23887872, 71663616, 78675968, 
    	102400000
    
    Allocating group tables:    0/6400         done
    Writing inode tables:    0/6400         done
    Creating journal (262144 blocks): done
    Writing superblocks and filesystem accounting information:    0/6400         done
    
  6. データのコピー
    SDカードに書き込んだRaspberry Pi OSは、パーティション/dev/mmcblk0p1がbootディレクトリ、/dev/mmcblk0p2がルートシステムファイルに割り当てられています。
    Device         Boot  Start     End Sectors  Size Id Type
    /dev/mmcblk0p1        8192  532479  524288  256M  c W95 FAT32 (LBA)
    /dev/mmcblk0p2      532480 7829503 7297024  3.5G 83 Linux
    
    bootディレクトリはSDカードのままにし、ルートファイルシステムをハードディスクへコピーします。パーティションまるごとクローンするのにddコマンドを使います。途中経過など表示してくれませんので止まったように見えますがしばらく時間かかります。
    pi@raspberrypi:~ $ sudo dd if=/dev/mmcblk0p2 of=/dev/sda1
    7297024+0 records in
    7297024+0 records out
    3736076288 bytes (3.7 GB, 3.5 GiB) copied, 904.685 s, 4.1 MB/s
    
    ファイルシステムのチェックと修正を行います。エラーがあると修正するか聞かれるのでyes。
    pi@raspberrypi:~ $ sudo e2fsck -f /dev/sda1
    e2fsck 1.44.5 (15-Dec-2018)
    rootfs: recovering journal
    Pass 1: Checking inodes, blocks, and sizes
    Pass 2: Checking directory structure
    Pass 3: Checking directory connectivity
    Pass 4: Checking reference counts
    Pass 5: Checking group summary information
    Free blocks count wrong (570755, counted=570785).
    Fix<y>? yes
    Free inodes count wrong (180829, counted=180895).
    Fix<y>? yes
    
    rootfs: ***** FILE SYSTEM WAS MODIFIED *****
    rootfs: 44449/225344 files (0.3% non-contiguous), 341343/912128 blocks
    
    クローンしたパーティションをハードディスク側のパーティションサイズに戻します。
    pi@raspberrypi:~ $ sudo resize2fs /dev/sda1
    resize2fs 1.44.5 (15-Dec-2018)
    Resizing the filesystem on /dev/sda1 to 209715200 (4k) blocks.
    The filesystem on /dev/sda1 is now 209715200 (4k) blocks long.
    
    ブート時にマウントするルートディレクトリをSDカード(/dev/mmcblk0p2)からハードディスク(/dev/sda1)に変更します。/boot/cmdline.txtのroot=PARTUUID=のところを変更します。
    pi@raspberrypi:~ $ cat /boot/cmdline.txt
    console=serial0,115200 console=tty1 root=PARTUUID=b4997375-01 rootfstype=ext4 elevator=deadline fsck.repair=yes rootwait
    
    /dev/sda1に対応するPARTUUIDは、blkidコマンドでわかります。
    pi@raspberrypi:~ $ sudo blkid
    /dev/mmcblk0p1: LABEL_FATBOOT="boot" LABEL="boot" UUID="AE82-4BC1" BLOCK_SIZE="512" TYPE="vfat" PARTUUID="6301e9a2-01"
    /dev/mmcblk0p2: LABEL="rootfs" UUID="6d2ff93e-eacd-415c-96d5-4611ad21e05f" BLOCK_SIZE="4096" TYPE="ext4" PARTUUID="6301e9a2-02"
    /dev/sda1: LABEL="rootfs" UUID="6d2ff93e-eacd-415c-96d5-4611ad21e05f" BLOCK_SIZE="4096" TYPE="ext4" PARTUUID="b4997375-01"
    /dev/sda2: UUID="05fa010d-bb53-4b06-9c5a-ad4832e07ec9" BLOCK_SIZE="4096" TYPE="ext4" PARTUUID="b4997375-02"
    

[ファイルサーバを作る]

  1. sambaをインストールする
    pi@raspberrypi:~ $ sudo apt install samba samba-common-bin
    (省略)
  2. fstabを編集する
    pi@raspberrypi:~ $ sudo vi /etc/fstab
    以下のように、/dev/mmcblk0p2(SDカードのルートディレクトリ用パーティション)に対応するPARTUUIDを/dev/sda1(ハードディスクのルートディレクトリ用パーティション)に差し替え、/dev/sda2(ハードディスクのファイルサーバ用パーティション)に対応するPARTUUIDを追加します。
    proc            /proc           proc    defaults          0       0
    PARTUUID=6301e9a2-01  /boot           vfat    defaults          0       2
    #PARTUUID=6301e9a2-02  /               ext4    defaults,noatime  0       1
    PARTUUID=b4997375-01  /               ext4    defaults,noatime  0       1
    PARTUUID=b4997375-02  /var/share      ext4    defaults,noatime  0       2
    # a swapfile is not a swap partition, no line here
    #   use  dphys-swapfile swap[on|off]  for that
  3. smb.confを編集する
    pi@raspberrypi:~ $ sudo vi /etc/samba/smb.conf
    以下を末尾に追加します。
    [samba]
        path = /var/share
        writable = yes
        guest ok = yes
        guest only = yes
        create mode = 0777
        directory mode = 0777
        share modes = yes
        hide unreadable = yes
  4. 再起動
    rebootコマンドで再起動します。
    pi@raspberrypi:~ $ sudo reboot
    これで、このハードディスクのパーティションをPCからファイルサーバとして使用することができます。¥¥192.168.1.24のようにIPアドレスをエクスプローラのアドレスに入力すると、「samba」というネットワークフォルダが見えるようになります。

    スマホからもアクセスできるので、写真の受け渡しなど便利です。

[監視カメラとディレクトリ共有する]

  1. 監視カメラを作る
    監視カメラの作り方にて別のRaspberry Piで共有ディレクトリを作成している場合に、その共有ディレクトリが見えるようにします。
  2. NFSをインストールする
    pi@raspberrypi:~ $ sudo apt install nfs-common
    (省略)
  3. マウントする
    監視カメラ側のディレクトリをmountコマンドでマウントします。IPアドレスは監視カメラの実際のIPアドレスに差し替えてください。port=2049はNFSで決められたポート番号なので固定です。これで、/mnt/cameraが出来て、監視カメラ側の/home/pi/cameraにアクセス可能になります。
    pi@raspberrypi:~ $ sudo mount -t nfs -o proto=tcp,port=2049 192.168.1.24:/ /mnt

[FTPサーバをインストールする]

  1. インストール
    PCからファイルを転送するときのため、FTPサーバを入れます。vsftpdをインストールします。インストール前にupdateとupgrade実施しておきます。
    pi@raspberrypi:~ $ sudo apt update
    (省略)
    pi@raspberrypi:~ $ sudo apt upgrade
    (省略)
    pi@raspberrypi:~ $ sudo apt install vsftpd
    (省略)
    /etc/vsftpd.confを以下変更します。たいていは、「#」でコメントアウト(無効化)されているだけなので、#を削除で有効になります。
    anonymous_enable=NO
    local_enable=YES
    write_enable=YES
    local_umask=022
    ascii_upload_enable=YES
    ascii_download_enable=YES
    chroot_local_user=YES
    chroot_list_enable=YES
    chroot_list_file=/etc/vsftpd.chroot_list
    テキストファイルで/etc/vsftpd.chroot_listを作成して1行だけユーザー名追加。デフォルトユーザー名ならpiの2文字だけのテキストファイル。
  2. リスタート&再起動
    serviceコマンドでリスタートし、rebootコマンドで再起動します。
    pi@raspberrypi:~ $ sudo service vsftpd restart
    Stopping FTP server: vsftpd.
    Starting FTP server: vsftpd.
    pi@raspberrypi:~ $ sudo reboot

[mpg321をインストールする]

  • mpg321はmp3ファイル再生に使用します。
    pi@raspberrypi:~ $ sudo apt install mpg321

[pygameをインストールする]

  • pygameはwavファイル再生に使用します。
    pi@raspberrypi:~ $ pip3 install pygame

[ディレクトリを作成する]

  • プログラム実行のホームディレクトリを/home/pi/Documents/python/home-std/としています。
    pi@raspberrypi:~ $ pi@raspberrypi:~ $ cd Documents/
    pi@raspberrypi:~/Documents $ mkdir python
    pi@raspberrypi:~/Documents $ cd python/
    pi@raspberrypi:~/Documents/python $ mkdir home-std
    pi@raspberrypi:~/Documents/python $ cd home-std/

[Open-Jtalkをインストールする]

  • インストール
    まず本体。
    pi@raspberrypi:~/Documents/python/home-std $ sudo apt install open-jtalk
    (省略)
    次に推奨パッケージ。
    pi@raspberrypi:~/Documents/python/home-std $ sudo apt install open-jtalk-mecab-naist-jdic hts-voice-nitech-jp-atr503-m001
    (省略)
    最後に声のサンプルデータのダウンロード。名工大の作成したもの。
    pi@raspberrypi:~/Documents/python/home-std $ wget https://sourceforge.net/projects/mmdagent/files/MMDAgent_Example/MMDAgent_Example-1.8/MMDAgent_Example-1.8.zip
    (省略)
    解凍して声のサンプルデータVoiceディレクトリを実行ディレクトリに置く
    pi@raspberrypi:~/Documents/python/home-std $ unzip MMDAgent_Example-1.8.zip
    pi@raspberrypi:~/Documents/python/home-std $ mv -i MMDAgent_Example-1.8/Voice .

[プッシュスイッチを接続する]

  • タッチパネルがスリープ中はタッチパネル以外の方法でスリープ解除できる必要があるので、以下のようにプッシュスイッチを接続します。GPIO4を入力信号として使用します。通常は内部でプルダウン0V入力、スイッチを押すと5V入力になります。

[メインプログラムをダウンロードする]

  • PCにFTPソフトをインストールする
    WinSCPなどのファイル転送のできるソフトをインストールします。
  • ダウンロード
    以下のファイルをダウンロードする。
    home.zip
    または、FTPソフトは使わず、Raspberry Piから直接ダウンロードすることもできます。
    pi@raspberrypi:~/Documents/python/home-std $ wget https://hinakin.main.jp/home.zip
  • FTPソフトからファイルを転送する
    作成したディレクトリ/home/pi/Documents/python/home-std/に、PCからhome.zipを転送します。

  • 解凍する
    unzipコマンドで解凍します。
    pi@raspberrypi:~/Documents/python/home-std $ unzip home.zip
    Archive:  home.zip
      inflating: home-std.service
      inflating: home.py
     extracting: home.sh
       creating: image/
     extracting: image/ame.png
      inflating: image/fullscreen-exit.png
      inflating: image/fullscreen.png
     extracting: image/hare.png
      inflating: image/kumori.png
     extracting: image/musicoff.png
     extracting: image/musicon.png
     extracting: image/wide0.png
     extracting: image/wide1.png
     extracting: image/wide2.png
     extracting: image/wide3.png
     extracting: image/wide4.png
     extracting: image/wide5.png
      inflating: image/wide6.png
     extracting: image/yuki.png
       creating: sound/
      inflating: sound/bomb.wav
      inflating: sound/poka.mp3

[home.pyの編集]

  • 全プログラムコード
    #!/usr/bin/env python3
    
    ###################################################################################################
    # <設定>
    #
    # ホームディレクトリ =========================================================================
    # このファイルを置くディレクトリ
    homedir = '/home/pi/Documents/python/home-std/'
    #
    # Google Calendar API ================================================================
    # 以下のPython クイックスタートを参照して、credentials.jsonをダウンロードし、ホームディレクトリに置くことで、Google Calendarと連携します。
    # 初回起動時はGoogleアカウントへログインが必要なため、Raspberry Pi本体から(リモートでssh経由ではなく)home.pyを起動してください。
    # chromeブラウザが起動するので、ログインをしてください。2回目以降は設定ファイル(token.pickle、token2.pickle)が出来てログイン不要になります。
    # https://developers.google.com/calendar/api/quickstart/python?hl=ja
    #
    # 子供 ==================================================================================
    # 名前を3人まで記入
    child=['一郎','二郎','三郎']
    # child=['一郎','二郎']
    # child=['一郎']
    #
    # お手伝い ===============================================================================
    # 子供の交替制お手伝い。子供の数と同じだけ設定すると、毎日順番に変わります。
    kakari=['テーブルふき','せんたくもの','そうじ']
    # kakari=['テーブルふき','せんたくもの']
    # kakari=['']
    #
    # チェックリスト ===============================================================================
    # list_morning : 平日朝
    # list_evening : 平日夕方
    # list_morning_holiday : 休日朝
    # list_evening_holiday : 休日夕方
    list_morning = ['着がえ','熱をはかる','歯をみがく','ハンカチ','きんちゃく','すいとう']
    list_evening = ['しゅくだい','プリント','鉛筆削る','歯をみがく']
    list_morning_holiday = ['着がえ','熱をはかる','歯をみがく']
    list_evening_holiday = ['うわばき','たいそうふく','たいそうふく','歯をみがく']
    #
    # Yahoo! 気象情報API URL ================================================================
    # 以下のcustom_yahooapiurlを設定すると有効になります。Yahoo!デベロッパーネットワークのご利用ガイドを参照してください。output=xmlとしてください。
    # custom_yahooapiurl = 'https://map.yahooapis.jp/weather/V1/place?appid=<あなたのappid>&coordinates=<緯度>,<経度>&output=xml'
    custom_yahooapiurl = ''
    #
    # 気象庁 天気予報エリア ===================================================================
    custom_jmaarea = '東京都'
    custom_jmasubarea = '東京地方'
    #
    # 音声 =================================================================================
    # 0 ... 男性
    # 1 ... 女性
    voice_select = 1
    #
    # 時刻をしゃべるONのときのしゃべる間隔 =========================================================
    # 単位:分
    speaktimespan = 5
    #
    # 自動ディスプレイON/OFF時刻 ===================================================================
    sleeph = 8 # AM 8:00以降、チェックボタン完了していればスリープ
    wake1h = 6 # Wake-1 6:30
    wake1m = 0
    wake2h = 15 # Wake-2 15:00
    wake2m = 0
    #
    # 監視カメラ連携 =========================================================================
    # 別のRaspberry Piによる監視カメラからの画像ファイル名データを受信できるようにする場合、homeenableを Trueにし、このRaspberry PiのIPアドレスと、使用するポート番号を合わせます。
    # 監視カメラの共有NFS(Network File Server)の画像ディレクトリをnfsdirに設定します。
    # 検出時のサウンドファイルをsoundディレクトリに入れて、detectsoundにファイル名を設定します。
    homeenable = True
    homeipaddress = '192.168.1.10'
    homeportaddress = 54321
    nfsdir = '/mnt/camera/large/'
    detectsound = 'poka.mp3'
    
    ###################################################################################################
    # インクルードファイル
    from tkinter import *
    from tkinter import ttk
    import RPi.GPIO as GPIO
    import json
    import requests
    import subprocess
    from time import strftime
    import datetime
    import urllib.request
    import xml.etree.ElementTree as ET
    import pickle
    import os.path
    from googleapiclient.discovery import build
    from google_auth_oauthlib.flow import InstalledAppFlow
    from google.auth.transport.requests import Request
    import threading
    import pygame.mixer
    import functools
    from time import sleep
    from PIL import Image, ImageTk
    import socket
    
    ###################################################################################################
    # GPIO設定
    # GPIO4をディスプレイON/OFFスイッチとして使用する。
    # ボード上のピン番号は7
    # Raspberry Pi内でプルダウンする。5V入力検出でディスプレイON/OFF切替とする。
    GPIO.setmode(GPIO.BCM)
    GPIO.setup(4, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
    
    ###################################################################################################
    # Tk設定・各種フラグ
    root = Tk()
    root.title('ホーム')
    root.attributes('-fullscreen', True)
    root.configure(bg='white')
    root.columnconfigure(0, weight=1)
    root.rowconfigure(0, weight=1)
    screen_width = root.winfo_screenwidth()
    screen_height = root.winfo_screenheight()
    
    displayon = True
    speaktime = False
    mute = False
    fullscreen = True
    
    ###################################################################################################
    # Grid Matrix
    #   | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10| 11| 12| 13| 14| 15| 16| 17| 18| 19| 20| 21| 22| 23|
    #---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
    #  0| rss0                                                                  |lb_rst |lb_mute|lb_full|
    #---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
    #  1|                                               | weather[0]            | weather[2]            |
    #---+ clock                                         +---+---+---+---+---+---+---+---+---+---+---+---+
    #  2|                                               | weather[1]            | weather[3]            |
    #---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
    #  3| rain[0]   | rain[1]   | rain[2]   | rain[3]   | rain[4]   | rain[5]   | rain[6]   |           |
    #---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
    #  4| lb_otetsudai[0]               | lb_otetsudai[1]               | lb_otetsudai[2]               |
    #---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
    #  5|lb_button[0][0]|lb_button[0][2]|lb_button[1][0]|lb_button[1][2]|lb_button[2][0]|lb_button[2][2]|
    #---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
    #  6|lb_button[0][1]|lb_button[0][3]|lb_button[1][1]|lb_button[1][3]|lb_button[2][1]|lb_button[2][3]|
    #---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
    colwidth=24
    rowheight=7
    frame1 = ttk.Frame(root)
    frame1.grid(sticky=(E,W,S,N))
    for col in range(colwidth):
        frame1.grid_columnconfigure(col, weight=1);
    for row in range(rowheight):
        frame1.grid_rowconfigure(row, weight=1);
    style = ttk.Style()
    style.theme_use('classic')
    style.configure('TFrame', background='white')
    
    ###################################################################################################
    # 監視カメラデータ受信
    def cameralisten():
        global homeipaddress, homeportaddress
        try:
            camerasocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            camerasocket.bind((homeipaddress, homeportaddress))
            camerasocket.listen(1)
            while True:
                clientsocket, address = camerasocket.accept()
                data = clientsocket.recv(1024)
                datatext = data.decode('utf-8')
                cameraon(datatext)
        except Exception as e:
            print(e)
        finally:
            camerasocket.close()
    
    ###################################################################################################
    # カメラ画像に切替
    cameraflag = 0
    camerawake = 0
    currentimage = PhotoImage(file=homedir+'/image/wide6.png')
    labelcamera = ttk.Label(frame1, background = 'white', font = ('Noto Sans Japanese',360 ,'bold'), foreground = 'red')
    def cameraon(filename):
        global detectsound,camerawake,displayon,labelcamera,cameraflag,mute,currentimage,nfsdir,rss0,lb_rst,lb_mute,lb_full,lb_clock,lb_weather,lb_rain,lb_otetsudai,lb_button,rowheight,colwidth
        cameraflag=60
        camerawake=0
        if not displayon:
            camerawake=60
            displayon=True
            subprocess.call('/usr/bin/vcgencmd display_power 1', shell=True)
        rss0.grid_remove()
        lb_rst.grid_remove()
        lb_mute.grid_remove()
        lb_full.grid_remove()
        lb_clock.grid_remove()
        for i,j in enumerate(lb_weather):
            lb_weather[i].grid_remove()
        for i,j in enumerate(lb_rain):
            lb_rain[i].grid_remove()
        for i,j in enumerate(lb_otetsudai):
            lb_otetsudai[i].grid_remove()
        for i,j in enumerate(lb_button):
            for m,n in enumerate(j):
                lb_button[i][m].grid_remove()
        labelcamera.grid(row=0, column=0, rowspan=rowheight, columnspan=colwidth)
        targetfile = nfsdir + filename
        img=Image.open(targetfile)
        img=img.resize((1368,768), Image.ANTIALIAS)
        currentimage = ImageTk.PhotoImage(img)
        labelcamera.config(image = currentimage)
        if not mute and camerawake==0:
            subprocess.call('/usr/bin/mpg321 '+homedir+'/sound/'+detectsound+' > /dev/null 2>&1', shell=True)
    
    ###################################################################################################
    # カメラ画像OFF
    def cameraoff():
        global labelcamera,cameraflag,rss0,lb_rst,lb_mute,lb_full,lb_clock,lb_weather,lb_rain,lb_otetsudai,lb_button
        cameraflag=0
        rss0.grid()
        lb_rst.grid()
        lb_mute.grid()
        lb_full.grid()
        lb_clock.grid()
        for i,j in enumerate(lb_weather):
            lb_weather[i].grid()
        for i,j in enumerate(lb_rain):
            lb_rain[i].grid()
        for i,j in enumerate(lb_otetsudai):
            lb_otetsudai[i].grid()
        for i,j in enumerate(lb_button):
            for m,n in enumerate(j):
                lb_button[i][m].grid()
        labelcamera.grid_remove()
    
    ###################################################################################################
    # 日付と天気image、気温
    lb_weather=[]
    lb_weather.append(ttk.Label(frame1, text = '10日', font = ('Noto Sans Japanese', 48, 'bold'), background = 'white', foreground = '#070'))
    lb_weather.append(ttk.Label(frame1, text = '11日', font = ('Noto Sans Japanese', 48, 'bold'), background = 'white', foreground = '#770'))
    lb_weather[0].grid(row=1, column=int(colwidth/2), columnspan=int(colwidth/4))
    lb_weather[1].grid(row=2, column=int(colwidth/2), columnspan=int(colwidth/4))
    lb_weather.append(ttk.Label(frame1, text = '5℃/15℃', font = ('Noto Sans Japanese', 48), background = 'white', foreground = '#070'))
    lb_weather.append(ttk.Label(frame1, text = '5℃/15℃', font = ('Noto Sans Japanese', 48), background = 'white', foreground = '#770'))
    lb_weather[2].grid(row=1, column=int(colwidth/2+colwidth/4), columnspan=int(colwidth/4))
    lb_weather[3].grid(row=2, column=int(colwidth/2+colwidth/4), columnspan=int(colwidth/4))
    ###################################################################################################
    # 降水量予報
    lb_rain=[]
    for i in range(8):
        lb_rain.append(ttk.Label(frame1, text = '', font = ('Noto Sans Japanese', 36), background = 'white', foreground = 'black'))
        lb_rain[i].grid(row=3, column=i*int(colwidth/8), columnspan=int(colwidth/8))
    ###################################################################################################
    # デジタル時計
    if speaktime:
        clock_color = "red"
    else:
        clock_color = "black"
    lb_clock = ttk.Label(frame1, font = ('FreeMono', 164 ,'bold'),background = 'white', foreground = clock_color, cursor='none')
    lb_clock.grid(row=1, column=0, rowspan=2, columnspan=int(colwidth/2))
    ###################################################################################################
    # RSS
    rss0 = ttk.Label(frame1, font = ('Noto Sans Japanese', 36), text = 'RSS', background = 'white', foreground = 'blue')
    rss0.grid(row=0, column=0, columnspan=int(colwidth/2+colwidth/4), sticky=(W))
    ###################################################################################################
    # チェックボタン定義
    lb_button=[]
    button_end=[]
    buttonimagenum=[]
    childcomplete=[]
    imgbutton=[]
    imgbutton.append(PhotoImage(file=homedir+'/image/wide0.png'))
    imgbutton.append(PhotoImage(file=homedir+'/image/wide1.png'))
    imgbutton.append(PhotoImage(file=homedir+'/image/wide2.png'))
    imgbutton.append(PhotoImage(file=homedir+'/image/wide3.png'))
    imgbutton.append(PhotoImage(file=homedir+'/image/wide4.png'))
    imgbutton.append(PhotoImage(file=homedir+'/image/wide5.png'))
    imgbutton.append(PhotoImage(file=homedir+'/image/wide6.png'))
    rowtemp=5
    coltemp=0
    lb_otetsudai=[]
    pygame.mixer.init()
    bombsound = pygame.mixer.Sound(homedir+'/sound/bomb.wav')
    
    ###################################################################################################
    # 爆発アニメーション
    def explosion(childnum, buttonnum):
        global buttonimagenum,button_end,mute,lb_button,childcomplete
        if buttonimagenum[childnum][buttonnum]<len(imgbutton):
            lb_button[childnum][buttonnum].config(image = imgbutton[buttonimagenum[childnum][buttonnum]])
            buttonimagenum[childnum][buttonnum]+=1
            lb_button[childnum][buttonnum].after(50, explosion, childnum, buttonnum)
        else:
            nextchk = getnextcheck(childnum)
            if nextchk != 'end':
                lb_button[childnum][buttonnum].config(image = imgbutton[0], text = nextchk)
            else:
                button_end[childnum][buttonnum]=True
                for button_end0 in button_end[childnum]:
                    if button_end0:
                        endcount+=1
                if endcount<len(button_end[childnum]) and not childcomplete[childnum]:
                    childcomplete[childnum] = True
    
    ###################################################################################################
    # チェックボタンクリック
    def clickbutton(event, childnum, buttonnum):
        global buttonimagenum,button_end,mute
        if pygame.mixer.music.get_busy()==False:
            buttonimagenum[childnum][buttonnum]=1
            event.widget['text']=''
            endcount = 0
            for button_end0 in button_end[childnum]:
                if button_end0:
                    endcount+=1
            if endcount<len(button_end[childnum]):
                if not mute:
                    free_channel = pygame.mixer.find_channel()
                    if free_channel != None:
                        free_channel.play(bombsound)
                explosion(childnum, buttonnum)
    
    ###################################################################################################
    # お手伝い取得
    def getotetsudai(num):
        global kakari ,child
        if len(child) <= num:
            return ''
        if len(child) > len(kakari):
            return child[num]
        basedate = datetime.datetime(2020,9,18)
        nowdate = datetime.datetime.now()
        diff = nowdate - basedate
        return str(child[num]+' '+kakari[(diff.days + num) % len(child)])
    
    ###################################################################################################
    # チェックボタン表示
    for i in range(int(len(child))):
        sublist=[]
        endbutton=[]
        buttonimagesub=[]
        coltemp = i*int(colwidth/len(child))
        rowtemp = 5
        coltemp2 = 0
        lb_otetsudai.append(ttk.Label(frame1, text = getotetsudai(i), font = ('Noto Sans Japanese', 34, 'bold'), background = 'white', foreground = '#339'))
        lb_otetsudai[i].grid(row=4, column=coltemp, columnspan=int(colwidth/len(child)), sticky=(S,N))
        for j in range(int(12/len(child))):
            sublist.append(ttk.Label(frame1, text = 't1', compound='center', font = ('Noto Sans Japanese', 24, 'bold'), image=imgbutton[0], background = 'white', foreground = 'white', cursor='none'))
            endbutton.append(False)
            buttonimagesub.append(1)
            sublist[j].grid(row=rowtemp, column=coltemp+coltemp2, columnspan=int(colwidth/6), sticky=(N))
            sublist[j].bind('<1>', functools.partial(clickbutton, childnum=i, buttonnum=j))
            if rowtemp == 5:
                rowtemp = 6
            else:
                rowtemp = 5
                coltemp2+=int(colwidth/6)
        lb_button.append(sublist)
        button_end.append(endbutton)
        buttonimagenum.append(buttonimagesub)
        childcomplete.append(False)
    
    ###################################################################################################
    # Yahoo! RSS News
    yahoonewsurl = 'https://news.yahoo.co.jp/rss/topics/top-picks.xml'
    rsstext = ''
    rsspos = 0
    
    ###################################################################################################
    # Yahoo! 気象情報API
    # 以下のサイトで提供されている、指定した緯度経度の60分後までの降水量予報を取得できるAPI。
    # https://developer.yahoo.co.jp/webapi/map/openlocalplatform/v1/weather.html
    # 10分ごとの予報を色付きで表示する。
    yahooapiurl = custom_yahooapiurl
    raintime=['','','','','','','','']
    rainfall=[0,0,0,0,0,0,0,0]
    rainmax=0
    
    ###################################################################################################
    # 降水量予報データ取得・表示
    def getBGcolor(rfstr, colstr):
        ret = 0
        rf = float(rfstr)
        if rf <= 2.0:
            ret = int(((2-rf)*255)/2)
        elif colstr=="R":
            ret = int((rf-2)*25)
        if colstr=="B":
            ret = 255
            if rf > 12.2:
                ret = int((22.5-rf)*25)
        if ret > 255:
            ret = 255
        if ret < 0:
            ret = 0
        return ret
    def getcolorforeground(rfstr):
        ret='white'
        if getBGcolor(rfstr,"R")+getBGcolor(rfstr,"G")+getBGcolor(rfstr,"B")>637:
           ret='black'
        return ret
    def getrain():
        global raintime, rainmax
        url = yahooapiurl
        if len(url) < 60:
            return
        try:
            xmldata = urllib.request.urlopen(url, timeout=5).read()
        except Exception as e:
            print(e)
            return
    
        rainmax=0
        root = ET.fromstring(xmldata)
        for ch1 in root:
            if ch1.tag.find('Feature') >= 0 :
                for ch2 in ch1:
                    if ch2.tag.find('Property') >= 0 :
                        for ch3 in ch2:
                            if ch3.tag.find('WeatherList') >= 0 :
                                for ch4 in ch3:
                                    if ch4.tag.find('Weather') >= 0 :
                                        for ch5 in ch4:
                                            if ch5.tag.find('Date') >= 0:
                                                raintime[rainmax] = ch5.text
                                            elif ch5.tag.find('Rainfall') >= 0:
                                                rainfall[rainmax] = ch5.text
                                        rainmax+=1
        for i in range(7):
            lb_rain[i].config(text = rainfall[i], background = '#%02X%02X%02X' % (getBGcolor(rainfall[i],"R"),getBGcolor(rainfall[i],"G"),getBGcolor(rainfall[i],"B")), foreground = getcolorforeground(rainfall[i]))
    
    ###################################################################################################
    # Google Calendar定義
    caltext = ''
    myevent=[]
    myeventwhen=[]
    myeventwho=[]
    myeventcolor=[]
    SCOPES = ['https://www.googleapis.com/auth/calendar.readonly']
    
    ###################################################################################################
    # 気象庁JSON定義
    jmaofficeurl = 'https://www.jma.go.jp/bosai/common/const/area.json'
    jmaurl = ''
    jmaofficedata = requests.get(jmaofficeurl)
    jmaofficedict = json.loads(jmaofficedata.text)
    for mykey in jmaofficedict['offices'].keys():
        if jmaofficedict['offices'][mykey]['name'] == custom_jmaarea:
            jmaurl = 'https://www.jma.go.jp/bosai/forecast/data/forecast/'+mykey+'.json'
    jma_weather = []
    jma_dateweather = []
    jma_datetemp = []
    jma_temp = []
    
    ame=PhotoImage(file=homedir+'/image/ame.png')
    yuki=PhotoImage(file=homedir+'/image/yuki.png')
    kumori=PhotoImage(file=homedir+'/image/kumori.png')
    hare=PhotoImage(file=homedir+'/image/hare.png')
    
    ###################################################################################################
    # 気象庁予報データ取得
    def get_jma_weather():
        global jmaurl, jma_weather, jma_dateweather, jma_datetemp, jma_temp, custom_jmasubarea
        if jmaurl == '':
            return
        jma_weather.clear()
        jma_dateweather.clear()
        jma_datetemp.clear()
        jma_temp.clear()
        try:
            jmadata = requests.get(jmaurl)
            jmadict = json.loads(jmadata.text)
            areaindex=0
            for myarea in jmadict[0]['timeSeries'][0]['areas']:
                if myarea['area']['name'] == custom_jmasubarea:
    
                    for myweather in jmadict[0]['timeSeries'][0]['areas'][areaindex]['weathers']:
                        jma_weather.append(myweather)
                    for mydateweather in jmadict[0]['timeSeries'][0]['timeDefines']:
                        jma_dateweather.append(mydateweather[8:10])
                        
                    for mydatetemp in jmadict[0]['timeSeries'][2]['timeDefines']:
                        jma_datetemp.append(mydatetemp[8:10])
                    for mytemp in jmadict[0]['timeSeries'][2]['areas'][areaindex]['temps']:
                        jma_temp.append(mytemp)
    
                areaindex+=1
        except Exception as e:
            print(e)
            return -1
        return min(len(jma_weather),len(jma_dateweather))
    
    ###################################################################################################
    # 気象庁予報データ表示
    def set_jma_weather():
        global lb_weather, hare, kumori, yuki, ame
        if get_jma_weather() >= 2:
            datenum=0
            for date0, weather0 in zip(jma_dateweather, jma_weather):
                lb_weather[datenum].config(text = date0+'日', compound='right')
                if weather0.find('雪') >= 0:
                    lb_weather[datenum].config(image = yuki)
                elif weather0.find('雨') >= 0:
                    lb_weather[datenum].config(image = ame)
                elif weather0.find('くもり') >= 0:
                    lb_weather[datenum].config(image = kumori)
                elif weather0.find('晴') >= 0:
                    lb_weather[datenum].config(image = hare)
            
                tempstr='-'
                for date1, temp1 in zip(jma_datetemp, jma_temp):
                    if date1 == date0:
                        if len(tempstr) > 1:
                            tempstr=tempstr+temp1+'℃'
                        else:
                            tempstr=temp1+'℃/'
                lb_weather[datenum+2].config(text = tempstr)
                datenum+=1
                if datenum >= 2:
                    break
    
    ###################################################################################################
    # Open JTalk実行
    # str_speak ... スピーチテキスト
    def exe_openjtalk(str_speak):
        global voice_select, homedir
        m = voice_select
        str='echo "'+str_speak+'" | /usr/bin/open_jtalk -g 10 -x /var/lib/mecab/dic/open-jtalk/naist-jdic '
        if m == 0:
            str+='-m '+homedir+'/Voice/takumi/takumi_normal.htsvoice '
        else:
            str+='-m '+homedir+'/Voice/mei/mei_normal.htsvoice '
        str+='-ow /dev/stdout | /usr/bin/aplay > /dev/null 2>&1'
        subprocess.call(str, shell=True)
    
    def speak_time():
        nowdate = datetime.datetime.now()
        h = nowdate.hour
        if h>12:
            h=h-12
        exe_openjtalk("%d時%d分 %d時%d分" % (h, nowdate.minute, h, nowdate.minute))
        
    ###################################################################################################
    # Yahoo! RSS News データ取得
    def getnews():
        global rsstext,caltext,rsspos
        url = yahoonewsurl
    
        try:
            xmldata = urllib.request.urlopen(url, timeout=5).read()
            rsstext = '          '
            root = ET.fromstring(xmldata)
            for ch1 in root:
                if ch1.tag.find('channel') >= 0 :
                    for ch2 in ch1:
                        if ch2.tag.find('item') >= 0 :
                            for ch3 in ch2:
                                if ch3.tag.find('title') >= 0:
                                    rsstext += ch3.text + ' '
            rsstext = caltext + rsstext
            rsspos = 0
        except Exception as e:
            print(e)
    
    ###################################################################################################
    # Googleカレンダーデータ取得
    def getcalendar():
        global caltext, homedir
        creds = None
        if not os.path.exists(homedir+'/credentials.json'):
            return
        if os.path.exists(homedir+'/token.pickle'):
            with open(homedir+'/token.pickle', 'rb') as token:
                creds = pickle.load(token)
        if not creds or not creds.valid:
            if creds and creds.expired and creds.refresh_token:
                creds.refresh(Request())
            else:
                flow = InstalledAppFlow.from_client_secrets_file(
                    homedir+'/credentials.json', SCOPES)
                creds = flow.run_local_server(port=0)
            with open(homedir+'/token.pickle', 'wb') as token:
                pickle.dump(creds, token)
    
        service = build('calendar', 'v3', credentials=creds)
    
        now = datetime.datetime.utcnow().isoformat() + 'Z' # 'Z' indicates UTC time
        events_result = service.events().list(calendarId='primary', timeMin=now,
                                            maxResults=10, singleEvents=True,
                                            orderBy='startTime').execute()
        events = events_result.get('items', [])
    
        myweek=['月','火','水','木','金','土','日']
        pretext = ''
        caltext = ''
        for event in events:
            start = event['start'].get('dateTime', event['start'].get('date'))
            d = datetime.datetime(int(start[0:4]), int(start[5:7]), int(start[8:10]))
            addtext = '%2d月%2d日(%s)' % (d.month, d.day, myweek[d.weekday()])
            if pretext == addtext: # 複数日期間のイベント
                caltext += ' '
            else:
                caltext += ' '+addtext
            caltext += event['summary']
            pretext = addtext
    
    ###################################################################################################
    # Googleカレンダー休日情報取得
    hmon=[1,1]
    hday=[1,2]
    def getholiday():
        global hmon, hday, homedir
        creds = None
        if not os.path.exists(homedir+'/credentials.json'):
            return
        if os.path.exists(homedir+'/token2.pickle'):
            with open(homedir+'/token2.pickle', 'rb') as token:
                creds = pickle.load(token)
        if not creds or not creds.valid:
            if creds and creds.expired and creds.refresh_token:
                creds.refresh(Request())
            else:
                flow = InstalledAppFlow.from_client_secrets_file(
                    homedir+'/credentials.json', SCOPES)
                creds = flow.run_local_server(port=0)
            with open(homedir+'/token2.pickle', 'wb') as token:
                pickle.dump(creds, token)
    
        service = build('calendar', 'v3', credentials=creds)
    
        now = datetime.datetime.utcnow().isoformat() + 'Z' # 'Z' indicates UTC time
        events_result = service.events().list(calendarId='ja.japanese#holiday@group.v.calendar.google.com', timeMin=now,
                                            maxResults=2, singleEvents=True,
                                            orderBy='startTime').execute()
        events = events_result.get('items', [])
    
        for event in events:
            start = event['start'].get('dateTime', event['start'].get('date'))
            hmon.append(int(start[5:7]))
            hday.append(int(start[8:10]))
    
    ###################################################################################################
    # 平日フラグ
    # daytomorrow : 今日0 / 明日1
    def isweekday(daytomorrow):
        dtnow = datetime.datetime.now() + datetime.timedelta(days=daytomorrow)
        isweek = True
        for hmon0, hday0 in zip(hmon, hday):
            if dtnow.month == hmon0 and dtnow.day == hday0:
                isweek = False
        if dtnow.weekday() >=5:
            isweek = False
        return isweek
    
    ###################################################################################################
    # チェックリスト初期化
    chklist=[]
    def initchklist():
        global chklist, list_morning, list_evening, list_morning_holiday, list_evening_holiday, child, childcomplete
        chklist.clear()
        dtnow = datetime.datetime.now()
        sublist=[]
        if dtnow.hour > 12:                       # 午後
            if isweekday(0) == True:              # 今日平日
                for evening in list_evening:
                    sublist.append(evening)
            else:                                 # 今日休日
                for evening_holiday in list_evening_holiday:
                    sublist.append(evening_holiday)
        else:                                     # 午前
            if isweekday(0) == True:              # 今日平日
                for morning in list_morning:
                    sublist.append(morning)
            else:                                 # 今日休日
                for morning_holiday in list_morning_holiday:
                    sublist.append(morning_holiday)
        for i,child0 in enumerate(child):
            chklist.append(sublist)
            childcomplete[i] = False
    
    ###################################################################################################
    # 次のチェックアイテム取得
    chknum=[]
    for i in child:
        chknum.append(0)
    def getnextcheck(childnum):
        global chknum,chklist
        ret='end'
        if chknum[childnum] < len(chklist[childnum]):
            ret = chklist[childnum][chknum[childnum]]
            chknum[childnum]+=1
        return ret
        
    ###################################################################################################
    # チェックボタン初期化
    def inittext():
        global chknum, button_end, imgbutton, lb_button
        initchklist()
        for k,chknum0 in enumerate(chknum):
            chknum[k]=0
        for i,button_end0 in enumerate(button_end):
            for j,button_end0sublist in enumerate(button_end0):
                button_end[i][j] = False
        for i,lb_button0 in enumerate(lb_button):
            for j,lb_button0sublist in enumerate(lb_button0):
                tempstr = getnextcheck(i)
                if tempstr == 'end':
                    button_end[i][j] = True
                    lb_button[i][j].config(image = imgbutton[6], text = "")
                else:
                    lb_button[i][j].config(image = imgbutton[0], text = tempstr)
    
    ###################################################################################################
    # 時刻をしゃべる切替
    def togglespeak(self):
        global speaktime, lb_clock, mute
        if not mute:
            if not speaktime:
                speaktime = True
                lb_clock.config(foreground = 'red')
                if threading.active_count()<=2:
                    th1 = threading.Thread(target=speak_time)
                    th1.start()
            else:
                lb_clock.config(foreground = 'black')
                speaktime = False
    lb_clock.bind('<1>', togglespeak)
    
    ###################################################################################################
    # ミュート
    musicon=PhotoImage(file=homedir+'/image/musicon.png')
    musicoff=PhotoImage(file=homedir+'/image/musicoff.png')
    def mutetoggle(self):
        global mute,lb_clock,speaktime,lb_mute
        if mute == False:
            mute = True
            lb_clock.config(foreground = '#090')
            lb_mute.config(image = musicoff)
            speaktime = False
        else:
            lb_clock.config(foreground = 'black')
            lb_mute.config(image = musicon)
            mute = False
    lb_mute = ttk.Label(frame1, font = ('Noto Serif CJK JP', 20), text = ' ', background = 'white', image = musicon, compound='center', cursor='none')
    lb_mute.grid(row=0, column=20, columnspan=2, sticky=(S,N))
    lb_mute.bind('<1>', mutetoggle)
    
    ###################################################################################################
    # リセット
    def resetbutton(self):
        inittext()
    lb_rst = ttk.Label(frame1, font = ('Noto Sans Japanese', 20), text = 'リセット', background = 'white', cursor='none')
    lb_rst.grid(row=0, column=18, columnspan=2, sticky=(S,N))
    lb_rst.bind('<1>', resetbutton)
    
    ###################################################################################################
    # ウィンドウ全画面
    imgfull=PhotoImage(file=homedir+'/image/fullscreen.png')
    imgfullexit=PhotoImage(file=homedir+'/image/fullscreen-exit.png')
    def togglefullscreen(self):
        global root,fullscreen,lb_full
        fullscreen = not fullscreen
        root.attributes('-fullscreen', fullscreen)
        if fullscreen == True:
            lb_full.config(image = imgfullexit)
        else:
            lb_full.config(image = imgfull)
    lb_full = ttk.Label(frame1, font = ('Noto Serif CJK JP', 20), text = ' ', background = 'white', image = imgfullexit, compound='center', cursor='none')
    lb_full.grid(row=0, column=22, columnspan=2, sticky=(S,N))
    lb_full.bind('<1>', togglefullscreen)
    
    ###################################################################################################
    # メインタイマー処理
    main_counter=0
    time_diff=False
    chkprehour=0
    chkpreminute=0
    chkpresecond=0
    closecount = 0
    def main_timer():
        global detectsound, homedir, mute,chkprehour, chkpreminute, chkpresecond, lb_clock, time_diff, rss0, displayon, rsstext, rsspos, speaktime, speaktimespan, closecount, camerawake, cameraflag, childcomplete, wakeh, sleepj, wake1h, wake1m, wake2h, wake2m
    
        nowdate = datetime.datetime.now()
        string = strftime('%H:%M')
        lb_clock.config(text=string)
        time_diff=True
        allend = True
    
        if nowdate.hour==chkprehour and nowdate.minute==chkpreminute and nowdate.second==chkpresecond:
            time_diff=False
        if displayon:
            #############################################
            # RSSテキスト
            if len(rsstext) > rsspos+19:
                rss0.config(text = rsstext[rsspos:rsspos+19])
            elif rsspos < len(rsstext):
                rss0.config(text = rsstext[rsspos:len(rsstext)])
            rsspos += 1
            if rsspos >= len(rsstext):
                rsspos = 0
            #############################################
            # 時刻をしゃべる
            if nowdate.minute % speaktimespan == 0 and speaktime and nowdate.second == 0 and time_diff and not mute:
                if threading.active_count()<=2:
                    th1 = threading.Thread(target=speak_time)
                    th1.start()
            for icomplete in childcomplete:
                allend = (allend and icomplete)
        #############################################
        # 監視カメラによるWake時の処理
        if camerawake>0:
            if camerawake==60:
                sleep(7)
                subprocess.call('/usr/bin/mpg321 '+homedir+'/sound/'+detectsound+' > /dev/null 2>&1', shell=True)
            camerawake=camerawake-1
            if camerawake==0:
                displayon=False
                subprocess.call('/usr/bin/vcgencmd display_power 0', shell=True)
                closecount=0
                cameraoff()
                cameraflag=0
        #############################################
        # 自動ディスプレイOFF
        elif nowdate.hour>=sleeph and allend and displayon:
            if closecount <20:
                closecount = closecount + 1
            else:
                displayon=False
                subprocess.call('/usr/bin/vcgencmd display_power 0', shell=True)
                closecount=0
                cameraoff()
                cameraflag=0
        #############################################
        # 自動ディスプレイON + 更新
        elif ((nowdate.hour==wake1h and nowdate.minute==wake1m) or (nowdate.hour==wake2h and nowdate.minute==wake2m)) and displayon==False:
            displayon=True
            closecount=0
            subprocess.call('/usr/bin/vcgencmd display_power 1', shell=True)
            sleep(3)
            getcalendar()
            getholiday()
            getnews()
            getrain()
            set_jma_weather()
            inittext()
        #############################################
        # 監視カメラOFF処理
        if cameraflag > 0:
            cameraflag-=1
            if cameraflag == 0:
                cameraoff()
        #############################################
        # 外部スイッチ制御
        if GPIO.input(4) == True:
            if displayon:
                displayon=False
                subprocess.call('/usr/bin/vcgencmd display_power 0', shell=True)
                sleep(1)
            else:
                displayon=True
                subprocess.call('/usr/bin/vcgencmd display_power 1', shell=True)
                sleep(4)
    
        chkprehour = nowdate.hour
        chkpreminute = nowdate.minute
        chkpresecond = nowdate.second
        lb_clock.after(500, main_timer)
    
    ###################################################################################################
    # メインプログラム
    getcalendar()
    getholiday()
    getnews()
    getrain()
    set_jma_weather()
    inittext()
    main_timer()
    if homeenable:
        th0 = threading.Thread(target=cameralisten)
        th0.start()
    root.mainloop()
    root.destroy()
    
  • ホームディレクトリを設定する
    ホームディレクトリが/home/pi/Documents/python/home-std/と異なる場合、homedirのところを変更します。
    # ホームディレクトリ =========================================================================
    # このファイルを置くディレクトリ
    homedir = '/home/pi/Documents/python/home-std/'
  • 子供の名前を設定する
    1人から3人まで設定可能です。
    # 子供 ==================================================================================
    # 名前を3人まで記入
    child=['一郎','二郎','三郎']
    # child=['一郎','二郎']
    # child=['一郎']
  • お手伝いを設定する
    子供の交替制お手伝い。子供の数と同じだけ設定すると、毎日順番に変わります。
    # お手伝い ===============================================================================
    # 子供の交替制お手伝い。子供の数と同じだけ設定すると、毎日順番に変わります。
    kakari=['テーブルふき','せんたくもの','そうじ']
    # kakari=['テーブルふき','せんたくもの']
    # kakari=['']
  • チェックリストを設定する
    平日、休日のそれぞれ朝、夕方の4種類です。
    # チェックリスト ===============================================================================
    # list_morning : 平日朝
    # list_evening : 平日夕方
    # list_morning_holiday : 休日朝
    # list_evening_holiday : 休日夕方
    list_morning = ['着がえ','熱をはかる','歯をみがく','ハンカチ','きんちゃく','すいとう']
    list_evening = ['しゅくだい','プリント','鉛筆削る','歯をみがく']
    list_morning_holiday = ['着がえ','熱をはかる','歯をみがく']
    list_evening_holiday = ['うわばき','たいそうふく','たいそうふく','歯をみがく']
  • 天気予報の地域を設定する
    今日・明日の気象庁の天気予報の地域を選択し、以下の部分を差し替えます。

    # 気象庁 天気予報エリア ===================================================================
    custom_jmaarea = ''
    custom_jmasubarea = ''
  • Yahoo!気象情報APIと連携する
    特定の場所の今後1時間の降水量予報を10分ごとに取得できます。https://developer.yahoo.co.jp/start/のYahoo!デベロッパーネットワークのご利用ガイドを参照し、アプリケーションIDを取得してください。custom_yahooapiurlに、取得したappidと緯度・経度情報を設定してください。例えばappidが「abcdefghijklmnopqrstuvwxyz1234567890」、設定する場所が皇居の場合、以下のようになります。
    custom_yahooapiurl = 'https://map.yahooapis.jp/weather/V1/place?appid=abcdefghijklmnopqrstuvwxyz1234567890&coordinates=139.75280433909657,35.6854093980868&output=xml'
    # Yahoo! 気象情報API URL ================================================================
    # 以下のcustom_yahooapiurlを設定すると有効になります。Yahoo!デベロッパーネットワークのご利用ガイドを参照してください。output=xmlとしてください。
    # custom_yahooapiurl = 'https://map.yahooapis.jp/weather/V1/place?appid=<あなたのappid>&coordinates=<緯度>,<経度>&output=xml'
    custom_yahooapiurl = ''
  • Googleカレンダーと連携する
    Googleカレンダーと連携する場合、https://developers.google.com/calendar/api/quickstart/python?hl=jaのPythonクイックスタートを参照し、credentials.jsonを作成・ダウンロードしてホームディレクトリに置きます。初回起動時はGoogleアカウントへのログインが必要なため、Raspberry Pi本体から(リモートssh経由ではなく)home.pyを起動してください。Chromeブラウザが起動するので、ログインをしてください。2回目以降は設定ファイル(token.pickle、token2.pickle)が出来てログイン不要になります。
    必要なライブラリをpip3コマンドでインストールします。
    pi@raspberrypi:~/Documents/python/home-std $ pip3 install --upgrade google-api-python-client google-auth-httplib2 google-auth-oauthlib
    (省略)
    設定ファイル作成のため、直接home.pyを起動する。token.pickle、token2.pickleが生成されれば成功です。
    pi@raspberrypi:~/Documents/python/home-std $ ./home.py
  • タッチパネルの自動ON/OFF時刻を設定する
    デフォルトでは朝6:00と昼15:00にスリープ解除し、朝8:00まではチェックボタンが完了していても自動スリープしません。
    # 自動ディスプレイON/OFF時刻 ===================================================================
    sleeph = 8 # AM 8:00以降、チェックボタン完了していればスリープ
    wake1h = 6 # Wake-1 6:00
    wake1m = 0
    wake2h = 15 # Wake-2 15:00
    wake2m = 0
  • 監視カメラと連携する
    監視カメラの作り方で作った監視カメラが動体検知したときに、タッチパネルに撮影画像を表示させることができます。homeipaddressに、タッチパネル側のRaspberry PiのIPアドレスを設定します。homeportaddressのポート番号は、監視カメラ側の設定に合わせます。nfsdirは、監視カメラと共有しているディレクトリです。
    # 監視カメラ連携 =========================================================================
    # 別のRaspberry Piによる監視カメラからの画像ファイル名データを受信できるようにする場合、homeenableを Trueにし、このRaspberry PiのIPアドレスと、使用するポート番号を合わせます。
    # 監視カメラの共有NFS(Network File Server)の画像ディレクトリをnfsdirに設定します。
    # 検出時のサウンドファイルをsoundディレクトリに入れて、detectsoundにファイル名を設定します。
    homeenable = True
    homeipaddress = '192.168.1.10'
    homeportaddress = 54321
    nfsdir = '/mnt/camera/large/'
    detectsound = 'poka.mp3'

[自動起動を設定する]

  • 解凍したhome-std.serviceをmvコマンドで/etc/systemd/system/ディレクトリに置きます。
    pi@raspberrypi:~/Documents/python/home-std $ sudo mv -i home-std.service /etc/systemd/system/.
    [home-std.service]
    [Unit]
    Description=home-std
    After=network-online.target
    
    [Service]
    ExecStart=/home/pi/Documents/python/home-std/home.sh
    Restart=no
    
    [Install]
    WantedBy=multi-user.target
  • systemctlコマンドでサービスをenableにし、startします。statusで状態確認できます。enable設定により、rebootしたときに自動で起動します。自動実行ファイルはシェルスクリプトのhome.shになります。シェルスクリプト内でDISPLAYの設定とhome.pyの実行を行います。
    sudo systemctl enable home-std.service
    sudo systemctl start home-std.service
    sudo systemctl status home-std.service
    
    停止するときはstopです。
    sudo systemctl stop home-std.service
 
 
TOP PAGE