Community project

Odradek Motion Light Controller

Arduino
Photo of Odradek Motion Light Controller
Generated with AI

Hfj Chcjc

Published October 24, 2026

The Odradek Motion Light Controller brings articulated movement and dynamic lighting to a five-arm mechanism with independent head positioning. This guide covers building a PCA9685-based servo controller that drives five arm servos and a 270-degree head servo, along with four independently dimmable RGB channels and a status indicator LED, all coordinated by an Arduino Uno.

Builders will receive a complete wiring diagram, detailed parts list with power distribution guidance, Arduino firmware with pose presets and safety limits, and step-by-step assembly instructions including mechanism alignment and unloaded testing. The controller supports multiple head scanning modes and arm motion alerts, with all servo endpoints calibrated for safe operation.

Wiring diagram

Wiring diagram for Odradek Motion Light Controller

Gather all the parts

QtyComponent
1

PCA9685 servo breakout, address 0x40

Adafruit-compatible; VPLUS is terminal marked V+. Only signals go through breakout; servo power uses external distribution.

1

Arm positional servo 1

ASSUMPTION: servo approved for 6 V and 50 Hz. Confirm current, torque and pulse range. No continuous-rotation servos.

1

Arm positional servo 2

ASSUMPTION: servo approved for 6 V and 50 Hz. Confirm current, torque and pulse range. No continuous-rotation servos.

1

Arm positional servo 3

ASSUMPTION: servo approved for 6 V and 50 Hz. Confirm current, torque and pulse range. No continuous-rotation servos.

1

Arm positional servo 4

ASSUMPTION: servo approved for 6 V and 50 Hz. Confirm current, torque and pulse range. No continuous-rotation servos.

1

Arm positional servo 5

ASSUMPTION: servo approved for 6 V and 50 Hz. Confirm current, torque and pulse range. No continuous-rotation servos.

1

Head positional servo, genuine 270-degree variant required for 270 travel

ASSUMPTION: servo approved for 6 V and 50 Hz. Confirm current, torque and pulse range. No continuous-rotation servos.

1

12 V common-positive analog RGB strip section

Assumed 12 V resistor-equipped strip, one section per arm. Length and current unknown; all sections show the same colour.

1

12 V common-positive analog RGB strip section

Assumed 12 V resistor-equipped strip, one section per arm. Length and current unknown; all sections show the same colour.

1

12 V common-positive analog RGB strip section

Assumed 12 V resistor-equipped strip, one section per arm. Length and current unknown; all sections show the same colour.

1

12 V common-positive analog RGB strip section

Assumed 12 V resistor-equipped strip, one section per arm. Length and current unknown; all sections show the same colour.

1

12 V common-positive analog RGB strip section

Assumed 12 V resistor-equipped strip, one section per arm. Length and current unknown; all sections show the same colour.

1

Blue indicator LED

Assumed ordinary low-current blue LED, NOT an unidentified high-power emitter. 1k resistor limits current to at most 12mA at 12V.

1

1 kohm 0.5 W LED series resistor

1 kohm 0.5 W LED series resistor; see assembly for connection and rating checks.

1

IRLZ44N logic-level N MOSFET

TO-220 front text facing you: 1 Gate, 2 Drain (also tab), 3 Source. Separate channels; keep tabs isolated.

1

330 ohm resistor

330 ohm resistor; see assembly for connection and rating checks.

1

10 kohm resistor

10 kohm resistor; see assembly for connection and rating checks.

1

IRLZ44N logic-level N MOSFET

TO-220 front text facing you: 1 Gate, 2 Drain (also tab), 3 Source. Separate channels; keep tabs isolated.

1

330 ohm resistor

330 ohm resistor; see assembly for connection and rating checks.

1

10 kohm resistor

10 kohm resistor; see assembly for connection and rating checks.

1

IRLZ44N logic-level N MOSFET

TO-220 front text facing you: 1 Gate, 2 Drain (also tab), 3 Source. Separate channels; keep tabs isolated.

1

330 ohm resistor

330 ohm resistor; see assembly for connection and rating checks.

1

10 kohm resistor

