choshi20260324

AIアプリ

https://app.ai4all.jp/app/auth/student/login?classCode=IP5KLS

M5Stackに書き込もう

https://uiflow2.m5stack.com

M5Stack CoreS3 サンプル

import M5
from M5 import *
from machine import Pin
import time
import json
import machine
import esp32
import network
import ntptime
import requests

# ============================================================
# --- 設定 ---
# ============================================================
WIFI_SSID    = "WIFI_SSID"
WIFI_PASS    = "WIFI_PASS"
NTP_HOST     = "ntp.nict.jp"

API_BASE     = "https://monapi.ichika.net/api"
API_USER     = "john"
API_APP      = "counter"
API_ENDPOINT = "{}/{}/{}".format(API_BASE, API_USER, API_APP)

mode           = 0
max_mode       = 3
counter        = 0
post_msg       = ""
post_msg_timer = 0
last_touch     = False

# --- Unit Dual Button (PORT.A: G1=青, G2=赤) ---
BTN_A_PIN  = 1
BTN_B_PIN  = 2
btn_a      = Pin(BTN_A_PIN, Pin.IN, Pin.PULL_UP)
btn_b      = Pin(BTN_B_PIN, Pin.IN, Pin.PULL_UP)
btn_a_last = True
btn_b_last = True

# --- 差分更新用・前回値 ---
last_time_str  = ""
last_date_str  = ""
last_counter   = None
last_post_msg  = ""
last_bat       = None
last_connected = None

# ============================================================
# --- 初期設定 ---
# ============================================================
M5.begin()
M5.Lcd.setRotation(1)
SELECTED_FONT = M5.Lcd.FONTS.EFontJA24

LCD_W = M5.Lcd.width()
LCD_H = M5.Lcd.height()

# --- カラーパレット(ダーク系) ---
C_BG        = 0x0A0A0F
C_PANEL     = 0x12121A
C_ACCENT    = 0x00CFFF
C_GREEN     = 0x00E676
C_RED       = 0xFF3D5A
C_YELLOW    = 0xFFD600
C_WHITE     = 0xEEEEEE
C_GRAY      = 0x555566
C_BAR_BG    = 0x1A1A2E
C_DIVIDER   = 0x2A2A3E
C_BTN_L_BG  = 0x0D1B2A
C_BTN_R_BG  = 0x0A1F0F
C_BTN_L_BD  = 0x00CFFF
C_BTN_R_BD  = 0x00E676

BAR_H  = 44
BAR_Y  = LCD_H - BAR_H
HDR_H  = 38
CONT_Y = HDR_H + 4
CONT_H = BAR_Y - CONT_Y - 4

# ============================================================
# --- UI ヘルパー ---
# ============================================================
def draw_touch_bar():
    M5.Lcd.fillRect(0, BAR_Y, LCD_W, BAR_H, C_BAR_BG)
    M5.Lcd.drawLine(0, BAR_Y, LCD_W, BAR_Y, C_ACCENT)

    MID = LCD_W // 2 - 2

    # 左ボタン(モード切替)
    M5.Lcd.fillRect(4, BAR_Y + 4, MID - 4, BAR_H - 8, C_BTN_L_BG)
    M5.Lcd.drawRect(4, BAR_Y + 4, MID - 4, BAR_H - 8, C_BTN_L_BD)
    M5.Lcd.setFont(SELECTED_FONT)
    M5.Lcd.setTextColor(C_BTN_L_BD)
    M5.Lcd.setCursor(14, BAR_Y + 12)
    M5.Lcd.print("◀ モード切替")

    # 右ボタン(+1)
    M5.Lcd.fillRect(MID + 4, BAR_Y + 4, LCD_W - MID - 8, BAR_H - 8, C_BTN_R_BG)
    M5.Lcd.drawRect(MID + 4, BAR_Y + 4, LCD_W - MID - 8, BAR_H - 8, C_BTN_R_BD)
    M5.Lcd.setTextColor(C_BTN_R_BD)
    M5.Lcd.setCursor(MID + 14, BAR_Y + 12)
    M5.Lcd.print("タップ +1 ▶")

def flash_touch_btn(side):
    MID = LCD_W // 2 - 2
    if side == "left":
        M5.Lcd.fillRect(4, BAR_Y + 4, MID - 4, BAR_H - 8, C_BTN_L_BD)
    else:
        M5.Lcd.fillRect(MID + 4, BAR_Y + 4, LCD_W - MID - 8, BAR_H - 8, C_BTN_R_BD)
    time.sleep(0.06)
    draw_touch_bar()

