Arduino Sketches skill for Codex

Sketches that run for weeks, not minutes. Codex builds Arduino sketches from the command line with pinned cores and libraries, restructures blocking delay() code into rollover-safe millis() tasks, guards ISR-shared variables, and keeps strings out of SRAM on AVR boards. Hardware bugs get a symptom table and an I2C scan before anyone rewrites a driver.

  • Embedded & IoT
  • $arduino-sketches
  • 194 lines
  • 2 files
  • ~1,850 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- arduino-sketches

Installs to ~/.agents/skills/arduino-sketches/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $arduino-sketches, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Writes, builds and debugs Arduino sketches with arduino-cli and sketch.yaml profiles - non-blocking millis() loops, interrupt-safe state, I2C/SPI sensor libraries, F() strings and RAM budgeting on AVR, SAMD, RP2040 and ESP boards. Use when a sketch hangs, resets, prints garbage on Serial, runs out of SRAM, a library will not compile, a sensor is not detected on I2C, or when turning a delay()-based prototype into a reliable loop.

Use it when

  • An Arduino sketch hangs, resets or prints garbage on Serial
  • A delay()-based prototype needs to handle several things at once
  • A sensor library compiles but the device is not detected
  • You want reproducible command-line builds of an .ino project

Use something else when

  • The project needs multiple environments, unit tests or CI: use platformio
  • You are writing native ESP-IDF code rather than the Arduino core: use esp32-firmware

How to check the result

Compile with --warnings all under the sketch.yaml profile, compare flash and RAM figures, and confirm a serial log from the real board covers longer than the slowest timer.

The mistake it prevents

Comparing millis() against a precomputed future timestamp breaks at rollover; always subtract the start time and compare the difference.

~/.agents/skills/arduino-sketches/SKILL.md

--- name: arduino-sketches description: Writes, builds and debugs Arduino sketches with arduino-cli and sketch.yaml profiles - non-blocking millis() loops, interrupt-safe state, I2C/SPI… license: MIT ---

Done well means: the sketch builds reproducibly from the command line with pinned core and library versions, loop() never blocks longer than the fastest thing it must react to, and every hardware claim (pin, address, baud) was checked on the board, not assumed.

Before touching code

  1. Read AGENTS.md and any README for the board, wiring and how the user flashes.
  2. Identify the exact board and core: arduino-cli board list shows the port and FQBN (arduino:avr:uno, arduino:samd:mkrwifi1010, rp2040:rp2040:rpipico, esp32:esp32:esp32s3). Memory limits differ by two orders of magnitude between them.
  3. Look for sketch.yaml; if present, it pins platform and library versions. Build with its profile instead of whatever is installed globally.
  4. Compile once, unchanged, with all warnings, and save the memory line from the output.
arduino-cli core update-index                      # network
arduino-cli core install arduino:avr               # network; may need approval in the sandbox
arduino-cli compile --fqbn arduino:avr:uno --warnings all ./Blink
arduino-cli upload  --fqbn arduino:avr:uno -p /dev/ttyACM0 ./Blink
arduino-cli monitor -p /dev/ttyACM0 -c baudrate=115200

Codex usually cannot see the USB port from its sandbox. Compile there; ask the user to run upload and monitor and paste the serial output when a step needs the real board.

Pin the build with sketch.yaml

# sketch.yaml, next to the .ino
profiles:
  uno:
    fqbn: arduino:avr:uno
    platforms:
      - platform: arduino:avr (1.8.6)
    libraries:
      - Adafruit BME280 Library (2.2.4)
      - Adafruit Unified Sensor (1.1.14)
default_profile: uno

arduino-cli compile --profile uno then installs exactly those versions. Use arduino-cli lib search bme280 to find exact library names; names with spaces must match.

Loop structure: never block

Replace delay() with elapsed-time checks. Unsigned subtraction is rollover-safe; adding to millis() and comparing is not.

const uint32_t SAMPLE_MS = 1000;
uint32_t lastSample = 0;

void loop() {
  const uint32_t now = millis();
  if (now - lastSample >= SAMPLE_MS) {     // correct across the 49.7-day wrap
    lastSample += SAMPLE_MS;               // fixed cadence, no drift
    readSensor();
  }
  pollButton();
  serviceSerial();
}
NeedUse
Periodic taskmillis() comparison as above, one timestamp per task
Sub-millisecond timingmicros() (wraps every ~71 min, same subtraction rule)
Multi-step behaviour (connect, wait, retry)enum state machine with a switch in loop()
Debounced buttonSample every 5-10 ms, accept after N stable reads; or the Bounce2 library
Exact pulse captureHardware interrupt or input capture, not polling

Interrupts

