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 board package that re-exports only the connected pins from the MCU package. 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 from a full board package | 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 Arduino Uno | 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_Microchip_ATmega:ATmega328-MM",
"reference": "U1",
"value": "",
"footprint": "Package_DFN_QFN:QFN-28-1EP_4x4mm_P0.45mm_EP2.4x2.4mm"
},
"connectedPins": {
"5": {
"pinName": "XTAL1/PB6",
"pinType": "bidirectional",
"net": "net11",
"externalComponents": [
{
"reference": "Y11",
"symbol": "Device:Crystal",
"value": "",
"footprint": "Crystal:Crystal_SMD_5032-2Pin_5.0x3.2mm"
}
]
},
"6": {
"pinName": "XTAL2/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 |
mcu.reference | Component reference designator (e.g. U1, R1 — the part label on the schematic) |
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: bidirectional, input, output, power_in, passive |
connectedPins[n].net | Net name the pin is connected to |
connectedPins[n].externalComponents | Optional list of components connected to this pin |
availablePeripherals | Which peripheral buses are available based on wiring |
Pin Name Matching
The transpiler matches contract pin names to MCU package pin names using substring matching. Compound pin names like XTAL1/PB6 are resolved to the MCU port name PB6. This handles the common KiCad convention of combining multiple pin functions into a single name.
Integrating with a Cuttlefish Project
Option 1: Explicit Configuration
Add a contract field to your cuttlefish.config.ts alongside an mcu package:
import type { CuttlefishConfig } from '@typecad/core';
const config: CuttlefishConfig = {
entry: './src/sketch.ts',
target: 'avr',
mcu: '@typecad/mcu-atmega328p',
contract: './src/my-board.contract.json',
framework: '@typecad/framework-arduino',
frameworkData: {
buildTarget: 'arduino:avr:uno',
},
output: {
framework: 'arduino',
optimize: 'size',
outDir: './out',
},
toolchain: {
type: 'arduino-cli',
},
};
export default config;The key differences from a standard board-based config:
mcuinstead ofboard— specifies the MCU silicon package (e.g.,@typecad/mcu-atmega328p)contract— path to the.contract.jsonfile- No
boardfield —boardandcontractare mutually exclusive
Option 2: Auto-Discovery
If your config specifies mcu without board or contract, the transpiler will auto-discover a contract file by searching for *.contract.json in:
- The project root directory
- The
src/subdirectory
If exactly one contract file is found, it’s used automatically.
⚙️ Advanced details — what the contract generates
What Gets Generated
When a contract is present, the transpiler generates a .cuttlefish/board.ts file that re-exports only the connected pins:
// .cuttlefish/board.ts — Auto-generated, do not edit
export {
HIGH, LOW, INPUT, OUTPUT, INPUT_PULLUP,
delay, millis, micros, delayMicroseconds,
map, constrain,
abs, min, max, Num,
pulseIn, pulseInLong, Pulse,
shiftIn, shiftOut, Shift,
randomSeed, random, Random,
noInterrupts, interrupts, attachInterrupt, detachInterrupt,
ADC, AsyncClass, Async
} from '@typecad/hal';
// Re-export ONLY connected pins from MCU package
export {
PB6,
PB7,
PC6
} from '@typecad/mcu-atmega328p';Your firmware code imports from the generated board:
import { PB6, PB7, OUTPUT } from './.cuttlefish/board';
const xtal1 = PB6.asOutput();Or more commonly, through the virtual @typecad/board module:
import { PB6, PB7, OUTPUT } from '@typecad/board';cuttlefish-env.d.ts
The transpiler also generates a 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 './.cuttlefish/board';
}typeCAD Integration
Exporting a Contract from typeCAD
A contract file is generated from your typeCAD PCB project. When you define your hardware in typeCAD:
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/
sketch.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 (Arduino Uno, ESP32 DevKit) | board: '@typecad/board-arduino-uno' |
| Custom PCB designed in typeCAD | mcu: '@typecad/mcu-atmega328p' + contract: './src/board.contract.json' |
| Bare MCU with no board definition | mcu: '@typecad/mcu-atmega328p' (all pins available) |
CLI Flags
The contract path can also be set via the CLI:
cuttlefish build --mcu @typecad/mcu-atmega328p --contract ./src/my-board.contract.jsonTroubleshooting
“MCU package does not export TypeCADManifest”
The MCU package must export a TypeCADManifest object with pinNames and peripheralNames for contract-based board generation to work. Ensure you’re using a supported MCU package (e.g., @typecad/mcu-atmega328p, @typecad/mcu-esp32).
“Specifying both ‘board’ and ‘contract’ is not allowed”
Remove one from your cuttlefish.config.ts. Use board for standard development boards, or mcu + contract for custom hardware.
Contract file not found
If using auto-discovery, ensure the .contract.json file is in the project root or src/ directory. For explicit paths, use a relative path from the config file location.
On This Page