Breadboard
docs
Example project · Wi-Fi & internet

Two boards: HTTP server + client

Board B runs a real MicroPython socket server; board A fetches from it every 1.5 s. The boards lease unique addresses (192.168.4.2 and .3) and every hop (ARP, SYN, the request, the reply) is a genuine 802.11 frame relayed across the virtual air, two real lwIP stacks end to end. Open the 📡 Sniffer and the per-board serial tabs to watch both sides.

The Two boards: HTTP server + client circuit as rendered by the simulator

How it works

Two ESP32-C3 boards boot MicroPython and join the same virtual access point (BreadboardNet), each getting its own DHCP lease — 192.168.4.2 and .3. A single script runs on both and self-assigns a role from the board's MAC: board B opens a TCP socket server on port 80 and blocks in accept(); board A connects to B every 1.5 s, sends a bare HTTP GET, and prints the reply. Both lwIP stacks are real, so every step is a genuine 802.11 frame — the ARP who-has, the TCP SYN/SYN-ACK, the request and the response. Each board mirrors its live status to its own SSD1306 OLED.

What's on the bench

  • 2× Battery
  • 2× ESP32-C3
  • 2× OLED 128×64

How it's wired

  • Battery · posESP32-C3 · vin
  • Battery · negESP32-C3 · gnd
  • ESP32-C3 · 3v3OLED 128×64 · vcc
  • ESP32-C3 · gnd2OLED 128×64 · gnd
  • ESP32-C3 · g9OLED 128×64 · scl
  • ESP32-C3 · g8OLED 128×64 · sda
  • Battery · posESP32-C3 · vin
  • Battery · negESP32-C3 · gnd
  • ESP32-C3 · 3v3OLED 128×64 · vcc
  • ESP32-C3 · gnd2OLED 128×64 · gnd
  • ESP32-C3 · g9OLED 128×64 · scl
  • ESP32-C3 · g8OLED 128×64 · sda

The code

This MicroPython script runs on the emulated board every boot; edit it in the Code tab.

from machine import I2C, Pin
import framebuf, time
i = I2C(0, scl=Pin(9), sda=Pin(8))
buf = bytearray(1024)
fb = framebuf.FrameBuffer(buf, 128, 64, framebuf.MONO_VLSB)
for c in b'\xae\xd5\x80\xa8\x3f\xd3\x00\x40\x8d\x14\x20\x00\xa1\xc8\xda\x12\x81\xcf\xd9\xf1\xdb\x40\xa4\xa6\xaf':
    i.writeto(0x3c, bytes([0, c]))
def show():
    i.writeto(0x3c, b'\x00\x21\x00\x7f\x22\x00\x07')
    i.writeto(0x3c, b'\x40' + buf)

import network, socket
w = network.WLAN(network.STA_IF)
w.active(True)
w.connect('BreadboardNet')
while not w.isconnected():
    time.sleep_ms(100)
ip = w.ifconfig()[0]
me = 'A' if w.config('mac')[5] == 0xc1 else 'B'
print('board', me, 'up at', ip)
def panel(title, lines):
    fb.fill(0)
    fb.text(title, 0, 0)
    fb.hline(0, 10, 128, 1)
    for k, ln in enumerate(lines):
        fb.text(ln[:16], 0, 16 + k * 14)
    show()
if me == 'B':
    srv = socket.socket()
    srv.bind(('0.0.0.0', 80))
    srv.listen(1)
    hits = 0
    panel('HTTP server B', ['IP ' + ip, 'waiting...'])
    while True:
        c, peer = srv.accept()
        req = c.recv(128)
        hits += 1
        c.send(b'HTTP/1.0 200 OK\r\n\r\nhello #%d from B' % hits)
        c.close()
        print('served', hits)
        panel('HTTP server B', ['IP ' + ip, 'hits: %d' % hits])
else:
    n = 0
    body = ''
    while True:
        time.sleep_ms(1500)
        try:
            cs = socket.socket()
            cs.connect(('192.168.4.3', 80))
            cs.send(b'GET / HTTP/1.0\r\n\r\n')
            r = cs.recv(256).decode()
            cs.close()
            n += 1
            body = r.split('\r\n\r\n')[-1]
            print('A got:', body)
        except OSError as e:
            print('A retry', e)
        panel('HTTP client A', ['GET .4.3:80', body, 'fetches: %d' % n])

Try this

  • Open the 📡 Sniffer and watch the ARP + TCP handshake, then every request/reply cross the air as an 802.11 data frame.
  • Open both boards' serial tabs (esp1 / esp2): the client's fetch count and the server's hit count climb together.
  • Edit the client loop’s sleep_ms(1500) in the Code tab and re-run to change the request rate.
  • Watch the two OLEDs — 'HTTP client A' counts fetches, 'HTTP server B' counts hits.

Related projects