yProcessingClub

すみません、許してください

【超かぐや姫!】スタンプラリー(富士)に行ってきた

www.city.fuji.shizuoka.jp

2月にやっていて、それに気づいた時にはまぁさすがにもう無理かなーと諦めていた(そしてその日にクリアファイル売り切れ)。
今回在庫が復活したので初日に行ってきた。

鈍行で気合で行ったわよ。横浜駅から3時間もかかった。

富士駅


男の娘がお出迎え。台紙をもらってないのでスルー。

富士駅→新富士駅にバスで向かう。7分くらい。

新富士駅


新富士駅の観光案内所。いろはがいた。
案内所で用紙をもらう。


スタンプラリー用紙ゲット。


さっそく一個目を押す。

新富士駅→富士駅に戻る。

富士駅からバスでロゼシアターに向かう。(バスをロゼシアター入口で降りる)

ロゼシアター


ヤッチョ。
右にあるモニターは何分かおきに超かぐや姫!のPVが流れるっぽい。


2個目。

そのまま歩いて市役所に向かう。徒歩15分くらい。

富士市役所


いっぱいいた。


帝スタンプを押した。


掲示板にもスタンプラリーのポスターが貼ってあった。

富士六所浅間神社に歩いて向かう。徒歩20分くらいかな。このあたりから大雨が降り出しておしまいになる。


浅間神社はたくさんあるので気を付けよう。市役所の近くの富士六所浅間神社がスタンプラリー会場である。

富士六所浅間神社


鳥居がいい感じ。建物もデカいわね。


ろかまみ。


ろかまみスタンプを押した。


金運おみくじをやってみた。運勢は大吉。宝くじを買うしかないわね。

吉原四丁目というバス停から富士駅にバスで戻った。
富士駅の改札を入り、最初に掲載した男の娘スタンプを押した。

そのまま吉原駅に電車で向かう。吉原駅のホームから「岳南鉄道乗り換えはこちら」のほうに向かう。

吉原駅


JR吉原駅と岳南鉄道吉原駅の境目にかぐやがいた。

入場券を購入して岳南鉄道吉原駅のホームに入る。

岳南鉄道吉原駅


円形のパネルが飾られていた。そういえば電車にも何かラッピングされてたんだっけか。すっかり忘れていて撮影できず。


駅の構造物に隠れているかぐや。

JR吉原駅に戻り、富士駅に電車で戻り、さらにバスで新富士駅。富士と新富士往復しすぎて自認が地元民になった。

クリアファイル


クリアファイルをゲットした。


こっちは友達がゲットした分。かぐやとヤッチョでランダムである。

ミッション終了


目標達成したので少しだけ観光。お土産コーナーにラブライブサンシャイングッズが置かれていた。なるほど静岡だからね。
ゆるキャンは一切無し。あれは山梨の縄張りか。


ラブライブロールちゃん。美味しそうだけどカロリーが凄そう。

新幹線

体力が尽きたので新富士駅から新幹線で戻ることにした。鈍行だと3時間かかるが新幹線だと1時間で済む。科学の力ってすげー。


食い物のチョイスが新幹線に乗るジジイすぎる。小腹すいたので買ってみたがこの手の海鮮はあまり得意ではないのでほとんど友達に食べてもらった。

完走した感想

多分これが一番早いと思います。本当に?

クリアファイル無くなるのが怖すぎたのでかなり足早に回った。本当なら観光しながらゆっくり回るべきだろう。でも富士駅と新富士駅を回った感じではそこまで観光地観光地してる感じも無かった。かなりシャッター街な感じ。富士駅からバスとか岳南鉄道とかそういうので観光地に行くハブの役割なのかもしれんね。

Raspberry PI Zero 2 WH + SSD1306 有機ELを制御する + タクトスイッチ

はじめに

yuri-processing-club.hatenablog.com
https://amzn.to/476s8Kg
前回は磁気センサとLチカを行った。

今回は有機ELに磁気センサの値を表示してみる。また、タクトスイッチを用意し、スイッチ押下で有機ELに表示される内容を切り替えてみる。

有機EL

配線


0.96インチ 128×64ドット有機ELディスプレイ(OLED) 白色: オプトエレクトロニクス 秋月電子通商-電子部品・ネット通販
分かりやすいドキュメントが無いのでアレだが、上部にピンコネクタがついていて、GND, VCC, SCL, SDAと書いてあるのでそれをそのままラズパイに配線する。VCCは3.3Vでも5Vでもどちらでもよい。