def draw_header(title):
    """モード切替時のみ呼ぶ・全画面リセット"""
    M5.Lcd.fillScreen(C_BG)
    M5.Lcd.fillRect(0, 0, LCD_W, HDR_H, C_PANEL)
    M5.Lcd.drawLine(0, HDR_H, LCD_W, HDR_H, C_ACCENT)
    M5.Lcd.setFont(SELECTED_FONT)
    M5.Lcd.setTextColor(C_ACCENT)
    M5.Lcd.setCursor(10, 8)
    M5.Lcd.print(title)
    # モードインジケーター
    for i in range(max_mode):
        x = LCD_W - 10 - (max_mode - i) * 12
        M5.Lcd.fillRect(x, 14, 8, 8, C_ACCENT if i == mode else C_GRAY)
    draw_touch_bar()
    # 前回値をリセット(モード切替後に全要素を再描画させる)
    reset_last_values()

def reset_last_values():
    global last_time_str, last_date_str, last_counter
    global last_post_msg, last_bat, last_connected
    last_time_str  = ""
    last_date_str  = ""
    last_counter   = None
    last_post_msg  = ""
    last_bat       = None
    last_connected = None

def draw_panel(x, y, w, h):
    M5.Lcd.fillRect(x, y, w, h, C_PANEL)
    M5.Lcd.drawRect(x, y, w, h, C_DIVIDER)

def flash_content(color):
    M5.Lcd.fillRect(0, CONT_Y, LCD_W, CONT_H, color)
    time.sleep(0.04)

# ============================================================
# --- タッチ判定 ---
# ============================================================
def get_touch_event():
    global last_touch
    touched = M5.Touch.getCount() > 0
    if touched and not last_touch:
        last_touch = True
        return (M5.Touch.getX(), M5.Touch.getY())
    if not touched:
        last_touch = False
    return None

# ============================================================
# --- Wi-Fi 接続 ---
# ============================================================
def connect_wifi():
    draw_header("Wi-Fi 接続中")
    M5.Lcd.setFont(SELECTED_FONT)
    M5.Lcd.setTextColor(C_WHITE)
    M5.Lcd.setCursor(10, CONT_Y + 10)
    M5.Lcd.print("SSID: " + WIFI_SSID)

    wlan = network.WLAN(network.STA_IF)
    wlan.active(True)
    if wlan.isconnected():
        return wlan

    wlan.connect(WIFI_SSID, WIFI_PASS)
    timeout = 15
    for i in range(timeout):
        if wlan.isconnected():
            break
        prog_w = int((LCD_W - 20) * (i + 1) / timeout)
        M5.Lcd.fillRect(10, CONT_Y + 50, LCD_W - 20, 14, C_PANEL)
        M5.Lcd.fillRect(10, CONT_Y + 50, prog_w, 14, C_ACCENT)
        M5.Lcd.fillRect(10, CONT_Y + 28, LCD_W - 20, 20, C_BG)
        M5.Lcd.setCursor(10, CONT_Y + 28)
        M5.Lcd.print("待機中 {}/{}s".format(i + 1, timeout))
        time.sleep(1)
    return wlan

# ============================================================
# --- NTP 時刻同期(JST) ---
# ============================================================
def sync_ntp():
    try:
        ntptime.host = NTP_HOST
        ntptime.settime()
        t = time.localtime(time.time() + 9 * 3600)
        machine.RTC().datetime((t[0], t[1], t[2], t[6], t[3], t[4], t[5], 0))
        return True
    except:
        return False

# ============================================================
# --- MonAPI POST / GET ---
# ============================================================
def post_count(count):
    try:
        body = json.dumps({"count": count})
        res  = requests.post(
            API_ENDPOINT,
            headers={"Content-Type": "application/json"},
            data=body
        )
        print("[POST] Status:", res.status_code)
        print("[POST] Response:", res.text)
        ok = (res.status_code == 201)
        res.close()
        return ok
    except Exception as e:
        print("[POST] Exception:", e)
        return False

def fetch_latest_count():
    try:
        res = requests.get(API_ENDPOINT)
        print("[GET] Status:", res.status_code)
        print("[GET] Response:", res.text)
        if res.status_code == 200:
            data = json.loads(res.text)
            if len(data) > 0 and "count" in data[0]:
                val = data[0]["count"]
                res.close()
                return val
        res.close()
        return 0
    except Exception as e:
        print("[GET] Exception:", e)
        return 0