10 kohm resistor; see assembly for connection and rating checks.

1

IRLZ44N logic-level N MOSFET

TO-220 front text facing you: 1 Gate, 2 Drain (also tab), 3 Source. Separate channels; keep tabs isolated.

1

330 ohm resistor

330 ohm resistor; see assembly for connection and rating checks.

1

10 kohm resistor

10 kohm resistor; see assembly for connection and rating checks.

1

Mean Well LDD-350L constant-current driver

350mA output. DIM 100-1000Hz, high >3.5V; never connect OUTM to VINM. Use maker pin drawing for your package.

1

White power LED on heatsink, rated at least 350mA

ASSUMPTION: single white emitter, forward voltage within driver compliance at 12V; confirm before use. Current limited by driver, not resistor.

1

10 kohm DIM pull-down resistor

10 kohm DIM pull-down resistor; see assembly for connection and rating checks.

1

Enclosed regulated 12 V DC supply

No exposed mains wiring. Capacity must exceed measured LED current / sum of six servo stall currents with headroom; ratings are not known.

1

Inline fuse and holder, value sized for load and wire

Inline fuse and holder, value sized for load and wire; see assembly for connection and rating checks.

1

Enclosed regulated 6 V DC supply

No exposed mains wiring. Capacity must exceed measured LED current / sum of six servo stall currents with headroom; ratings are not known.

1

Inline fuse and holder, value sized for load and wire

Inline fuse and holder, value sized for load and wire; see assembly for connection and rating checks.

1

1000 uF 16 V electrolytic capacitor

1000 uF 16 V electrolytic capacitor; see assembly for connection and rating checks.

1

10 kohm servo-enable pull-up resistor

Keeps PCA outputs disabled while UNO resets.

Assemble it in 6 steps

1. Verify the assumed parts before connecting power

This is a new reference circuit, not a recovered existing Odradek circuit. Use exactly five arm positional servos and one head positional servo approved for 6 V, 50 Hz and 5 V control. For full head travel, buy the genuine 270-degree positional version, never a continuous-rotation servo. Existing arm torque, stall current, LED voltage and current are unknown; verify them before connecting. The five RGB strips are assumed 12 V common-positive with built-in resistors; the extra blue LED is an ordinary low-current LED.

  • Never apply the assumed voltages to unverified existing parts. A standard 180-degree servo cannot turn 270 degrees.

2. Build separate fused power distribution

With supplies unplugged, connect supply12 PLUS through its fuse to the 12 V lighting rail; supply6 PLUS through its fuse to the 6 V servo rail. Join both MINUS terminals, UNO GND and PCA GND at a common ground distribution point. Power UNO through USB only. Keep all positive rails separate. Connect the capacitor PLUS to 6 V and MINUS to ground. Use heavy distribution wires and screw terminals for servo current, not UNO pins or breadboard rails. Use enclosed low-voltage adapters only, no mains wiring.

  • Size the 6 V supply from the sum of all six actual stall currents plus 25% margin. Size 12 V supply from full-white RGB current plus LED driver input and blue LED current, plus 25%. Fuse values must protect the actual wire gauge and fit the load. The power estimate is a provisional planning budget, not a measured load.
  • Do not connect 6 V or 12 V to UNO 5V. Reversing the capacitor can make it burst.

3. Connect the servo signal controller

PCA VCC to UNO 5V (logic), SDA to A4 (data), SCL to A5 (clock), GND to common ground, and VPLUS to 6 V. OE to D2 (output enable), with its own 10 kohm pull-up to 5V. Leave address jumpers open for 0x40. Connect CH0 through CH4 signal pins to arm servos 1 through 5; CH5 signal to head servo 6. Each servo gets 6 V and ground directly from external distribution. Do not pass all motor current through the breakout's small header tracks.

  • OE disables signals, not motor power. Have an accessible supply disconnect. An unsupported mechanism can fall when signals or power are removed.

4. Build the four MOSFET dimmer channels

