Documentation

Set up your kit, talk to the board from your own code, and look up what every module sends and understands.

Ask ClaudeAsk ChatGPTOpens your own account with these docs loaded.

Quick start

For parents, schools and learners

You describe what you want to build, Atech picks the modules and writes the firmware, and you upload it to the board from the browser. No soldering and nothing to install.

A desktop Chromium browser

Chrome, Edge, Brave, Opera or Arc. Uploading uses Web Serial, which Firefox, Safari and phone browsers do not have.

A USB-C data cable

Charge-only cables cannot program the board. One that came with a phone or laptop works.

  1. 1
    Describe what to build

    Log in at atech.dev/editor and type your idea in the prompt box, for example “a button toggles the light grid”. You get a board diagram and generated firmware.

  2. 2
    Put the modules where the diagram shows

    Slide each module into its port and push it in firmly. You can rearrange them in the editor at any time; the firmware is rebuilt for the new layout.

  3. 3
    Plug in the board

    USB-C from the board to your computer. No driver needed.

  4. 4
    Pick WiFi or USB

    On WiFi, open the connection pill and add your WiFi credentials so the board can join your network. On USB, open the pill and click “Connect device via USB” once.

  5. 5
    Upload the code

    Click “Upload Code”. The board restarts, and its events start streaming in the connection pill.

Build with your own agent

For software developers, vibe-coders and companies

You can skip the editor and build with Claude Code, Codex, Cursor, Lovable or any coding agent that can run Python. The open-source Atech SDK gives the agent the module catalogue, each module's API and a bundled compiler that builds and flashes the board offline. No account needed.

1. Install the SDK

pip install atech

2. Give your agent this prompt

Paste it into the agent's chat, then describe what you want to build. It asks which board and modules you have before it writes any code.

Install the Atech Python SDK and use it to build my project:

    pip install atech

Atech is a modular electronics platform: pre-built hardware modules snap into
numbered ports on an ESP32 motherboard, with no soldering or wiring. The SDK
ships the module catalog, each module's API, and a bundled compiler that builds
and flashes the board offline.

Before you write any code, ask me:

1. Which Atech board do I have, and how many ports does it have?
2. Which modules do I own? Run `atech list-modules` to see every module the
   SDK knows about, and ask me which of those I actually have in hand.
3. Which port is each module plugged into?

If I don't have a board yet, or I'm missing a module the build needs, point me
to https://atech.dev/buy and suggest which kit fits what I described.

Then:

- Write the project file describing the build, using only modules I confirmed
  I own and the ports I confirmed they're in.
- Run `atech build` and fix anything it reports.
- Run `atech upload` to flash my board, and tell me what to expect it to do.

3. What the agent runs

  • atech list-modulesEvery module in the catalog, with its API.
  • atech buildCompile the project to ESP32 firmware, offline.
  • atech uploadFlash the board over USB-C.

The SDK is Python and open source, on PyPI. No board yet? Your agent will point you to a kit that fits.

The board

Every kit uses the same motherboard: an ESP32-S3 with 12 module ports around its edge. The port numbers are printed on the board.

  • Module ports: 1–7, 9–11 and 13–14.
  • 8 is the USB-C socket and 12 is the reset button.
  • Most modules take one port. Double-width modules (Color Display, Big Screen, Knob, Microphone, Speaker) take two neighbouring ports on the same side: 1–2, 2–3, 3–4, 4–5, 5–6, 9–10, 10–11, 13–14.

The firmware is built for the ports in your project's diagram. If you move a module on the board, move it in the editor too and upload again.

The motherboard seen from above, with ports numbered 1 to 14

Events & actions

A running board speaks JSON, and there are only two kinds of message. Events come from the board on their own: a sensor reading, a button press. Actions are commands you send to it: set a colour, play a tone, draw text.

// Event — the board tells you something
{
  "event_type": "sensor",        // sensor | button | state | log
  "key":        "temperature",   // which reading; listed per module below
  "value":      23.4             // number, string, object or bool
}