Raspberry PI - 有機ELを以下のように接続する。
3V3 (1) - VCC
GPIO2 (3) - SDA
GPIO3 (5) - SCL
GND (6) - GND

認識できているかをi2cdetectで確認する。

$ i2cdetect -y 1
     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:                         -- -- -- -- -- -- 0e --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- -- -- -- -- 3c -- -- --
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --

0x3Cがおそらく有機ELのアドレスだろう。

インストール

有機EL制御用のライブラリluma.oledを入れる。ただし、そのままpip installしようとするとexternally-managed-environmentエラーが出て怒られるので仮想環境にインストールする。

$ cd ~/magproj
$ sudo apt update
$ sudo apt install -y python3-gpiozero python3-lgpio python3-full python3-venv python3-pil
$ python3 -m venv .venv
$ source .venv/bin/activate
$ python -m pip install --upgrade pip
$ source .venv/bin/activate

lgpioは後ほどのタクトスイッチ用。

今まで入れていたsmbus2などのライブラリも仮想環境で入れ直す必要がある。

$ cd ~/magproj
$ source .venv/bin/activate
$ python -m pip install smbus2 luma.oled

pythonスクリプト

oled_display.py
from luma.core.interface.serial import i2c
from luma.core.render import canvas
from luma.oled.device import ssd1306
class OledDisplay:
    def __init__(self, port: int = 1, address: int = 0x3C,
                 width: int = 128, height: int = 64):
        serial = i2c(port=port, address=address)
        self._device = ssd1306(serial, width=width, height=height)

    def clear(self) -> None:
        self._device.clear()

    def show_lines(self, lines: list[str]) -> None:
        with canvas(self._device) as draw:
            y = 0
            for line in lines[:6]:
                draw.text((0, y), line, fill="white")
                y += 10
main.py
import time
from mag_sensor import MagSensor
from status_led import StatusLed
from oled_display import OledDisplay
LED1_GPIO = 17
LED2_GPIO = 18
PERIOD_SEC = 1.0
FLASH_SEC = 0.1
def main() -> None:
    run_led = StatusLed(gpio_pin=LED1_GPIO)
    event_led = StatusLed(gpio_pin=LED2_GPIO)
    oled = OledDisplay(port=1, address=0x3C)

    try:
        run_led.on()
        oled.show_lines([
            "Mag system start",
            "",
            "Initializing..."
        ])

        with MagSensor(bus_id=1, address=0x0E) as sensor:
            next_time = time.monotonic()

            while True:
                next_time += PERIOD_SEC

                sample = sensor.read_raw()

                print(
                    f"Hx={sample.hx:6d} "
                    f"Hy={sample.hy:6d} "
                    f"Hz={sample.hz:6d} "
                    f"Traw={sample.temp_raw:5d}"
                )

                oled.show_lines([
                    "BM1422 raw",
                    f"Hx: {sample.hx:6d}",
                    f"Hy: {sample.hy:6d}",
                    f"Hz: {sample.hz:6d}",
                    f"T : {sample.temp_raw:5d}",
                ])

                event_led.flash(duration_sec=FLASH_SEC)

                remaining = next_time - time.monotonic()
                if remaining > 0:
                    time.sleep(remaining)

    finally:
        run_led.off()
        event_led.off()
        oled.clear()
        run_led.close()
        event_led.close()


if __name__ == "__main__":
    main()

実行結果


おお
無事に表示できた。1秒おきに値が更新される。チラつきなども無く綺麗である。

タクトスイッチ

配線

GPIO22 (15) - タクトスイッチ - GND (6)
とする。

pythonスクリプト

tact_switch.py
from collections.abc import Callable
from gpiozero import Button


class TactSwitch:
    """Tact switch wrapper using gpiozero.Button."""

    def __init__(
        self,
        gpio_pin: int,
        *,
        pull_up: bool = True,
        bounce_time: float = 0.05,
        hold_time: float = 1.0,
    ):
        self._button = Button(
            gpio_pin,
            pull_up=pull_up,
            bounce_time=bounce_time,
            hold_time=hold_time,
        )

    def set_on_pressed(self, callback: Callable[[], None] | None) -> None:
        """Register callback called once when button is pressed."""
        self._button.when_pressed = callback

    def set_on_released(self, callback: Callable[[], None] | None) -> None:
        """Register callback called once when button is released."""
        self._button.when_released = callback

    def set_on_held(self, callback: Callable[[], None] | None) -> None:
        """Register callback called when button is held."""
        self._button.when_held = callback

    def is_pressed(self) -> bool:
        return self._button.is_pressed

    def wait_for_press(self, timeout: float | None = None) -> bool:
        return self._button.wait_for_press(timeout=timeout)

    def wait_for_release(self, timeout: float | None = None) -> bool:
        return self._button.wait_for_release(timeout=timeout)

    def close(self) -> None:
        self._button.close()