For each IRLZ44N, text facing you and legs down: left is gate, middle is drain, right is source; metal tab is also drain. Source goes to common ground. D3 through its 330 ohm resistor to qR gate; D5 through 330 ohm to qG gate; D6 through 330 ohm to qB gate; D9 through 330 ohm to qBlue gate. Fit each separate 10 kohm gate pull-down to ground. Connect all five strip PLUS pads to 12 V, all R pads to qR drain, all G pads to qG drain, all B pads to qB drain. Put blue_limit 1 kohm 0.5 W between 12 V and blue LED anode; blue cathode goes to qBlue drain.

  • All strip sections share the same colour; conventional analog strips cannot animate individual LEDs. Each section contains its own current-limiting resistors. The separate blue LED has its own external series resistor.
  • MOSFETs switch the loads, not GPIO pins. Check actual channel current, MOSFET heat and connectors under full load; fit isolated heatsinks if needed. Never join drain tabs together.

5. Wire the independent white LED driver

Use a white emitter rated for at least 350 mA continuous on a suitable heatsink. LDD-350L VINP to 12 V; VINM to common ground; OUTP to white anode; OUTM to white cathode. DIM to D10 (approximately 490 Hz PWM), with white_pull 10 kohm to ground. Identify these names using the exact LDD package's maker drawing; do not guess physical pin orientation. Its 350 mA constant-current regulation replaces a series resistor.

  • Do not connect OUTM to ground or bypass the driver. Never run a power LED without its heatsink; do not look directly into the bright emitter. An unknown existing white LED may require a different current-rated driver.

6. Align and test the mechanism unloaded

Leave servo horns/linkages disconnected for the first test. With power off inspect polarity and shorts. Support the mechanism. Use firmware calibration values appropriate for the actual servos. Centre/test one servo at a time, then switch power off before installing each horn so its known folded/home command matches the resting pose. Check all five arm limits separately and allow cable slack through the entire head sweep. Do not force servo shafts manually. Only reconnect the assembled mechanism once verified free of collisions.

  • Software has no position feedback and cannot prevent an initial snap if the physical pose does not match its first command. On every restart or after off, align and support the folded/home pose before enabling. Do not use hard-stop impacts or stalled servos to create clicking. The effect is a rhythmic movement, not guaranteed audible sound.

Review all connections

1. Connections between "pca" and "Arduino"

FunctionpcaArduino
powerVCC5V
groundGNDGND
i2cSDAGPIO 18
i2cSCLGPIO 19
powerVPLUS6V
digitalOEGPIO 2

2. Connections between "servo1" and "Arduino"

Functionservo1Arduino
powerVCC6V
groundGNDGND
pwmSIG → PCA9685 servo breakout, address 0x40 CH0EXT

3. Connections between "servo2" and "Arduino"

Functionservo2Arduino
powerVCC6V
groundGNDGND
pwmSIG → PCA9685 servo breakout, address 0x40 CH1EXT

4. Connections between "servo3" and "Arduino"

Functionservo3Arduino
powerVCC6V
groundGNDGND
pwmSIG → PCA9685 servo breakout, address 0x40 CH2EXT

5. Connections between "servo4" and "Arduino"

Functionservo4Arduino
powerVCC6V
groundGNDGND
pwmSIG → PCA9685 servo breakout, address 0x40 CH3EXT

6. Connections between "servo5" and "Arduino"

Functionservo5Arduino
powerVCC6V
groundGNDGND
pwmSIG → PCA9685 servo breakout, address 0x40 CH4EXT

7. Connections between "servo6" and "Arduino"

Functionservo6Arduino
powerVCC6V
groundGNDGND
pwmSIG → PCA9685 servo breakout, address 0x40 CH5EXT

8. Connections between "rgb1" and "Arduino"

Functionrgb1Arduino
powerPLUS12V
dataR → IRLZ44N logic-level N MOSFET DEXT
dataG → IRLZ44N logic-level N MOSFET DEXT
dataB → IRLZ44N logic-level N MOSFET DEXT

9. Connections between "rgb2" and "Arduino"

Functionrgb2Arduino
powerPLUS12V
dataR → IRLZ44N logic-level N MOSFET DEXT
dataG → IRLZ44N logic-level N MOSFET DEXT
dataB → IRLZ44N logic-level N MOSFET DEXT

