4 Commits
Author SHA1 Message Date
jessikitty b2016565f7 Restart hub scans that find nothing
A Legoino scan that never locates its hub ends quietly - isConnecting()
never goes true, so the old code left 'initialised' set and never touched
retryAt. That hub then sat on a dead scan until the board was power
cycled, which is why first connects needed several attempts.

Adds scanExpiresAt per hub and restarts the scan after 12s. Also
documents the startup order and the LED grounding in the header.
2026-09-15 23:08:38 +10:00
jessikitty 0c149272bd Smoother arms, and swap head tilt/turn ports
- ARM_STEP_DEG 8 -> 3 and a dedicated ARM_MIN_GAP_MS of 60, so the arm
  tracks the trigger instead of jumping between targets. Speed and spans
  left as they were.
- hub1 port B is the tilt motor, C is the turn motor - swapped from what
  the first build assumed.
- Zero the arm encoders once per boot rather than on every reconnect. The
  hub keeps its preset, and re-zeroing mid-session would redefine zero at
  whatever position the arms were in when the link dropped.
2026-09-11 12:34:56 +10:00
jessikitty 343544510f Position-controlled arms
Trigger position now sets arm ANGLE rather than power, via
setAbsoluteMotorPosition with BrakingStyle::HOLD. Encoders are zeroed on
hub1 connect, so both arms must be down at that moment.

Spans measured with tools/arm_calibrate: roughly +280 left, -275 right,
both comfortably under one motor revolution so the absolute encoder is
unambiguous. Set to +/-250 to keep backlash clear of the end stop.

L3/R3 are no longer bound - releasing the trigger is lowering.
Also documents the async-init trap in serviceHub.
2026-09-10 21:31:30 +10:00
jessikitty f5e7517cbd Evolved control scheme
- Correct hub1 port map: A is the left arm, C is the head tilt.
- Arms: L2/R2 raise proportionally, L3/R3 lower at reduced power.
- Head turn onto L1/R1, head tilt onto d-pad left/right, body lift onto
  d-pad up/down. All move while held.
- Square is now the panic stop; Cross toggles 50/100% track speed.
- Circle and Triangle toggle two GPIO LED circuits on pins 25 and 26.
- Latching controls are edge-detected; frames arrive at ~50Hz so a held
  button would otherwise toggle continuously.