# ============================================================
# --- 起動シーケンス ---
# ============================================================
wlan = connect_wifi()
wifi_ok = wlan.isconnected()
rtc = machine.RTC()

if wifi_ok:
    draw_header("NTP 同期中")
    sync_ntp()
    draw_header("データ取得中")
    counter = fetch_latest_count()
    print("[INIT] 初期カウント:", counter)
    M5.Lcd.setFont(SELECTED_FONT)
    M5.Lcd.setTextColor(C_GREEN)
    M5.Lcd.setCursor(10, CONT_Y + 10)
    M5.Lcd.print("接続成功!")
    M5.Lcd.setTextColor(C_WHITE)
    M5.Lcd.setCursor(10, CONT_Y + 36)
    M5.Lcd.print(wlan.ifconfig()[0])
    M5.Lcd.setTextColor(C_ACCENT)
    M5.Lcd.setCursor(10, CONT_Y + 62)
    M5.Lcd.print("{} から再開".format(counter))
    time.sleep(2)
else:
    rtc.datetime((2026, 3, 13, 4, 22, 30, 0, 0))
    counter = 0
    draw_header("Wi-Fi 失敗")
    M5.Lcd.setFont(SELECTED_FONT)
    M5.Lcd.setTextColor(C_RED)
    M5.Lcd.setCursor(10, CONT_Y + 20)
    M5.Lcd.print("オフラインで継続")
    time.sleep(2)

# ============================================================
# --- カウンター操作の共通処理 ---
# ============================================================
def do_count(delta):
    global counter, post_msg, post_msg_timer
    flash_content(0x003020 if delta > 0 else 0x200010)
    counter += delta
    ok = post_count(counter)
    post_msg = ("+1  送信OK" if delta > 0 else "-1  送信OK") if ok else "送信失敗"
    post_msg_timer = 25
    if mode != 2:
        return True
    draw_header("カウンター")
    return False

# ============================================================
# --- メインループ ---
# ============================================================
draw_header("時刻と挨拶")