// Action — you tell the board to do something
{
  "action": "set_color",         // listed per module below
  "value":  { "r": 255, "g": 0, "b": 0 }
}

Which keys and actions exist depends on the modules in your build. The module reference lists them all, with an example for each.

Connect from code

There are two ways to reach the board, chosen per project with the WiFi / USB toggle in the editor. The event and action payloads are the same on both; only the wrapper around them differs. Switching transport rebuilds the firmware.

WiFi

The board runs on its own. Control it from another machine, a phone or a deployed web app.

Address
wss://gateway.atech.dev/ws/live/<project id>
From board
{ "type": "device_event", "payload": {…} }
To board
{ "type": "send_to_device", "device_id": "<project id>", "payload": {…} }
USB

Local and offline. The computer the board is plugged into talks straight to it, no cloud.

Port
115200 baud, one JSON object per line
From board
{ "type": "event", "payload": {…} }
To board
{ "action": "…", "value": … }

Your project id is in the editor: open the project's Docs tab and look under Your project.

Over WiFi

const PROJECT_ID = "<your project id>";
const ws = new WebSocket(`wss://gateway.atech.dev/ws/live/${PROJECT_ID}`);

// Every event the board sends
ws.onmessage = (e) => console.log(JSON.parse(e.data));

// Send an action
ws.onopen = () => ws.send(JSON.stringify({
  type: "send_to_device",
  device_id: PROJECT_ID,
  payload: { action: "set_color", value: { r: 255, g: 0, b: 0 } },
}));

Over USB

// Browser, Web Serial API
const port = await navigator.serial.requestPort();
await port.open({ baudRate: 115200 });

const writer = port.writable.getWriter();
const enc = new TextEncoder();
const send = (action) => writer.write(enc.encode(JSON.stringify(action) + "\n"));

// Read the board's events, one JSON object per line
const reader = port.readable.pipeThrough(new TextDecoderStream()).getReader();
(async () => {
  let buf = "";
  while (true) {
    const { value, done } = await reader.read();
    if (done) break;
    buf += value;
    let i;
    while ((i = buf.indexOf("\n")) >= 0) {
      console.log(buf.slice(0, i));
      buf = buf.slice(i + 1);
    }
  }
})();

// Send an action
send({ action: "set_color", value: { r: 255, g: 0, b: 0 } });

Module reference

Every event a module sends and every action it accepts, each with an example. Open a module to see its list.

Temperature & Humidity
2 events

Module id aht20_sensor

Events — the board sends these

  • temperaturefloat · C

    Temperature reading in Celsius

    { "type": "sensor", "key": "temperature", "value": 23.5, "module_type": "aht20" }
  • humidityfloat · %RH

    Relative humidity percentage

    { "type": "sensor", "key": "humidity", "value": 65.2, "module_type": "aht20" }
Air Quality
3 events

Module id scd40_air_quality

