#include #include "feetech.h" #include "animation.h" #include "nodegraph.h" #include "RobotConfig.h" RobotConfig config; #define DEVICE_NAME "Little Sophia" #define FIRMWARE_VERSION "0.0.1" #define HEADER1 0xAA #define HEADER2 0x55 #define MAX_PAYLOAD_SIZE 6000 // 10 KB uint8_t payload[MAX_PAYLOAD_SIZE]; // Global or static #define CMD_ID_REQUEST 0x01 #define CMD_FILE_LIST 0x02 #define CMD_LOAD_FILE 0x03 #define CMD_DELETE_FILE 0x04 #define CMD_SAVE_FILE 0x05 #define CMD_MESSAGE 0x06 #define CMD_SET_POSITION 0x07 #define CMD_PLAY_FILE 0x08 #define CMD_SCAN_CHANNEL 0x09 #define CMD_WRITE_DATA 0x10 #define CMD_WRITE_CONFIG_UPDATE 0x12 #define CMD_START_POSITION_STREAM 0x14 #define POSITION_STREAM 0x15 // ESP32 S2 PINOUT #define CH0_RX_PIN 13 #define CH0_TX_PIN 12 // #define RX_PIN 18 // RO // #define TX_PIN 17 // DI #define CH1_RX_PIN 11 // RO #define CH1_TX_PIN 10 // DI #define DE_PIN 7 // Driver Enable #define RE_PIN 8 // Receiver Enable #define PLAY_IDLE 0x00 #define PLAY_ONCE 0x01 #define PLAY_LOOP 0x02 #define PLAY_REPEAT 0x03 uint8_t playMode = PLAY_IDLE; uint8_t repeatsRemaining = 0; Animation anim; Animation* currentAnimation; bool streamPositions = false; unsigned long lastStreamPositions = 0; Feetech* servos[2]; uint16_t flipBytes(uint16_t value) { return (value >> 8) | (value << 8); } uint8_t ids[NUM_CHANNELS] = { 10, 11, 12, 13, 14 }; uint16_t pos1[] = { 0, 0, 0, 0, 0 }; uint16_t pos2[] = { 1023, 1023, 1023, 1023, 1023 }; uint8_t idsSTS[NUM_CHANNELS] = { 15, 103 }; uint16_t pos1STS[] = { 0, 0 }; uint16_t pos2STS[] = { 1023, 1023 }; #define WAVE_PERIOD_CS 400 // 4 seconds = 400 centiseconds #define WAVE_MAX 4095 uint16_t getSineWaveValue(unsigned long centiseconds) { float theta = (2.0 * PI * centiseconds) / WAVE_PERIOD_CS; float sine = sin(theta); // ranges from -1 to +1 float scaled = (sine + 1.0) * (WAVE_MAX / 2.0); // scale to 0–4095 return (uint16_t)round(scaled); } void setup() { Serial.begin(1000000); Serial.setRxBufferSize(1024); for (int i = 0; i < 5; i++) { Serial.println(i); delay(500); } // config.deviceName = "Little Elephant"; // config.firmwareVersion.major = 0; // config.firmwareVersion.minor = 2; // Add a few motors // config.motors.push_back({ "Ankle Left", 10, 2048 }); // config.motors.push_back({ "Ankle Right", 11, 2048 }); // config.motors.push_back({ "Ankle Center", 12, 2048 }); // config.motors.push_back({ "Knee", 13, 2048 }); // config.motors.push_back({ "Upper Leg", 14, 2048 }); pinMode(6, INPUT); pinMode(7, INPUT); servos[0] = new Feetech(Serial1, DE_PIN, RE_PIN, CH0_TX_PIN, CH0_RX_PIN); // SCS servos[0]->setFeetechMode(Feetech::MODE_SCS); servos[1] = new Feetech(Serial2, DE_PIN, RE_PIN, CH1_TX_PIN, CH1_RX_PIN); // STS servos[1]->setFeetechMode(Feetech::MODE_SCS); // pinMode(RX_PIN, OUTPUT); // pinMode(TX_PIN, OUTPUT); // pinMode(DE_PIN, OUTPUT); // while (true){ // Serial.println(!digitalRead(RX_PIN)); // digitalWrite(RX_PIN, !digitalRead(RX_PIN)); // digitalWrite(TX_PIN, !digitalRead(TX_PIN)); // digitalWrite(DE_PIN, !digitalRead(DE_PIN)); // delay(2000); // } pos2[3] = flipBytes(pos2[3]); servos[0]->begin(); servos[1]->begin(); if (!FFat.begin(true)) { Serial.println("FFat mount failed"); return; } else { Serial.println("FFat mount successful"); } if (config.loadOrCreateDefault()) { Serial.println("Robot config ready."); } else { Serial.println("Failed to initialize robot config."); } // Print serialized config Serial.println(config.serializeJSON()); std::vector bytes = config.serializeToBytes(); String output = ""; for (size_t i = 0; i < bytes.size(); ++i) { if (bytes[i] < 16) output += "0"; // pad single-digit hex output += String(bytes[i], HEX); if (i < bytes.size() - 1) output += " "; } Serial.println(output); // PLAY ANIMATION // anim.loadFromFile("/aa.anim"); // currentAnimation = &anim; // currentAnimation->setActive(true); // playMode = PLAY_ONCE; //Serial.println(anim.header.frameCount); //Serial.println(anim.printCurves()); // Serial.println("Model"); // Serial.println(servos[0]->getModel(10)); // delay(20); // Serial.println(servos[0]->getModel(11)); // delay(50); // Serial.println(servos[0]->getModel(12)); // Serial.println(servos[0]->getModel(17)); // Serial.println(servos[0]->getModel(14)); } void SetID(uint8_t oldID, uint8_t newID) { Serial.println("Setting Lock to 0"); Serial.println(servos[1]->setLock(oldID, 0)); delay(1000); Serial.print("Changing ID "); Serial.print(oldID); Serial.print(" to "); Serial.println(newID); Serial.println(servos[1]->setID(oldID, newID)); delay(1000); Serial.println("Setting Lock to 1"); Serial.println(servos[1]->setLock(newID, 1)); delay(1000); } void HandleSerialRequests() { if (Serial.available() >= 6) { // 2 headers + 1 command + 2 length + 1 checksum minimum if (Serial.read() == HEADER1 && Serial.read() == HEADER2) { uint8_t command = Serial.read(); uint16_t length = (Serial.read() << 8) | Serial.read(); if (length > MAX_PAYLOAD_SIZE) { Serial.println("Payload too large"); while (Serial.available()) Serial.read(); // flush junk return; } unsigned long start = millis(); while (Serial.available() < length + 1) { if (millis() - start > 1000) { // 1 second timeout Serial.println("Serial timeout"); return; } } uint8_t checksum = command ^ (length >> 8) ^ (length & 0xFF); for (uint16_t i = 0; i < length; i++) { payload[i] = Serial.read(); checksum ^= payload[i]; } uint8_t receivedChecksum = Serial.read(); if (checksum == receivedChecksum) { handleCommand(command, payload, length); } else { Serial.println("Checksum mismatch"); } } } } void sendMessage(const String& payload, uint8_t command = CMD_MESSAGE) { uint16_t length = payload.length(); uint8_t checksum = CMD_MESSAGE ^ (length >> 8) ^ (length & 0xFF); for (int i = 0; i < length; i++) { checksum ^= payload[i]; } Serial.write(HEADER1); Serial.write(HEADER2); Serial.write(command); Serial.write((length >> 8) & 0xFF); Serial.write(length & 0xFF); Serial.write((const uint8_t*)payload.c_str(), length); Serial.write(checksum); } void sendMessage(const uint8_t* payload, uint16_t length, uint8_t command = CMD_MESSAGE) { uint8_t checksum = command ^ (length >> 8) ^ (length & 0xFF); for (uint16_t i = 0; i < length; i++) { checksum ^= payload[i]; } Serial.write(HEADER1); Serial.write(HEADER2); Serial.write(command); Serial.write((length >> 8) & 0xFF); Serial.write(length & 0xFF); Serial.write(payload, length); Serial.write(checksum); } void handleCommand(uint8_t command, const uint8_t* payload, uint16_t length) { switch (command) { case CMD_ID_REQUEST: // CMD_ID_REQUEST handleIdRequest(); break; case CMD_FILE_LIST: // handleFileList(); break; case CMD_LOAD_FILE: // handleLoadFile(payload, length); break; case CMD_DELETE_FILE: // handleDeleteFile(payload, length); break; case CMD_SAVE_FILE: handleSaveFile(payload, length); break; case CMD_SET_POSITION: handleSetPosition(payload, length); break; case CMD_PLAY_FILE: handlePlayAnimation(payload, length); break; case CMD_SCAN_CHANNEL: handleScanChannel(payload, length); break; case CMD_WRITE_DATA: handleDataWrite(payload, length); break; case CMD_START_POSITION_STREAM: startPositionStream(payload, length); break; case CMD_WRITE_CONFIG_UPDATE: handleConfigUpdate(payload, length); break; default: Serial.print("Unknown command: "); Serial.println(command, HEX); break; } } void startPositionStream(const uint8_t* payload, uint16_t length) { bool start = payload[0] != 0; for (const Motor& motor : config.motors) { servos[payload[0]]->setFeetechMode(motor.servoModel.major); // put feetech interface in correct mode servos[0]->disableTorque(motor.motorID); servos[0]->waitOnData1Byte(10); } streamPositions = start; } void handleConfigUpdate(const uint8_t* payload, uint16_t length) { RobotConfig newConfig; uint16_t offset = 0; // 🔹 Decode robot name uint8_t nameLength = payload[offset++]; newConfig.deviceName = ""; for (uint8_t i = 0; i < nameLength && offset < length; ++i) { newConfig.deviceName += (char)payload[offset++]; } // 🔹 Decode firmware version if (offset + 2 > length) return; newConfig.firmwareVersion.major = payload[offset++]; newConfig.firmwareVersion.minor = payload[offset++]; // 🔹 Decode motor count if (offset >= length) return; uint8_t motorCount = payload[offset++]; // 🔹 Decode motors for (uint8_t i = 0; i < motorCount && offset < length; ++i) { if (offset + 5 > length) break; // MODEL(2) + ID(2) + nameLength(1) ServoModel model; model.major = payload[offset++]; model.minor = payload[offset++]; uint16_t motorID = payload[offset++] | (payload[offset++] << 8); uint8_t motorNameLength = payload[offset++]; String motorName = ""; for (uint8_t j = 0; j < motorNameLength && offset < length; ++j) { motorName += (char)payload[offset++]; } Motor motor; motor.motorID = motorID; motor.servoModel = model; motor.name = motorName; motor.position = 0; motor.isEnabled = true; newConfig.motors.push_back(motor); } // 🔧 Now you can use `config` however you like Serial.println("Robot name: " + newConfig.deviceName); Serial.printf("Firmware: %d.%d\n", newConfig.firmwareVersion.major, config.firmwareVersion.minor); Serial.printf("Motors: %d\n", newConfig.motors.size()); for (const Motor& m : newConfig.motors) { Serial.printf("Motor %d (%s) - Model %d.%d\n", m.motorID, m.name.c_str(), m.servoModel.major, m.servoModel.minor); } sendMessage(newConfig.deviceName); config = newConfig; config.saveToFFat(); } void handleDataWrite(const uint8_t* payload, uint16_t length) { uint8_t channel = payload[0]; uint8_t id = payload[1]; uint8_t writeByte = payload[2]; uint8_t model = servos[payload[0]]->getMajorModel(id);// config.getMotorModel(id); sendMessage(String(model)); servos[payload[0]]->setFeetechMode(model); // put feetech interface in correct mode // Special case for ID changes, perform unlock, change, lock routine if (writeByte == 5) { sendMessage("CHANGING ID"); servos[channel]->changeID(id, payload[4]); return; } // payload[4] indicates if its a single byte, or 2 bytes of information if (payload[4] == 1) { servos[channel]->write1Byte(id, writeByte, payload[4]); uint8_t response = servos[channel]->waitOnData1Byte(10); sendMessage(&response, 1, CMD_WRITE_DATA); } else { servos[channel]->write2Bytes(id, writeByte, payload[4] | (payload[5] << 8)); uint16_t response = servos[channel]->waitOnData2Bytes(10); uint8_t buffer[2]; buffer[0] = (response >> 8) & 0xFF; // high byte buffer[1] = response & 0xFF; // low byte sendMessage(buffer, 2, CMD_WRITE_DATA); } } void handleIdRequest() { std::vector payload = config.serializeToBytes(); uint16_t length = payload.size(); // Compute checksum: XOR of CMD_ID_REQUEST, length bytes, and all payload bytes uint8_t checksum = CMD_ID_REQUEST ^ (length >> 8) ^ (length & 0xFF); for (uint8_t byte : payload) { checksum ^= byte; } // Send header Serial.write(HEADER1); Serial.write(HEADER2); Serial.write(CMD_ID_REQUEST); Serial.write((length >> 8) & 0xFF); Serial.write(length & 0xFF); // Send payload for (uint8_t byte : payload) { Serial.write(byte); } // Send checksum Serial.write(checksum); } void handleFileList() { File root = FFat.open("/"); if (!root || !root.isDirectory()) { sendFileListResponse(""); // empty payload return; } String payload = ""; File file = root.openNextFile(); while (file) { if (!file.isDirectory()) { payload += String(file.name()) + "\n"; } file = root.openNextFile(); } sendFileListResponse(payload); } void sendFileListResponse(const String& payloadStr) { uint16_t length = payloadStr.length(); if (length > MAX_PAYLOAD_SIZE) { Serial.println("File list too large"); return; } uint8_t checksum = CMD_FILE_LIST ^ (length >> 8) ^ (length & 0xFF); for (uint16_t i = 0; i < length; i++) { checksum ^= payloadStr[i]; } Serial.write(HEADER1); Serial.write(HEADER2); Serial.write(CMD_FILE_LIST); Serial.write((length >> 8) & 0xFF); Serial.write(length & 0xFF); Serial.write((const uint8_t*)payloadStr.c_str(), length); Serial.write(checksum); } void handleLoadFile(const uint8_t* payload, uint16_t length) { if (length == 0 || length >= 128) { Serial.println("Invalid filename"); return; } char filename[128]; memcpy(filename, payload, length); filename[length] = '\0'; File file = FFat.open(filename, "r"); if (!file || !file.available()) { Serial.println("File not found or empty"); return; } size_t fileSize = file.size(); if (fileSize > 65535) { Serial.println("File too large for single transfer"); file.close(); return; } uint8_t* buffer = (uint8_t*)malloc(fileSize); if (!buffer) { Serial.println("Memory allocation failed"); file.close(); return; } size_t bytesRead = file.read(buffer, fileSize); file.close(); if (bytesRead != fileSize) { Serial.println("File read error"); free(buffer); return; } // 🔹 Compute checksum uint8_t checksum = CMD_LOAD_FILE ^ (fileSize >> 8) ^ (fileSize & 0xFF); for (size_t i = 0; i < fileSize; i++) { checksum ^= buffer[i]; } // 🔹 Send packet Serial.write(HEADER1); Serial.write(HEADER2); Serial.write(CMD_LOAD_FILE); Serial.write((fileSize >> 8) & 0xFF); Serial.write(fileSize & 0xFF); Serial.write(buffer, fileSize); Serial.write(checksum); free(buffer); Serial.println("File sent in one go"); } void handleDeleteFile(const uint8_t* payload, uint16_t length) { sendMessage("Deleting FILE"); if (length < 1) { Serial.println("Payload too short for filename length"); sendMessage("Payload too short for filename length"); return; } // 🔹 Parse filename uint16_t filenameLength = payload[0] | (payload[1] << 8); if (length < 2 + filenameLength) { Serial.println("Payload too short for filename"); sendMessage("Payload too short for filename"); return; } char filename[filenameLength + 1]; memcpy(filename, payload + 2, filenameLength); filename[filenameLength] = '\0'; deleteFile(FFat, ("/" + String(filename)).c_str()); sendMessage(("File Deleted: " + String(filename)).c_str(), CMD_DELETE_FILE); } void handlePlayAnimation(const uint8_t* payload, uint16_t length) { sendMessage("Playing FILE"); if (length < 1) { Serial.println("Payload too short for filename length"); sendMessage("Payload too short for filename length"); return; } // 🔹 Parse filename uint16_t filenameLength = payload[0] | (payload[1] << 8); if (length < 2 + filenameLength) { Serial.println("Payload too short for filename"); sendMessage("Payload too short for filename"); return; } char filename[filenameLength + 1]; memcpy(filename, payload + 2, filenameLength); filename[filenameLength] = '\0'; // 🔹 Extract playback mode and repeat count playMode = payload[2 + filenameLength]; repeatsRemaining = payload[3 + filenameLength]; //deleteFile(FFat, ("/" + String(filename)).c_str()); anim.clear(); anim.loadFromFile(("/" + String(filename)).c_str()); //playAnimation(anim); currentAnimation = &anim; currentAnimation->setActive(true); // ✅ mark it as running sendMessage("File Played", CMD_PLAY_FILE); } void handleSaveFile(const uint8_t* payload, uint16_t length) { bool valid = parseAnimationPayload(payload, length, anim); if (valid) { //Serial.println("Animation parsed successfully!"); //anim.printKeyframes(); } else { //Serial.println("Failed to parse animation."); length = 1; // Override length to 1 for fallback response payload = nullptr; // We'll send a single 0x00 instead } if (length > MAX_PAYLOAD_SIZE) { Serial.println("File list too large"); return; } // Calculate checksum uint8_t checksum = CMD_SAVE_FILE ^ (length >> 8) ^ (length & 0xFF); if (valid) { for (uint16_t i = 0; i < length; i++) { checksum ^= payload[i]; } } else { checksum ^= 0x00; } // Send response Serial.write(HEADER1); Serial.write(HEADER2); Serial.write(CMD_SAVE_FILE); Serial.write((length >> 8) & 0xFF); Serial.write(length & 0xFF); if (valid) { for (uint16_t i = 0; i < length; i++) { Serial.write(payload[i]); } } else { Serial.write(0x00); } Serial.write(checksum); } bool parseAnimationPayload(const uint8_t* payload, uint16_t length, Animation& animation) { sendMessage("SAVING FILE"); if (length < 1) { Serial.println("Payload too short for filename length"); return false; } // 🔹 Parse filename uint16_t filenameLength = payload[0] | (payload[1] << 8); if (length < 2 + filenameLength + 18) { Serial.println("Payload too short for filename and header"); return false; } char filename[filenameLength + 1]; memcpy(filename, payload + 2, filenameLength); filename[filenameLength] = '\0'; const uint8_t* ptr = payload + 2 + filenameLength; String msg = "SAVING FILE: "; msg += filename; sendMessage(msg); // 🔹 Parse header memcpy(animation.header.magic, ptr, 4); if (strncmp(animation.header.magic, "ANIM", 4) != 0) { Serial.println("Invalid magic header"); sendMessage("invalid magic header"); return false; } animation.header.frameCount = ptr[5] << 8 | ptr[4]; // little-endian ✅ sendMessage(String(animation.header.frameCount)); animation.header.version = ptr[6]; animation.header.frameRate = ptr[7]; memcpy(animation.header.reserved, ptr + 8, 8); uint16_t curveCount = ptr[17] << 8 | ptr[16]; // little-endian ✅ Serial.print("curveCount: "); Serial.println(curveCount); ptr += 18; if (length < (ptr - payload) + curveCount * sizeof(CurveSegment)) { Serial.println("Payload too short for curve segments"); sendMessage("Payload too short"); return false; } // 🔹 Parse curve segments animation.clearAllCurves(); for (uint16_t i = 0; i < curveCount; i++) { CurveSegment seg; memcpy(&seg, ptr, sizeof(CurveSegment)); // Serial.print("Segment "); // Serial.print(i); // Serial.print(": motorID="); // Serial.print(seg.motorID); // Serial.print(", startTime="); // Serial.print(seg.startTime); // Serial.print(", endTime="); // Serial.print(seg.endTime); // Serial.print(", startPointY="); // Serial.print(seg.startPointY); // Serial.print(", startHandleX="); // Serial.print(seg.startHandleX); // Serial.print(", startHandleY="); // Serial.print(seg.startHandleY); // Serial.print(", endHandleX="); // Serial.print(seg.endHandleX); // Serial.print(", endHandleY="); // Serial.print(seg.endHandleY); // Serial.print(", endPointY="); // Serial.println(seg.endPointY); animation.addCurveSegment(seg); ptr += sizeof(CurveSegment); } sendMessage(anim.printCurves()); // for (int i = 0; i < 800; i++){ // sendMessage(String(anim.getMotorPosition(11, i))); // } // ✅ Advance ptr to node graph payload //ptr += curveCount * sizeof(CurveSegment); // 🔹 Parse node graph uint8_t nodeCount = ptr[0]; Serial.print("Node count: "); Serial.println(nodeCount); // sendMessage(String("Node count: ") + String(nodeCount)); // sendMessage(String("ptr offset: ") + String(ptr - payload)); uint16_t remainingLength = length - (ptr - payload); if (remainingLength > 0) { loadNodeGraph(ptr, remainingLength, animation.nodeGraph); animation.nodeGraph.bindAnimationContext(&animation); } else { Serial.println("No node graph data found"); } sendMessage(printNodeGraph(animation.nodeGraph)); // currentAnimation = &animation; // currentAnimation->setActive(true); // ✅ mark it as running // 🔹 Save using received filename animation.saveToFile(("/" + String(filename)).c_str()); sendMessage("SAVED"); return true; } void handleSetPosition(const uint8_t* payload, uint16_t length) { if (length % 3 != 0) { Serial.println("Invalid sync packet length"); return; } uint8_t motorCount = length / 3; // Dynamically allocate arrays uint8_t ids[motorCount]; uint16_t positions[motorCount]; uint16_t speeds[motorCount]; for (uint8_t i = 0; i < motorCount; ++i) { uint8_t motorId = payload[i * 3]; uint16_t position = (payload[i * 3 + 2] << 8) | payload[i * 3 + 1]; ids[i] = motorId; positions[i] = position; speeds[i] = 0; // Optional: update internal state or debug // Serial.printf("Motor %d → %d\n", motorId, position); } // FLIP BYTES FOR STS SERVOS for (int i = 0; i < motorCount; i++) { if (config.getMotorModel(ids[i]) == 9) { positions[i] = flipBytes(positions[i]); speeds[i] = map(speeds[i], 0, 4095, 0, 254); speeds[i] = flipBytes(speeds[i]); } else { positions[i] = map(positions[i], 0, 4095, 0, 1023); speeds[i] = map(speeds[i], 0, 4095, 0, 1000); } } // Send sync write to all motors servos[0]->syncWritePos(ids, positions, speeds, motorCount); String readablePayload = encodeMotorPositionsReadable(ids, positions, motorCount); sendMessage(readablePayload, CMD_MESSAGE); } String encodeMotorPositionsReadable(const uint8_t* ids, const uint16_t* positions, uint8_t motorCount) { String payload = ""; for (uint8_t i = 0; i < motorCount; ++i) { payload += "ID:"; payload += String(ids[i]); payload += ",POS:"; payload += String(positions[i]); if (i < motorCount - 1) { payload += ";"; // separate entries } } return payload; } // Scans 0-254 and responds with the channel and ID as each successful ping is received // Signals end by responding with channel and 255 void handleScanChannel(const uint8_t* payload, uint16_t length) { if (length != 1) { sendMessage("Length of scanChannel Request Wrong"); return; } for (int i = 0; i < 254; i++) { servos[payload[0]]->sendPing(i); uint8_t val = servos[payload[0]]->waitOnData1Byte(10); if (val != 0) { uint8_t response[44]; // Adjusted size to fit all values response[0] = payload[0]; // channel response[1] = i; // responding address uint16_t model = servos[payload[0]]->getModel(i); servos[payload[0]]->setFeetechMode(model); // put feetech interface in correct mode uint16_t minAngleLimit = servos[payload[0]]->getMinAngleLimit(i); uint16_t maxAngleLimit = servos[payload[0]]->getMaxAngleLimit(i); uint16_t position = servos[payload[0]]->getPosition(i); uint8_t CWDeadZone = servos[payload[0]]->getCWDeadZone(i); uint8_t CCWDeadZone = servos[payload[0]]->getCCWDeadZone(i); uint16_t offset = servos[payload[0]]->getOffset(i); uint8_t mode = servos[payload[0]]->getMode(i); uint8_t torqueEnable = servos[payload[0]]->getTorqueEnable(i); uint8_t acceleration = servos[payload[0]]->getAcceleration(i); uint16_t goalPosition = servos[payload[0]]->getGoalPosition(i); uint16_t goalTime = servos[payload[0]]->getGoalTime(i); uint16_t goalSpeed = servos[payload[0]]->getGoalSpeed(i); uint8_t lock = servos[payload[0]]->getLock(i); int16_t speed = servos[payload[0]]->getSpeed(i); uint16_t load = servos[payload[0]]->getLoad(i); uint8_t temperature = servos[payload[0]]->getTemperature(i); uint8_t moving = servos[payload[0]]->getMoving(i); uint8_t current = servos[payload[0]]->getCurrent(i); uint8_t voltage = servos[payload[0]]->getVoltage(i); // Pack values into response response[2] = (model >> 8) & 0xFF; response[3] = model & 0xFF; response[4] = (minAngleLimit >> 8) & 0xFF; response[5] = minAngleLimit & 0xFF; response[6] = (maxAngleLimit >> 8) & 0xFF; response[7] = maxAngleLimit & 0xFF; response[8] = (position >> 8) & 0xFF; response[9] = position & 0xFF; response[10] = CWDeadZone; response[11] = CCWDeadZone; response[12] = (offset >> 8) & 0xFF; response[13] = offset & 0xFF; response[14] = mode; response[15] = torqueEnable; response[16] = acceleration; response[17] = (goalPosition >> 8) & 0xFF; response[18] = goalPosition & 0xFF; response[19] = (goalTime >> 8) & 0xFF; response[20] = goalTime & 0xFF; response[21] = (goalSpeed >> 8) & 0xFF; response[22] = goalSpeed & 0xFF; response[23] = lock; response[24] = (speed >> 8) & 0xFF; response[25] = speed & 0xFF; response[26] = (load >> 8) & 0xFF; response[27] = load & 0xFF; response[28] = temperature; response[29] = moving; response[30] = (current >> 8) & 0xFF; response[31] = current & 0xFF; response[32] = voltage; // You can continue adding more fields here if needed... sendMessage(response, 33, CMD_SCAN_CHANNEL); // updated length } } uint8_t r[2]; r[0] = payload[0]; // channel r[1] = 255; // responding address sendMessage(r, 2, CMD_SCAN_CHANNEL); // std::vector successfulAddresses; // servos[payload[0]]->pingAll(successfulAddresses); // std::vector response; // response.push_back(payload[0]); // channel // response.push_back(successfulAddresses.size()); // count // for (uint8_t address : successfulAddresses) { // response.push_back(address); // } // sendMessage(response.data(), response.size(), CMD_SCAN_CHANNEL); } bool flip = false; unsigned long lastSend = 0; void loop() { runNodeAnimation(); HandleSerialRequests(); if (millis() - lastSend > 50) { // Update config motor positions for (const Motor& motor : config.motors) { servos[0]->setFeetechMode(motor.servoModel.major); // put feetech interface in correct mode uint16_t position = servos[0]->getPosition(motor.motorID); config.setMotorPosition(motor.motorID, position); //Serial.print(position); //Serial.print("\t"); } //Serial.println(); lastSend = millis(); // for (const Motor& motor : config.motors) { // uint16_t position = motor.position; // uint8_t v1 = position >> 8; // uint8_t v2 = position & 0xFF; // Serial.print(v1); // Serial.print("\t"); // Serial.print(v2); // Serial.print(",\t"); // //Serial.print(position); // //Serial.print(" "); // //Serial.print((v1 << 8) | v2); // } // Serial.println(); } if (streamPositions) { if (millis() - lastStreamPositions > 50) { lastStreamPositions = millis(); SendMotorPositions(); } } } void runNodeAnimation() { static uint32_t lastTickTime = 0; static uint32_t currentTick = 0; const uint16_t FRAME_INTERVAL_MS = 1000 / 48; if (!currentAnimation) return; // ✅ Prevent crash if (!currentAnimation->isActive()) return; config.enableAllMotors(); uint32_t now = millis(); if (now - lastTickTime >= FRAME_INTERVAL_MS) { lastTickTime = now; currentAnimation->nodeGraph.tick(currentTick, *currentAnimation); // if (currentTick == 100){ // message += String(anim.getMotorPosition(10, currentTick)); // message += "\n"; // } auto outputs = currentAnimation->nodeGraph.getServoOutputs(); std::vector motorIDs; std::vector positions; std::vector speeds; String message = String(currentTick) + String("\n"); for (const auto& [motorID, value] : outputs) { if (value != 65535) { motorIDs.push_back(motorID); positions.push_back(value); speeds.push_back(0); config.setMotorPosition(motorID, value); if (config.setMotorEnabled(motorID, true)) { servos[0]->enableTorque(motorID); } } else { if (config.setMotorEnabled(motorID, false)) { servos[0]->disableTorque(motorID); } } message += "Motor "; message += String(motorID); message += " → "; message += String(value); message += "\n"; } uint8_t motorCount = motorIDs.size(); // ✅ number of motors if (currentTick % 20 == 0) { sendMessage(message); } // FLIP BYTES FOR STS SERVOS for (int i = 0; i < motorCount; i++) { if (config.getMotorModel(motorIDs[i]) == 9) { positions[i] = flipBytes(positions[i]); speeds[i] = map(speeds[i], 0, 4095, 0, 254); speeds[i] = flipBytes(speeds[i]); } else { positions[i] = map(positions[i], 0, 4095, 0, 1023); speeds[i] = map(speeds[i], 0, 4095, 0, 1000); } } // ✅ Send to servo controller servos[0]->syncWritePos(motorIDs.data(), positions.data(), speeds.data(), motorCount); currentTick++; if (currentTick > currentAnimation->getFrameCount()) { switch (playMode) { case PLAY_ONCE: currentAnimation->setActive(false); break; case PLAY_LOOP: break; case PLAY_REPEAT: repeatsRemaining--; if (repeatsRemaining == 0) { currentAnimation->setActive(false); } break; } currentTick = 0; } } } void SendMotorPositions() { std::vector payload; for (const Motor& motor : config.motors) { uint16_t position = motor.position; payload.push_back(position & 0xFF); // Low byte payload.push_back(position >> 8); // High byte //Serial.print(position); //Serial.print("\t"); // Serial.print(position & 0xFF); // Serial.print(",\t"); } //Serial.println(); sendMessage(payload.data(), payload.size(), POSITION_STREAM); } void PrintFileList() { File root = FFat.open("/"); if (!root || !root.isDirectory()) { Serial.println("Failed to open FFat root directory"); return; } Serial.println("Files in FFat:"); File file = root.openNextFile(); while (file) { Serial.print(" "); Serial.print(file.name()); Serial.print(" | Size: "); Serial.println(file.size()); file = root.openNextFile(); } Serial.println("End of file list."); } void ClearFiles() { File root = FFat.open("/"); if (!root || !root.isDirectory()) { Serial.println("Failed to open FFat root directory"); return; } Serial.println("Files in FFat:"); File file = root.openNextFile(); while (file) { String filename = "/" + String(file.name()); // Add leading slash Serial.print(" Deleting: "); Serial.println(filename); file.close(); // Close before deleting deleteFile(FFat, filename.c_str()); // Use corrected path file = root.openNextFile(); } Serial.println("FFat cleanup complete."); } void deleteFile(fs::FS& fs, const char* path) { Serial.printf("Deleting file: %s\r\n", path); if (fs.remove(path)) { Serial.println("- file deleted"); } else { Serial.println("- delete failed"); } } void playAnimation(Animation& animation) { uint16_t durationCS = animation.getFrameCount(); const uint8_t fps = 48; const uint32_t frameIntervalMS = 1000 / fps; // ~20.83 ms const uint32_t totalDurationMS = durationCS * 10; uint32_t startTime = millis(); uint32_t nextFrameTime = startTime; while (millis() - startTime < totalDurationMS) { uint32_t currentTime = millis(); if (currentTime >= nextFrameTime) { uint16_t timeCS = (currentTime - startTime) / 10; //for (uint8_t motorID = 0; motorID < NUM_CHANNELS; motorID++) { uint16_t pos = animation.getMotorPosition(0, timeCS); pos = pos / 4; servos[0]->sendWritePos(10, pos); Serial.println(pos); //} nextFrameTime += frameIntervalMS; } // Optional: yield or small delay to avoid busy loop delay(1); } // Optional: reset motors to center // for (uint8_t motorID = 0; motorID < NUM_CHANNELS; motorID++) { // servos[0]->sendWritePos(motorID, 2048); // } } void SCSPingAll() { std::vector successfulAddresses; servos[0]->pingAll(successfulAddresses); // Now successfulAddresses contains all successful pings Serial.println("Successful Addresses:"); for (uint8_t address : successfulAddresses) { Serial.print(address); Serial.print(" "); } Serial.println(); } void STSPingAll() { std::vector successfulAddresses; servos[1]->pingAll(successfulAddresses); // Now successfulAddresses contains all successful pings Serial.println("Successful Addresses:"); for (uint8_t address : successfulAddresses) { Serial.print(address); Serial.print(" "); } Serial.println(); }