
Type f
google/mozc-devices
Circuit diagrams and firmware source code for Gboard DIY keyboards

Esp8266 Interface
Esp8266Interface is an open-source printed circuit board by jimharrigan, from the jimharrigan/Esp8266Interface repository on GitHub, published under the GPL-3.0 licence. It is a 2-layer board of 60 × 50 mm with 25 parts on 16 nets, routed with 158 tracks and 2 vias.
Its parts include 7 connectors, 6 resistors, 5 capacitors, 3 diodes and 2 ICs. It belongs with the interfaces and adapters boards and the ESP8266 boards of this library.
A WiFi bridge between a dry-contact (or opto-isolated) signal and MQTT / HTTP, built around an ESP-01 module. It reports the state of an external input to an MQTT topic and/or a pair of webhook URLs, and drives a relay output in response to MQTT messages or a polled URL.
Typical use is retrofitting network connectivity onto existing wired equipment — a doorbell transformer, an alarm panel output, a gate contact — without modifying it.
The repository contains both halves of the project:
| Path | Contents |
|---|---|
Esp8266Interface.ino | The Arduino firmware (single sketch) |
Esp8266Interface/ | KiCad schematic and PCB for the carrier board |
Esp8266Interface/production/ | Fabrication output: gerbers, BOM, positions, IPC netlist |
Esp8266Interface.sln, *.vcxproj | Visual Studio + Visual Micro project |
Esp8266Interface/test/ | Scratch KiCad project, not part of the build |
Input → network. An external signal pulls GPIO3 low, either through the on-board
PC817 optocoupler (from the HV In header) or by a plain contact closure on the In3
header. A pin-change interrupt records the new level; the main loop notices the change
and:
Trigger (pin low) or Reset (pin high) to the configured MQTT topic, andGET to the configured trigger URL or reset URL.Network → output. The firmware subscribes to a control topic. A payload of 1 or
trigger drives GPIO1 high; anything else drives it low. GPIO1 gates a 2N7000
low-side switch on the Rly header, so this is intended to energise a relay coil. The
new state is echoed back to the status topic as Output 1 / Output 0.
The same output can instead — or as well — be driven by polling. If a control URL is
configured, the board fetches it every 2 s and looks at the reply: a body containing
Trigger closes the relay, one containing Reset opens it. This suits a far end that
has no MQTT broker and cannot reach in to the board, only be asked.
Silkscreen labels come from the BOM; the descriptions below are derived from the netlist.
| Ref | Label | Function |
|---|---|---|
| J1 | ESP01 | 2x4 socket for the ESP-01 module |
| J4 | Pin | Power in. Pin 1 GND, pin 2 raw DC into the LM1084-3.3 regulator |
| J5 | Pout | Power out. Pin 1 raw input rail, pin 2 regulated +3.3 V |
| J7 | HV In | Isolated sense input. Pin 1 is the common return; pin 2 feeds the optocoupler through R2 (1 k), pin 3 through R3 (68 k) + R2 for a higher-voltage source. D2 (1N4004) rectifies, so an AC source works; D1 (zener) and C5 clamp and smooth what reaches the PC817 LED via R1 (2.2 k) |
| J2 | In3 | Non-isolated input. Shorting the two pins pulls GPIO3 to GND, in parallel with the optocoupler |
| J3 | In | Config-portal button. Shorts GPIO0 to GPIO2, which the firmware holds low after boot |
| J6 | Rly | Relay output. Pin 1 is the raw input rail, pin 2 the 2N7000 drain; D3 (1N4001) is the flyback diode across the coil |
| SW1 | — | Momentary reset for the ESP-01 (RST to GND) |
The zener D1 has no value in the BOM, so the usable voltage range of each HV In tap
depends on the part actually fitted. Work out the range for your source before relying
on it, and treat anything on HV In as hazardous unless you know otherwise — nothing on
the board isolates HV In from its own source, only from the ESP side.
| Pin | Direction | Use |
|---|---|---|
GPIO0 | input, 3.3 k pull-up (R6) | Config-portal button, read in loop(); also the ESP-01 flash-mode strap |
GPIO1 | output | Relay drive, through R4 (47 k) into the 2N7000 gate. Also the UART TX pin |
GPIO2 | output, driven low | Return path for the GPIO0 button, so the button cannot hold the board in flash mode at boot. 3.3 k pull-up (R5) |
GPIO3 | input, internal pull-up | Sense input, from the optocoupler or In3. Also the UART RX pin |
Two pins do double duty as the serial port, which shapes the firmware:
Serial.begin() is called with SERIAL_TX_ONLY so GPIO3 stays usable as an input.
GPIO1 cannot be the UART TX pin and the relay drive at the same time. The
DEBUG_SERIAL switch at the top of the sketch picks one:
DEBUG_SERIAL | Behaviour |
|---|---|
0 (default) | Boot messages print, then setup() calls Serial.end() and claims GPIO1 as an output. Logging stops; the relay works |
1 | Serial logging stays on for the whole run; the relay output does nothing |
Use DBG_PRINT() / DBG_PRINTLN() for any log line added after boot — plain
Serial calls past the end of setup() go to a closed port in the default build.
GPIO1 toggles as TX during boot. C1 (33 µF) on the MOSFET gate with R4 gives roughly
a 1.5 s time constant, which stops the relay chattering while the module starts up —
at the cost of the output switching slowly.
The sketch targets the Arduino core for ESP8266 and needs these libraries:
DynamicJsonDocumentLittleFS, ESP8266HTTPClient, WiFiClientSecure — all part of the ESP8266 coreBuild with the Arduino IDE, arduino-cli, or the supplied Visual Studio / Visual Micro
solution. Choose a flash layout that includes a LittleFS partition — the configuration
is stored in /config.json and there is nowhere to save settings without it.
The board has no USB interface and no auto-reset circuit. To flash, use an external
3.3 V USB-to-serial adapter on the ESP-01's TX/RX/GND, hold GPIO0 low, and pulse
reset. Doing this with the module out of the socket avoids back-feeding the Rly and
HV In circuitry through GPIO1 and GPIO3.
At boot the board tries the WiFi credentials WiFiManager has stored, giving up after 20 s. Failing to connect is not fatal — it keeps running while the ESP8266 keeps retrying WiFi in the background — and it deliberately does not open the config portal on its own, so a router outage cannot leave an access point running unattended in someone's wall. If WiFi stays down for 5 minutes the board reboots (see Failure handling).
The portal is opened on demand instead. Press the button on J3 and the board comes up
as an access point named ESP with the password 1234567890, which is also how
you configure it on first boot, when nothing is stored yet. The portal offers the
standard WiFi and info pages plus a param page with these fields:
| Field | Stored as | Notes |
|---|---|---|
| mqtt server | mqtt_server | Leave empty to disable MQTT entirely |
| mqtt port | mqtt_port | Defaults to 8883 |
| mqtt username | mqtt_username | |
| mqtt password | mqtt_password | |
| mqtt topic | mqtt_topic | Status topic the board publishes to |
| mqtt control topic | mqtt_control_topic | Subscribed topic for output commands; leave empty to skip |
| Trigger Url | trigger_url | Fetched when the input goes low, at boot if it is low, and every 10 minutes while it stays low |
| Reset Url | reset_url | Fetched when the input goes high, at boot if it is high, and every 10 minutes while it stays high |
| Control Url | control_url | Polled every 2 s for the output state; leave empty to skip |
Settings are written to /config.json in LittleFS and reloaded at boot. The portal
timeout is 300 s and runs non-blocking, so the rest of the loop keeps running while it
is open. Saving MQTT settings takes effect immediately: the client is re-pointed at the
new broker and any live session is dropped, so the next reconnect picks up the new
server, port, credentials and control topic without a restart.
| Direction | Topic | Payload |
|---|---|---|
| Published | mqtt_topic | Trigger when the input goes low, Reset when it goes high, and the current state each time the broker connection comes up |
| Published | mqtt_topic | Output 1 / Output 0 after a control message |
| Subscribed | mqtt_control_topic | 1 or trigger turns the output on; any other payload turns it off |
Payload matching is case-insensitive. An HTTP request counts as successful if the server answers with any 2xx status; the response body is only read for the control URL.
A control URL is polled every 2 s and the reply decides where the output sits:
| Reply body | Effect |
|---|---|
contains Trigger | Output on — relay energised |
contains Reset | Output off |
| contains both | Treated as Trigger |
| contains neither | Output left as it is |
| request failed | Output left as it is |
The match is case-insensitive and anywhere in the body, so a page carrying other
markup around the word still works. Only Output 1 / Output 0 transitions are
published to the status topic — a poll that finds the same state as last time is
silent, so a 2 s poll does not flood the broker.
Nothing about the poll reboots the board: a control URL that stops answering leaves the relay where it was rather than dropping it, and the next poll tries again. The request timeout is 1.5 s, under the 2 s interval, so an unreachable server cannot stall the loop for longer than one period, and the interval is measured from the end of each request rather than the start. The poll runs on the first pass through the loop, so a reboot picks the output back up immediately instead of waiting, and is held off entirely while the config portal is open, so a bad URL can still be corrected.
Note that the request is plain HTTP over WiFiClient — an https:// control URL will
not work, and the reply is trusted as-is.
MQTT runs over WiFiClientSecure, but setInsecure() is called, so the server
certificate is not validated. Credentials are stored in plaintext in the filesystem.
Treat this as suitable for a trusted LAN, not the open internet.
The input is read at boot, so its state is sent even if it never changes: the matching URL on the first pass through the loop, and the MQTT status as soon as the broker connects.
Neither reboot happens while the config portal is open, and the 5-minute WiFi timer starts again when the portal closes. A reboot turns the output off until a control message arrives; a retained control message restores it as soon as MQTT reconnects, and a control URL restores it on the first poll.
On the ESP8266 the first software restart after a serial upload can hang in the bootloader, so power-cycle the board once after flashing.
The sketch compiles clean — no warnings — against ESP8266 core 3.1.2 with
--warnings all, in both DEBUG_SERIAL configurations.
A few behaviours are deliberate rather than oversights:
MQTT_RECONNECT_INTERVAL (5 s) apart and
skipped while WiFi is down, so they do not stall the loop. Saving settings in the
portal resets that, so a new broker is tried on the next pass.publishMessage() honours its retained argument; every call site passes true, so
every status message is retained.SetOutput(), so they share one idea of where the
output is. An MQTT command is acknowledged on the status topic whether or not it
changed anything; a control URL poll only publishes when the level actually moves.GPL-3.0 — see LICENSE.
| Qty | Value | Footprint | References |
|---|---|---|---|
| 2 | .01 | Capacitor_THT:C_Disc_D3.8mm_W2.6mm_P2.50mm | C2, C4 |
| 2 | 3.3K | Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P2.54mm_Vertical | R5, R6 |
| 1 | 10UF | Capacitor_THT:CP_Radial_D5.0mm_P2.00mm | C3 |
| 1 | 1N4001 | Diode_THT:D_DO-41_SOD81_P3.81mm_Vertical_KathodeUp | D3 |
| 1 | 1N4004 | Diode_THT:D_DO-41_SOD81_P5.08mm_Vertical_KathodeUp | D2 |
| 1 | 1k | Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P2.54mm_Vertical | R2 |
| 1 | 2.2k | Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P2.54mm_Vertical | R1 |
| 1 | 22UF | Capacitor_THT:CP_Radial_D5.0mm_P2.00mm | C5 |
| 1 | 2N7000 | Package_TO_SOT_THT:TO-92_Inline | Q1 |
| 1 | 33UF | Capacitor_THT:CP_Radial_D5.0mm_P2.00mm | C1 |
| 1 | 47K | Resistor_THT:R_Axial_DIN0207_L6.3mm_D2.5mm_P2.54mm_Vertical | R4 |
| 1 | 68K | Resistor_THT:R_Axial_DIN0309_L9.0mm_D3.2mm_P2.54mm_Vertical | R3 |
| 1 | D_Zener | Diode_THT:D_A-405_P5.08mm_Vertical_AnodeUp | D1 |
| 1 | ESP01 | Connector_PinHeader_2.54mm:PinHeader_2x04_P2.54mm_Vertical | J1 |
| 1 | HV In | Connector_PinHeader_2.54mm:PinHeader_1x03_P2.54mm_Vertical | J7 |
| 1 | In | Connector_PinHeader_2.54mm:PinHeader_1x02_P2.54mm_Vertical | J3 |
| 1 | In3 | Connector_PinHeader_2.54mm:PinHeader_1x02_P2.54mm_Vertical | J2 |
| 1 | LM1084-3.3 | Package_TO_SOT_THT:TO-220-3_Horizontal_TabDown | U1 |
| 1 | PC817 | Package_DIP:DIP-4_W7.62mm | U2 |
| 1 | Pin | Connector_PinHeader_2.54mm:PinHeader_1x02_P2.54mm_Vertical | J4 |
| 1 | Pout | Connector_PinHeader_2.54mm:PinHeader_1x02_P2.54mm_Vertical | J5 |
| 1 | Rly | Connector_PinHeader_2.54mm:PinHeader_1x02_P2.54mm_Vertical | J6 |
| 1 | SW_Push | Button_Switch_THT:SW_Tactile_Straight_KSL0Axx1LFTR | SW1 |
It measures 60 × 50 mm and has 2 copper layers, 1.6 mm thick.
25 parts. The full list of its parts is on this page, with links to each part's symbol and footprint where PCBKit's library has them.
From the jimharrigan/Esp8266Interface repository on GitHub, at the commit this page was made from: the board is Esp8266Interface/Esp8266Interface.kicad_pcb and its schematic Esp8266Interface/Esp8266Interface.kicad_sch. PCBKit opens the board in the browser, with nothing to install.
Its repository publishes it under the GPL-3.0 licence: you may reuse it on that licence's terms (keep the author's notice, and for a copyleft licence share your changes alike).

google/mozc-devices
Circuit diagrams and firmware source code for Gboard DIY keyboards

davidphilipbarr/Sweep
Sweep - a small promicro based keyboard inspired by the Ferris.


hackclub/OnBoard
💡 Join 1,000 teenagers and make your first PCB with a $100 grant!

xyphro/UsbGpib
Versatile, cheap and portable USB to GPIB converter (USBTMC class based)


jes/scamp-cpu
A homebrew 16-bit CPU with a homebrew Unix-like-ish operating system.
