Contracts — Custom Hardware from typeCAD
When you design a custom board in typeCAD, a contract is a small JSON file that tells Cuttlefish exactly which pins on the chip are wired to what. Cuttlefish uses it to build a board definition that only exposes the pins your hardware actually uses — so you can’t accidentally write code for a pin that isn’t connected. Use this for any custom PCB (the circuit board), production board, or design that differs from a standard development board.
What’s in a Contract
A contract is a JSON file (.contract.json) exported from a typeCAD project. It captures:
- Connected pins — which MCU (the main chip on the board) pins are wired to something on the PCB
- Pin metadata — pin name, type (I/O, power, bidirectional), and net assignment
- External components — what each pin connects to (resistors, capacitors, connectors, etc.)
- Available peripherals — which peripheral buses (I2C, SPI, UART) are wired up
The transpiler reads this contract and generates a narrowed .cuttlefish/board.ts that exports only the pads the contract wires (plus the bus instances whose families the PCB routes). This means your TypeScript firmware can only import and use pins that are physically wired on your board — invalid pin access becomes a compile-time error.
With and Without a Contract
| Without a Contract | With a Contract |
|---|---|
| Import any pin of the generated board module | Only connected pins are available |
| Hardware/software mismatch caught at runtime | Mismatch caught at compile time |
| Must manually track which pins are wired | typeCAD design is the source of truth |
| Works for standard dev boards like the ESP32-S3 DevKitC | Works for any custom PCB |
Contracts are essential when working with custom hardware — custom PCBs, production boards, or any design where the MCU pins differ from a standard development board.
Contract File Format
A contract file is a JSON document with this structure:
{
"version": 1,
"mcu": {
"symbol": "MCU_ST_STM32:STM32F411CEU6",
"reference": "U1",
"value": "",
"footprint": "Package_DFN_QFN:QFN-28-1EP_4x4mm_P0.45mm_EP2.4x2.4mm"
},
"connectedPins": {
"5": {
"pinName": "PB6",
"pinType": "bidirectional",
"net": "net11",
"externalComponents": [
{
"reference": "Y11",
"dnp": false,
"symbol": "Device:Crystal",
"value": "",
"footprint": "Crystal:Crystal_SMD_5032-2Pin_5.0x3.2mm"
}
]
},
"6": {
"pinName": "PB7",
"pinType": "bidirectional",
"net": "net12"
}
},
"availablePeripherals": {
"i2c": false,
"spi": false,
"uart": false
}
}Fields
| Field | Description |
|---|---|
version | Contract format version (currently 1) |
mcu.symbol | KiCad symbol library identifier for the MCU — the only mcu field Cuttlefish reads |
connectedPins | Map of pin number to pin info — only pins wired on the PCB |
connectedPins[n].pinName | Full pin name from the MCU symbol (may be compound like XTAL1/PB6) |
connectedPins[n].pinType | Pin direction from KiCad (e.g. bidirectional, input, output, power_in, passive) |
connectedPins[n].boardName | Optional functional name from the typeCAD design (TX, LED); when it is a pad name it is used directly, otherwise it becomes an alias for the pad |
connectedPins[n].net | Net name the pin is connected to |
connectedPins[n].externalComponents | Optional list of components on the net — each needs at least reference and dnp |
availablePeripherals | Which peripheral buses have all their required pins wired (i2c, spi, uart) |
Pin Name Matching
Cuttlefish resolves each wired pin to a datasheet pad name: a pad-form boardName (PA9, P0.28, GPIO9, GP25) is used directly; otherwise the KiCad pinName is split on / and - and each segment checked for a pad-form name (XTAL1/PB6 resolves to PB6). Pins whose type or net marks them as power/ground (power_in, passive, nets like VCC/GND) are excluded — they are never exported as GPIO. A boardName that is not a pad name rides along as a functional alias (TX → PA9).
Integrating with a Cuttlefish Project
Add soc and contract fields to your cuttlefish.config.ts:
import type { CuttlefishConfig } from '@typecad/cuttlefish/api';
const config: CuttlefishConfig = {
entry: './src/main.ts',
target: 'stm32f411',
soc: 'stm32f411xe',
contract: './src/my-board.contract.json',
framework: '@typecad/framework-zephyr',
output: {
outDir: './out',
},
toolchain: {
type: 'west',
},
};
export default config;The key differences from a standard board-based config:
socinstead ofboard— the Zephyr SoC name (e.g.'stm32f411xe')contract— path to the.contract.jsonfile- No
boardfield —boardandcontractare mutually exclusive
Both soc and contract are required for custom hardware (plus a framework — contract board generation requires @typecad/framework-zephyr); the transpile fails with an error naming the missing key when either is absent, and there is no auto-discovery of contract files.
⚙️ Advanced details — what the contract generates
What Gets Generated
When a contract is present, the transpiler generates two files. .cuttlefish/board.json is the full board manifest for the SoC (the same artifact a board-target project carries — the transpiler’s pin map and chip resolution read it), and .cuttlefish/board.ts is the narrowed module re-exported as @typecad/board:
// Excerpt of the generated .cuttlefish/board.ts — auto-generated, do not edit.
import { Pin } from '@typecad/hal';
// Narrowed pin set — only pins the contract declares connected.
export const PB6 = Pin.fromPort('PB6');
export const PB7 = Pin.fromPort('PB7');
// A bus family the PCB routes gets its instance — e.g. when
// availablePeripherals.i2c is true, `export const I2C0 = new I2CBus('I2C0');`Your firmware imports the wired pads from the virtual @typecad/board module and hardware classes from @typecad/hal: import { GPIO } from '@typecad/hal'; import { PB6, PB7 } from '@typecad/board';, then const xtal1 = new GPIO(PB6, GPIO.OUTPUT);.
cuttlefish-env.d.ts
The transpiler also generates a .cuttlefish/cuttlefish-env.d.ts file that declares the @typecad/board ambient module. When using a contract, it points to the generated board:
declare module '@typecad/board' {
export * from './board.js';
}typeCAD Integration
Exporting a Contract from typeCAD
A contract file is generated from your typeCAD PCB project. When you define your hardware in typeCAD (fragment — mcu is your project’s MCU component):
import { PCB } from '@typecad/typecad';
import { Resistor, LED } from '@typecad/passives/0603';
let typecad = new PCB('my-board');
let r1 = new Resistor({ value: '330' });
let d1 = new LED();
typecad.net(mcu.PB5, r1.pin(1));
typecad.net(r1.pin(2), d1.pin(2));
typecad.create(r1, d1);The typeCAD tooling can export a .contract.json that describes which MCU pins are connected and what they’re connected to. Place this file in your Cuttlefish project’s src/ directory.
Typical Project Setup
For a project that uses both typeCAD (hardware) and Cuttlefish (firmware), the recommended structure is:
my-project/
hardware/ # typeCAD project
src/
board.ts # PCB design
kiCAD/
my-board.kicad_sch # Generated schematic
package.json
firmware/ # Cuttlefish project
src/
main.ts # Firmware code
my-board.contract.json # Exported from hardware/
cuttlefish.config.ts
package.jsonWorkflow
- Design hardware in typeCAD — define the schematic, place components, wire nets
- Export contract — generate a
.contract.jsonfrom the typeCAD project - Write firmware in Cuttlefish — the contract ensures you only use physically connected pins
- Transpile and compile —
cuttlefish build --compile --upload
Board vs Contract
The board and contract config fields are mutually exclusive:
| Scenario | Config |
|---|---|
| Standard dev board (ESP32-S3 DevKitC, XIAO nRF52840) | board: 'esp32s3_devkitc/esp32s3/procpu' |
| Custom PCB designed in typeCAD | soc: 'stm32f411xe' + contract: './src/board.contract.json' |
Troubleshooting
“A ‘contract’ config requires a ‘soc’ …”
The contract narrows against a SoC, named by soc: in your cuttlefish.config.ts (e.g. 'stm32f411xe'). Add it alongside contract:.
“Framework ’…’ provides no contract board generation”
Contract board generation derives the SoC’s bus controllers from the installed Zephyr tree, so it requires framework: '@typecad/framework-zephyr' (and a Zephyr tree — install one with the framework’s zephyr-installer or set ZEPHYR_BASE).
“Specifying both ‘board’ and ‘contract’ is not allowed”
Remove one from your cuttlefish.config.ts. Use board for standard development boards, or soc + contract for custom hardware.
Contract file not found
contract: is a path resolved relative to the project root (the directory holding cuttlefish.config.ts) — there are no CLI flags and no auto-discovery. Check the path and that the file is valid JSON with "version": 1.
On This Page