Events — the board sends these

  • co2int · ppm

    CO2 concentration in parts per million

    { "type": "sensor", "key": "co2", "value": 712, "module_type": "scd40" }
  • temperaturefloat · C

    Temperature reading in Celsius (from the SCD40's onboard sensor used for CO2 compensation)

    { "type": "sensor", "key": "temperature", "value": 23.5, "module_type": "scd40" }
  • humidityfloat · %RH

    Relative humidity percentage (from the SCD40's onboard sensor used for CO2 compensation)

    { "type": "sensor", "key": "humidity", "value": 48.1, "module_type": "scd40" }
Distance Sensor
2 events

Module id vl53l5cx_distance

Events — the board sends these

  • distanceint · mm

    Distance measurement in millimeters

    { "type": "sensor", "key": "distance", "value": 1250, "module_type": "vl53l5cx" }
  • min_distanceint · mm

    Minimum distance across all 64 zones

    { "type": "sensor", "key": "min_distance", "value": 450, "module_type": "vl53l5cx" }
Orientation Sensor
11 events

Module id icm40608_imu

Events — the board sends these

  • accel_xfloat · g

    X-axis acceleration in g

    { "type": "sensor", "key": "accel_x", "value": 0.98, "module_type": "icm40608" }
  • accel_yfloat · g

    Y-axis acceleration in g

    { "type": "sensor", "key": "accel_y", "value": 0.05, "module_type": "icm40608" }
  • accel_zfloat · g

    Z-axis acceleration in g

    { "type": "sensor", "key": "accel_z", "value": 0.12, "module_type": "icm40608" }
  • gyro_xfloat · deg/s

    X-axis rotation rate in degrees/second

    { "type": "sensor", "key": "gyro_x", "value": 12.5, "module_type": "icm40608" }
  • gyro_yfloat · deg/s

    Y-axis rotation rate in degrees/second

    { "type": "sensor", "key": "gyro_y", "value": -5.2, "module_type": "icm40608" }
  • gyro_zfloat · deg/s

    Z-axis rotation rate in degrees/second

    { "type": "sensor", "key": "gyro_z", "value": 0.8, "module_type": "icm40608" }
  • pitchfloat · deg

    Pitch angle (nose up/down), -90..+90

    { "type": "sensor", "key": "pitch", "value": 15.2, "module_type": "icm40608" }
  • rollfloat · deg

    Roll angle (left/right tilt), -180..+180

    { "type": "sensor", "key": "roll", "value": -8.4, "module_type": "icm40608" }
  • tiltfloat · deg

    Total tilt from upright (0 = flat face-up, 180 = flat face-down)

    { "type": "sensor", "key": "tilt", "value": 12, "module_type": "icm40608" }
  • orientationstring

    Coarse orientation: face_up, face_down, edge_right, edge_left, edge_front, edge_back, unknown

    { "type": "state", "key": "orientation", "value": "face_up", "module_type": "icm40608" }
  • shakeint

    Edge-triggered shake/tap event (fires once per spike >1.8g)

    { "type": "button", "key": "shake", "value": 1, "module_type": "icm40608" }
Button
2 events

Module id button_module

Events — the board sends these

  • button_pressint

    Button press event (value=1)

    { "type": "button", "key": "button_1", "value": 1 }
  • button_releaseint

    Button release event (value=0)

    { "type": "button", "key": "button_1", "value": 0 }
Light Grid
7 actions

Module id neopixel_grid

Actions — you send these

  • set_colorrgb_object

    Set all LEDs to RGB color

    { "action": "set_color", "value": { "r": 255, "g": 0, "b": 0 } }
  • brightnessint

    Set LED brightness (0-255)

    { "action": "brightness", "value": 128 }
  • clearno value

    Turn off all LEDs

    { "action": "clear", "value": null }
  • set_pixelpixel_object

    Light a single LED by linear index (0-8 for 3x3 grid). r/g/b are 0-255.

    { "action": "set_pixel", "value": { "index": 4, "r": 255, "g": 0, "b": 0 } }
  • set_pixel_xypixel_xy_object

    Light a single LED by row/column (0-2 each on the 3x3 grid)

    { "action": "set_pixel_xy", "value": { "row": 1, "col": 1, "r": 255, "g": 0, "b": 0 } }
  • set_rowpixel_row_object

    Set every LED in row N (0-2) to RGB

    { "action": "set_row", "value": { "row": 0, "r": 0, "g": 255, "b": 0 } }
  • set_columnpixel_col_object

    Set every LED in column N (0-2) to RGB

    { "action": "set_column", "value": { "col": 0, "r": 0, "g": 0, "b": 255 } }
Color Display
11 actions · 2 ports

Module id st7735_tft

Actions — you send these

  • display_textstring

    Display text on a cleared screen using the default font (white on black)

    { "action": "display_text", "value": "Hello World" }
  • draw_textstyled_text_object

    Draw text at (x, y) with size multiplier and RGB565 color. Does NOT clear screen first.

    { "action": "draw_text", "value": { "text": "Hi", "x": 10, "y": 20, "size": 2, "color": 65535 } }
  • draw_pixelpoint_object

    Draw a single pixel at (x, y) in RGB565 color

    { "action": "draw_pixel", "value": { "x": 80, "y": 40, "color": 65504 } }
  • draw_lineline_object

    Draw a line from (x0, y0) to (x1, y1) in RGB565 color

    { "action": "draw_line", "value": { "x0": 0, "y0": 0, "x1": 159, "y1": 79, "color": 65535 } }
  • draw_rectrect_object

    Draw an unfilled rectangle outline at (x, y) with size (w, h) in RGB565 color

    { "action": "draw_rect", "value": { "x": 20, "y": 20, "w": 40, "h": 30, "color": 2016 } }
  • fill_rectrect_object

    Draw a filled rectangle at (x, y) with size (w, h) in RGB565 color

    { "action": "fill_rect", "value": { "x": 20, "y": 20, "w": 40, "h": 30, "color": 63488 } }
  • draw_circlecircle_object

    Draw an unfilled circle outline centered at (x, y) with radius r in RGB565 color

    { "action": "draw_circle", "value": { "x": 80, "y": 40, "r": 20, "color": 2047 } }
  • fill_circlecircle_object

    Draw a filled circle centered at (x, y) with radius r in RGB565 color

    { "action": "fill_circle", "value": { "x": 80, "y": 40, "r": 20, "color": 2047 } }
  • fill_screenint

    Fill the entire screen with a single RGB565 color (use 0 for black, 65535 for white)

    { "action": "fill_screen", "value": 0 }
  • set_rotationint

    Set display rotation: 0 = portrait, 1 = landscape (default), 2 = portrait flipped, 3 = landscape flipped

    { "action": "set_rotation", "value": 1 }
  • clear_screenno value

    Clear the display to black

    { "action": "clear_screen", "value": null }
Big Screen
11 actions · 2 ports

Module id st7789_tft

Actions — you send these

  • display_textstring

    Display text on a cleared screen using the default font (white on black)

    { "action": "display_text", "value": "Hello World" }
  • draw_textstyled_text_object

    Draw text at (x, y) with size multiplier and RGB565 color. Does NOT clear screen first.

    { "action": "draw_text", "value": { "text": "Hi", "x": 20, "y": 60, "size": 2, "color": 65535 } }
  • draw_pixelpoint_object

    Draw a single pixel at (x, y) in RGB565 color

    { "action": "draw_pixel", "value": { "x": 160, "y": 120, "color": 65504 } }
  • draw_lineline_object

    Draw a line from (x0, y0) to (x1, y1) in RGB565 color

    { "action": "draw_line", "value": { "x0": 0, "y0": 0, "x1": 319, "y1": 239, "color": 65535 } }
  • draw_rectrect_object

    Draw an unfilled rectangle outline at (x, y) with size (w, h) in RGB565 color

    { "action": "draw_rect", "value": { "x": 40, "y": 60, "w": 80, "h": 90, "color": 2016 } }
  • fill_rectrect_object

    Draw a filled rectangle at (x, y) with size (w, h) in RGB565 color

    { "action": "fill_rect", "value": { "x": 40, "y": 60, "w": 80, "h": 90, "color": 63488 } }
  • draw_circlecircle_object

    Draw an unfilled circle outline centered at (x, y) with radius r in RGB565 color

    { "action": "draw_circle", "value": { "x": 160, "y": 120, "r": 40, "color": 2047 } }
  • fill_circlecircle_object

    Draw a filled circle centered at (x, y) with radius r in RGB565 color

    { "action": "fill_circle", "value": { "x": 160, "y": 120, "r": 40, "color": 2047 } }
  • fill_screenint

    Fill the entire screen with a single RGB565 color (use 0 for black, 65535 for white)

    { "action": "fill_screen", "value": 0 }
  • set_rotationint

    Set display rotation: 0 = portrait, 1 = landscape upright on a left-side pair, 2 = portrait flipped, 3 = landscape upright on a right-side pair. Boot already picks the right landscape for the board side.

    { "action": "set_rotation", "value": 1 }
  • clear_screenno value

    Clear the display to black

    { "action": "clear_screen", "value": null }
DC Motor
3 actions

Module id dc_motor

Actions — you send these

  • motor_speedint

    Set motor speed (-255 to +255, negative=reverse)

    { "action": "motor_speed", "value": 200 }
  • motor_stopno value

    Coast to stop (free spin, no holding torque)

    { "action": "motor_stop", "value": null }
  • motor_brakeno value

    Active brake (short the motor terminals — stops faster, holds position briefly)

    { "action": "motor_brake", "value": null }
Knob
2 events · 2 actions · 2 ports

Module id rotary_encoder_knob

Events — the board sends these

  • knob_positionint

    Current encoder position (cumulative turns)

    { "type": "sensor", "key": "knob_position", "value": 42, "module_type": "rotary_encoder" }
  • knob_pressint

    Knob button pressed

    { "type": "button", "key": "knob_press", "value": 1 }

Actions — you send these

  • set_ring_colorrgb_object

    Set LED ring color

    { "action": "set_ring_color", "value": { "r": 255, "g": 0, "b": 0 } }
  • set_ring_brightnessint

    Set LED ring brightness (0-255)

    { "action": "set_ring_brightness", "value": 128 }
Microphone
2 events · 2 ports

Module id microphone

Events — the board sends these

  • sound_levelint

    RMS sound level (0-32767)

    { "type": "sensor", "key": "sound_level", "value": 1200, "module_type": "microphone" }
  • sound_detectedint

    Sound above threshold detected

    { "type": "button", "key": "sound_detected", "value": 1 }
Robot Arm Integrator
2 events · 4 actions

Module id robot_arm

Events — the board sends these

  • joint_positionsstring

    All 6 joint positions as comma-separated values (0-4095 each)

    { "type": "sensor", "key": "joint_positions", "value": "2048,2048,2048,2048,2048,2048", "module_type": "robot_arm" }
  • joint_tempint · C

    Joint temperature in Celsius (specify joint in module_type)

    { "type": "sensor", "key": "joint_temp", "value": 38, "module_type": "robot_arm" }

Actions — you send these

  • move_jointstring

    Move a single joint: value is 'joint_id:position[:time_ms]' e.g. '3:2048:500'

    { "action": "move_joint", "value": "3:2048:500" }
  • move_allstring

    Move all 6 joints: value is 'p1,p2,p3,p4,p5,p6[:time_ms]' e.g. '2048,2048,2048,2048,2048,2048:1000'

    { "action": "move_all", "value": "2048,2048,2048,2048,2048,2048:1000" }
  • center_allstring

    Center all joints to 2048. Value is optional time_ms e.g. '1000'

    { "action": "center_all", "value": "1000" }
  • set_torquestring

    Set torque: value is 'on' or 'off' (all joints)

    { "action": "set_torque", "value": "on" }
Speaker
5 actions · 2 ports

Module id speaker

Actions — you send these

  • play_tonefrequency_object

    Play a tone at given frequency for duration (ms)

    { "action": "play_tone", "value": { "frequency": 440, "duration": 1000 } }
  • speaker_stopno value

    Stop any sound

    { "action": "speaker_stop", "value": null }
  • play_notefrequency_object

    Like play_tone but with a short gap between notes — better for melodies (85% sound, 15% silence)

    { "action": "play_note", "value": { "frequency": 523, "duration": 250 } }
  • play_chordchord_object

    Play multiple frequencies simultaneously (max 4) for the given duration in ms

    { "action": "play_chord", "value": { "frequencies": [ 261, 329, 392 ], "duration": 600 } }
  • set_volumefloat

    Set output volume (0.0 silent .. 1.0 max). Persists across tones.

    { "action": "set_volume", "value": 0.5 }
Soil Sensor
4 events · 7 actions

Module id soil_sensor

Events — the board sends these

  • moisturefloat · %

    Calibrated soil moisture, 0-100 %

    { "type": "sensor", "key": "moisture", "value": 42.7, "module_type": "soil_sensor" }
  • conductancefloat · uS

    Conductance between the prongs in microsiemens — the uncalibrated physical measurement

    { "type": "sensor", "key": "conductance", "value": 18.4, "module_type": "soil_sensor" }
  • soil_rawint · counts

    Raw oversampled ADC reading, 0-65472. High means dry. Record this when calibrating

    { "type": "sensor", "key": "soil_raw", "value": 31200, "module_type": "soil_sensor" }
  • soilstring

    Probe condition: warming up, probe open, saturated, or ok

    { "type": "state", "key": "soil", "value": "probe open" }

Actions — you send these

  • calibrate_dryno value

    Record the current reading as the 0% point. Call with the probe held in air

    { "action": "calibrate_dry", "value": null }
  • calibrate_wetno value

    Record the current reading as the 100% point. Call with the prongs standing in water

    { "action": "calibrate_wet", "value": null }
  • read_nowno value

    Force an immediate measurement instead of waiting for the next timer tick

    { "action": "read_now", "value": null }
  • set_status_colorstring

    Set the on-module status LED, as "r,g,b" with each 0-255. Takes the LED out of its automatic red-to-green-to-blue moisture ramp until set_status_auto

    { "action": "set_status_color", "value": "0,40,0" }
  • set_status_autono value

    Return the status LED to its automatic moisture ramp: red dry, green moist, blue submerged

    { "action": "set_status_auto", "value": null }
  • set_status_offno value

    Turn the on-module status LED off. set_status_auto turns it back on

    { "action": "set_status_off", "value": null }
  • set_status_brightnessint

    Brightness of the automatic LED ramp, 0-255; default 24

    { "action": "set_status_brightness", "value": 24 }

Troubleshooting

The board will not connect, or the upload fails

  1. Unplug the USB cable from the board, wait a second, plug it back in and try again. This fixes most cases.
  2. Close anything else that holds the port: another atech.dev tab (only one tab can use the board at a time) or the Arduino IDE Serial Monitor.
  3. Use a data USB-C cable. Many cables are charge-only and cannot program the board.
  4. Plug straight into the computer, not through a hub.
  5. Press the board's reset button, then try again.
  6. On Linux, your user has to be in the dialout group.

There is no way to upload from my browser

Uploading needs the Web Serial API, which only desktop Chromium browsers have. Open the project in Chrome, Edge, Brave, Opera or Arc on a computer.

The upload finished but the board stays offline

After an upload the board restarts and reconnects, which can take up to 60 seconds. If it has not come back by then, unplug and replug it or press its reset button. On WiFi, check the network name and password you entered; the board joins 2.4 GHz networks only.

My code connects but nothing arrives

The board is not online. Open the connection pill in the editor and check that “Receiving frames” is green.

The connection is refused or closes at once

Check the address. It ends in your project id; copy it fresh from the project's Docs tab, under Your project.

I send an action and the board ignores it

The firmware only accepts the actions of the modules in that build. Compare the action name and value with the module reference.

Get help

Stuck, or found something that does not work as described here? Ask in Discord, where the team and other builders answer, or write to [email protected].

Ask ClaudeAsk ChatGPTOpens your own account with these docs loaded.

Or add Atech to your assistant for good: in Claude (Settings, Connectors) or ChatGPT (Settings, Connectors) add a connector with the address https://atech.dev/mcp. It then answers from these docs every time, nothing to paste.