main.py
import time

from mag_sensor import MagSensor
from status_led import StatusLed
from oled_display import OledDisplay
from tact_switch import TactSwitch

LED1_GPIO = 17
LED2_GPIO = 18

BUTTON_GPIO = 22

PERIOD_SEC = 1.0
FLASH_SEC = 0.1

UT_PER_LSB = 0.042

def main() -> None:
    run_led = StatusLed(gpio_pin=LED1_GPIO)
    event_led = StatusLed(gpio_pin=LED2_GPIO)
    oled = OledDisplay(port=1, address=0x3C)
    button = TactSwitch(gpio_pin=BUTTON_GPIO, pull_up=True, bounce_time=0.05, hold_time=1.0)

    state = {
        "display_mode": "raw",   # "raw" or "uT"
    }
    def toggle_display_mode() -> None:
        if state["display_mode"] == "raw":
            state["display_mode"] = "uT"
        else:
            state["display_mode"] = "raw"
        print(f"Display mode changed to: {state['display_mode']}")

    button.set_on_pressed(toggle_display_mode)
    try:
        run_led.on()
        oled.show_lines([
            "Mag system start",
            "",
            "Initializing..."
        ])
        time.sleep(2.0)
        with MagSensor(bus_id=1, address=0x0E) as sensor:
            next_time = time.monotonic()

            while True:
                # 次回予定時刻を先に決める
                next_time += PERIOD_SEC

                # センサ読み取り
                sample = sensor.read_raw()

                # 表示
                if state["display_mode"] == "raw":
                    print(
                        "[RAW] "
                        f"Hx={sample.hx:6d} "
                        f"Hy={sample.hy:6d} "
                        f"Hz={sample.hz:6d} "
                        f"Traw={sample.temp_raw:5d}"
                    )

                    oled.show_lines([
                        "BM1422 raw",
                        f"Hx: {sample.hx:6d}",
                        f"Hy: {sample.hy:6d}",
                        f"Hz: {sample.hz:6d}",
                        f"T : {sample.temp_raw:5d}",
                    ])

                else:
                    hx_ut = sample.hx * UT_PER_LSB
                    hy_ut = sample.hy * UT_PER_LSB
                    hz_ut = sample.hz * UT_PER_LSB

                    print(
                        "[uT ] "
                        f"Hx={hx_ut:8.3f} "
                        f"Hy={hy_ut:8.3f} "
                        f"Hz={hz_ut:8.3f} "
                        f"Traw={sample.temp_raw:5d}"
                    )

                    oled.show_lines([
                        "BM1422 uT",
                        f"Hx: {hx_ut:8.3f}",
                        f"Hy: {hy_ut:8.3f}",
                        f"Hz: {hz_ut:8.3f}",
                        f"T : {sample.temp_raw:5d}",
                    ])

                # イベントLEDを一瞬点灯
                event_led.flash(duration_sec=FLASH_SEC)

                # 次の予定時刻まで待つ
                remaining = next_time - time.monotonic()
                if remaining > 0:
                    time.sleep(remaining)
                else:
                    # 処理が周期を超過した場合は、ここでは何もしない
                    # 次ループで再び PERIOD_SEC 刻みで進める
                    pass

    finally:
        run_led.off()
        event_led.off()
        button.close()
        oled.clear()
        run_led.close()
        event_led.close()


if __name__ == "__main__":
    main()

button.set_on_pressed(toggle_display_mode)でボタン押下時のイベントハンドラを設定している感じだ。

実行結果


タクトスイッチを押すと有機ELの表示が生値⇔μTと切り替わる。

おしまい

・磁気センサ
・Lチカ
・有機EL
・タクトスイッチ
まで実装することができた。次はIMUをつけていく。

Raspberry PI Zero 2 WH + AE-BM1422AGMV

はじめに


https://amzn.to/476s8Kg
Raspberry PI Zero 2 WH + 磁気センサで何かしたいなと思ってアキバに行って色々買って来た。
なお、電子工作はほぼ初心者である。

yuri-processing-club.hatenablog.com
10年前にarduinoを買ったが、何とLチカをやってそれで終わりという三日坊主以下のアレであったのであった。