2026-09-10 16:20:44 +10:00
+228 -72
View File
@@ -1,32 +1,49 @@
/* /*
* receiver.ino -- UART -> two LEGO Powered Up hubs (ESP32 "B") * receiver.ino -- UART -> two LEGO Powered Up hubs (ESP32 "B")
* *
* Model: Johnny 5 (Short Circuit) MOC, 7 motors across 2 Technic hubs. * Model: Johnny 5 (Short Circuit) MOC - "Evolved" control scheme.
* 7 motors across 2 Technic hubs, plus 2 GPIO-driven LED circuits.
* *
* Board package: esp32 (the normal Espressif one), core 2.0.17 * Board package: esp32 (the normal Espressif one), core 2.0.17
* Libraries: Legoino + NimBLE-Arduino 1.4.x (both via Library Manager) * Libraries: Legoino + NimBLE-Arduino 1.4.x (both via Library Manager)
* *
* Core 3.x will not build Legoino - you get 'std::string does not name a type' * Core 3.x will not build Legoino - you get 'std::string does not name a type'
* and a ReadUInt32LE declaration mismatch. Stay on 2.0.17 for this board. * and a ReadUInt32LE declaration mismatch. Stay on 2.0.17 for this board.
* * Do NOT select the esp32_bluepad32 package here either: it starts BTstack
* Listens for gamepad frames from the Bluepad32 board on Serial2 and drives * before setup() runs and NimBLE aborts with ESP_ERR_INVALID_STATE.
* both hubs over BLE using Legoino. This board must NOT have the Bluepad32
* board package selected - keeping BTstack and NimBLE on separate chips is
* the entire reason there are two boards.
* *
* Wiring to the transmitter board: * Wiring to the transmitter board:
* RX GPIO16 <- TX GPIO17 on transmitter * RX GPIO16 <- TX GPIO17 on transmitter
* TX GPIO17 -> RX GPIO16 on transmitter * TX GPIO17 -> RX GPIO16 on transmitter
* GND -> GND (mandatory - common ground) * GND -> GND (mandatory - common ground)
* *
* Control scheme is tank drive: every input drives exactly one motor. * LED circuits - both on THIS board, returning to THIS board's GND:
* GPIO25 -> resistor -> LED pair -> GND
* GPIO26 -> resistor -> LED pair -> GND
* Pins output 3.3V, not 3V. ~12mA per pin is comfortable, 40mA is the hard
* limit. Anything drawing more than ~20mA per circuit needs a transistor.
* Do not tap the LED return off the UART ground wire to the other board -
* that reference needs to stay clean.
*
* STARTUP ORDER:
* Wake BOTH hubs with their green buttons and check both are blinking,
* THEN power this board. hub1 is only serviced once hub0 is connected, so a
* sleeping hub0 blocks the whole sequence. Hubs stop advertising after a
* couple of minutes idle.
*
* ARMS ARE POSITION CONTROLLED:
* The triggers set an ANGLE, not a power level. Trigger released holds the
* arm at 0, fully depressed holds it at ARM_SPAN_*. The encoders are zeroed
* once, on the first hub1 connect after boot, so BOTH ARMS MUST BE DOWN at
* that moment. They are deliberately NOT re-zeroed on a reconnect - the hub
* keeps its encoder preset, and re-zeroing mid-session would redefine zero
* at whatever position the arms happened to be in.
* *
* FILE ORDER MATTERS: * FILE ORDER MATTERS:
* The Arduino IDE injects generated function prototypes immediately before * The Arduino IDE injects generated function prototypes immediately before
* the FIRST function definition in the file. Any type used in a function * the FIRST function definition in the file. Any type used in a function
* signature must be declared above that point - which is why HubLink, * signature must be declared above that point - which is why HubLink,
* PortState and Frame all live in the types block. Move a function above * PortState, ArmState and Frame all live in the types block.
* them and you get "'Frame' has not been declared".
* *
* Created by: Jess Rogerson (yelling commands at Claude.AI) * Created by: Jess Rogerson (yelling commands at Claude.AI)
*/ */
@@ -38,55 +55,64 @@
// Hub BLE addresses. Run tools/hub_scanner to find them - do not guess. // Hub BLE addresses. Run tools/hub_scanner to find them - do not guess.
// //
// hub 0 - lower body hub 1 - upper body // hub 0 - lower body hub 1 - upper body
// A right track A head tilt // A right track A left arm
// B left track B head turn // B left track B head tilt
// D body lift C left arm // C (free) C head turn
// D right arm // D body lift D right arm
static const char *HUB0_ADDR = "90:84:2b:61:f2:d7"; static const char *HUB0_ADDR = "90:84:2b:61:f2:d7";
static const char *HUB1_ADDR = "90:84:2b:61:e6:8c"; static const char *HUB1_ADDR = "90:84:2b:61:e6:8c";
// Port numbers are just bytes in the LEGO protocol, same on every hub type.
static const byte PORT_A = 0x00; static const byte PORT_A = 0x00;
static const byte PORT_B = 0x01; static const byte PORT_B = 0x01;
static const byte PORT_C = 0x02; static const byte PORT_C = 0x02;
static const byte PORT_D = 0x03; static const byte PORT_D = 0x03;
// Technic / Control+ motors are tacho motors -> leave this at 1. // LED circuits. Safe GPIOs - no boot strapping or flash duties, unlike 0, 2,
// Plain train motors and the simple Powered Up motors -> set it to 0. // 12 and 15.
static const int LED_1_PIN = 25; // Circle toggles this pair
static const int LED_2_PIN = 26; // Triangle toggles this pair
#define USE_TACHO_MOTORS 1 #define USE_TACHO_MOTORS 1
// Set to 1 to log every motor command that goes out. Useful for proving which // Set to 1 to log every command that goes out. Noisy - turn it back off.
// port a command actually lands on. Noisy - turn it back off afterwards.
#define DEBUG_MOTORS 0 #define DEBUG_MOTORS 0
static const int DEADZONE = 40; // raw stick counts ignored around centre static const int DEADZONE = 40; // raw stick counts ignored around centre
// Motor directions. Flip to -1 if an axis runs backwards - mirrored mountings // Motor directions. Flip to -1 if an axis runs backwards.
// are normal on a symmetric model, and this is cheaper than editing signs
// scattered through applyFrame().
static const int DIR_LEFT_TRACK = 1; static const int DIR_LEFT_TRACK = 1;
static const int DIR_RIGHT_TRACK = -1; static const int DIR_RIGHT_TRACK = -1;
static const int DIR_BODY_LIFT = 1; static const int DIR_BODY_LIFT = 1;
static const int DIR_HEAD_TILT = 1; static const int DIR_HEAD_TILT = 1;
static const int DIR_HEAD_TURN = 1; static const int DIR_HEAD_TURN = 1;
static const int DIR_LEFT_ARM = 1;
static const int DIR_RIGHT_ARM = 1;
// Per-axis power caps. Everything except the tracks runs into a mechanical end // Arm travel in MOTOR degrees, measured with tools/arm_calibrate.
// stop and there is no position feedback, so holding a direction at a stop // Raw measurements were roughly +280 (left) and -275 (right). These are set
// stalls the motor. Lower these if an axis feels forceful. // slightly short so backlash cannot stall the motor against the top stop.
// The sign carries the direction - there is no DIR_ constant for the arms.
static const int32_t ARM_SPAN_LEFT = 250;
static const int32_t ARM_SPAN_RIGHT = -250;
static const int ARM_SPEED = 60; // how fast it travels to the target
static const byte ARM_MAX_POWER = 40; // torque cap - keeps a jam survivable
static const int ARM_STEP_DEG = 3; // ignore target changes smaller than this
// Arms get their own command interval. They are position controlled, so the
// target moves continuously with the trigger and needs updating more often
// than a velocity axis does - at the per-port 100ms the arm sprints to each
// target then sits idle, which feels like stepping.
static const unsigned long ARM_MIN_GAP_MS = 60;
// Per-axis power caps for the velocity-controlled axes.
static const int TRACK_MAX = 100; // LEGO speed range is -100..100 static const int TRACK_MAX = 100; // LEGO speed range is -100..100
static const int HEAD_MAX = 45; static const int HEAD_MAX = 45;
static const int LIFT_MAX = 60; static const int LIFT_MAX = 60;
static const int ARM_MAX = 45;
// Track scaling: normal, L1 held (precision), R1 held (full). // Track speed, toggled by Cross.
static const int SCALE_NORMAL = 75; static const int SPEED_SLOW = 50;
static const int SCALE_PRECISION = 40; static const int SPEED_FAST = 100;
static const int SCALE_FULL = 100;
// Bluepad32 button masks. Verify against your own pad with DEBUG_BUTTONS in // Bluepad32 button masks. Verify with DEBUG_BUTTONS in the transmitter.
// the transmitter sketch if any of these seem wrong.
static const unsigned BTN_A = 0x0001; // Cross static const unsigned BTN_A = 0x0001; // Cross
static const unsigned BTN_B = 0x0002; // Circle static const unsigned BTN_B = 0x0002; // Circle
static const unsigned BTN_X = 0x0004; // Square static const unsigned BTN_X = 0x0004; // Square
@@ -99,11 +125,18 @@ static const unsigned DPAD_D = 0x02;
static const unsigned DPAD_R = 0x04; static const unsigned DPAD_R = 0x04;
static const unsigned DPAD_L = 0x08; static const unsigned DPAD_L = 0x08;
// Triggers rest at 0 and are noisy near the bottom of their travel.
static const int TRIGGER_DEADZONE = 60;
static const unsigned long MOTOR_MIN_GAP_MS = 100; // per port static const unsigned long MOTOR_MIN_GAP_MS = 100; // per port
static const unsigned long HUB_MIN_GAP_MS = 25; // per hub, ~40 cmd/sec static const unsigned long HUB_MIN_GAP_MS = 25; // per hub, ~40 cmd/sec
static const unsigned long LINK_TIMEOUT_MS = 400; // failsafe static const unsigned long LINK_TIMEOUT_MS = 400; // failsafe
static const unsigned long RECONNECT_GAP_MS = 2000; static const unsigned long RECONNECT_GAP_MS = 2000;
// A Legoino scan that never finds its hub expires silently, leaving the hub
// stuck waiting forever. This is how long we give it before starting over.
static const unsigned long SCAN_TIMEOUT_MS = 12000;
static const int LINK_RX_PIN = 16; static const int LINK_RX_PIN = 16;
static const int LINK_TX_PIN = 17; static const int LINK_TX_PIN = 17;
static const long LINK_BAUD = 115200; static const long LINK_BAUD = 115200;
@@ -118,7 +151,8 @@ struct HubLink {
const char *label; const char *label;
bool initialised; bool initialised;
unsigned long retryAt; unsigned long retryAt;
unsigned long lastCmdAt; // per-hub rate limit, shared across its ports unsigned long lastCmdAt; // per-hub rate limit, shared across ports
unsigned long scanExpiresAt; // when to give up on the current scan
}; };
struct PortState { struct PortState {
@@ -126,6 +160,11 @@ struct PortState {
unsigned long lastSentAt; unsigned long lastSentAt;
}; };
struct ArmState {
int32_t lastTarget;
unsigned long lastSentAt;
};
struct Frame { struct Frame {
int lx, ly, rx, ry; int lx, ly, rx, ry;
unsigned buttons, dpad; unsigned buttons, dpad;
@@ -135,18 +174,28 @@ struct Frame {
// ================================================================= globals // ================================================================= globals
static HubLink gHubs[2] = { static HubLink gHubs[2] = {
{Lpf2Hub(), HUB0_ADDR, "hub0", false, 0, 0}, {Lpf2Hub(), HUB0_ADDR, "hub0", false, 0, 0, 0},
{Lpf2Hub(), HUB1_ADDR, "hub1", false, 0, 0}, {Lpf2Hub(), HUB1_ADDR, "hub1", false, 0, 0, 0},
}; };
// Indexes: 0 rightTrack, 1 leftTrack, 2 bodyLift, // Indexes follow physical ports, not functions:
// 3 headTilt, 4 headTurn, 5 leftArm, 6 rightArm // 0 hub0/A 1 hub0/B 2 hub0/D 3 hub1/B 4 hub1/C
static PortState gPort[7] = {{999, 0}, {999, 0}, {999, 0}, {999, 0}, static PortState gPort[5] = {{999, 0}, {999, 0}, {999, 0}, {999, 0}, {999, 0}};
{999, 0}, {999, 0}, {999, 0}};
// Arms are position controlled, so they get their own state.
static ArmState gArmLeft = {INT32_MIN, 0};
static ArmState gArmRight = {INT32_MIN, 0};
static bool gArmsZeroed = false;
static unsigned long lastFrameAt = 0; static unsigned long lastFrameAt = 0;
static bool failsafeEngaged = true; static bool failsafeEngaged = true;
// Latched state, changed on button press rather than while held.
static int gTrackSpeed = SPEED_SLOW;
static bool gLed1On = false;
static bool gLed2On = false;
static unsigned gPrevButtons = 0;
// ================================================================= helpers // ================================================================= helpers
// Deadzone, then rescale so the remaining travel still reaches full speed. // Deadzone, then rescale so the remaining travel still reaches full speed.
@@ -159,6 +208,15 @@ static int stickToSpeed(int raw, int maxSpeed) {
return sign * (int)scaled; return sign * (int)scaled;
} }
// Analog trigger, 0..1023, to a target angle between 0 and span.
static int32_t triggerToAngle(int raw, int32_t span) {
if (raw <= TRIGGER_DEADZONE) return 0;
long travel = (long)raw - TRIGGER_DEADZONE;
long full = 1023L - TRIGGER_DEADZONE;
if (travel > full) travel = full;
return (int32_t)((travel * span) / full);
}
static void driveMotor(HubLink &hl, byte port, int speed, PortState &st) { static void driveMotor(HubLink &hl, byte port, int speed, PortState &st) {
if (!hl.hub.isConnected()) return; if (!hl.hub.isConnected()) return;
@@ -166,8 +224,8 @@ static void driveMotor(HubLink &hl, byte port, int speed, PortState &st) {
bool stopping = (speed == 0 && st.lastSpeed != 0); bool stopping = (speed == 0 && st.lastSpeed != 0);
// Stops always go out immediately. Everything else is rate limited twice: // Stops always go out immediately. Everything else is rate limited twice:
// per port, and per hub - hub 1 has four motors on it, and the per-port // per port, and per hub - the per-port limit alone lets through more than
// limit alone lets through more than the hub will swallow. // a hub with several motors on it will swallow.
if (!stopping) { if (!stopping) {
if (speed == st.lastSpeed) return; if (speed == st.lastSpeed) return;
if ((now - st.lastSentAt) < MOTOR_MIN_GAP_MS) return; if ((now - st.lastSentAt) < MOTOR_MIN_GAP_MS) return;
@@ -189,18 +247,56 @@ static void driveMotor(HubLink &hl, byte port, int speed, PortState &st) {
hl.lastCmdAt = now; hl.lastCmdAt = now;
} }
// Position control. HOLD keeps the motor actively at the target rather than
// letting gravity drag the arm back down.
static void driveArm(HubLink &hl, byte port, int32_t target, ArmState &st) {
if (!hl.hub.isConnected() || !gArmsZeroed) return;
unsigned long now = millis();
if (labs((long)target - (long)st.lastTarget) < ARM_STEP_DEG) return;
if ((now - st.lastSentAt) < ARM_MIN_GAP_MS) return;
if ((now - hl.lastCmdAt) < HUB_MIN_GAP_MS) return;
#if DEBUG_MOTORS
Serial.printf("TX %s port %u angle %ld\n", hl.label, port, (long)target);
#endif
hl.hub.setAbsoluteMotorPosition(port, ARM_SPEED, target, ARM_MAX_POWER,
BrakingStyle::HOLD);
st.lastTarget = target;
st.lastSentAt = now;
hl.lastCmdAt = now;
}
static void stopEverything() { static void stopEverything() {
driveMotor(gHubs[0], PORT_A, 0, gPort[0]); driveMotor(gHubs[0], PORT_A, 0, gPort[0]);
driveMotor(gHubs[0], PORT_B, 0, gPort[1]); driveMotor(gHubs[0], PORT_B, 0, gPort[1]);
driveMotor(gHubs[0], PORT_D, 0, gPort[2]); driveMotor(gHubs[0], PORT_D, 0, gPort[2]);
driveMotor(gHubs[1], PORT_A, 0, gPort[3]); driveMotor(gHubs[1], PORT_B, 0, gPort[3]);
driveMotor(gHubs[1], PORT_B, 0, gPort[4]); driveMotor(gHubs[1], PORT_C, 0, gPort[4]);
driveMotor(gHubs[1], PORT_C, 0, gPort[5]);
driveMotor(gHubs[1], PORT_D, 0, gPort[6]); // Arms: a plain speed command overrides the position hold and goes limp.
// Reset the cached targets so the next trigger movement re-commands.
if (gHubs[1].hub.isConnected()) {
gHubs[1].hub.setTachoMotorSpeed(PORT_A, 0);
gHubs[1].hub.setTachoMotorSpeed(PORT_D, 0);
}
gArmLeft.lastTarget = INT32_MIN;
gArmRight.lastTarget = INT32_MIN;
} }
// Connect the hubs one at a time. Kicking off two scans at once upsets the // Connect the hubs one at a time. Kicking off two scans at once upsets the
// shared NimBLE scanner and you end up with one hub connected and one sulking. // shared NimBLE scanner and you end up with one hub connected and one sulking.
//
// Note the 'initialised' one-shot. init() starts an ASYNCHRONOUS scan, so
// immediately afterwards isConnected() and isConnecting() are both still
// false. Guarding on those alone re-enters NimBLEDevice::init() thousands of
// times a second and the Bluetooth controller aborts.
//
// The scanExpiresAt deadline exists because a scan that finds nothing just
// ends quietly - isConnecting() never goes true, so without a timeout the hub
// sits on a dead scan until the board is power cycled.
static void serviceHub(HubLink &hl) { static void serviceHub(HubLink &hl) {
if (hl.hub.isConnected()) return; if (hl.hub.isConnected()) return;
@@ -217,13 +313,40 @@ static void serviceHub(HubLink &hl) {
return; return;
} }
if (hl.initialised && millis() >= hl.scanExpiresAt) {
Serial.printf("[%s] scan timed out, restarting\n", hl.label);
hl.initialised = false;
hl.retryAt = millis() + RECONNECT_GAP_MS;
return;
}
if (!hl.initialised && millis() >= hl.retryAt) { if (!hl.initialised && millis() >= hl.retryAt) {
Serial.printf("[%s] scanning for %s\n", hl.label, hl.addr); Serial.printf("[%s] scanning for %s\n", hl.label, hl.addr);
hl.hub.init(std::string(hl.addr)); hl.hub.init(std::string(hl.addr));
hl.initialised = true; hl.initialised = true;
hl.scanExpiresAt = millis() + SCAN_TIMEOUT_MS;
} }
} }
// Define "arms down" as angle zero. Runs once per boot, after hub1 connects,
// with the arms physically at the bottom of their travel. Not repeated on a
// reconnect: the hub keeps its encoder preset, and re-zeroing mid-session
// would redefine zero wherever the arms happened to be sitting.
static void zeroArms() {
if (gArmsZeroed || !gHubs[1].hub.isConnected()) return;
delay(500); // let the hub finish reporting its ports
gHubs[1].hub.setAbsoluteMotorEncoderPosition(PORT_A, 0);
delay(200);
gHubs[1].hub.setAbsoluteMotorEncoderPosition(PORT_D, 0);
delay(200);
gArmsZeroed = true;
gArmLeft.lastTarget = INT32_MIN;
gArmRight.lastTarget = INT32_MIN;
Serial.println("Arms zeroed at current position");
}
static uint8_t xorChecksum(const char *s, size_t len) { static uint8_t xorChecksum(const char *s, size_t len) {
uint8_t c = 0; uint8_t c = 0;
for (size_t i = 0; i < len; i++) c ^= (uint8_t)s[i]; for (size_t i = 0; i < len; i++) c ^= (uint8_t)s[i];
@@ -244,44 +367,68 @@ static bool parseFrame(char *line, Frame &f) {
&f.buttons, &f.dpad, &f.l2, &f.r2) == 8; &f.buttons, &f.dpad, &f.l2, &f.r2) == 8;
} }
// Latching controls fire once per press, not continuously while held. Frames
// arrive at ~50 Hz, so without edge detection a single press would toggle
// twenty times.
static void handleLatchingButtons(unsigned buttons) {
unsigned pressed = buttons & ~gPrevButtons;
gPrevButtons = buttons;
if (pressed & BTN_A) { // Cross - alternate track speed
gTrackSpeed = (gTrackSpeed == SPEED_FAST) ? SPEED_SLOW : SPEED_FAST;
Serial.printf("track speed %d%%\n", gTrackSpeed);
}
if (pressed & BTN_B) { // Circle - LED pair 1
gLed1On = !gLed1On;
digitalWrite(LED_1_PIN, gLed1On ? HIGH : LOW);
}
if (pressed & BTN_Y) { // Triangle - LED pair 2
gLed2On = !gLed2On;
digitalWrite(LED_2_PIN, gLed2On ? HIGH : LOW);
}
}
// Tank drive. One input per motor - nothing is mixed. // Tank drive. One input per motor - nothing is mixed.
static void applyFrame(const Frame &f) { static void applyFrame(const Frame &f) {
// Both shoulders together is the panic stop. handleLatchingButtons(f.buttons);
if ((f.buttons & BTN_L1) && (f.buttons & BTN_R1)) {
// Square is the panic stop. LEDs are left alone - they are not motion.
if (f.buttons & BTN_X) {
stopEverything(); stopEverything();
return; return;
} }
int scale = SCALE_NORMAL; int leftTrack = stickToSpeed(-f.ly, TRACK_MAX) * gTrackSpeed / 100
if (f.buttons & BTN_L1) scale = SCALE_PRECISION; * DIR_LEFT_TRACK;
if (f.buttons & BTN_R1) scale = SCALE_FULL; int rightTrack = stickToSpeed(-f.ry, TRACK_MAX) * gTrackSpeed / 100
* DIR_RIGHT_TRACK;
int leftTrack = stickToSpeed(-f.ly, TRACK_MAX) * scale / 100 * DIR_LEFT_TRACK; // L1 / R1 turn the head while held.
int rightTrack = stickToSpeed(-f.ry, TRACK_MAX) * scale / 100 * DIR_RIGHT_TRACK; int headTurn = ((f.buttons & BTN_R1) ? HEAD_MAX
: (f.buttons & BTN_L1) ? -HEAD_MAX : 0) * DIR_HEAD_TURN;
int headTilt = ((f.dpad & DPAD_U) ? HEAD_MAX // D-pad: up/down lifts the body, left/right tilts the head.
: (f.dpad & DPAD_D) ? -HEAD_MAX : 0) * DIR_HEAD_TILT; int bodyLift = ((f.dpad & DPAD_U) ? LIFT_MAX
int headTurn = ((f.dpad & DPAD_R) ? HEAD_MAX : (f.dpad & DPAD_D) ? -LIFT_MAX : 0) * DIR_BODY_LIFT;
: (f.dpad & DPAD_L) ? -HEAD_MAX : 0) * DIR_HEAD_TURN; int headTilt = ((f.dpad & DPAD_R) ? HEAD_MAX
: (f.dpad & DPAD_L) ? -HEAD_MAX : 0) * DIR_HEAD_TILT;
// R2 raises, L2 lowers. Both analog 0..1023, so the lift stays proportional. // Arms: trigger position IS arm angle. Released means "go to zero", which
int bodyLift = constrain((f.r2 - f.l2) * LIFT_MAX / 1023, // gravity is already doing, so the motor mostly just catches it.
-LIFT_MAX, LIFT_MAX) * DIR_BODY_LIFT; int32_t leftTarget = triggerToAngle(f.l2, ARM_SPAN_LEFT);
int32_t rightTarget = triggerToAngle(f.r2, ARM_SPAN_RIGHT);
// Square raises the left arm, Circle lowers it.
int leftArm = ((f.buttons & BTN_X) ? ARM_MAX
: (f.buttons & BTN_B) ? -ARM_MAX : 0) * DIR_LEFT_ARM;
// Cross raises the right arm, Triangle lowers it.
int rightArm = ((f.buttons & BTN_A) ? ARM_MAX
: (f.buttons & BTN_Y) ? -ARM_MAX : 0) * DIR_RIGHT_ARM;
driveMotor(gHubs[0], PORT_A, rightTrack, gPort[0]); driveMotor(gHubs[0], PORT_A, rightTrack, gPort[0]);
driveMotor(gHubs[0], PORT_B, leftTrack, gPort[1]); driveMotor(gHubs[0], PORT_B, leftTrack, gPort[1]);
driveMotor(gHubs[0], PORT_D, bodyLift, gPort[2]); driveMotor(gHubs[0], PORT_D, bodyLift, gPort[2]);
driveMotor(gHubs[1], PORT_A, headTilt, gPort[3]);
driveMotor(gHubs[1], PORT_B, headTurn, gPort[4]); // hub1 B is the tilt motor and C is the turn motor - the reverse of what
driveMotor(gHubs[1], PORT_C, leftArm, gPort[5]); // the first build assumed.
driveMotor(gHubs[1], PORT_D, rightArm, gPort[6]); driveMotor(gHubs[1], PORT_B, headTilt, gPort[3]);
driveMotor(gHubs[1], PORT_C, headTurn, gPort[4]);
driveArm(gHubs[1], PORT_A, leftTarget, gArmLeft);
driveArm(gHubs[1], PORT_D, rightTarget, gArmRight);
} }
// ==================================================================== main // ==================================================================== main
@@ -293,7 +440,13 @@ void setup() {
pinMode(STATUS_LED_PIN, OUTPUT); pinMode(STATUS_LED_PIN, OUTPUT);
digitalWrite(STATUS_LED_PIN, LOW); digitalWrite(STATUS_LED_PIN, LOW);
Serial.println("LEGO hub receiver starting"); pinMode(LED_1_PIN, OUTPUT);
pinMode(LED_2_PIN, OUTPUT);
digitalWrite(LED_1_PIN, LOW);
digitalWrite(LED_2_PIN, LOW);
Serial.println("LEGO hub receiver starting (Johnny 5 Evolved)");
Serial.println("Wake both hubs first. Both arms must be DOWN.");
} }
void loop() { void loop() {
@@ -301,6 +454,9 @@ void loop() {
serviceHub(gHubs[0]); serviceHub(gHubs[0]);
if (gHubs[0].hub.isConnected()) serviceHub(gHubs[1]); if (gHubs[0].hub.isConnected()) serviceHub(gHubs[1]);
// Zeroes once per boot. Deliberately not reset when the hub drops.
if (gHubs[1].hub.isConnected()) zeroArms();
bool ready = gHubs[0].hub.isConnected() && gHubs[1].hub.isConnected(); bool ready = gHubs[0].hub.isConnected() && gHubs[1].hub.isConnected();
digitalWrite(STATUS_LED_PIN, ready ? HIGH : LOW); digitalWrite(STATUS_LED_PIN, ready ? HIGH : LOW);