10. Connections between "rgb3" and "Arduino"

Functionrgb3Arduino
powerPLUS12V
dataR → IRLZ44N logic-level N MOSFET DEXT
dataG → IRLZ44N logic-level N MOSFET DEXT
dataB → IRLZ44N logic-level N MOSFET DEXT

11. Connections between "rgb4" and "Arduino"

Functionrgb4Arduino
powerPLUS12V
dataR → IRLZ44N logic-level N MOSFET DEXT
dataG → IRLZ44N logic-level N MOSFET DEXT
dataB → IRLZ44N logic-level N MOSFET DEXT

12. Connections between "rgb5" and "Arduino"

Functionrgb5Arduino
powerPLUS12V
dataR → IRLZ44N logic-level N MOSFET DEXT
dataG → IRLZ44N logic-level N MOSFET DEXT
dataB → IRLZ44N logic-level N MOSFET DEXT

13. Connections between "blue_limit" and "Arduino"

Functionblue_limitArduino
power112V
power2 → Blue indicator LED AEXT

14. Connections between "blue" and "Arduino"

FunctionblueArduino
dataK → IRLZ44N logic-level N MOSFET DEXT

15. Connections between "R_gate" and "Arduino"

FunctionR_gateArduino
data1GPIO 3
data2 → IRLZ44N logic-level N MOSFET GEXT

16. Connections between "R_pull" and "Arduino"

FunctionR_pullArduino
data1 → IRLZ44N logic-level N MOSFET GEXT
ground2GND

17. Connections between "qR" and "Arduino"

FunctionqRArduino
groundSGND

18. Connections between "G_gate" and "Arduino"

FunctionG_gateArduino
data1GPIO 5
data2 → IRLZ44N logic-level N MOSFET GEXT

19. Connections between "G_pull" and "Arduino"

FunctionG_pullArduino
data1 → IRLZ44N logic-level N MOSFET GEXT
ground2GND

20. Connections between "qG" and "Arduino"

FunctionqGArduino
groundSGND

21. Connections between "B_gate" and "Arduino"

FunctionB_gateArduino
data1GPIO 6
data2 → IRLZ44N logic-level N MOSFET GEXT

22. Connections between "B_pull" and "Arduino"

FunctionB_pullArduino
data1 → IRLZ44N logic-level N MOSFET GEXT
ground2GND

23. Connections between "qB" and "Arduino"

FunctionqBArduino
groundSGND

24. Connections between "Blue_gate" and "Arduino"

FunctionBlue_gateArduino
data1GPIO 9
data2 → IRLZ44N logic-level N MOSFET GEXT

25. Connections between "Blue_pull" and "Arduino"

FunctionBlue_pullArduino
data1 → IRLZ44N logic-level N MOSFET GEXT
ground2GND

26. Connections between "qBlue" and "Arduino"

FunctionqBlueArduino
groundSGND

27. Connections between "white_driver" and "Arduino"

Functionwhite_driverArduino
powerVINP12V
groundVINMGND
powerOUTP → White power LED on heatsink, rated at least 350mA AEXT
dataOUTM → White power LED on heatsink, rated at least 350mA KEXT
pwmDIMGPIO 10

28. Connections between "white_pull" and "Arduino"

Functionwhite_pullArduino
data1 → Mean Well LDD-350L constant-current driver DIMEXT
ground2GND

29. Connections between "supply12" and "Arduino"

Functionsupply12Arduino
powerPLUS → Inline fuse and holder, value sized for load and wire INEXT
groundMINUSGND

30. Connections between "supply12_fuse" and "Arduino"

Functionsupply12_fuseArduino
powerOUT12V

31. Connections between "supply6" and "Arduino"

Functionsupply6Arduino
powerPLUS → Inline fuse and holder, value sized for load and wire INEXT
groundMINUSGND

32. Connections between "supply6_fuse" and "Arduino"

Functionsupply6_fuseArduino
powerOUT6V

33. Connections between "cap" and "Arduino"

FunctioncapArduino
powerPLUS6V
groundMINUSGND

34. Connections between "oe_pull" and "Arduino"

Functionoe_pullArduino
data1 → PCA9685 servo breakout, address 0x40 OEEXT
power25V

Deploy the firmware

#include <Arduino.h>
#include <Wire.h>
#include <math.h>
// All angles are installation-specific examples, NOT measured mechanism limits.
// Test unloaded, align horns at the folded pose, then type arm. No boot pulses.
namespace Config {
const uint8_t PCA = 0x40;
const uint8_t enablePin=2; // PCA OE active LOW
const uint32_t OSC_HZ = 25000000UL; // Measure/calibrate your PCA oscillator if needed.
const uint8_t PRESCALE = 121; // nominal 50 Hz, no Servo library/timer conflict
const float closed[5] = {90,90,90,90,90};
const float active[5] = {100,100,100,100,100};
const float opened[5] = {140,140,140,140,140};
const float armMin[5] = {70,70,70,70,70};
const float armMax[5] = {150,150,150,150,150};
const uint16_t armPulseMin[5] = {1000,1000,1000,1000,1000};
const uint16_t armPulseMax[5] = {2000,2000,2000,2000,2000};
// These pulses map to 0..180 logical degrees. Verify actual arm servo endpoints.
// Head: actual POSITIONAL servo's angle between calibrated pulse endpoints.
// A normal 180-degree servo stays 180 here. Genuine 270 variant: set span=270
// and its VERIFIED pulses (not automatically 500..2500), then safe endpoints.
const float headServoSpan = 180;
const uint16_t headPulseMin = 1000, headPulseMax = 2000;
const float headSafeMin = 60, headSafeMax = 120, headHome = 90;
// Head sweep is always bounded to <=270 degrees, even if config is mistaken.
const bool headScanMode2=false, headScanMode3=true, headScanClick=true;
const bool alertArmMotion=false;
const float poseSpeed=22, clickSpeed=40, headSpeed=8; // degrees per second
const float clickTravel=1.0f; // 0..1 of active->open distance
const uint16_t clickPauseMs=300, armStaggerMs=65, alertHalfPeriodMs=850;
const float alertTravel=3;
const uint16_t servoTickMs=20, lightTickMs=10;
// Channels: strip R,G,B; independent blue LED; independent white driver.
const uint8_t lightPins[5]={3,5,6,9,10};
struct Light { uint8_t r,g,b,stripBrightness,blue,white; };
const Light lights[5]={
 {165,220,255,255,255,240}, // mode 1 icy
 {100,180,255,180,170,150}, // mode 2 deeper blue
 {175,225,255,255,255,255}, // mode 3
 {175,225,255,255,255,255}, // mode 3.5
 {255,65,0,255,0,0}        // mode 4: no white to wash out orange
};
}
enum Mode : uint8_t {IDLE,ACTIVE,SCAN,CLICK,ALERT};
Mode mode=IDLE;
float pos[6], target[6];
uint8_t lightNow[5]={}, lightTarget[5]={};
uint16_t lastPulse[6]={};
bool enabled=false, healthy=true, outward=true, headForward=true, waiting=false;
uint32_t entered=0, phaseStart=0, pauseStart=0, lastServo=0,lastLight=0;
char command[16]; uint8_t commandLen=0; bool overflow=false;
float bounded(float v,float lo,float hi){return max(lo,min(hi,v));}
float headLo(){return bounded(Config::headSafeMin,0,min(270.0f,Config::headServoSpan));}
float headHi(){return bounded(Config::headSafeMax,headLo(),min(270.0f,Config::headServoSpan));}
bool regWrite(uint8_t reg,uint8_t val){Wire.beginTransmission(Config::PCA);Wire.write(reg);Wire.write(val);return Wire.endTransmission()==0;}
bool pulseWrite(uint8_t ch,uint16_t us){
 uint16_t ticks=(uint32_t)us* (Config::OSC_HZ/1000UL) / ((uint32_t)(Config::PRESCALE+1)*1000UL);
 Wire.beginTransmission(Config::PCA);Wire.write(0x06+4*ch);
 Wire.write(0);Wire.write(0);Wire.write(ticks&255);Wire.write(ticks>>8);
 return Wire.endTransmission()==0;
}
void stopOutputs(){digitalWrite(Config::enablePin,HIGH);regWrite(0xFD,0x10);enabled=false;}
void fail(){healthy=false;stopOutputs();Serial.println(F("I2C fault: cut servo power; inspect wiring and reset."));}
void setLighting(){
 const Config::Light &l=Config::lights[mode];
 lightTarget[0]=(uint16_t)l.r*l.stripBrightness/255;
 lightTarget[1]=(uint16_t)l.g*l.stripBrightness/255;
 lightTarget[2]=(uint16_t)l.b*l.stripBrightness/255;
 lightTarget[3]=l.blue; lightTarget[4]=l.white;
}
void selectMode(Mode m){mode=m;entered=phaseStart=millis();waiting=false;outward=true;headForward=true;setLighting();Serial.println(F("Mode selected."));}
void armsPose(const float *pose){for(uint8_t i=0;i<5;i++)target[i]=pose[i];}
void headScan(bool scan){
 if(!scan){target[5]=bounded(Config::headHome,headLo(),headHi());return;}
 if(headForward && pos[5]>=headHi()-0.01f)headForward=false;
 else if(!headForward && pos[5]<=headLo()+0.01f)headForward=true;
 target[5]=headForward?headHi():headLo();
}
void mode1FoldedIdle(){armsPose(Config::closed);headScan(false);}
void mode2FoldedActive(){armsPose(Config::active);headScan(Config::headScanMode2);}
void mode3FullScan(){armsPose(Config::opened);headScan(Config::headScanMode3);}
void mode35ArticulatedClick(uint32_t now){
 bool reached=true;
 for(uint8_t i=0;i<5;i++){
  if((uint32_t)(now-phaseStart)<(uint32_t)i*Config::armStaggerMs){reached=false;continue;}
  float end=Config::active[i]+bounded(Config::clickTravel,0,1)*(Config::opened[i]-Config::active[i]);
  target[i]=bounded(outward?end:Config::active[i],Config::armMin[i],Config::armMax[i]);
  if(fabs(pos[i]-target[i])>0.01f)reached=false;
 }
 if(reached){
  if(!waiting){waiting=true;pauseStart=now;}
  if((uint32_t)(now-pauseStart)>=Config::clickPauseMs){outward=!outward;phaseStart=now;waiting=false;}
 }
 headScan(Config::headScanClick);
}
void mode4OrangeAlert(uint32_t now){
 armsPose(Config::opened);headScan(true); // bounded sweep, ALWAYS reverses
 if(Config::alertArmMotion){
  bool twitch=((now-entered)/Config::alertHalfPeriodMs)&1;
  for(uint8_t i=0;i<5;i++)if(twitch)target[i]-=Config::alertTravel;
 }
}
void updateMotion(uint32_t now){
 if(!enabled)return;
 if((uint32_t)(now-lastServo)<Config::servoTickMs)return;
 lastServo=now; // cap step after a stall; never make a catch-up jump
 switch(mode){case IDLE:mode1FoldedIdle();break;case ACTIVE:mode2FoldedActive();break;case SCAN:mode3FullScan();break;case CLICK:mode35ArticulatedClick(now);break;case ALERT:mode4OrangeAlert(now);break;}
 for(uint8_t i=0;i<6;i++){
  target[i]=i<5?bounded(target[i],Config::armMin[i],Config::armMax[i]):bounded(target[i],headLo(),headHi());
  float step=(i==5?Config::headSpeed:(mode==CLICK?Config::clickSpeed:Config::poseSpeed))*Config::servoTickMs/1000.0f;
  float delta=target[i]-pos[i];pos[i]+=bounded(delta,-step,step);
  uint16_t us=i<5 ? Config::armPulseMin[i]+pos[i]/180.0f*(Config::armPulseMax[i]-Config::armPulseMin[i]) : Config::headPulseMin+pos[i]/Config::headServoSpan*(Config::headPulseMax-Config::headPulseMin);
  if(us!=lastPulse[i]){if(!pulseWrite(i,us)){fail();return;}lastPulse[i]=us;}
 }
 // Once pose is reached no new writes. Servos retain holding pulses/torque.
}
void updateLights(uint32_t now){
 if((uint32_t)(now-lastLight)<Config::lightTickMs)return;lastLight=now;
 for(uint8_t i=0;i<5;i++){
  if(lightNow[i]==lightTarget[i])continue;
  lightNow[i]+=lightNow[i]<lightTarget[i]?1:-1;
  analogWrite(Config::lightPins[i],lightNow[i]);
 }
}
void handleCommand(){
 if(!strcmp(command,"1")||!strcmp(command,"home")){selectMode(IDLE);}
 else if(!strcmp(command,"2"))selectMode(ACTIVE);
 else if(!strcmp(command,"3"))selectMode(SCAN);
 else if(!strcmp(command,"3.5"))selectMode(CLICK);
 else if(!strcmp(command,"4"))selectMode(ALERT);
 else if(!strcmp(command,"off")){stopOutputs();Serial.println(F("Signals OFF, load can fall. Re-align folded pose before arm."));}
 else if(!strcmp(command,"arm")){
  if(enabled){selectMode(IDLE);return;}
  if(!healthy){Serial.println(F("Reset after fixing I2C."));return;}
  stopOutputs();selectMode(IDLE);
  for(uint8_t i=0;i<6;i++){pos[i]=target[i]=i<5?Config::closed[i]:bounded(Config::headHome,headLo(),headHi());lastPulse[i]=0;}
  // Seed known aligned pose with OE still HIGH, then enable all at once.
  for(uint8_t i=0;i<6;i++){
   uint16_t us=i<5 ? Config::armPulseMin[i]+pos[i]/180.0f*(Config::armPulseMax[i]-Config::armPulseMin[i]) : Config::headPulseMin+pos[i]/Config::headServoSpan*(Config::headPulseMax-Config::headPulseMin);
   if(!pulseWrite(i,us)){fail();return;}lastPulse[i]=us;
  }
  digitalWrite(Config::enablePin,LOW);
  enabled=true;Serial.println(F("Enabled: assumes physically aligned folded/home pose."));
 }else Serial.println(F("Commands: arm, 1, 2, 3, 3.5, 4, home, off"));
}
void readSerial(){
 // Bounded work per loop prevents continuous serial traffic starving motion.
 for(uint8_t n=0;n<16 && Serial.available();n++){
  char c=Serial.read();
  if(c=='\r'||c=='\n'){
   if(overflow)Serial.println(F("Command too long."));
   else if(commandLen){command[commandLen]=0;handleCommand();}
   commandLen=0;overflow=false;
  }else if(!overflow){if(commandLen<sizeof(command)-1)command[commandLen++]=c;else overflow=true;}
 }
}
void setup(){
 digitalWrite(Config::enablePin,HIGH);pinMode(Config::enablePin,OUTPUT);
 Serial.begin(115200);
 for(uint8_t i=0;i<5;i++){digitalWrite(Config::lightPins[i],LOW);pinMode(Config::lightPins[i],OUTPUT);}
 Wire.begin();Wire.setClock(100000);Wire.setWireTimeout(25000,true);
 // Direct framework Wire driver; no external libraries required. PCA MODE1 AI,
 // MODE2 non-inverted totem-pole. Sleep before prescaler, wake then restart.
 healthy=regWrite(0x00,0x30) && regWrite(0x01,0x04) && regWrite(0xFD,0x10) && regWrite(0xFE,Config::PRESCALE) && regWrite(0x00,0x20);
 delayMicroseconds(600); // oscillator startup only, not an animation delay
 healthy=healthy && regWrite(0x00,0xA0);
 if(!healthy)fail();
 setLighting();
 Serial.println(F("Odradek: outputs disabled. Calibrate unloaded; align folded/home; type arm."));
 Serial.println(F("115200 baud; newline; 1 / 2 / 3 / 3.5 / 4 / home / off"));
}
void loop(){uint32_t now=millis();readSerial();updateMotion(now);updateLights(now);}

Remix this project

Make it yours in one click

Open a full copy of this project in your own Schematik workspace — diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.

Open in Schematik