今回は多少頑張るわよ。

ラズパイセットアップ

本体のセットアップ


ケースに入れた。ヒートシンクも貼っておいた。

SSHを有効にした。PCからteraterm等でログインして操作できるので便利。なお、LANケーブルを指す穴は無いのでwifiを使うか、USB-LANアダプタを別途調達する必要がある。USB端子はmicro-Bが1個(2個あるけど1個は電源用)なのでtype A変換コネクタやマウスキーボード接続も考えるとUSBハブも用意したほうがいい。また、HDMIはminiなので普通のHDMIケーブルを指す場合も変換コネクタが必要。あとmicro-Bの電源ケーブルも必要か。

pythonの導入

$ sudo apt update
$ sudo apt install -y python3-smbus i2c-tools

磁気センサ

配線


地磁気センサーモジュール AE-BM1422AGMV: 計測器・センサー・ロガー 秋月電子通商-電子部品・ネット通販
地磁気センサーモジュール AE-BM1422AGMVを買った。3軸磁気センサである。インタフェースがI2Cなのでラズパイからは楽に触れるとのこと。

・使用センサ:BM1422
・電源入力:VIN=3.3V~5.5V
・I2Cアドレス:0x0E


sudo raspi-configでI2Cを有効にした。


AE-BM1422AGMVを接続する。

Raspberry PIのピンアサインはpinoutで見られる。

AE-BM1422AGMVのピンアサインはデータシートで見られる。

2 VIN 5V入力Qwiicケーブル赤
3 GND GND Qwiicケーブル黒
4 SCL I2CクロックQwiicケーブル黄
5 SDA I2CデータQwiicケーブル青

※「I2Cレベルコンバーターとレギュレーターを搭載しており3.3Vと5Vどちらで使用できます。」と書いてあるのでVINは3.3Vでも良い。

Raspberry PI - AE-BM1422AGMVを以下のように接続する。
3V3 (1) - 赤
GPIO2 (3) - 青
GPIO3 (5) - 黄
GND (6) - 黒

i2cdetect -y 1を実行して磁気センサが接続できていることを確認する。

$ i2cdetect -y 1

     0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
00:                         -- -- -- -- -- -- 0e --
10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
40: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
70: -- -- -- -- -- -- -- --

0x0Eが見えていれば良い。

基板端の2つの隣り合ったジャンパーは通常使用しません。BM1422のADDRピンの設定を変える必要がある場合にご使用下さい。

とあり、AE-BM1422AGMVではジャンパをいじくるとI2Cのアドレスを変えられるようである。その場合は0x0Fになるのかな。


pythonスクリプトの作成

AIをタコ殴りにしてスクリプトを書いてもらう。
チャッピー 人間をつかまえて胃の中を調べる道具 出して

mag_sensor.py
from dataclasses import dataclass
from smbus2 import SMBus
import time


@dataclass(frozen=True)
class MagSampleRaw:
    hx: int
    hy: int
    hz: int
    temp_raw: int


