2025年は梅雨らしい気候がなく一気に暑くなって体調がイマイチ、水槽の水温も不安定で困っているgoldear@JiN_C125です。
※今回からスマートアクアリウムもスマートホームもまとめて、スマートIoTという括りに変更しました。
目次
背景/目的

以前に水槽の水温測定は本ブログで紹介しましたが、水温を取得したところで手動でエアコンをつけているようでは全くスマートではありません。
今回はスマートIoTのベースとなるプラットフォームを作り、第一弾としてPC用のPWMファンを遠隔制御できるものを作っていきます。
ポイント
・スマートIoTの基盤を作成
・PWMファンを遠隔制御
・PWMファン状態を取得
MQTT通信
ベースとなる通信プロトコルはMQTT(Message Queuing Telemetry Transport)を使用します。MQTTはIoT向けの通信プロトコルで、通信ヘッダ処理によるオーバーヘッドが少ないのが特徴です。
WebSocketやTCPソケット通信くらいなら、私でも実装できそうですが、一番手っ取り早く、かつ楽に実装するならMQTT一択ですね。
システム全体像
簡単なシステム構成図です。サーバー/クライアント構成ですが、今回はクライアント側を設計・実装していきます。クライアント側ハードウェアで重要なのはPWMファン制御回路になりますので、まずはそこにフォーカスしていきます。W5100-EVA-PICOはEthernetを使用可能なRP2040搭載の評価基板です。面倒なの以降はマイコンと呼びます。
回路設計
PWM制御回路をKiCadで書いてみました。基本は以下の2つになります。
① +12V電源制御回路
② +3.3 -> +5Vレベルシフト回路
①はPoEスプリッターからDC ジャックを経由して最大+12V1Aの電源が供給されるので、この電源をPWMファンの駆動用電源に使用します。またPWMファンの回転をON/OFFできるように、マイコンのGPIOをMOSFETのゲート駆動用に使用します。
②はPWMファンの回転数を制御するPWM信号のレベル変換を行います。PWMファンの仕様に依存しますが、基本的に5VのPWM信号を入力します。マイコンでは3.3VのPWM信号しか生成できませんので、外部で5Vに変換します。
あまり大したことはしていません。部品点数も少ないので初心者でも安心ですね。
とは言うものの簡易のシミュレーションくらいは実施しておきましょう。私が最近使用しているのはCircuitJSという回路シミュレータです。JavaScriptで作られているのでブラウザ動作するのがポイントです。サクッと作れて実際の波形も確認できるので重宝しています。アプリ版もあるのでこちらでもOKです。
とりあえず、KiCadの回路をそのまま作り、回路に適当に入力をセットすれば動作確認完了です。楽チンです。
部品表
クライアント実装で必要になる機材や電子部品の一覧表です。PoE (Power over Ethernet)を利用したいので少し価格は上がってしまいますが、電源を別で用意できるっていう方はそれでもOKです。
| 図番 | メーカー | 品名 | 購入先 |
|---|---|---|---|
| 無 | WIZnet | W5100S-EVB-Pico | 秋月電子 |
| 無 | 秋月電子 | SOT23変換基板 金フラッシュ | 秋月電子 |
| J1 | SwitchScience | ブレッドボードで使えるDCジャック(2.1mm) | SwitchScience |
| Q1,Q4 | International Rectifier | NchMOSFET 30V5A IRLML6344TRPBFTR | 秋月電子 |
| Q2,Q3 | International Rectifier | PchチップMOSFET IRLML6402(20V3.7A) | 秋月電子 |
| R1 | タクマン | カーボン抵抗 1/4W 3.3KΩ±5% | 千石電商 |
| R2,R4,R5 | タクマン | カーボン抵抗 1/4W 10KΩ±5% | 千石電商< |
| R3 | タクマン | カーボン抵抗 1/4W 1KΩ±5% | 千石電商< |
| C1 | 村田製作所 | 積層セラミックコンデンサー 0.1μF50V X7R 2.54mm | 秋月電子 |
| 無 | ローム | 三端子DCDCレギュレーター 5V BP5293-50 | 秋月電子 |
| 無 | サンハヤト | ブレッドボード SAD-101 | Amazon |
| 無 | Foxneo | FNS-1200P | Amazon |
| 無 | 10Gtek | PoEスプリッター DC出力12V/1A | Amazon |
回路実装
(すでにファンが回り始めていますが。。。) ブレッドボード上に、さきほどの回路を組み上げるだけの簡単な作業になります。
補足事項としてマイコンの電源電圧について触れておきます。マイコン電源は12V → 5V の降圧DCDCコンバータで作成しています。ローム製のBP5293-50というDCDCコンバータで必要な部品がワンチップになっており、外付け部品を必要としません。3端子レギュレータのように使用できて、パーツ点数を減らしたい・発熱を減らしたいと言う理由で採用しています。
またMOSFETはチップ部品を使用しているので秋月電子で販売されている変換基板を使用しています。PCB基板に手を出してから基本がチップ部品になってしまった。
プログラミング
ハードウェア実装はできたので、次にソフトウェア実装にかかります。マイコンはRP2040チップを搭載しているので、MicroPythonかC言語のどちらかでの実装となります。
私はもっぱらMicroPythonを使用しているので、今回はMicroPythonで実装します。いずれC言語でも実装するかも。IDEはThonnyを使用します。ぶっちゃけ書ければ何でもいいのでメモ帳でも構いません。
ツリー構造図
project/ │ ├── lib ├── umqtt ├── robust.py ├── main.py ├── config.py ├── w5100_network.py ├── mqtt_client.py ├── fan_controller.py ├── gpio_control.py ├── w5100_network.py
・robust.py (umqtt)
MicroPython向けのMQTTライブラリです。GitHubで公開されているので、そのまま使わせていただきます。robust.pyの他にもsimple.py というのがあるのですが、ネットワーク切断後の再接続の機能を使いたいので今回はrobust.pyを使用します。サンプルコードもあるので使い方は難しくないと思います。
・main.py
プログラム起動時に最初に読み込まれるメインスクリプトです。(ソフト的にはエントリポイントと言った方が良いのかな?)必要なモジュールの初期化と、システム全体の制御を担当します。
・config.py
プログラムで使用する設定情報(ピン番号・MQTT設定・ネットワーク設定など)をまとめて記述した設定ファイルです。すべてのモジュールが共通に利用します。
・w5100_network.py
W5100S-EVA-PICO をネットワーク接続するために初期化するモジュールです。
・mqtt_client.py
MQTT通信を担当するモジュールです。クライアントとしてサーバとの送受信を制御します。コマンドの送信(Publish)、受信(Subscribe)を実行します。
・fan_controller.py
PWMファンのON/OFF制御、回転速度の設定など、ファン動作を制御するモジュールです。
・gpio_control.py
GPIOの状態(HIGH/LOW)を制御する補助モジュールです。ファン制御に使用していますが、その他の汎用用途にも使用可能です。
ソースコード
実装したソースコードを添付しておきます。ソフトウェア素人ですが、参考までに利用してください。
########################################################
# MicroPython
# for WIZnet W5100S-EVB-Pico
# Author: goldear.net
# Date: 2025/06/28
# Rev: 00
########################################################
import time
import uasyncio as asyncio
from w5100_network import wiznet_init
from mqtt_client import MqttHandler
async def publish_loop():
count = 0
while True:
#mqtt.publish(f"Hello {count}")
#count += 1
await asyncio.sleep(1)
async def subscribe_loop():
while True:
mqtt.check_msg()
await asyncio.sleep(0.1)
async def main():
mqtt.connect()
await asyncio.gather(
publish_loop(),
subscribe_loop()
)
if __name__ == "__main__":
# Create instance
nic = wiznet_init()
mqtt = MqttHandler()
try:
asyncio.run(main())
finally:
asyncio.new_event_loop()
asyncio を使った非同期ループで、MQTTメッセージ受信とセンサー読み取りを並列処理しています。送信(Publish)は1秒、受信(Subscribe)は0.1秒の周期で処理しています。
# Config.py
class NetworkConfig:
## network
DEVICE_IP = "192.168.11.2"
GATEWAY = "192.168.11.1"
NETMASK = "255.255.255.0"
DNS = "8.8.8.8"
@staticmethod
def get():
return (NetworkConfig.DEVICE_IP \
,NetworkConfig.GATEWAY \
,NetworkConfig.NETMASK \
,NetworkConfig.DNS)
class Spi0Config:
## SPI
SPI0_ID = 0
SPI0_RX = 16 # MISO
SPI0_CSn = 17
SPI0_SCK = 18
SPI0_TX = 19 # MOSI
SPI0_RSTn = 20
SPI0_BR = 2000000
@staticmethod
def get():
return (Spi0Config.SPI0_ID \
,Spi0Config.SPI0_RX \
,Spi0Config.SPI0_CSn \
,Spi0Config.SPI0_SCK \
,Spi0Config.SPI0_TX \
,Spi0Config.SPI0_RSTn \
,Spi0Config.SPI0_BR)
class MqttConfig:
MQTT_BROKER = "192.168.11.3" # RPi5(Broker)
MQTT_PORT = 1883
MQTT_CLIENT_ID = "w5100s-1"
MQTT_TOPIC_PUB = b"fan/stat"
MQTT_TOPIC_SUB = b"fan/cmd"
MQTT_USER = "<user_id>"
MQTT_PASS = "<user_passwd>"
class FanConfig:
FAN_CTL_PIN = 9
FAN_PWM_PIN = 14
FAN_PWM_FREQ = 25000
使用環境に依存する設定項目は、NetworkConfig、MqttConfig、FanConfigの3つです。
NetworkConfigのDEVICE_IPは今回でいうとマイコン(W5100S-EVA-PICO)に設定するIPアドレスです。
MqttConfigはMQTTブローカー(サーバー)となる端末のIPアドレス、使用するポート番号、任意のクライアントID、Publish/Subscribeのトピック、ユーザー認証を採用しているのでユーザーIDとパスワードを設定します。 (詳細は次回のサーバー実装編を参照)
FanConfigはGPIOのピン設定です。FAN_CTL_PINはGPIO番号9、FAN_PWN_PINはGPIO番号14に設定しています。任意なので好きなGPIO番号を設定してください。ピン番号ではなくてGPIO番号なので注意です。
FAN_PWN_FREQは25kHzのPWM信号を生成するため、25000を設定しています。
# w5100_network.py
import time
import network
from machine import SPI
from machine import Pin
from config import NetworkConfig
from config import Spi0Config
def wiznet_init():
# Create Instance
spi = SPI(Spi0Config.SPI0_ID \
,baudrate=Spi0Config.SPI0_BR \
,mosi=Pin(Spi0Config.SPI0_TX) \
,miso=Pin(Spi0Config.SPI0_RX) \
,sck=Pin(Spi0Config.SPI0_SCK))
nic = network.WIZNET5K(spi \
,Pin(Spi0Config.SPI0_CSn) \
,Pin(Spi0Config.SPI0_RSTn))
# Active
nic.active(True)
DEV_IP, NETMASK, GATEWAY, DNS = NetworkConfig.get()
nic.ifconfig((DEV_IP, NETMASK, GATEWAY, DNS))
timeout=10
while not nic.isconnected() and timeout > 0:
print (f"Wait Connecting...{timeout}")
time.sleep(1)
timeout -= 1
if nic.isconnected():
print(f"IP address: {nic.ifconfig()}")
return nic
else:
return RuntimeError("Failed Connect")
特記事項は特になく、ネットワーク接続する時の定型文です。Config.pyから設定を読み取って、ネットワーク接続します。
# mqtt_client.py
import ujson
from lib.umqtt.robust import MQTTClient
from config import MqttConfig
from fan_controller import FanController
class MqttHandler:
def __init__(self):
self.client = MQTTClient(
client_id=MqttConfig.MQTT_CLIENT_ID
,server=MqttConfig.MQTT_BROKER
,port=MqttConfig.MQTT_PORT
,user=MqttConfig.MQTT_USER
,password=MqttConfig.MQTT_PASS
)
self.client.set_callback(self._on_message)
self.fan = FanController()
def connect(self):
try:
print("Connecting to MQTT broker...")
self.client.connect()
self.client.subscribe(MqttConfig.MQTT_TOPIC_SUB)
print("Connected and subscribed to:", MqttConfig.MQTT_TOPIC_SUB)
except Exception as e:
print("MQTT connection failed:", e)
def publish(self, msg: str):
try:
self.client.publish(MqttConfig.MQTT_TOPIC_PUB, msg)
print("Published:", msg)
except Exception as e:
print("Publish failed:", e)
self.reconnect()
def check_msg(self):
try:
self.client.check_msg()
except Exception as e:
print("Error checking massages:", e)
self.reconnect()
def reconnect(self):
try:
print("Attempting MQTT reconnect...")
self.client.reconnect()
except Exception as e:
print("Reconnect failed:", e)
def disconnect(self):
try:
self.client.disconnect()
except Exception as e:
print("Disconnect failed:", e)
def _on_message(self, topic, msg):
print("Received:", topic, msg)
try:
# Analysis json
try:
data = ujson.loads(msg)
cmd = data.get("cmd")
val = data.get("val")
self._handle_command(cmd, val)
return
except Exception:
pass
# Analysis key:value
decoded = msg.decode("utf-8").strip()
if ":" in decoded:
cmd, val = decoded.split(":", 1)
self._handle_command(cmd.strip(), val.strip())
else:
self._handle_command(decoded.lower())
except Exception as e:
print("Message handling error:", e)
def _handle_command(self, cmd, val=None):
print("Handling:", cmd, val)
if cmd == "fan_on":
self.fan.on()
elif cmd == "fan_off":
self.fan.off()
elif cmd == "fan_speed" and val is not None:
try:
speed = int(val)
self.fan.set_speed(speed)
except:
print("Invalid fan_speed value:", val)
elif cmd == "fan_status":
status = self.fan.get_status()
self.publish(status)
else:
print("Unknown command:", cmd)
MQTT通信処理を記述しています。MqttHandlerクラスでは、MQTTブローカー(サーバー)との接続処理、MQTTメッセージの送信、MQTTメッセージ受信確認処理、再接続処理、切断処理、MQTTメッセージ受信時処理を記述。_handle_commandクラスで特定のコマンドを受信した時の処理を記述。
# fan_controller.py
from machine import Pin
from machine import PWM
from config import FanConfig
from gpio_control import GPIOController
class FanController:
def __init__(self):
self.state = "off"
self.speed = 0
# init
self.ctl_pin_no = FanConfig.FAN_CTL_PIN
self.pwm_pin_no = FanConfig.FAN_PWM_PIN
self.pin0 = Pin(self.ctl_pin_no, Pin.OUT)
self.pin1 = Pin(self.pwm_pin_no, Pin.OUT)
self.pwm = PWM(self.pin1)
self.pwm.freq(FanConfig.FAN_PWM_FREQ)
# create instance
self.gpio_ctl = GPIOController(self.ctl_pin_no)
def on(self):
self.state = "on"
self.set_speed(50)
self.gpio_ctl.high()
print("Fan ON")
def off(self):
self.state = "off"
self.set_speed(0)
self.gpio_ctl.low()
print("Fan OFF")
def set_speed(self, speed):
# set PWM duty
duty = int(65535 * (speed / 100))
self.pwm.duty_u16(duty)
def get_status(self):
return '{"fan_state":"%s", "fan_speed":%d}' % (self.state, self.pwm.duty_u16())
PWMファン制御処理を記述しています。FanControllerクラスは ファン起動、ファン停止、回転数設定、ファン状態の取得処理を記述しています。さきほどの_handle_commandでこれらの処理が呼び出されて、実行されます
# gpio_control.py
from machine import Pin
class GPIOController:
def __init__(self, pin_no):
self.pin = Pin(pin_no, Pin.OUT)
self.state = False
self.pin.value(0)
def high(self):
self.pin.value(1)
self.state = True
print("GPIO HIGH")
def low(self):
self.pin.value(0)
self.state = False
print("GPIO LOW")
def toggle(self):
self.state = not self.state
self.pin.value(1 if self.state else 0)
def get_status(self):
return "on" if self.state else "off"
FanController内のファン起動、ファン停止処理の補助を行うGPIO処理を記述しています。GPIOをHIGH/LOWレベルに設定する処理、トグルさせる処理、状態を取得する処理を記述しています。
まとめ
今回はMQTT通信でPWMファンを遠隔操作するためのシステムの「クライアント」側の実装が完了しました。次回はMQTTブローカーとWebブラウザ経由の遠隔操作を実装します。以下のようなUIでWeb経由でPWMファンを遠隔操作できるようにします。システム自体は完成しているので、近日中に紹介記事を公開します。
追記:2025/07/21 サーバー側を実装した記事を公開しました











コメント