LoginSignup
2
0

ラズパイとLCDディスプレイでお天気ディスプレイを作る

Last updated at Posted at 2023-07-27

完成品

これです
image.png

気象庁から天気予報データを取得して
今日、明日の天気、最高気温、最低気温、降水確率
を表示します

古いラズパイを久々に起動したら...

今回使った Raspberry Pi の OS は jessie
古いバージョンなので、サポートが終了しています

$ sudo apt-get update
を実行したところ、aptの参照先が無くなっていてエラー発生

/etc/apt/sources.list を書き換えて対応しました

/etc/apt/sources.list
pi@raspberrypi:~ $ sudo cat /etc/apt/sources.list
deb http://legacy.raspbian.org/raspbian/ jessie main contrib non-free rpi

LCDをつなげる

買ったもの

ラズパイから制御できる LCDキャラクタディスプレイモジュール を買いました
以下の条件で探してAmazonで購入

  • 20文字 x 4行
  • I2CシリアルIFモジュールつき
  • バックライトつき

949円でした

結線

ジャンパワイヤーで、以下のように結線します

I2CシリアルIFモジュール Raspberry Pi
GND PIN6 (GND)
VCC PIN4 (5V)
SDA PIN3 (GPIO2)
SCL PIN5 (GPIO3)

image.png

ドライバ、ライブラリ、テストコード

TheRaspberryPiGuy氏のLCD制御プログラムを利用しました

$ git clone https://github.com/the-raspberry-pi-gui/lcd.git
$ cd lcd/
$ sudo sh install.sh

$ python demo_clock.py

無事ディスプレイに時計が表示されました
I2CシリアルIFモジュール と TheRaspberryPiGuy氏 優秀すぎ
試行錯誤を覚悟していたのですが、電子工作もプログラミングも不要でした

カタカナの表示

Extended Strings を利用すると半角カナが表示できます

UTF-8のコードと比較して、簡単な変換関数を作りました

UTF-8 : 半角カナ
https://orange-factory.com/sample/utf8/code3/ef.html#HalfwidthandFullwidthForms
ヲ : 0xEF 0xBD 0xA6
タ : 0xEF 0xBE 0x80

lcd : Extended string code
https://github.com/the-raspberry-pi-guy/lcd/blob/master/README.md#extended-strings
ヲ : 0xA6 ( + 0x00 )
タ : 0xC0 ( + 0x40 )

def convert_to_code(input_string):
    char_codes = []
    for char in input_string:
        c = ord(char) & 0xFF
        c += 0x40 if c < 0xA0 else 0
        char_code = "{{0x{:02X}}}".format(c)
        char_codes.append(char_code)
    return "".join(char_codes[2::3])

ヒヤヒヤするコードですね
ロバスト性皆無なので、絶対に半角カナ以外入れちゃだめだぞ~

demo_clock.pyを改造して、こんな感じ (↓) でカタカナの表示に成功しました
image.png

天気予報の取得

気象庁のURLとjson

天気予報は気象庁が提供するjsonを利用します
このようなURLからjsonがとれます。素晴らしいです
https://www.jma.go.jp/bosai/forecast/data/forecast/130000.json

jsonの例です

[
	{
		"publishingOffice": "気象庁",
		"reportDatetime": "2023-07-26T17:00:00+09:00",
		"timeSeries": [
			{
				"timeDefines": [
					"2023-07-26T17:00:00+09:00",
					"2023-07-27T00:00:00+09:00",
					"2023-07-28T00:00:00+09:00"
				],
				"areas": [
					{
						"area": {
							"name": "東京地方",
							"code": "130010"
						},
						"weatherCodes": [
							"100",
							"100",
							"100"
						],
						"weathers": [
							"晴れ",
							"晴れ 多摩西部 では 昼過ぎ から 雨 で 雷を伴う",
							"晴れ"
						],

weatherCodesweathers に天気情報が入っています
weathers は日本語かつ長い(ときもある)ので、weatherCodes を使います

気象庁のjsonについては以下のblogなどを参考にしました

天気コードと "読み" への変換

weatherCodesは天気に対応していて、例えば 100晴れ です
対応表は、 Forcast.Const.TELOPS を気象庁の天気予報のページから入手して
手元でそれを変換して使いました

詳しくはこちらの記事を参照のこと

手順

  1. Forcast.Const.TELOPS をローカルにファイル保存
  2. 読みを付加
  3. 全角カタカナを半角カタカナに置換 (by 秀丸エディタ)
  4. TELOPS_hankaku_kana.csv というファイル名で保存

プログラム・コード

お天気ディスプレイのコードです

demo_tenki.py
#! /usr/bin/env python
# -*- coding: utf-8 -*-

# Weather information program.
# Demo program for the I2C 20x4 Display from Ryanteck.uk
# Created by https://github.com/Tomonobu3110

# Import necessary libraries for communication and display use

## for weather data
import requests
import json
import csv
import unicodedata

## for LCD display
import drivers
from time import sleep
from datetime import datetime

# Convart charactor code
# from Hankaku Kana (UTF-8) to extended string code
# utf-8           : https://orange-factory.com/sample/utf8/code3/ef.html#HalfwidthandFullwidthForms
# extended string : https://github.com/the-raspberry-pi-guy/lcd/blob/master/README.md#extended-strings
def convert_to_code(input_string):
    char_codes = []
    
    for char in input_string:
        c = ord(char) & 0xFF
        c += 0x40 if c < 0xA0 else 0
        char_code = "{{0x{:02X}}}".format(c)
        char_codes.append(char_code)
    
    return "".join(char_codes[2::3]) ## Python syntax : array[start:stop:step]

# Get json from specified URL
def get_json_from_url(url):
    try:
        response = requests.get(url)
        response.raise_for_status()  # error check
        return response.json()
    except requests.exceptions.RequestException as e:
        print("Error during request: {}".format(e))
        return None
    except json.JSONDecodeError as e:
        print("Error parsing JSON: {}".format(e))
        return None

# open TELOPS dictionary
def read_csv_dict(csv_file):
    data_dict = {}  # 空の辞書を作成
    
    with open(csv_file, 'rb') as file:
        csv_reader = csv.reader(file)  # ファイルハンドルからreaderオブジェクトを作成
        
        for row in csv_reader:  # 各行に対してループ
            key = int(row[0])  # 各行の最初の数をキーとして取得
            data_dict[key] = row[1:]  # キーと値を辞書に追加
            
    return data_dict

def get_date_index(json, datetime):
    if datetime[0:10] == json["today"]["date"][0:10]:
        return "today"
    if datetime[0:10] == json["tomorrow"]["date"][0:10]:
        return "tomorrow"
    return "undef"

def get_time_index(datetime):
    return int(datetime[11:13]) / 6

def get_minmax_index(datetime):
    if datetime[11:13] == "00":
        return "min"
    if datetime[11:13] == "09":
        return "max"
    return "undef"

def get_disp_str(value):
    if -1 == value:
        return "-"
    return str(value)

# Load the driver and set it to "display"
# If you use something from the driver library use the "display." prefix first
display = drivers.Lcd()

yomi_dict = read_csv_dict("TELOPS_hankaku_kana.csv")

# display weather information
try:
    while True:
        # get weather information
        url = "https://www.jma.go.jp/bosai/forecast/data/forecast/130000.json"
        json_data = get_json_from_url(url)
        
        if json_data:
            try:
                # (1) Weather
                weather_times = json_data[0]["timeSeries"][0]["timeDefines"]
                weather_areas = json_data[0]["timeSeries"][0]["areas"]
                for area in weather_areas:
                    if int(area["area"]["code"]) == 130010: # 東京地方 130010
                        weather_codes = area["weatherCodes"]
                        dates = [s[8:10] for s in weather_times]
                        weather = [convert_to_code(yomi_dict[int(c)][1]) for c in weather_codes]
                
                # (2) Rainy percent
                rainy_times = json_data[0]["timeSeries"][1]["timeDefines"]
                rainy_areas = json_data[0]["timeSeries"][1]["areas"]
                for area in rainy_areas:
                    if int(area["area"]["code"]) == 130010: # 東京地方 130010
                        rainy_pops = area["pops"]
                
                # (3) Temperature
                temp_times = json_data[0]["timeSeries"][2]["timeDefines"]
                temp_areas = json_data[0]["timeSeries"][2]["areas"]
                for area in temp_areas:
                    if int(area["area"]["code"]) == 44132: # 東京 44132
                        temp_temps = area["temps"]
                
            except KeyError:
                print("The specified path does not exist in the JSON data.")
        
        # template of weather data
        weather_data = '''\
        {
            "today": {
                "date": "2023-01-01",
                "code": 0,
                "pops": [
                    -1,
                    -1,
                    -1,
                    -1
                ],
                "temps": {
                    "min": -1,
                    "max": -1
                }
            },
            "tomorrow": {
                "date": "2023-01-02",
                "code": 0,
                "pops": [
                    -1,
                    -1,
                    -1,
                    -1
                ],
                "temps": {
                    "min": -1,
                    "max": -1
                }
            }
        }
        '''
        
        # conver to intermidiate data format
        weather_json = json.loads(weather_data)
        weather_json["today"]["date"]    = weather_times[0][0:10]
        weather_json["today"]["code"]    = int(weather_codes[0])
        weather_json["tomorrow"]["date"] = weather_times[1][0:10]
        weather_json["tomorrow"]["code"] = int(weather_codes[1])
        for i, dt in enumerate(rainy_times):
            weather_json[get_date_index(weather_json, dt)]["pops"][get_time_index(dt)] = int(rainy_pops[i])
        for i, dt in enumerate(temp_times):
            weather_json[get_date_index(weather_json, dt)]["temps"][get_minmax_index(dt)] = int(temp_temps[i])
        if weather_json["today"]["temps"]["min"] == weather_json["today"]["temps"]["max"]:
          weather_json["today"]["temps"]["min"] = -1
        #print(weather_json)
        
        print("Writing to display")
        
        display.lcd_display_extended_string(weather_json["today"]   ["date"][8:10] + convert_to_code("ニチ") + " " + convert_to_code(yomi_dict[weather_json["today"]   ["code"]][1]), 1)
        display.lcd_display_extended_string(weather_json["tomorrow"]["date"][8:10] + convert_to_code("ニチ") + " " + convert_to_code(yomi_dict[weather_json["tomorrow"]["code"]][1]), 2)
        
        temp_str = convert_to_code("キオン") + "  "
        temp_str += get_disp_str(weather_json["today"]["temps"]["min"]) + " "
        temp_str += get_disp_str(weather_json["today"]["temps"]["max"]) + " | "
        temp_str += get_disp_str(weather_json["tomorrow"]["temps"]["min"]) + " "
        temp_str += get_disp_str(weather_json["tomorrow"]["temps"]["max"]) + " "
        display.lcd_display_extended_string(temp_str, 3)
        
        pops_str = convert_to_code("アメ") + "   "
        pops_str += get_disp_str(max(weather_json["today"]["pops"])) + "% | "
        pops_str += get_disp_str(max(weather_json["tomorrow"]["pops"])) + "%"
        display.lcd_display_extended_string(pops_str, 4)
        
        # wait 5 min
        sleep(5 * 60)
        display.lcd_clear()
except KeyboardInterrupt:
    # If there is a KeyboardInterrupt (when you press ctrl+c), exit the program and cleanup
    print("Cleaning up!")
    display.lcd_clear()

中間形式のjsonのフォーマットをもうちょっと賢く作ればよかったかな...
ほかにも突っ込みどころ満載ですが、とりあえず動いているのでよしとします

自動起動の設定

ラズパイ起動時に自動的にスクリプトを動作させます

ほぼ、公式のREADME.md通り
https://github.com/the-raspberry-pi-guy/lcd/blob/master/README.md#systemd

一点だけ WorkingDirectory を足しました

serviceファイルの作成

$ sudo nano /lib/systemd/system/rpi-lcd.service

serviceファイルの内容

rpi-lcd.service
[Unit]
Description=RPi Python script for a 16x2 LCD

[Service]
Type=simple
## Edit the following according to the script permissions
User=pi
#Group=users

## Working directory
WorkingDirectory=/home/pi/lcd

## Edit the following with the full path to the compatible Python version and your script
ExecStart=/usr/bin/python /home/pi/lcd/demo_tenki.py

Restart=always
RestartSec=5

KillMode=process
KillSignal=SIGINT

[Install]
WantedBy=multi-user.target

サービスの有効化

$ sudo systemctl enable rpi-lcd.service
$ sudo systemctl start rpi-lcd.service

完成品を再掲!
image.png

以上です

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0