class MagSensor:
    """BM1422 raw data reader."""

    # I2C / register map
    REG_WIA = 0x0F
    REG_DATAX_L = 0x10
    REG_DATAY_L = 0x12
    REG_DATAZ_L = 0x14
    REG_STA1 = 0x18
    REG_CNTL1 = 0x1B
    REG_CNTL2 = 0x1C
    REG_CNTL3 = 0x1D
    REG_CNTL4_L = 0x5C
    REG_CNTL4_H = 0x5D
    REG_TEMP_L = 0x60

    WIA_EXPECTED = 0x41

    def __init__(self, bus_id: int = 1, address: int = 0x0E):
        self._bus_id = bus_id
        self._address = address
        self._bus = SMBus(bus_id)
        self._initialized = False

    def initialize(self) -> None:
        """Check sensor identity and mark ready."""
        wia = self._read_u8(self.REG_WIA)
        if wia != self.WIA_EXPECTED:
            raise RuntimeError(
                f"BM1422 WIA mismatch: got 0x{wia:02X}, expected 0x{self.WIA_EXPECTED:02X}"
            )
        self._initialized = True

    def read_raw(self) -> MagSampleRaw:
        """Perform one single-shot measurement and return raw values."""
        if not self._initialized:
            raise RuntimeError("MagSensor is not initialized. Call initialize() first.")

        self._start_single_measurement()

        sta1 = self._wait_drdy_high(timeout_s=0.5, poll_s=0.005)
        if (sta1 & 0x40) == 0:
            raise TimeoutError(f"BM1422 DRDY timeout. STA1=0x{sta1:02X}")

        hx = self._read_s16_le(self.REG_DATAX_L)
        hy = self._read_s16_le(self.REG_DATAY_L)
        hz = self._read_s16_le(self.REG_DATAZ_L)
        temp_raw = self._read_u16_le(self.REG_TEMP_L)

        return MagSampleRaw(hx=hx, hy=hy, hz=hz, temp_raw=temp_raw)

    def close(self) -> None:
        if self._bus is not None:
            self._bus.close()
            self._bus = None
        self._initialized = False

    def __enter__(self):
        self.initialize()
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        self.close()

    def _start_single_measurement(self) -> None:
        # PowerDown / Reset side first
        self._write_u8(self.REG_CNTL1, 0x23)
        time.sleep(0.01)

        # 14bit single-shot mode
        self._write_u8(self.REG_CNTL1, 0xC2)

        # CNTL4 per current working sequence
        self._write_u8(self.REG_CNTL4_L, 0x00)
        self._write_u8(self.REG_CNTL4_H, 0x00)
        time.sleep(0.01)

        # Enable DRDY-related behavior
        self._write_u8(self.REG_CNTL2, 0x0C)
        time.sleep(0.01)

        # FORCE=1
        self._write_u8(self.REG_CNTL3, 0x40)

    def _wait_drdy_high(self, timeout_s: float, poll_s: float) -> int:
        t0 = time.time()
        while time.time() - t0 < timeout_s:
            sta1 = self._read_u8(self.REG_STA1)
            if sta1 & 0x40:
                return sta1
            time.sleep(poll_s)
        return self._read_u8(self.REG_STA1)

    def _read_u8(self, reg: int) -> int:
        return self._bus.read_byte_data(self._address, reg)

    def _write_u8(self, reg: int, val: int) -> None:
        self._bus.write_byte_data(self._address, reg, val & 0xFF)

    def _read_s16_le(self, reg_l: int) -> int:
        lo = self._bus.read_byte_data(self._address, reg_l)
        hi = self._bus.read_byte_data(self._address, reg_l + 1)
        value = (hi << 8) | lo
        if value & 0x8000:
            value -= 0x10000
        return value

    def _read_u16_le(self, reg_l: int) -> int:
        lo = self._bus.read_byte_data(self._address, reg_l)
        hi = self._bus.read_byte_data(self._address, reg_l + 1)
        return (hi << 8) | lo
main.py
import time
from mag_sensor import MagSensor
PERIOD_SEC = 1.0
def main() -> None:
    try:
        with MagSensor(bus_id=1, address=0x0E) as sensor:
            next_time = time.monotonic()

            while True:
                # 次回予定時刻を先に決める
                next_time += PERIOD_SEC

                # センサ読み取り
                sample = sensor.read_raw()

                # 表示
                print(
                    f"Hx={sample.hx:6d} "
                    f"Hy={sample.hy:6d} "
                    f"Hz={sample.hz:6d} "
                    f"Traw={sample.temp_raw:5d}"
                )

                # 次の予定時刻まで待つ
                remaining = next_time - time.monotonic()
                if remaining > 0:
                    time.sleep(remaining)
                else:
                    # 処理が周期を超過した場合は、ここでは何もしない
                    # 次ループで再び PERIOD_SEC 刻みで進める
                    pass

if __name__ == "__main__":
    main()
実行結果

これでmainを実行すると、PERIOD_SEC周期で磁気センサの値を取得して表示する。

$ python3 main.py
Hx=   888 Hy=   297 Hz= -1961 Traw= 7128
Hx=   887 Hy=   326 Hz= -1971 Traw= 7132
Hx=   900 Hy=   311 Hz= -1973 Traw= 7132
Hx=   887 Hy=   322 Hz= -1977 Traw= 7132
Hx=   893 Hy=   293 Hz= -1968 Traw= 7128
Hx=   890 Hy=   319 Hz= -1968 Traw= 7128
Hx=   891 Hy=   318 Hz= -1968 Traw= 7128
Hx=   893 Hy=   319 Hz= -1954 Traw= 7128

表示しているのは生値であり、これを磁気量に換算する必要がある。

磁気感度 DeltaV - 0.042 - µT/ LSB

