はじめに
こんにちは、ほうき星です。
皆さん、AWS 全冠トロフィーを受け取ってから、そのまま箱にしまい込んでいませんか?
このトロフィーは 2025 Japan All AWS Certifications Engineers の SWAG として、AWS Summit Japan で配布されたものです。
この手のクリスタルトロフィーは台座付きなら飾りやすいのですが、AWS 全冠トロフィーには台座がなく、立てて飾るには心もとないです。横に寝かせるとせっかくの文字が見えづらくなってしまいます。結果として「箱にしまったまま…」という方も多いのではないでしょうか。
せっかくの記念品、どうせならいい感じに飾りたいですよね。
そこで今回は、AWS 全冠トロフィーを映える形で飾れる IoT スタンドを作りましたので作り方をご紹介します。
作ったもの
作成した AWS 全冠トロフィーを飾る IoT スタンドの概要を紹介します。
- AWS 全冠トロフィーに完全にフィットする3Dプリント製のスタンド
- 内部にESP32を内蔵し LED を制御
- AWS IoT Core と連携し、スマホや PC から操作
作り方
AWS IoT Core の設定
(物理ボタンで制御できた方が使い勝手良い説ありますが)
IoT Core に接続してスマホなどから遠隔操作できる IoT なスタンドにします。
モノ(Thing)を作成する
モノ(Thing)とポリシーを作成し、IoT スタンドに書き込む証明書を取得します。
-
マネジメントコンソールにて AWS IoT > 管理 > モノ を開きます
-
モノのプロパティ は今回以下のようにしました
-
モノの名前:
all-cert-trophy-stand -
Device Shadow:
名前のないシャドウ(クラシック)
シャドウステートメントを編集 でbrightnessを持たせていますシャドウステートメント{ "state": { "reported": { "brightness": 0 }, "desired": { "brightness": 0 } } }
-
モノの名前:
-
証明書にポリシーをアタッチ で
ポリシーを作成から以下のようなポリシーを作成し、アタッチしますポリシー{ "Version": "2012-10-17", "Statement": [ { "Condition": { "Bool": { "iot:Connection.Thing.IsAttached": "true" } }, "Effect": "Allow", "Action": "iot:Connect", "Resource": "arn:aws:iot:<リージョン>:<AWSアカウントID>:client/${iot:Connection.Thing.ThingName}" }, { "Effect": "Allow", "Action": [ "iot:Publish", "iot:Receive" ], "Resource": "arn:aws:iot:<リージョン>:<AWSアカウントID>:topic/$aws/things/${iot:Connection.Thing.ThingName}/shadow/*" }, { "Effect": "Allow", "Action": "iot:Subscribe", "Resource": "arn:aws:iot:<リージョン>:<AWSアカウントID>:topicfilter/$aws/things/${iot:Connection.Thing.ThingName}/shadow/*" } ] }上記ポリシーは ポリシー変数 を利用しています。
ポリシー変数についてはこちらの記事: [AWS IoT] ポリシー変数で複数のモノをセキュアに管理してみた で詳しく解説されていますのでご確認ください。
補足
IoT ポリシー とは
IoT ポリシーはデバイスに付与するアクセス許可(IAMポリシーのようなもの)です。
モノは証明書を使用し IoT Core に接続しますが、その証明書にポリシーをアタッチして「何を許可するか」を定義します。
IoT ポリシーで定義できるアクションやリソース指定の方法等は以下ドキュメントを参考にしてください。
Device Shadow とは
Device Shadow は IoT Core 上に保存されるモノの状態です。
モノがオフラインでも、最新の状態を IoT Core 上に保持したり、あるいはモノのあるべき状態を設定して、オンライン時に同期させることができます。
Device Shadow の設定では以下の2つのフィールドを設定できます。
- desired:モノのあるべき状態
- reported:モノの現在の状態
desired と reported に差がある場合、delta フィールドが生成され差分を取得することができます。
詳しくは以下ドキュメントを参考にしてください。
また、Device Shadow は MQTT を使用して取得することができます。
現在の Device Shadow を get(取得) することができるほか、差分(delta)が発生したタイミングにメッセージを受取ることもできます。
こちらも詳しくは以下ドキュメントを参考にしてください。
ESP32と電子工作
IoT スタンドには Seeed Studio XIAO ESP32S3 と テープLED を組み込み、IoT Core への接続と LED の制御を行います。
- Seeed Studio XIAO ESP32S3 は秋月電子等で購入するのが、技適などの面でも安心です
- Nch MOSFET は同じく秋月電子で購入可能な 2N7000 を選定しました
- テープLEDは COB タイプで白色のものを選定しました
配線と電子工作
ESP32S3 と COBテープLED、Nch MOSFET は以下のような配線としています。
- ESP32S3の5Vピンを COBテープLED の+端子と接続
- COBテープLED の-端子を Nch MOSFET のドレインと接続
- Nch MOSFET のソースは ESP32S3 の GND へ接続
- Nch MOSFET のゲートは ESP32S3 のGPIO4 と接続し、プルダウン抵抗として22kΩを入れています
参考:ESP32S3 のピン配置
配線図
実際の配線
プログラム書き込み
今回 ESP32S3 の制御には MicroPython を使用しました。
以下のプログラムと IoT Core にてモノ(Thing)を作成した時にダウンロードした証明書を書き込みます。
ESP32S3 への MicroPython の導入やThonnyを使用したプログラムの書き込みは、こちらのドキュメント:MicroPython のインストールをご確認ください。
フォルダ構成図
書き込むプログラムと証明書の階層は以下の通りです。
ESP32S3(root)/
├── certs/
│ ├── certificate.der ※certificate.pem.crtをDER形式に変換したもの
│ ├── private.der ※private.pem.keyをDER形式に変換したもの
│ └── routeca.der ※AmazonRootCA1.pemをDER形式に変換したもの
├── lib/
│ └── umqtt/
│ └── simple.py
└── main.py
MicroPython での MQTT の利用には umqtt.simple ライブラリを利用します。
こちらのgitリポジトリ:umqtt.simpleから入手してください。
モノ(Thing)を作成した際にダウンロードした証明書は umqtt.simple での利用にあたりDER形式に変換してください。
PEM形式からDER形式への変換にはこちらの記事:TLS/SSL証明書のファイル形式(PEMとDER)をご確認ください。
プログラム本体
import gc
import time
import json
from network import WLAN, STA_IF
from machine import Pin, PWM, reset
from ssl import SSLContext, PROTOCOL_TLS_CLIENT
from asyncio import run, Loop, sleep_ms, get_event_loop
from umqtt.simple import MQTTClient
class LED:
"""LEDをPWM制御するクラス
"""
FREQUENCY: int = 100
def __init__(self) -> None:
self._brightness: int = 0
self._leds: list[PWM] = [
PWM(Pin(4), freq=self.FREQUENCY),
]
self._set_duty(0)
def _clamp(self, value: int | float, min_value: int = 0, max_value: int = 100) -> int:
return int(max(min_value, min(max_value, value)))
def _set_duty(self, duty: int) -> None:
for led in self._leds:
led.duty_u16(int(65535 * self._clamp(duty) / 100))
def get_brightness(self) -> int:
return self._brightness
def set_brightness(self, brightness: int) -> None:
self._brightness = self._clamp(brightness)
async def routine(self): # -> Corutine
while True:
self._set_duty(self._brightness)
await sleep_ms(100)
class AllCertTrophyStand:
"""AWS IoT Coreと接続して、Device Shadowを操作するクラス
"""
SSID: str = "Your WiFi SSID"
PASSWORD: str = "Your WiFi Password"
THING_NAME: str = "all-cert-trophy-stand"
IOT_CORE_ENDPOINT: str = "Your AWS IoT Core Endpoint"
HEARTBEAT_INTERVAL: int = 5
def __init__(self) -> None:
self._led: LED = LED()
def _connect_wifi(self) -> None:
wlan: WLAN = WLAN(STA_IF)
wlan.active(True)
if not wlan.isconnected():
print("connecting to wifi...")
wlan.connect(self.SSID, self.PASSWORD)
while not wlan.isconnected():
pass
print("connected to wifi:", wlan.ifconfig())
def _connect_iot_core(self) -> None:
ssl_context: SSLContext = SSLContext(PROTOCOL_TLS_CLIENT)
ssl_context.load_cert_chain("/certs/certificate.der", "/certs/private.der")
self.mqtt_client: MQTTClient = MQTTClient(
self.THING_NAME,
self.IOT_CORE_ENDPOINT,
keepalive=self.HEARTBEAT_INTERVAL * 3,
ssl=ssl_context
)
self.mqtt_client.connect()
print("connected to iot core")
def _report_device_shadow(self, brightness: int) -> None:
shadow: dict = {
"brightness": brightness
}
self.mqtt_client.publish(
f"$aws/things/{self.THING_NAME}/shadow/update",
json.dumps({
"state": {
"reported": shadow
}
}).encode()
)
def _subscribe_shadow_update_delta(self) -> None:
def set_brightness(topic: bytes, message: bytes) -> None:
delta: dict = json.loads(message.decode())["state"]
if "brightness" in delta:
brightness: int = delta["brightness"]
self._led.set_brightness(brightness)
else:
brightness: int = self._led.get_brightness()
self._report_device_shadow(brightness)
self.mqtt_client.set_callback(set_brightness)
self.mqtt_client.subscribe(f"$aws/things/{self.THING_NAME}/shadow/update/delta")
self._report_device_shadow(0)
async def _start_routine(self): # -> coroutine
def handle_exception(loop: Loop, context: dict) -> None:
print(f"loop error! reason: {context['exception']}")
time.sleep(1)
reset()
loop: Loop = get_event_loop()
loop.set_exception_handler(handle_exception)
loop.create_task(self._check_msg())
loop.create_task(self._ping())
loop.create_task(self._led.routine())
loop.run_forever()
async def _check_msg(self): # -> Coroutine
while True:
gc.collect()
await sleep_ms(50)
self.mqtt_client.check_msg()
async def _ping(self): # -> Coroutine
while True:
gc.collect()
await sleep_ms(self.HEARTBEAT_INTERVAL * 1000)
self.mqtt_client.ping()
def execute(self) -> None:
self._connect_wifi()
self._connect_iot_core()
self._subscribe_shadow_update_delta()
run(self._start_routine())
if __name__ == "__main__":
AllCertTrophyStand().execute()
SSID や PASSWORD、IOT_CORE_ENDPOINT を自身の環境に合わせて書き換えてください。
エンドポイントはAWS IoT > 接続 > ドメイン設定のドメイン名から確認できます。
動作確認
TypeCケーブルを接続し、モノ(Thing)の Devoce shadow の desired フィールドのbrightness を100に設定し、LED が点灯すれば正常に動作しています。

スタンドの組み立て
筐体の印刷
一般のご家庭にある3Dプリンタを使用して筐体を印刷します。
STLデータは以下に置いていますので、個人利用の範囲でご自由にご利用ください。
組み立て
印刷した筐体と ESP32(とLED等)を組み立てます。
さいごに
本記事では AWS 全冠トロフィーを展示するための IoT スタンドの作成をご紹介しました。
今回は IoT スタンドの作成まででしたが、EventBridgeスケジュール等を使用して定時にオン/オフすると少し実用的になると思います。
また、作ったものに記載したXのポストのように、S3 Hosting 等と組み合わせてスマホからオン/オフできるようにすると、さらに使い勝手が良くなりますので皆さんもお好みの操作UIを構築してもらえればと思います。
この記事が皆さんの「箱にしまったまま」の AWS 全冠トロフィーの活用の一助になれば幸いです。








