在開始撰寫程式之前,先來認識這塊擴充板。
IR_Codes.py
# ir_codes.py
# 專門放「紅外線資料」
# 電源鍵
NEC_TV_POWER = [ # 定義一組 NEC 協議的紅外線脈衝資料
8860, 4465, # Header:9ms mark + 4.5ms space
619, 1664, 609, 555, 581, 557, 579, 1665,
608, 556, 581, 557, 579, 559, 578, 560,
576, 561, 576, 1679, 593, 1667, 606, 559,
584, 1660, 606, 1665, 608, 1662, 620, 1651,
602, 1669, 603, 562, 575, 1668, 604, 561,
576, 561, 575, 563, 573, 1671, 602, 562,
574, 564, 573, 1671, 601, 563, 574, 1670,
602, 1669, 604, 1667, 606, 559, 577, 1667,
605
]
NEC_TV_VOL_UP = [
# 另一組 pulses
]
NEC_TV_VOL_DOWN = [
# ...
]
# 用 dict 管理所有遙控碼
CODES = {
"NEC_TV_POWER": NEC_TV_POWER,
"NEC_TV_VOL_UP": NEC_TV_VOL_UP,
"NEC_TV_VOL_DOWN": NEC_TV_VOL_DOWN
}
IR_Receiver.py
from machine import Pin # 從 machine 模組匯入 Pin,用來操作 GPIO
import time # 匯入 time 模組,用來取得微秒時間
ir = Pin(6, Pin.IN) # 將 GPIO6 設定為輸入腳位,接紅外線接收器輸出腳
pulses = [] # 建立一個 list,用來儲存每一段脈衝的時間長度
last = time.ticks_us() # 紀錄上一次中斷發生的時間(微秒)
def irq(pin): # 定義 GPIO 中斷處理函式
global last, pulses # 使用全域變數 last 與 pulses
now = time.ticks_us() # 取得目前時間(微秒)
pulses.append( # 將兩次中斷之間的時間差加入 pulses
time.ticks_diff(now, last) # 計算 now 與 last 的差值(避免溢位)
)
last = now # 更新 last 為本次中斷時間
ir.irq( # 設定 GPIO 中斷
trigger=Pin.IRQ_FALLING | Pin.IRQ_RISING, # 上升沿與下降沿都觸發
handler=irq # 指定中斷處理函式
)
print("📡 IR Receiver ready") # 提示紅外線接收器已就緒
while True:
time.sleep(0.1) # 100ms 檢查一次
# 如果已經有資料,而且 50ms 沒再收到新脈衝
if pulses and time.ticks_diff(time.ticks_us(), last) > 50_000:
print("📥 pulses 數量 =", len(pulses))
print("📥 pulses =", pulses)
pulses = [] # 只在「真的結束」後才清空
IR_Sender.py
from machine import Pin, PWM # 匯入 Pin 與 PWM 類別,用來控制 GPIO 與硬體 PWM
import time # 匯入 time 模組,用來做延遲(微秒級)
# ===== 定義 IR 發射類別 =====
class IRSender:
def __init__(self, pin=1, freq=38000):
# 建立 PWM 物件,控制紅外線 LED 載波
# pin: 使用哪個 GPIO 腳位輸出
# freq: PWM 頻率,NEC 協議通常 38kHz
# duty=0: 初始不輸出紅外線
self.pwm = PWM(Pin(pin), freq=freq, duty=0)
# 發射紅外線脈衝序列
# pulses: [mark, space, mark, space ...] 單位為微秒
def send(self, pulses):
if not pulses:
return False # 若脈衝序列為空,直接返回 False 表示發射失敗
for i, duration in enumerate(pulses):
if i % 2 == 0:
self.pwm.duty(512) # 偶數 index = mark,紅外線開啟,duty 50%
else:
self.pwm.duty(0) # 奇數 index = space,紅外線關閉
time.sleep_us(duration) # 延遲對應微秒數,維持 mark/space 寬度
self.pwm.duty(0) # 發射完成後,確保紅外線關閉
return True # 回傳 True 表示發射成功
main.py
import network
import time
import ure
import StaModeHttpServer as http
import OledShowText as oled
from IR_Sender import IRSender
import IR_Codes
# ===== WiFi =====
SSID = "您家的SSID"
PASSWORD = "您家的PASSWORD"
sta = network.WLAN(network.STA_IF)
if sta.active():
sta.active(False)
time.sleep(0.5)
sta.active(True)
sta.connect(SSID, PASSWORD)
oled.show("WiFi Connecting...")
while not sta.isconnected():
time.sleep(0.5)
ip = sta.ifconfig()[0]
oled.show("WiFi OK", "IP:", ip)
print("IP:", ip)
# ===== IR =====
ir = IRSender(pin=1)
def send_ir(code_name):
pulses = IR_Codes.CODES.get(code_name)
if pulses:
ir.send(pulses)
print("IR pulses:", pulses)
print("IR 發射:", code_name)
# ===== HTTP =====
server = http.start_server()
oled.show("HTTP Server Ready", ip) # OLED 只顯示 IP
def on_request(cl):
req = cl.recv(1024).decode()
#print("REQ:", req)
# 直接用 if 判斷 URL,對應 IR
for url, code_name in IR_Codes.CODES.items():
if f"GET /{url} " in req:
send_ir(url)
response_body = f"{url} 發射完成"
break
else:
response_body = "未知指令"
response = (
"HTTP/1.1 200 OK\r\n"
"Content-Type: text/plain; charset=utf-8\r\n\r\n"
+ response_body
)
cl.send(response.encode('utf-8'))
# ===== 主迴圈 =====
while True:
http.handle_client(server, on_request)
time.sleep_ms(10)
OledShowText.py
from machine import Pin, I2C # 從 machine 模組匯入 Pin 與 I2C 類別,用來控制 GPIO 與 I2C 通訊
import ssd1306 # 匯入 SSD1306 OLED 顯示驅動模組
# === 初始化 I2C ===
i2c = I2C(1, scl=Pin(6), sda=Pin(5), freq=400000) # 建立 I2C 物件,使用 I2C1,SCL 接 GPIO6,SDA 接 GPIO5,頻率 400kHz
oled = ssd1306.SSD1306_I2C(128, 64, i2c) # 建立 OLED 物件,解析度 128x64,透過上面建立的 I2C 物件控制
# === 顯示文字函式 ===
def show(*lines): # 定義 show 函式,可傳入多行文字參數
oled.fill(0) # 將 OLED 畫面填滿黑色(清空畫面)
y = 0 # 初始 y 座標為 0,用來控制文字行位置
for line in lines: # 逐行讀取傳入的文字
oled.text(line, 0, y) # 在 OLED 畫面指定 x=0, y 位置顯示文字
y += 15 # y 座標增加 15 像素,為下一行文字預留空間
oled.show() # 將畫面更新到 OLED,實際顯示文字
ssd1306.py
# MicroPython SSD1306 OLED driver, I2C and SPI interfaces
from micropython import const
import framebuf
SET_CONTRAST = const(0x81)
SET_ENTIRE_ON = const(0xA4)
SET_NORM_INV = const(0xA6)
SET_DISP = const(0xAE)
SET_MEM_ADDR = const(0x20)
SET_COL_ADDR = const(0x21)
SET_PAGE_ADDR = const(0x22)
SET_DISP_START_LINE = const(0x40)
SET_SEG_REMAP = const(0xA0)
SET_MUX_RATIO = const(0xA8)
SET_COM_OUT_DIR = const(0xC0)
SET_DISP_OFFSET = const(0xD3)
SET_COM_PIN_CFG = const(0xDA)
SET_DISP_CLK_DIV = const(0xD5)
SET_PRECHARGE = const(0xD9)
SET_VCOM_DESEL = const(0xDB)
SET_CHARGE_PUMP = const(0x8D)
class SSD1306:
def __init__(self, width, height, external_vcc):
self.width = width
self.height = height
self.external_vcc = external_vcc
self.pages = self.height // 8
self.buffer = bytearray(self.pages * self.width)
self.fb = framebuf.FrameBuffer(
self.buffer, self.width, self.height, framebuf.MONO_VLSB
)
self.init_display()
def init_display(self):
for cmd in (
SET_DISP | 0x00,
SET_MEM_ADDR, 0x00,
SET_DISP_START_LINE | 0x00,
SET_SEG_REMAP | 0x01,
SET_MUX_RATIO, self.height - 1,
SET_COM_OUT_DIR | 0x08,
SET_DISP_OFFSET, 0x00,
SET_COM_PIN_CFG, 0x02 if self.height == 32 else 0x12,
SET_DISP_CLK_DIV, 0x80,
SET_PRECHARGE, 0x22 if self.external_vcc else 0xF1,
SET_VCOM_DESEL, 0x30,
SET_CONTRAST, 0xFF,
SET_ENTIRE_ON,
SET_NORM_INV,
SET_CHARGE_PUMP, 0x10 if self.external_vcc else 0x14,
SET_DISP | 0x01,
):
self.write_cmd(cmd)
def fill(self, col):
self.fb.fill(col)
def pixel(self, x, y, col):
self.fb.pixel(x, y, col)
def text(self, string, x, y, col=1):
self.fb.text(string, x, y, col)
def show(self):
self.write_cmd(SET_COL_ADDR)
self.write_cmd(0)
self.write_cmd(self.width - 1)
self.write_cmd(SET_PAGE_ADDR)
self.write_cmd(0)
self.write_cmd(self.pages - 1)
self.write_data(self.buffer)
class SSD1306_I2C(SSD1306):
def __init__(self, width, height, i2c, addr=0x3C, external_vcc=False):
self.i2c = i2c
self.addr = addr
self.temp = bytearray(2)
super().__init__(width, height, external_vcc)
def write_cmd(self, cmd):
self.temp[0] = 0x80
self.temp[1] = cmd
self.i2c.writeto(self.addr, self.temp)
def write_data(self, buf):
self.i2c.writeto(self.addr, b"\x40" + buf)
StaModeHttpServer.py
import socket # 匯入 socket 模組,用來建立網路連線(HTTP Server)
# ===== 建立 HTTP Server =====
def start_server():
addr = socket.getaddrinfo("0.0.0.0", 80)[0][-1] # 取得 0.0.0.0:80 的地址資訊(監聽所有網卡)
s = socket.socket() # 建立 TCP socket 物件
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # 設定 socket 選項,允許重複使用同一個地址
s.bind(addr) # 綁定 socket 到指定地址(IP + port)
s.listen(1) # 開始監聽,最多排隊 1 個連線
s.settimeout(0.1) # 設定 accept() 最多等待 0.1 秒,不會阻塞主程式
return s # 回傳建立好的 socket 物件
# ===== 處理 HTTP Client =====
def handle_client(server, on_request):
try:
cl, addr = server.accept() # 嘗試接受 client 連線,cl 為 client socket,addr 為 client 地址
except:
return # 若沒有連線或超時,直接回傳,不做任何處理
try:
on_request(cl) # 呼叫傳入的處理函式 on_request(),由使用者定義如何處理 HTTP 請求
except Exception as e:
print("處理 client 發生錯誤:", e) # 捕捉錯誤並印出
finally:
cl.close() # 不論是否出錯,都關閉 client socket,釋放資源
IR_Receiver.py
⚠️ 程式為無限迴圈執行,
把pulses複製出來。(我拿之前錄好的來做)
📥 pulses = [810740, 8860, 4465,
619, 1664, 609, 555, 581, 557, 579, 1665,
608, 556, 581, 557, 579, 559, 578, 560,
576, 561, 576, 1679, 593, 1667, 606, 559,
584, 1660, 606, 1665, 608, 1662, 620, 1651,
602, 1669, 603, 562, 575, 1668, 604, 561,
576, 561, 575, 563, 573, 1671, 602, 562,
574, 564, 573, 1671, 601, 563, 574, 1670,
602, 1669, 604, 1667, 606, 559, 577, 1667,
605
]
把第1個pulse刪除,810740這個值沒有意義。
📥 pulses = [8860, 4465,
619, 1664, 609, 555, 581, 557, 579, 1665,
608, 556, 581, 557, 579, 559, 578, 560,
576, 561, 576, 1679, 593, 1667, 606, 559,
584, 1660, 606, 1665, 608, 1662, 620, 1651,
602, 1669, 603, 562, 575, 1668, 604, 561,
576, 561, 575, 563, 573, 1671, 602, 562,
574, 564, 573, 1671, 601, 563, 574, 1670,
602, 1669, 604, 1667, 606, 559, 577, 1667,
605
]
main.py
⚠️ 程式為無限迴圈執行,
我有一塊Esp32已連上網路,透過10.0.4.68/NEC_TV_POWER來發射紅外線脈衝。
用Swiftui做一個View給我,按鈕按下它會傳送10.0.4.68/NEC_TV_POWER。