とあるのでHx = Hx * 0.042をすると生値からμTに変換できる。この辺の変換処理は別のクラスで行うことにした。なぜなら個体差があるため0.042は変わり得るし、温度によっても変化するしセンサの直交性なども気に出すと磁気量への変換はかなり規模が大きいからである。MagSensorクラスはあくまで磁気センサの生値のみ表示する。なお、BM1422の温度センサの生値も表示しているが、これの物理量(℃)への変換はなかった。

Lチカ

配線

LEDを光らせる。


適当なLEDと1kΩ抵抗。これを
GPIO(17)-抵抗-LED(緑)-GND
GPIO(18)-抵抗-LED(赤)-GND
として接続する。LEDの向きに注意。

pythonスクリプトの作成

status_led.py
import time
from gpiozero import LED


class StatusLed:
    def __init__(self, gpio_pin: int):
        self._led = LED(gpio_pin)

    def on(self) -> None:
        self._led.on()

    def off(self) -> None:
        self._led.off()

    def flash(self, duration_sec: float = 0.1) -> None:
        self.on()
        time.sleep(duration_sec)
        self.off()

    def close(self) -> None:
        self._led.close()
main.py

mainにLEDを光らせる処理を追加する。
・スクリプト起動時に緑LEDを点灯する。スクリプト終了時に消灯。これにより外部からスクリプトが立ち上がっているかどうかを判断できる。
・磁気センサ取得時に赤LEDを一瞬光らせる。

import time

from mag_sensor import MagSensor
from status_led import StatusLed


LED1_GPIO = 17
LED2_GPIO = 18

PERIOD_SEC = 1.0
FLASH_SEC = 0.1


def main() -> None:
    run_led = StatusLed(gpio_pin=LED1_GPIO)
    event_led = StatusLed(gpio_pin=LED2_GPIO)

    try:
        run_led.on()

        with MagSensor(bus_id=1, address=0x0E) as sensor:
            next_time = time.monotonic()

            while True:
                # 次回予定時刻を先に決める
                next_time += PERIOD_SEC

                # センサ読み取り
                sample = sensor.read_raw()

                # 表示
                print(
                    f"Hx={sample.hx:6d} "
                    f"Hy={sample.hy:6d} "
                    f"Hz={sample.hz:6d} "
                    f"Traw={sample.temp_raw:5d}"
                )

                # イベントLEDを一瞬点灯
                event_led.flash(duration_sec=FLASH_SEC)

                # 次の予定時刻まで待つ
                remaining = next_time - time.monotonic()
                if remaining > 0:
                    time.sleep(remaining)
                else:
                    # 処理が周期を超過した場合は、ここでは何もしない
                    # 次ループで再び PERIOD_SEC 刻みで進める
                    pass

    finally:
        run_led.off()
        event_led.off()
        run_led.close()
        event_led.close()


if __name__ == "__main__":
    main()
実行結果


光ったわね。

おしまい

最低限のところはできた気がする。次はOLEDで表示するわよ。

【超かぐや姫!】VRライブを見てきた

v-puroland.sanrio.co.jp

イベント開催日
2月14日(土) 12:00 [JST]
2月15日(日) 12:00 / 18:00 [JST]
3月1日(日) 18:00 [JST]
3月7日(土) 12:00 [JST]
3月8日(日) 12:00 [JST]

3/1の部を見てきた(といっても全部内容は同じである)。「私は、わたしの事が好き。」一曲を歌うミニライブである。生で歌うのではなくあらかじめ収録した歌/ダンスモーションのパフォーマンス映像を流す感じ。
VTuberやるちゅって大昔に購入したVive Cosmosを引っ張り出してきてVRで見たわよ。パーティクルもちょこちょこ飛んでたがホロライブとか大手のVライブと比べると見劣りはしたわね。クラファンマネーでヤッチョかぐやいろはの3人が歌って踊るすごいやつ作れないかしら。

無料だし来週もやるのでまだ見ていない方はぜひ。VRゴーグル無くても大丈夫わよ。

あと、ファンが作った?ツクヨミワールドもあるっぽいので行ってみようかしら。コミュ障にとってはVRChatめっちゃ怖いからインしたくないんだけどね。完全一人用でインさせてくれ!

【超かぐや姫!】聖地巡礼してきた

先駆者様の記事を読んで行ったので特に真新しいものは無い。ぼくはねぇ、ゲームの攻略本を買って攻略本の通りにプレイするのがすきなの。


かぐやが尾行中隠れる街路樹。ここではたくさんのオタクが写真を撮っていた。


着いてこい!の場面。ここが巡礼していて一番感動した。


