Api Unggun LED Raspberry Pi Pico (Matriks NeoPixel 8x8 + Potensiometer + Buzzer)
Api unggun yang tidak membakar apa pun, tetapi tetap berderak! Raspberry Pi Pico menggambar nyala api yang realistis di matriks NeoPixel 8x8, dihitung ulang setiap frame: percikan muncul di dasar, panasnya mendingin ketika naik, dan berpendar dari merah sampai putih. Potensiometer mengatur tinggi api, tombol mengganti tema Api, Gas, Sihir, dan Ametis, dan buzzer menambahkan derak kayu terbakar. Animasi prosedural, ADC, PWM, dan MicroPython dalam satu proyek.

Video
Skema rangkaian

Kode sumber
1# ─────────────────────────────────────────
2# BlueGrays · bluegrays.com
3# Api Unggun LED Raspberry Pi Pico (Matriks NeoPixel 8x8 + Potensiometer + Buzzer)
4# ─────────────────────────────────────────
5# Animasi api realistis di matriks NeoPixel 8x8. Potensiometer mengatur
6# tinggi api, tekan tombol singkat mengganti tema (Fire, Gas, Magic,
7# Amethyst) dan tekan lama menyalakan/mematikan suara gemeretak.
8#
9# Pin:
10# GP2 -> DIN matriks (VDD -> VBUS, VSS -> GND)
11# GP26 -> Wiper potensiometer (ADC0)
12# GP14 -> Tombol (kaki lainnya ke GND)
13# GP17 -> Buzzer (+)
14
15from machine import Pin, PWM, ADC
16from neopixel import NeoPixel
17import time
18import random
19
20# ============ PENGATURAN ============
21W = 8 # Kolom
22H = 8 # Baris
23SERPENTINE = False # True untuk panel dengan kabel zigzag
24BRIGHTNESS = 0.4 # Wokwi menggambar warna apa adanya, 0.4 menampilkan api dengan jelas
25 # 0.15-0.2 disarankan di USB 2.0 (500 mA), pakai 5V eksternal untuk yang
26 # lebih terang
27FRAME_MS = 35 # Waktu frame (~28 FPS)
28LONG_PRESS_MS = 900 # Waktu tahan untuk menyalakan/mematikan suara
29
30# ============ HARDWARE ============
31matrix = NeoPixel(Pin(2), W * H)
32pot = ADC(26) # ADC0
33button = Pin(14, Pin.IN, Pin.PULL_UP)
34buzzer = PWM(Pin(17))
35buzzer.duty_u16(0)
36
37# ============ STATUS ============
38# heat[x][y]: kolom x, y = 0 adalah baris bawah; 0 = dingin, 255 = paling panas
39heat = [[0] * H for _ in range(W)]
40theme = 0
41THEME_NAMES = ["Ates / Fire", "Gaz / Gas", "Buyu / Magic", "Ametist / Amethyst"]
42sound_on = True
43
44
45# ============ PALET WARNA ============
46def heat_color(t):
47 # Petakan panas (0-255) ke warna sesuai tema: hitam -> gelap -> terang -> putih
48 t192 = (t * 191) // 255
49 ramp = (t192 & 0x3F) << 2 # Ramp 0-252 per pita
50 if t192 & 0x80:
51 r, g, b = 255, 255, ramp # Paling panas: kuning -> putih
52 elif t192 & 0x40:
53 r, g, b = 255, ramp, 0 # Tengah: merah -> kuning
54 else:
55 r, g, b = ramp, 0, 0 # Dingin: hitam -> merah
56 if theme == 1:
57 r, b = b, r # Gas: api biru
58 elif theme == 2:
59 r, g = g, r # Magic: api hijau
60 elif theme == 3:
61 b = max(b, r * 3 // 4) # Amethyst: api ungu
62 g = g // 2
63 return (int(r * BRIGHTNESS), int(g * BRIGHTNESS), int(b * BRIGHTNESS))
64
65
66def pixel_index(x, row):
67 # baris 0 = baris atas
68 if SERPENTINE and row % 2 == 1:
69 return row * W + (W - 1 - x)
70 return row * W + x
71
72
73# ============ SIMULASI API ============
74def fire_step(intensity):
75 # intensity 0-1: tinggi api. Tinggi = pendinginan lebih sedikit + percikan lebih banyak
76 cooling = int(115 - 55 * intensity) # 115 (pendek) -> 60 (tinggi)
77 sparking = int(70 + 150 * intensity) # 70 -> 220 (probabilitas)
78 max_cool = (cooling * 10) // H + 2
79
80 for x in range(W):
81 col = heat[x]
82 # Setiap sel mendingin sedikit
83 for y in range(H):
84 c = col[y] - random.getrandbits(8) % max_cool
85 col[y] = c if c > 0 else 0
86 # Panas naik ke atas dan menyebar
87 for y in range(H - 1, 1, -1):
88 col[y] = (col[y - 1] + col[y - 2] + col[y - 2]) // 3
89 # Percikan acak di dasar
90 if random.getrandbits(8) < sparking:
91 y = random.getrandbits(1)
92 col[y] = min(255, col[y] + 160 + random.getrandbits(6))
93
94 # 4) Sedikit pembauran dengan kolom tetangga: api bergelombang lebih alami
95 for y in range(2, H):
96 row_vals = [heat[x][y] for x in range(W)]
97 for x in range(W):
98 left = row_vals[x - 1] if x > 0 else row_vals[x]
99 right = row_vals[x + 1] if x < W - 1 else row_vals[x]
100 heat[x][y] = (row_vals[x] * 2 + left + right) // 4
101
102
103def render():
104 for x in range(W):
105 for y in range(H):
106 matrix[pixel_index(x, H - 1 - y)] = heat_color(heat[x][y])
107 matrix.write()
108
109
110# ============ SUARA GEMERETAK ============
111def crackle(intensity):
112 # "ticks" sangat pendek secara acak = kayu yang bergemeretak
113 if not sound_on:
114 return
115 if random.getrandbits(8) < int(10 + 50 * intensity):
116 buzzer.freq(1500 + random.getrandbits(12)) # 1500-5595 Hz
117 buzzer.duty_u16(6000)
118 time.sleep_ms(2 + random.getrandbits(2))
119 buzzer.duty_u16(0)
120
121
122def beep(freq, ms):
123 buzzer.freq(freq)
124 buzzer.duty_u16(20000)
125 time.sleep_ms(ms)
126 buzzer.duty_u16(0)
127
128
129# ============ TOMBOL ============
130btn_state = 1
131btn_raw = 1
132btn_changed = time.ticks_ms()
133press_start = 0
134long_done = False
135
136
137def handle_button():
138 # Tekan singkat = tema, tekan lama = suara nyala/mati
139 global btn_state, btn_raw, btn_changed, press_start, long_done, theme, sound_on
140 now = time.ticks_ms()
141 raw = button.value()
142 if raw != btn_raw:
143 btn_raw = raw
144 btn_changed = now
145 if time.ticks_diff(now, btn_changed) > 25 and raw != btn_state:
146 btn_state = raw
147 if raw == 0:
148 press_start = now
149 long_done = False
150 elif not long_done:
151 theme = (theme + 1) % len(THEME_NAMES)
152 print("Tema / Theme:", THEME_NAMES[theme])
153 beep(1200, 30)
154 if btn_state == 0 and not long_done and time.ticks_diff(now, press_start) > LONG_PRESS_MS:
155 long_done = True
156 sound_on = not sound_on
157 print("Ses / Sound:", "ACIK / ON" if sound_on else "KAPALI / OFF")
158 beep(1600 if sound_on else 500, 80)
159
160
161# ============ LOOP UTAMA ============
162print("== BlueGrays · bluegrays.com ==")
163print("Pico 8x8 Kamp Atesi / Campfire")
164print("Pot = alev boyu / flame height | Kisa basis = tema / short press = theme | Uzun basis = ses / long press = sound")
165
166smooth = pot.read_u16() / 65535
167while True:
168 start = time.ticks_ms()
169 handle_button()
170
171 # Haluskan nilai pot (getaran nilai tidak boleh membuat api melompat)
172 smooth += (pot.read_u16() / 65535 - smooth) * 0.2
173 fire_step(smooth)
174 render()
175 crackle(smooth)
176
177 # Frame rate konstan
178 spent = time.ticks_diff(time.ticks_ms(), start)
179 if spent < FRAME_MS:
180 time.sleep_ms(FRAME_MS - spent)
181FAQ
Kenapa aku harus menurunkan kecerahan di rangkaian sungguhan?
Kode memakai `BRIGHTNESS = 0.4` karena Wokwi menggambar warna persis seperti yang dikirim. Matriks sungguhan bisa menarik sekitar 1 A dengan api besar, sementara port USB 2.0 hanya menyuplai 500 mA. Dengan USB, atur ke 0,15–0,2; untuk kecerahan lebih tinggi, beri daya matriks dari adaptor 5V 2A dan sambungkan ground-nya.
Bagaimana cara mengubah tinggi dan warna api?
Putar potensiometer ke kiri untuk bara yang berpendar dan ke kanan untuk api yang mencapai puncak matriks. Tekanan singkat tombol berganti antara Api, Gas, Sihir, dan Ametis. Di Wokwi, klik potensiometer dan pakai tombol panah, lalu tekan T untuk mengganti tema.
Bagaimana cara mematikan bunyi derak atau membuatnya lebih jarang?
Tahan tombol sekitar satu detik: bunyi bip tinggi berarti suara menyala, bunyi bip rendah berarti mati. Untuk derak yang lebih jarang, turunkan angka 10 dan 50 di `10 + 50 * intensity` di dalam `crackle()`. Pakai buzzer pasif untuk suaranya.
Apakah aku perlu menginstal library?
Tidak. Modul `neopixel`, `machine`, dan `random` sudah ikut dalam MicroPython untuk Pico. Pasang MicroPython di Pico dan simpan kodenya sebagai `main.py` di Thonny; api lalu menyala sendiri setiap kali Pico dinyalakan.
