This document is for anyone maintaining or extending the in-browser Designer's board support — adding a new
board to an existing chip family, adding a whole new chip family, or editing the generated .ino
sketch bodies. It describes how the variants/ directory is laid out, how
pfodWeb_src/build_boards.js turns it into the Designer's per-board data, and how the
per-connection .ino templates are resolved.
Nothing in this document is user-facing — end users never see variants/ at all; it only
exists at build time to generate pfodWeb_src/designer/boards/<Board>/<Board>.json
files, which are what actually gets bundled into pfodWeb.html.
The Designer needs, for every supported board: its display name, which pins exist and what each one can
do (digital I/O, PWM, analog input, DAC, SPI/I2C roles, serial RX/TX), which connection types it supports
(Serial/BLE/TCP/HTTP), and the .ino sketch body to generate for each connection type.
Rather than hand-writing that per-board data as JSON, build_boards.js derives most of it
from the same pins_arduino.h files the Arduino IDE itself uses (via a board's own
hardware package), plus a small hand-written board.json per chip/variant supplying the facts
the .h file doesn't carry (default baud, DAC/PWM/ADC ranges, boot-strapping pin notes, etc.).
This keeps board data trivially updatable — drop in a new pins_arduino.h, write a short
board.json, re-run the build — instead of hand-authoring a full pin map for every board.
variants/ sits at the pfodWeb project root (a sibling of pfodWeb_src/). Two
layouts are supported, matching how different vendors structure their pins_arduino.h files:
variants/
serial.ino # shared Serial .ino template (see section 6)
arduino/
avr/
family.json # "Arduino AVR" family, "Serial" connection types
boards.txt # Arduino-style board-id -> variant-dir mapping
standard/
board.json # shared by every board.json below via ancestor lookup... no --
pins_arduino.h # see note below: board.json sits BESIDE each variant here
mega/
board.json
pins_arduino.h
leonardo/
board.json
pins_arduino.h
...
esp32/
family.json # "ESP32 and related chips", "Serial, BLE, TCP/IP, HTTP"
boards.txt # maps esp32/esp32wrover/esp32cam/... -> esp32/esp32/
ble.ino # family-root BLE .ino template — see section 6
tcp.ino
http.ino
esp32/
board.json # ONE board.json shared by every board under this chip
pins_arduino.h
m5stack_core2/
pins_arduino.h # no board.json here -- inherits esp32/esp32/board.json
lolin32/
pins_arduino.h
...
esp32s3/
board.json
pins_arduino.h
... # one directory per esp32s3 board variant
esp32c3/
board.json
...
ccode/
family.json # "Minimal C code" — no Arduino core, no pins
Ccode/
board.json
pins_arduino.h # stub file, contents never parsed
unlistedBoard/
family.json # "Unlisted Board" -- generic no-pin Arduino target
UnlistedBoard/
board.json
pins_arduino.h # stub file, contents never parsed
The two layouts:
board.json sits beside its
pins_arduino.h — one config per board variant directory.board.json sits at the chip level
(variants/esp32/esp32/board.json) and is shared by every board directory nested under
it (m5stack_core2/, lolin32/, ...), each of which supplies only its own
pins_arduino.h.Both layouts work because of one rule, used throughout build_boards.js:
X
(board.json, family.json, boards.txt, or a
<connectionId>.ino template), the build walks up from a variant's own directory,
through each parent, until it finds a file called X or reaches variants/
itself. The closest one found wins. This is what lets ESP32 boards share one chip-level
board.json while AVR boards each have their own right next to the header, with no special-
casing in the build script.Every field below is required on every board.json — build_boards.js
reports both missing and unknown fields as configuration errors, so a typo or a stale schema is
caught immediately rather than silently ignored. Use the sentinel shown ([], {},
empty string) for anything that doesn't apply to a particular family. The file is JSONC — //
and /* */ comments are allowed and stripped before parsing.
| Field | Type | Meaning |
|---|---|---|
boardName | string | Fallback output id (directory + filename basename). Overridden per-board when a
boards.txt entry matches (see section 5). |
displayName | string | Fallback human-readable name shown in the Designer UI. Also overridden by
boards.txt when present. |
family | string | Which pin-map parser to use: "avr", "esp32", "ccode", or
"unlistedBoard" (see section 4). |
inputOnlyGpios | number[] | GPIO numbers with no output driver (ESP32-style chips only) — drops
digital_output/pwm_output from those pins. [] for AVR (no such
concept — every AVR pin is bidirectional). |
connections | string[] | Transports this board can carry pfod over, lowercase:
"serial", "ble", "tcp", "http". AVR boards
typically list only ["serial"]; ESP32 chips with WiFi/BLE list all four. |
pinNotes | object | Map of "<gpio>" → plain-text warning shown under that pin in the picker (e.g.
ESP32 strapping-pin cautions). {} when nothing to flag. |
chipGpios | object | Map of "<gpio>" → { capabilities: [...], note?: "..." } describing
every GPIO the chip silicon exposes (from the datasheet) — used to fill in GPIOs the board's
pins_arduino.h doesn't itself declare, tagged "Check if available on board". {}
for AVR (no GPIO concept). |
pwm | object | { min, max, method } for analogWrite()-style output, e.g.
{ "min": 0, "max": 255, "method": "analogWrite" }. {} if the target has no
pins at all (ccode/unlistedBoard). |
dac | object | { min, max, minVolts, maxVolts, method } for a hardware DAC, e.g. ESP32's
{ "min":0, "max":255, "minVolts":"0.0", "maxVolts":"3.3", "method":"dacWrite" }.
{} when the chip has no DAC. |
adc | object | { bits, max, defaultRefVolts }, e.g. AVR's { "bits":10, "max":1023,
"defaultRefVolts":"5.0" }. |
boards.txt has an entry whose <id>.build.variant= matches the variant
directory's name, that entry's <id> and <id>.name= win instead — see
section 5. This is what lets one chip-level ESP32 board.json still produce dozens of
differently-named output boards.family parsersOne family.json sits at the root of each family's directory tree and supplies the
human-readable family name + connection-type summary shown in the Designer's target picker. Resolved by
the same nearest-ancestor lookup as board.json, so it's optional — a brand-new family
directory builds fine before its family.json is added (falling back to the raw
family id and a default sort position).
{
"familyDisplayName": "ESP32 and related chips",
"connectionTypes": "Serial, BLE, TCP/IP, HTTP"
}
| Field | Required | Meaning |
|---|---|---|
familyDisplayName | yes | Shown as the family name in the target picker. |
connectionTypes | yes | Shown after the display name, e.g. "ESP32 - Serial, BLE, TCP/IP, HTTP". |
sortOrder | no | Lower sorts earlier in the family picker. Omit to use the default (sorts after any family with an explicit value). |
board.json's family field picks which of four pin-map parsers
build_boards.js uses — this is a different, code-level concept from the directory
family (which is just where the file happens to live):
family value | Parser | Notes |
|---|---|---|
avr | buildAvrBoard() |
Reads #define-based pin macros + PROGMEM arrays with USART#_RX/TX
trailing comments. |
esp32 | buildEsp32Board() |
Reads static const/constexpr uint8_t NAME = value; pin aliases
(TX, RX, SDA, A0..A19, DAC1, ...). |
ccode | buildCcodeBoard() |
"Minimal C Code" target — no Arduino core, no pins at all.
pins_arduino.h is a stub whose contents are never parsed; always emits pins: [].
Routes code generation to generateCcode.js (plain C), not the Arduino/C++ generator. |
unlistedBoard | buildCcodeBoard() |
Same no-pin structure as ccode (also emits pins: []), but
board.family stays "unlistedBoard" so generateCode.js routes
to the normal Arduino/C++ generator instead. This is the family that lets a user still generate a full
sketch for a board that isn't in the picker, using the ? placeholder-pin toggle (see
editMenuItem.js/editChart.js's canPlaceholder logic) instead of a
real pin list. |
Arduino hardware packages routinely map several distinct board ids onto one shared
pins_arduino.h (e.g. esp32, esp32wrover, esp32cam and
kb32 all declare build.variant=esp32). build_boards.js reads the
real Arduino-style boards.txt next to (or above) each variant to recover this, and emits
one output JSON per matching board id — not one per pins_arduino.h file.
# boards.txt format build_boards.js looks for: esp32.name=ESP32 Dev Module esp32.build.variant=esp32 esp32wrover.name=ESP32 Wrover Module esp32wrover.build.variant=esp32
Both lines above share build.variant=esp32, so both esp32 and
esp32wrover get their own designer/boards/<id>/<id>.json, each using
the SAME pins_arduino.h + chip-level board.json, but with
boardName/displayName taken from their own .name= line instead of
the fallback in board.json.
Entries without a matching .name= line (e.g. Arduino IDE menu sub-options like
board.menu.Revision.V2.build.variant=...) are silently skipped. A variant directory with no
boards.txt anywhere on its ancestor path (e.g. AVR's standard/) just uses
board.json's own boardName/displayName directly.
Each board's connections array lists which transports it supports; for every one of them,
the Designer needs an actual .ino sketch body to emit when the user clicks Generate
Code. Rather than hard-coding these bodies inside generateCode.js, they live as plain
.ino files under variants/, resolved by the same nearest-ancestor lookup
used for board.json/family.json:
| File | Covers |
|---|---|
variants/serial.ino | Every board's Serial connection — sits at the
variants/ root, so it's the fallback for any board/chip that doesn't override it. |
variants/esp32/ble.ino | BLE connection for every board under the ESP32 family (any chip: esp32, esp32s3, esp32c3, ...) — sits at the family root so all of them inherit it. |
variants/esp32/tcp.ino | TCP/IP Socket connection, same family-root scope. |
variants/esp32/http.ino | HTTP connection, same family-root scope. |
The filename (minus .ino) is the connection id — it must match one of the strings that can
appear in a board.json's connections array: serial, ble,
tcp, http.
ble.ino deeper in the tree — e.g. variants/esp32/esp32c3/ble.ino — and every board
under that chip will resolve to it instead, while every other ESP32 chip keeps using the family-root
version. Same rule for a single board directory overriding its chip's default. Closest file wins.For every board and every connection id it supports, build_boards.js's
registerInoTemplate() walks up from the variant's own directory to find the nearest matching
<connectionId>.ino, then stores the file's contents once — keyed by its
path relative to variants/ (e.g. "serial.ino", "esp32/ble.ino") — in
a shared registry written out as designer/boards/shared/inoTemplates.js
(const INO_TEMPLATES = {...}). Each board's own JSON only carries a small
connections.<proto>.inoTemplateId string referencing that registry — not a copy of the
file content — so hundreds of boards sharing serial.ino only store it once in the bundle.
serial is required to resolve for every board that lists it (which is effectively every
board) — build_boards.js reports a config error and fails the build if
variants/serial.ino is ever missing entirely.
designer/menus/generateCode.js's _generateIno() looks up
state.board.connections[state.connection].inoTemplateId, pulls the matching string out of
INO_TEMPLATES, and emits it verbatim — no placeholder substitution of any
kind. This is deliberate: every template is expected to be real, directly
compilable Arduino code you could open and build as-is, not a text template with holes to fill in. Only the
// Board: ... // Connection: ... /* Code generated by pfodWeb <version> */ header lines
are generated dynamically in JS and prepended — see _fileHeader() in
generateCode.js.
.ino file under
variants/ directly, then re-run build_boards.js (see section 8) to refresh
inoTemplates.js, then re-run build-bundle.js (or one of the top-level
build-* scripts) to fold the refreshed data into pfodWeb.html. No JS code changes
needed for an edit to an existing template file.variants/arduino/avr/
(or under whichever family you're extending) containing that board's pins_arduino.h
(copied from the Arduino core's own hardware package) and its own board.json (copy a sibling
board's and adjust the fields in section 3).variants/esp32/esp32s3/my_new_board/) containing only
pins_arduino.h — no board.json needed, it inherits the chip-level one via
nearest-ancestor lookup.displayName/id than the chip-level fallback, add (or
extend) a boards.txt entry per section 5 instead of writing a whole new board.json.build_boards.js (section 8) and check its output for the new board's pin/connection
summary line, and for any validation errors.family parsers fits (section 4). If neither avr nor
esp32's pin-declaration style matches the new chip's pins_arduino.h convention,
a new parser function needs to be added to build_boards.js and registered in
SUPPORTED_FAMILIES — this is the one case that genuinely requires a code change, not just new
data files.variants/ (e.g. variants/newfamily/),
with a family.json at its root (section 4).board.json file(s) per section 3, and a
boards.txt if the vendor's hardware package ships one and you want its per-board id/name
mapping honoured..ino templates should live (mirroring section 6's ESP32 example — usually
variants/newfamily/ble.ino etc.) and write them as real, compilable sketches.build_boards.js; fix any validation errors it reports (missing/unknown
board.json fields, unsupported family value, missing serial.ino
resolution, etc.) until it completes cleanly.build-bundle.js (or a top-level build-pfodWeb.* script) to fold the new
per-board JSONs + template registry into pfodWeb.html, then open the Designer and confirm the
new family/chip/board appears correctly in the target picker.From pfodWeb_src/:
node build_boards.js
(or double-click build_boards.bat on Windows). This walks ../variants/,
regenerates every designer/boards/<Board>/<Board>.json file and
designer/boards/shared/inoTemplates.js, and prints one line per board summarising its pin
count, serial port count, and supported transports. Any configuration problem (missing field, bad JSON,
unresolvable board.json/serial.ino) is printed with the offending file's path and
a non-zero exit code — nothing is silently skipped.
build_boards.js only regenerates the per-board JSON/template data; it does not rebuild
pfodWeb.html itself. Follow it with node build-bundle.js (from
pfodWeb_src/) or one of the top-level build-pfodWeb.bat/.sh scripts
to fold the refreshed data into the bundled HTML.
variants/ ships in pfodWeb.html
directly. It's a build-time-only input — the generated
designer/boards/<Board>/<Board>.json files and
designer/boards/shared/inoTemplates.js are what actually get inlined into the bundle.