映画館内のポップ。


監督のサイン入りポスター。


超かぐや姫缶。値段見ずに買ったら1本800円でビビったよね。

 聖地巡礼を済ませた後で映画2回目を見てきた。音響がとにかく良く感動した。「映画館 高音質」で調べて関東の色々な映画館に行ったことがあるがその中でも特段に良い。明るめで曇った音が全くない。PAスピーカーにありがちな高音キンキンでもない。低音はスピード感ある。めっちゃハイエンドなオーディオという感じだった。音響監督監修のサウンドチューニングということでイコライザの調整も多分に寄与しているのだろう。
 本編の感想としてはもはやネットの感想や考察と自我が混ざっているので言えることは無いのだが、かぐやが「結婚しよ」って言った途端にフラグが立って月からお迎えが来たシーンが印象に残った。かぐやが多少地球でエンジョイする分には良いけど結婚となると話は別だぞと月人が思ったのかしらね。この辺原典ではどうなっているのかしら。
 また、27日上映分からエンディングの後でray MVが流れる仕様になっていた。MVはyoutubeで公開されているがネタバレ回避のため意図的に見ないようにしていたので初見である。10年後の復活ライブ映像だけだと思っていたがそれ以外の映像も多くて情報量が凄かった。戦時中?のかぐやの顔で非常にしんどくなった。この時点(今から50~100年前くらい)でもヤチヨではなくかぐやだったのか。


横浜でタピオカラーメンを食べた。"映え"が凄い。甘さは無くて小さいお餅という感じ。正直ラーメンには合わないかな。麺がうどんレベルの超極太でほとんど茹でていなくて小麦粉を強く感じた。スープは大分しょっぱめかな。普段食べる神豚のほうが好み。


ヤッチョの部屋とか花火会場も巡礼したいわね。

【ネタバレあり】超かぐや姫!を見てきた

ネタバレあり感想。

超かぐや姫!を劇場で見てきた。完全初見である。
まぁ全くのノー知識というわけではなく、
・かぐや姫が求婚を断ったのは百合だったから!?(新解釈)
・ネトフリマネーで作られた物凄い百合
・ワールドイズマイン、メルトが流れる
・のじゃおじが言及
・ヤチヨの笑顔が平成アニメっぽい
・マンガが百合ではなくユウジョウ路線で解釈違いのオタクが出ている?
など若干の要素はツイッター経由で把握していたが、ネタバレをなるべく見ないようにしていたのでそこそこ清い心で見ることができた。

 今後ネットの大海原に出て様々な考察や感想を摂取していく中で自身の感想が変質していくことが確実であるので現時点での感想を書いておく。後日すっかり変わってしまった自分が当時何を思っていたかを振り返るのが目的である。劇場で一回見ただけの感想なので間違っている部分も多いとは思うが許して欲しい。
 まず、かぐやがずっとコロコロ表情が変わるし動きも凄くて可愛いくて一気にファンになってしまった。そんな天真爛漫な子が自分の役割を悟って帰るシーンは本当につらかった。最後まで感情を出さずに帰ってしまうんだもんよォ。あんなに竹取物語を読んで月に帰ってオシマイなんてハッピーエンドじゃないその後はどうしたって言ってた子にそんなこと言わせるのはあまりにも芸術が過ぎる。
 でも月に戻ったあとに彩葉の歌を聴いてやっぱり地球に戻ろうってなったのは完全に心が死んでたわけじゃないんだと分かって良かった。しっかり仕事を頑張って終わらせて引き継ぎまでしている。でもそんな簡単に(簡単とはいっても何十年何百年、それ以上?かかってるとは思うが)かぐや姫の役職を降りられるとは思わないので色々物凄いことがあったのだろう(信頼できない語り部)。
 でもまさかその後事故で8000年旅することになるとはね。8000年待ち続けるのは本当に恐縮してしまう。ほんでヤチヨはマジでどんな心境で再会した彩葉と接してたんですかね。また映画見る時は今度はヤチヨの表情に注目していくけどさぁ。ここで以前読んだインターネットのオタクの感想によるヤチヨは自分のことをおばあちゃんだと思ってるっぽくて確かに劇中でもセリフで出てきたが彩葉はヤチヨのこと推してるしそんな自分を卑下せずイチャコラしてほしいが8000年の時間経過で距離感があるのはそれはそう。
 彩葉のパンケーキをまた食べたい→(VRは味覚が無いのは伏線)→ヤチヨの願いを叶えるために彩葉の進路がVR味覚研究になる、この流れはなるほどすぐに分かったが、10年後にかぐやが現実世界に復活?したのはちょっとどういうことが分からなかった。てっきりVR空間でヤチヨにパンケーキを食べてもらうのだと思っていたため。まぁVR空間で味覚を再現したところで彩葉の作ったパンケーキが食べられるわけではないのでVR世界のヤチヨをこの世界に顕現させる何らかの媒体が必要で、そうなると蘇ったかぐやとはサイボーグとかロボット(中でヤチヨの分身が動いている)とかそういう感じなんですかね。彩葉は単なる味覚の再現だけでなく(味覚の再現"だけ"といってもおそらく彩葉のパンケーキをまた食べたい一心でヤチヨは8000年乗り切ったのだとも思うが)もっと深い意味でヤチヨ/かぐやを救済する計画があるような、例えば神話にちなんだ物凄いことをしてそうな気はしていて、まぁこのあたりは再度映画を見たのちに考察サイト等で理解することにする。
 ラストでこの話がハッピーエンドだと思うかとこちらに問いかけていたが自分としてはやはりどうしてもハッピーエンドとは思えず、8000年待ち続けたヤチヨをしっかりと救済してほしいが例えば因果を変えて(事故が起きなかったことにして)無かったことにもして欲しくなく、完璧に丸く収めるのは難しいのは百も承知である。総じて彩葉はかぐやを月の住人から人間にしていく工程を行っているのだろうが本当に正直なことを言うと彩葉にはかぐやは月の住人スペックのままでむしろ彩葉側を月の住人に寄せていって(具体的には不老不死の薬を飲んでもろて)未来永劫(少なくとも8000年以上は)かぐやとイチャコラしていて欲しい(未来永劫生き続けると人は狂うと言うが彩葉に限っては絶対にこれができるはず)、これこそが究極のハッピーエンドだと思うわけだが皆様はどう思われるだろうか。

ゲーム開発日記 その4

前回買った建物アセットが予想以上に素材が少なかったので、別のアセットを購入することにした。

assetstore.unity.com

適当に配置して屋敷を作った。適当といっても作成に一週間くらいかかっているしまだ未完成である。


時間帯が変わる際にお嬢様をランダム移動(Nav Mesh内の適当な場所にワープ)させていたのを、ランダム位置を完全ランダムではなく、メインホール、キッチン、自室など部屋単位で設定、さらに室内をぶらぶら歩かせるか室内の椅子に座るかなど状態を決められるようにした。


ソファから座標ずれして座っているお嬢様。バグってはいるが、主目的のぶらぶら歩く、座るの切り替えはできているのでヨシ!

assetstore.unity.com
なお、座るアニメーションはKAWAII ANIMATIONS 100を購入して使用。金がどんどん溶けていくぞ。ちょうどブラックフライデーで安く売ってて助かった。


お嬢様がうろついている時は話しかけると体ごとプレイヤーを向く仕様だったが、ソファ等に座っている際には体を向けるとソファ向きとの不整合が生じてしまう。座っている際には顔だけプレイヤーを向く実装を追加した。実装はFinal IKのSetLookAtを使用。

assetstore.unity.com
Final IKはかなり高額のアセットである。VTuberVRモーションキャプチャーみたいなのやる際に必要で買っていたので良かった。まさかVTuber活動が役に立つとはね。


座る位置を微調整。NavMeshが悪さをしていた。ソファは移動できないのでソファ周辺にはNavMeshを張っていない。その状態でソファ(NavMesh外)に座らせ(AIを設置し)ようとすると強制的に最寄りのNavMeshに吸着するためソファの前の空間に移動してしまう。座る際にはNavMeshを無効にする処理を追加して上手く動かすことができた。


ついでにメイドにもお嬢様が近かったらそちらを向く処理を実装した。こちらもFinal IKである。挙動としてはちょっと違和感があるかも。バイオハザード3か何かでアイテムが落ちてたら顔を向ける処理があったが、まさにそれと同じ感じ。SetLookAtは頭だけじゃなくneckやspineも登録できて腰~肩までやんわりと動かして目標を向くこともできるのでそれも試してみたが今度は上半身をターゲットに向けるアーマードコアみたいな動きとなってさらに違和感が出た。近くに人物がいて注目する場合には目だけ動かすくらいが自然かもね。まぁとりあえず頭だけ向ける処理で続行する。


会話データを揃えるなどの肉付けはせずに引き続き骨格部分の開発を行っていく。