volatile uint32_t pulseCount = 0;

void onPulse() { pulseCount++; }           // short: no Serial, no delay, no allocation

void setup() {
  pinMode(2, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(2), onPulse, FALLING);
}

uint32_t readPulses() {
  noInterrupts();                          // a 32-bit read is not atomic on 8-bit AVR
  uint32_t c = pulseCount;
  pulseCount = 0;
  interrupts();
  return c;
}
  • Only certain pins support attachInterrupt (Uno: 2 and 3). Always go through digitalPinToInterrupt.
  • On ESP32 mark the handler IRAM_ATTR, or it can crash when flash cache is disabled.
  • millis() does not advance inside an ISR on AVR; delay() there hangs forever.

Memory on small boards

An Uno has 2 KB of SRAM. The compile output’s “Global variables use X bytes” is static use only; the stack and heap need the rest. Stay below roughly 75 % static on AVR.

  • Wrap literal strings: Serial.println(F("Sensor not found")); keeps them in flash.
  • Avoid String concatenation in loop(); it fragments the heap and fails after hours. Use fixed char buf[32] with snprintf. On AVR, %f is not supported by snprintf; use dtostrf().
  • Large constant tables: const uint8_t table[] PROGMEM = {...}; and pgm_read_byte().
  • Quick free-RAM probe on AVR (gap between heap and stack):
extern int __heap_start, *__brkval;
int freeRam() { int v; return (int)&v - (__brkval == 0 ? (int)&__heap_start : (int)__brkval); }

Sensors and buses

  1. Run an I2C scan first; never trust the address in a tutorial (BME280 is 0x76 or 0x77).
#include <Wire.h>
void setup() {
  Serial.begin(115200);
  while (!Serial && millis() < 3000) {}    // native-USB boards; times out on Uno-style
  Wire.begin();
  for (uint8_t a = 1; a < 127; a++) {
    Wire.beginTransmission(a);
    if (Wire.endTransmission() == 0) { Serial.print(F("found 0x")); Serial.println(a, HEX); }
  }
}
void loop() {}
  1. Nothing found: check pull-ups (4.7 kOhm to the logic voltage), SDA/SCL swapped, 3.3 V sensor on a 5 V board without a level shifter, and that Wire.begin(sda, scl) uses the right pins on ESP32/RP2040.
  2. Check every library begin() return value and print a clear error; many libraries return false silently.

Debugging symptoms

SymptomLikely causeCheck
Garbage on SerialBaud mismatchMonitor rate equals Serial.begin rate
Board resets repeatedlySRAM exhausted, brownout from motor or servo on USB powerFree-RAM print; separate supply with common ground
Works on USB, fails on batteryCode waits on while (!Serial) foreverAdd a timeout, as above
Upload fails “programmer not responding”Wrong port or board, serial monitor holding portClose monitor, re-run board list
ESP32 boot loop with “Guru Meditation”Stack overflow, null pointer, ISR not in IRAMDecode the backtrace (esp32-firmware)
Reading floats at randomFloating input pinINPUT_PULLUP or external resistor
multiple definition of at linkTwo libraries with the same header, wrong one pickedCompile with -v, read “Multiple libraries were found”

Add debug output behind a macro so release builds stay small:

#define DEBUG 1
#if DEBUG
  #define DBG(x) Serial.println(x)
#else
  #define DBG(x)
#endif

Code organisation

  • Past ~300 lines, move drivers into .h/.cpp tabs in the sketch folder or into src/; the IDE’s auto-prototype generation only applies to .ino files.
  • Keep pin numbers and tunables as constexpr at the top, one place to change wiring.
  • For multiple environments, unit tests or CI, move to platformio rather than scripting around the IDE.

Verification

  • arduino-cli compile --warnings all with the profile: zero new warnings
  • Flash and RAM use recorded before and after the change
  • Serial log from the real board shows the expected sequence (ask the user to paste it)
  • Ran for longer than the longest timer (for String/heap issues: hours, not seconds)
  • Unplug-and-replug cold boot works without the serial monitor open

Report

**Board:** Arduino Uno R3 (arduino:avr 1.8.6), BME280 on I2C 0x76
**Change:** replaced delay()-based loop with 3 millis() tasks; ISR counter read atomically
**Memory:** flash 14,212 -> 12,980 B; globals 1,610 -> 1,094 B (53 %)
**Verified:** compile clean; user-provided serial log shows 1 Hz samples for 30 min
**Open:** sensor at 0x77 on the second board revision needs a config flag

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "Arduino Sketches"
  short_description: "Sketches that run for weeks, not minutes"
  default_prompt: "$arduino-sketches My Arduino resets every few hours, find out why"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc