Turing Smart Screen にアメダス情報や雨雲レーダーを表示してみる
2025/07/04 時点での情報です
Turing Smart Screen とは
PC の USB ポートに接続してシリアル通信で描画を行う小型のディスプレイです
任意の情報(いつものアメダス)を簡単に表示できるかな~と検索してみたら以下のリポジトリがヒット
これを参考にすれば表示できそうだな~ということでアリエクで 1386 円でゲット
Amazon だと 3000 円くらい
ソースコード
上記のリポジトリに含まれている simple-program.py
が非常に参考になります
simple-program.py
を書き換えながらできたのが以下のソース
turing-smart-screen-amedas.py
# -*- coding: utf-8 -*-
# turing-smart-screen-python - a Python system monitor and library for USB-C displays like Turing Smart Screen or XuanFang
# https://github.com/mathoudebine/turing-smart-screen-python/
# Copyright (C) 2021-2023 Matthieu Houdebine (mathoudebine)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# This file is a simple Python test program using the library code to display custom content on screen (see README)
import os
import signal
import sys
import time
from datetime import datetime
import threading
# Import only the modules for LCD communication
from library.lcd.lcd_comm_rev_a import LcdCommRevA, Orientation
from library.log import logger
import schedule
import requests
# Set your COM port e.g. COM3 for Windows, /dev/ttyACM0 for Linux, etc. or "AUTO" for auto-discovery
# COM_PORT = "/dev/ttyACM0"
# COM_PORT = "COM5"
COM_PORT = "AUTO"
# Display revision:
# - A for Turing 3.5" and UsbPCMonitor 3.5"/5"
# - B for Xuanfang 3.5" (inc. flagship)
# - C for Turing 5"
# - D for Kipye Qiye Smart Display 3.5"
# - SIMU for simulated display (image written in screencap.png)
# To identify your smart screen: https://github.com/mathoudebine/turing-smart-screen-python/wiki/Hardware-revisions
REVISION = "A"
# Display width & height in pixels for portrait orientation
# /!\ Do not switch width/height here for landscape, use lcd_comm.SetOrientation below
# 320x480 for 3.5" models
# 480x480 for 2.1" models
# 480x800 for 5" models
# 480x1920 for 8.8" models
WIDTH, HEIGHT = 320, 480
assert WIDTH <= HEIGHT, "Indicate display width/height for PORTRAIT orientation: width <= height"
stop = False
contents = list()
amedastable = dict()
last_modified = None
latest_time_uri = 'https://www.jma.go.jp/bosai/amedas/data/latest_time.txt'
pre_latest_time = str()
def update():
global amedastable, last_modified, pre_latest_time, contents
# update amedastable if modified
with requests.get(
'https://www.jma.go.jp/bosai/amedas/const/amedastable.json',
headers={
'If-Modified-Since': last_modified,
},
) as r:
if r.status_code == 200:
amedastable = r.json()
last_modified = r.headers.get('Last-Modified')
logger.info(f"amedastable updated ({last_modified})")
# get latest time
with requests.get(latest_time_uri) as r:
latest_time = r.content.decode('utf-8')
dt = datetime.strptime(latest_time, '%Y-%m-%dT%H:%M:%S%z')
# print(dt.strftime('%Y-%m-%d %H:%M:%S'))
yyyymmdd = dt.strftime('%Y%m%d')
HH = dt.strftime('%H')
hh = f'{int(HH) // 3 * 3:02d}'
if pre_latest_time == latest_time:
return
logger.info(f"latest_time updated ({pre_latest_time} -> {latest_time})")
pre_latest_time = latest_time
# update contents
codes = sys.argv[1:] if len(sys.argv) > 1 else ['44132']
try:
contents = list()
for code in list(reversed(codes)):
url = f'https://www.jma.go.jp/bosai/amedas/data/point/{code}/{yyyymmdd}_{hh}.json'
with requests.get(url) as r:
data = r.json()
base_key = f'{yyyymmdd}{HH}0000' # 積雪は1時間毎
last_key = list(data.keys())[-1]
_vars = data[base_key]
for k in data[last_key]:
_vars[k] = data[last_key][k]
h = last_key[8:10]
if h == '00':
h = '24'
m = last_key[10:12]
lines = [
amedastable[code].get('kjName', '-') + f' {h}:{m}'
]
for x in [
'temp 度',
'humidity %',
'precipitation1h mm/h',
'snow cm',
'pressure hPa',
]:
k, u = x.split()
if k in _vars:
# print(k, _vars[k])
if _vars[k][1] != 0:
continue
else:
lines.append(f'{_vars[k][0]}{u}')
contents.append(' '.join(lines))
logger.info("contents updated")
except Exception as e:
logger.debug(e)
def runSchedule():
global stop
schedule.every(1).minutes.do(update)
while not stop:
schedule.run_pending()
time.sleep(1)
if __name__ == "__main__":
def sighandler(signum, frame):
global stop
stop = True
task_thread.join()
# Set the signal handlers, to send a complete frame to the LCD before exit
signal.signal(signal.SIGINT, sighandler)
signal.signal(signal.SIGTERM, sighandler)
is_posix = os.name == 'posix'
if is_posix:
signal.signal(signal.SIGQUIT, sighandler)
task_thread = threading.Thread(target=runSchedule, daemon=True)
task_thread.start()
update()
# Build your LcdComm object based on the HW revision
lcd_comm = LcdCommRevA(com_port=COM_PORT, display_width=WIDTH, display_height=HEIGHT)
# Reset screen in case it was in an unstable state (screen is also cleared)
lcd_comm.Reset()
# Send initialization commands
lcd_comm.InitializeComm()
# Set brightness in % (warning: revision A display can get hot at high brightness! Keep value at 50% max for rev. A)
lcd_comm.SetBrightness(level=10)
# Set orientation (screen starts in Portrait)
lcd_comm.SetOrientation(orientation=Orientation.REVERSE_LANDSCAPE)
# Define background picture
background = f"res/backgrounds/example_{lcd_comm.get_width()}x{lcd_comm.get_height()}.png"
# Display sample picture
logger.debug("setting background picture")
start = time.perf_counter()
lcd_comm.DisplayBitmap(background)
end = time.perf_counter()
logger.debug(f"background picture set (took {end - start:.3f} s)")
# Display the current time and some progress bars as fast as possible
while not stop:
start = time.perf_counter()
lcd_comm.DisplayText(datetime.now().strftime("%H:%M:%S"), HEIGHT - (12 * 8) - 2, WIDTH - 24 - 2,
font="res/fonts/roboto/Roboto-Bold.ttf",
font_size=24,
font_color=(255, 255, 255),
background_image=background)
idx = 1
for content in contents:
lcd_comm.DisplayText(content,
5, WIDTH - 24 - 18 * idx - 8,
font="C:/Windows/Fonts/meiryob.ttc",
font_size=18,
font_color=(255, 255, 255),
background_image=background)
idx += 1
end = time.perf_counter()
# logger.debug(f"refresh done (took {end - start:.3f} s)")
if end - start < 1:
time.sleep(1 - (end - start))
# Close serial connection at exit
lcd_comm.closeSerial()
実行手順
ソースコードの取得
以下のいずれかの方法でソースコードを取得して
- 上記のリポジトリの Releases から zip をダウンロードして展開
-
git clone https://github.com/mathoudebine/turing-smart-screen-python.git
を実行 -
git clone git@github.com:mathoudebine/turing-smart-screen-python.git
を実行 -
gh repo clone mathoudebine/turing-smart-screen-python
を実行
その中に turing-smart-screen-amedas.py
を入れます
python実行環境の構築
Python 3.12 では依存モジュールがインストールできないので 3.11 で実行環境を構築します
Python 3.11 のインストールはここからがいちばんお手軽です
PS> python3.11.exe -m venv .venv
PS> .\.venv\Scripts\activate
(.venv) PS> python -m pip install -U pip
(.venv) PS> pip install -U -r .\requirements.txt schedule
schedule
はアメダスサーバに負荷をかけないよう定期的にアメダスの情報を取得するために入れています
実行
(.venv) PS> python .\turing-smart-screen-amedas.py
以下のように表示されれば成功です(写真ヘタクソ)
引数なしの場合は東京の情報を表示します
アメダスの観測地点コードを指定すると複数表示が可能です
(.venv) PS> python .\turing-smart-screen-amedas.py 14163 44132 62078 86141
14163:札幌 44132:東京 62078:大阪 86141:熊本
都道府県と観測地点を選択したときの URL の #amdno=数字 が観測地点コードになります
背景画像を任意のものに簡単に変更可能(ソースを変更する必要あり)
背景を定期的に変更してみる
どうせなら雨雲レーダーも表示したいなあ……といろいろやってみた
amedas-zoomradar.py
# -*- coding: utf-8 -*-
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import io
import os
import math
import signal
import sys
import threading
import time
from datetime import datetime
# Import only the modules for LCD communication
from library.lcd.lcd_comm_rev_a import LcdCommRevA, Orientation
from library.lcd.lcd_simulated import LcdSimulated
from library.log import logger
from PIL import Image
from bs4 import BeautifulSoup
import requests
import schedule
# Set your COM port e.g. COM3 for Windows, /dev/ttyACM0 for Linux, etc. or "AUTO" for auto-discovery
# COM_PORT = "/dev/ttyACM0"
# COM_PORT = "COM5"
COM_PORT = "AUTO"
# Display revision:
# - A for Turing 3.5" and UsbPCMonitor 3.5"/5"
# - B for Xuanfang 3.5" (inc. flagship)
# - C for Turing 5"
# - D for Kipye Qiye Smart Display 3.5"
# - SIMU for simulated display (image written in screencap.png)
# To identify your smart screen: https://github.com/mathoudebine/turing-smart-screen-python/wiki/Hardware-revisions
REVISION = "A"
# Display width & height in pixels for portrait orientation
# /!\ Do not switch width/height here for landscape, use lcd_comm.SetOrientation below
# 320x480 for 3.5" models
# 480x480 for 2.1" models
# 480x800 for 5" models
# 480x1920 for 8.8" models
WIDTH, HEIGHT = 320, 480
assert WIDTH <= HEIGHT, "Indicate display width/height for PORTRAIT orientation: width <= height"
INTERVAL = 1 # minutes
stop = False
content_updated = False
radar_image_path = 'dummy'
radar_image = None
lat, lon, z = 43.055, 141.341, 8 # 札幌
amedas = str()
pre_latest_time = str()
if len(sys.argv) > 2:
REVISION = "SIMU"
def deg2dec(deg):
degree, minute = deg
return degree + minute / 60
def getNearAmedas(lat, lng):
with requests.get('https://www.jma.go.jp/bosai/amedas/const/amedastable.json') as r:
lines = []
data = r.json()
for key in data:
name = data[key]['kjName']
elem = data[key]['elems']
_lat = deg2dec(data[key]['lat'])
_lng = deg2dec(data[key]['lon'])
dist = math.dist((lat, lng), (_lat, _lng))
# snow
# if elem[5] == '1':
if elem[0] == '1':
lines.append([key, name, dist])
return sorted(lines, key=lambda x: x[2])[0]
return []
def updateAmedas():
global code, loc, amedas, pre_latest_time
# get latest time
with requests.get('https://www.jma.go.jp/bosai/amedas/data/latest_time.txt') as r:
latest_time = r.content.decode('utf-8')
dt = datetime.strptime(latest_time, '%Y-%m-%dT%H:%M:%S%z')
# print(dt.strftime('%Y-%m-%d %H:%M:%S'))
yyyymmdd = dt.strftime('%Y%m%d')
HH = dt.strftime('%H')
hh = f'{int(HH) // 3 * 3:02d}'
if pre_latest_time == latest_time:
return
logger.debug(f"latest_time updated ({pre_latest_time} -> {latest_time})")
pre_latest_time = latest_time
# update contents
url = f'https://www.jma.go.jp/bosai/amedas/data/point/{code}/{yyyymmdd}_{hh}.json'
with requests.get(url) as r:
data = r.json()
base_key = f'{yyyymmdd}{HH}0000' # 積雪は1時間毎
last_key = list(data.keys())[-1]
_vars = data.get(base_key)
# 存在しない時がある
if _vars is None:
return
for k in data[last_key]:
_vars[k] = data[last_key][k]
h = last_key[8:10]
if h == '00':
h = '24'
m = last_key[10:12]
lines = [f'{loc} {h}:{m}']
for x in [
'temp 度',
'humidity %',
'precipitation1h mm/h',
'snow cm',
'pressure hPa',
]:
k, u = x.split()
if k in _vars:
# print(k, _vars[k])
if _vars[k][1] != 0:
continue
else:
lines.append(f'{_vars[k][0]}{u}')
amedas = ' '.join(lines) + ' '
def update():
global content_updated, radar_image, lat, lon, z
with requests.get(f"https://weather.yahoo.co.jp/weather/zoomradar/?lat={lat}&lon={lon}&z={z}") as r:
soup = BeautifulSoup(r.content, 'html.parser')
og_image = soup.find('meta', property='og:image')
if og_image:
img_url = og_image.get('content').replace('1200x630', f'{HEIGHT}x{WIDTH}')
with requests.get(img_url) as r2:
img = Image.open(io.BytesIO(r2.content)).convert('RGB')
if radar_image != img:
radar_image = img
content_updated = True
logger.debug('radar_image updated')
def runSchedule():
global stop
schedule.every(INTERVAL).minutes.do(update)
schedule.every(INTERVAL).minutes.do(updateAmedas)
while not stop:
schedule.run_pending()
time.sleep(1)
try:
lat, lon, z, _ = [float(kv.split('=')[1]) for kv in sys.argv[1].split('?')[1].split('&')]
except Exception:
pass
code, loc, _ = getNearAmedas(lat, lon)
updateAmedas()
print(lat, lon, z, code, amedas, REVISION)
if __name__ == "__main__":
def sighandler(signum, frame):
global stop
stop = True
for th in threading.enumerate():
if th.name != 'MainThread':
th.join()
# Set the signal handlers, to send a complete frame to the LCD before exit
signal.signal(signal.SIGINT, sighandler)
signal.signal(signal.SIGTERM, sighandler)
is_posix = os.name == 'posix'
if is_posix:
signal.signal(signal.SIGQUIT, sighandler)
task_thread = threading.Thread(target=runSchedule, daemon=True)
task_thread.start()
# Build your LcdComm object based on the HW revision
if REVISION == "A":
lcd_comm = LcdCommRevA(com_port=COM_PORT, display_width=WIDTH, display_height=HEIGHT)
elif REVISION == "SIMU":
lcd_comm = LcdSimulated(display_width=WIDTH, display_height=HEIGHT)
else:
logger.error("Unknown revision")
try:
sys.exit(1)
except Exception:
os._exit(1)
# Reset screen in case it was in an unstable state (screen is also cleared)
lcd_comm.Reset()
# Send initialization commands
lcd_comm.InitializeComm()
# Set brightness in % (warning: revision A display can get hot at high brightness! Keep value at 50% max for rev. A)
lcd_comm.SetBrightness(level=10)
# Set orientation (screen starts in Portrait)
lcd_comm.SetOrientation(orientation=Orientation.REVERSE_LANDSCAPE)
update()
# Display the current time and some progress bars as fast as possible
while not stop:
start = time.perf_counter()
if content_updated:
lcd_comm.DisplayPILImage(radar_image, 0, 0)
end = time.perf_counter()
logger.debug(f"background picture set (took {end - start:.3f} s)")
lcd_comm.image_cache[radar_image_path] = radar_image
content_updated = False
lcd_comm.DisplayText(
datetime.now().strftime("%H:%M:%S"), 0, 0,
font="res/fonts/roboto/Roboto-Bold.ttf",
font_size=36,
font_color=(0, 0, 0),
background_image=radar_image_path,
)
font_size = int(HEIGHT / len(amedas)) + 4
lcd_comm.DisplayText(
amedas, 0, WIDTH - font_size - 8,
font="C:/Windows/Fonts/meiryob.ttc",
font_size=font_size,
font_color=(0, 0, 0),
background_image=radar_image_path,
)
end = time.perf_counter()
# logger.debug(f"refresh done (took {end - start:.3f} s)")
if end - start < 1:
time.sleep(1 - (end - start))
# Close serial connection at exit
lcd_comm.closeSerial()
雨雲レーダーが更新されるたびに背景を描画し直しているため、ちょっとラグが出ます……
(※revision A と呼ばれている 3.5inch 版はめっちゃ遅い 5inch版以降は速いらしいです)
(.venv) PS> python .\amedas-zoomradar.py
(※LcdSimulated
を使うことできれいなスクリーンショットを撮ることができることを知りました)
雨雲レーダーのパーマネントURLを引数に指定することで任意の地点を表示することができます
(.venv) PS> python .\amedas-zoomradar.py "https://weather.yahoo.co.jp/weather/zoomradar/?lat=35.694&lon=139.703&z=8&t=20250704191000"
今回のキモ
透過テキストを実現するために lcd_comm
オブジェクトの内部変数に直接アクセスしています
lcd_comm.image_cache[radar_image_path] = radar_image
参考情報
最後に
非常に簡単に任意の情報を表示することができました
スタンドにはこれを使っています
Enjoy!!
Discussion