while True:
    M5.update()

    # --- Unit Dual Button ---
    btn_a_now = btn_a.value()
    btn_b_now = btn_b.value()

    if btn_a_now == False and btn_a_last == True:
        if do_count(+1):
            mode = 2
            draw_header("カウンター")

    if btn_b_now == False and btn_b_last == True:
        if do_count(-1):
            mode = 2
            draw_header("カウンター")

    btn_a_last = btn_a_now
    btn_b_last = btn_b_now

    # --- タッチ操作 ---
    touch = get_touch_event()
    if touch:
        tx, ty = touch
        if ty >= BAR_Y:
            if tx < LCD_W // 2:
                flash_touch_btn("left")
                mode = (mode + 1) % max_mode
                post_msg = ""
                if mode == 0:   draw_header("時刻と挨拶")
                elif mode == 1: draw_header("ネットワーク")
                elif mode == 2: draw_header("カウンター")
            else:
                flash_touch_btn("right")
                if do_count(+1):
                    mode = 2
                    draw_header("カウンター")

    # ============================================================
    # --- 差分描画 ---
    # ============================================================
    if mode == 0:
        t = rtc.datetime()
        time_str = "{:02d}:{:02d}:{:02d}".format(t[4], t[5], t[6])
        date_str = "{:04d}/{:02d}/{:02d}".format(t[0], t[1], t[2])

        # 時刻カード(初回のみ枠を描画)
        if last_time_str == "":
            draw_panel(8, CONT_Y + 4, LCD_W - 16, 56)
            M5.Lcd.setFont(SELECTED_FONT)
            M5.Lcd.setTextColor(C_GRAY)
            M5.Lcd.setCursor(16, CONT_Y + 10)
            M5.Lcd.print("TIME")

        if time_str != last_time_str:
            M5.Lcd.fillRect(16, CONT_Y + 28, LCD_W - 32, 26, C_PANEL)
            M5.Lcd.setFont(SELECTED_FONT)
            M5.Lcd.setTextColor(C_ACCENT)
            M5.Lcd.setCursor(16, CONT_Y + 30)
            M5.Lcd.print(time_str)
            last_time_str = time_str

        if last_date_str == "":
            draw_panel(8, CONT_Y + 68, LCD_W - 16, 30)

        if date_str != last_date_str:
            M5.Lcd.fillRect(16, CONT_Y + 76, LCD_W - 32, 18, C_PANEL)
            M5.Lcd.setFont(SELECTED_FONT)
            M5.Lcd.setTextColor(C_GRAY)
            M5.Lcd.setCursor(16, CONT_Y + 76)
            M5.Lcd.print(date_str)
            last_date_str = date_str

        # 挨拶(固定・初回のみ)
        if last_time_str == time_str and last_date_str == date_str:
            pass  # 変化なし
        draw_panel(8, CONT_Y + 106, LCD_W - 16, 30) if last_date_str == date_str and last_time_str == time_str else None

    elif mode == 1:
        connected = wlan.isconnected()

        if last_connected is None:
            draw_panel(8, CONT_Y + 4,   LCD_W - 16, 30)
            draw_panel(8, CONT_Y + 42,  LCD_W - 16, 30)
            draw_panel(8, CONT_Y + 80,  LCD_W - 16, 30)
            draw_panel(8, CONT_Y + 118, LCD_W - 16, 30)

        if connected != last_connected:
            M5.Lcd.fillRect(16, CONT_Y + 12, LCD_W - 32, 18, C_PANEL)
            M5.Lcd.setFont(SELECTED_FONT)
            M5.Lcd.setTextColor(C_GREEN if connected else C_RED)
            M5.Lcd.setCursor(16, CONT_Y + 12)
            M5.Lcd.print("WiFi  " + ("接続済" if connected else "未接続"))
            # IP
            M5.Lcd.fillRect(16, CONT_Y + 50, LCD_W - 32, 18, C_PANEL)
            if connected:
                M5.Lcd.setTextColor(C_GRAY)
                M5.Lcd.setCursor(16, CONT_Y + 50)
                M5.Lcd.print(wlan.ifconfig()[0])
            last_connected = connected

        bat = Power.getBatteryLevel()
        if bat != last_bat:
            bat_color = C_GREEN if bat > 50 else C_YELLOW if bat > 20 else C_RED
            M5.Lcd.fillRect(16, CONT_Y + 88, LCD_W - 32, 18, C_PANEL)
            M5.Lcd.setFont(SELECTED_FONT)
            M5.Lcd.setTextColor(bat_color)
            M5.Lcd.setCursor(16, CONT_Y + 88)
            M5.Lcd.print("電池  {}%".format(bat))
            last_bat = bat

        # CPU温度は毎回更新(変化が多いため差分不要)
        temp = esp32.mcu_temperature()
        M5.Lcd.fillRect(16, CONT_Y + 126, LCD_W - 32, 18, C_PANEL)
        M5.Lcd.setFont(SELECTED_FONT)
        M5.Lcd.setTextColor(C_YELLOW)
        M5.Lcd.setCursor(16, CONT_Y + 126)
        M5.Lcd.print("CPU  {:.1f}度".format(temp))

    elif mode == 2:
        if last_counter is None:
            draw_panel(8, CONT_Y + 4,  LCD_W - 16, 76)
            M5.Lcd.setFont(SELECTED_FONT)
            M5.Lcd.setTextColor(C_GRAY)
            M5.Lcd.setCursor(16, CONT_Y + 12)
            M5.Lcd.print("COUNT")
            draw_panel(8, CONT_Y + 88, LCD_W - 16, 30)
            M5.Lcd.setTextColor(C_GRAY)
            M5.Lcd.setCursor(16, CONT_Y + 96)
            M5.Lcd.print("青+1  赤-1  右タップ+1")

        if counter != last_counter:
            num_color = C_GREEN if counter > 0 else C_RED if counter < 0 else C_GRAY
            M5.Lcd.fillRect(16, CONT_Y + 36, LCD_W - 32, 38, C_PANEL)
            M5.Lcd.setFont(SELECTED_FONT)
            M5.Lcd.setTextColor(num_color)
            M5.Lcd.setCursor(16, CONT_Y + 40)
            M5.Lcd.print("{}".format(counter))
            last_counter = counter

        # POST結果(タイマーで消える)
        if post_msg != last_post_msg or post_msg_timer == 0:
            M5.Lcd.fillRect(8, CONT_Y + 126, LCD_W - 16, 24, C_BG)
            if post_msg_timer > 0:
                draw_panel(8, CONT_Y + 126, LCD_W - 16, 24)
                M5.Lcd.setFont(SELECTED_FONT)
                M5.Lcd.setTextColor(C_GREEN if "OK" in post_msg else C_RED)
                M5.Lcd.setCursor(16, CONT_Y + 132)
                M5.Lcd.print(post_msg)
            last_post_msg = post_msg

        if post_msg_timer > 0:
            post_msg_timer -= 1

    time.sleep(0.1)