
--- firmware.ino
+++ firmware.ino
... | ... | @@ -1,271 +1,431 @@ |
1 | 1 |
/* |
2 |
- * Multi Servo Control via Serial (JSON-like format) |
|
2 |
+ * Multi Servo Control via Serial (C-String Version) |
|
3 |
+ * Includes Record/Playback and Pre-defined Sequence execution. |
|
3 | 4 |
* |
4 |
- * Receives commands over Serial monitor to control multiple servos. |
|
5 |
- * Expected command format: {S<index>:<angle>,S<index>:<angle>,...} |
|
6 |
- * Example: {S0:90,S1:45,S5:180} |
|
7 |
- * |
|
8 |
- * - Validates the overall { } structure. |
|
9 |
- * - Parses comma-separated segments. |
|
10 |
- * - Validates each segment format (S<index>:<angle>). |
|
11 |
- * - Uses robust integer parsing (strtol) for index and angle. |
|
12 |
- * - Checks if servo index is within valid range. |
|
13 |
- * - Constrains requested angle to servo's min/max limits. |
|
14 |
- * - Controls the servos. |
|
15 |
- * - Provides detailed feedback and error messages via Serial. |
|
16 |
- * |
|
17 |
- * Refactored based on discussions to improve structure, robustness, and readability. |
|
5 |
+ * Serial Commands: |
|
6 |
+ * {S<index>:<angle>,...} : Direct servo control (only when IDLE) |
|
7 |
+ * RECORD : Start recording commands |
|
8 |
+ * PLAY : Play back recorded sequence |
|
9 |
+ * STOP : Stop recording or playback or sequence |
|
10 |
+ * SEQUENCE : Execute pre-defined interpolated sequence |
|
18 | 11 |
*/ |
19 | 12 |
|
20 |
-#include <Servo.h> // Include the Servo library |
|
21 |
-#include <errno.h> // For strtol error checking |
|
22 |
-#include <stdlib.h> // For strtol |
|
23 |
-#include <limits.h> // For INT_MIN, INT_MAX |
|
13 |
+#include <Servo.h> |
|
14 |
+#include <errno.h> |
|
15 |
+#include <stdlib.h> |
|
16 |
+#include <string.h> |
|
17 |
+#include <ctype.h> |
|
18 |
+#include <limits.h> |
|
19 |
+#include <math.h> // For round() during interpolation |
|
20 |
+#include <avr/pgmspace.h> // For PROGMEM functions |
|
24 | 21 |
|
25 | 22 |
// --- User Defined Constants --- |
26 |
-const int SERVO_NUM = 6; // Total number of servos |
|
23 |
+const int SERVO_NUM = 6; |
|
24 |
+const byte SERVO_MAX[SERVO_NUM] PROGMEM = { 180, 180, 180, 130, 180, 180 }; // Store in Flash |
|
25 |
+const byte SERVO_MIN[SERVO_NUM] PROGMEM = { 0, 0, 0, 0, 0, 0 }; // Store in Flash |
|
26 |
+const byte SERVO_PIN[SERVO_NUM] PROGMEM = { 3, 2, 8, 9, 12, 13 }; // Store in Flash |
|
27 | 27 |
|
28 |
-// Maximum angle limits for each servo (adjust as needed) |
|
29 |
-const int SERVO_MAX[SERVO_NUM] = { |
|
30 |
- 180, 180, 180, 180, 180, 180 |
|
28 |
+// === Input Buffer === |
|
29 |
+const byte MAX_COMMAND_LEN = 100; |
|
30 |
+char commandInputBuffer[MAX_COMMAND_LEN]; |
|
31 |
+byte commandInputIndex = 0; |
|
32 |
+ |
|
33 |
+// === Recording & Playback Constants & Data === |
|
34 |
+// Max steps - Adjust based on RAM. ~280 should be safe with ~1150 bytes free RAM |
|
35 |
+const int MAX_RECORDED_STEPS = 280; |
|
36 |
+//const unsigned long RECORDING_TIMEOUT_MS = 30000; // 30 seconds timeout - Optional |
|
37 |
+ |
|
38 |
+struct RecordedStep { |
|
39 |
+ uint16_t timeOffsetMs; // 2 bytes |
|
40 |
+ byte servoIndex; // 1 byte |
|
41 |
+ byte servoAngle; // 1 byte |
|
42 |
+}; // Total 4 bytes per step |
|
43 |
+ |
|
44 |
+RecordedStep recordedSequence[MAX_RECORDED_STEPS]; // ~280 * 4 = 1120 bytes SRAM |
|
45 |
+int recordedStepsCount = 0; |
|
46 |
+ |
|
47 |
+// === Pre-defined Sequence Constants & Data === |
|
48 |
+const byte NUM_SEQUENCE_STATES = 7; |
|
49 |
+const unsigned long SEQUENCE_INTERPOLATION_TIME = 1000; // 1 second interpolation |
|
50 |
+ |
|
51 |
+// Store sequence data in Flash memory |
|
52 |
+const byte sequenceData[NUM_SEQUENCE_STATES][SERVO_NUM] PROGMEM = { |
|
53 |
+ {100, 85, 175, 80, 90, 0}, // State 0 |
|
54 |
+ {100, 85, 175, 80, 90, 78}, // State 1 |
|
55 |
+ {100, 100, 105, 80, 90, 78}, // State 2 |
|
56 |
+ {180, 90, 120, 80, 90, 78}, // State 3 |
|
57 |
+ {180, 90, 130, 80, 90, 78}, // State 4 |
|
58 |
+ {180, 90, 130, 80, 90, 0}, // State 5 |
|
59 |
+ {100, 90, 90, 90, 90, 0} // State 6 |
|
31 | 60 |
}; |
32 | 61 |
|
33 |
-// Minimum angle limits for each servo (adjust as needed) |
|
34 |
-const int SERVO_MIN[SERVO_NUM] = { |
|
35 |
- 0, 0, 0, 0, 0, 0 |
|
36 |
-}; |
|
62 |
+// === State Machine & Timing Variables === |
|
63 |
+enum State { IDLE, RECORDING, PLAYBACK }; |
|
64 |
+State currentState = IDLE; |
|
37 | 65 |
|
38 |
-// Arduino pins connected to each servo's signal line |
|
39 |
-// Ensure these pins support PWM (usually marked with ~ on Arduino Uno/Nano) |
|
40 |
-const int SERVO_PIN[SERVO_NUM] = { |
|
41 |
- 3, 2, 8, 9, 12, 13 |
|
42 |
-}; |
|
43 |
-// --- End Constants --- |
|
66 |
+unsigned long recordingStartTime = 0; |
|
67 |
+unsigned long playbackStartTime = 0; |
|
68 |
+int playbackIndex = 0; |
|
44 | 69 |
|
45 |
-// Array to hold the Servo objects |
|
70 |
+bool isRunningSequence = false; |
|
71 |
+int currentSequenceStateIndex = 0; // Index of the *target* state |
|
72 |
+unsigned long sequenceStateStartTime = 0; |
|
73 |
+int sequenceStartPos[SERVO_NUM]; // Servo positions at the start of an interpolation step |
|
74 |
+ |
|
75 |
+// --- Global Variables --- |
|
46 | 76 |
Servo myServos[SERVO_NUM]; |
47 | 77 |
|
48 |
-// --- Helper Function for Robust Integer Parsing --- |
|
49 |
-// Attempts to parse the entire String s as an integer. |
|
50 |
-// Returns true on success, false otherwise. |
|
51 |
-// Stores the result in 'value' on success. |
|
52 |
-bool parseStringToInt(const String& s, int& value) { |
|
53 |
- if (s.length() == 0) { |
|
54 |
- return false; // Empty string is not a valid number |
|
55 |
- } |
|
56 |
- // Get C-style string for strtol |
|
57 |
- const char* s_cstr = s.c_str(); |
|
58 |
- char* endptr; |
|
59 |
- errno = 0; // Reset error number |
|
60 |
- |
|
61 |
- long result = strtol(s_cstr, &endptr, 10); // Base 10 |
|
62 |
- |
|
63 |
- // Check for errors: |
|
64 |
- // 1. Did strtol encounter an error (e.g., overflow)? |
|
65 |
- // 2. Was the entire string consumed? (endptr should point to the null terminator '\0') |
|
66 |
- // 3. Check if the result fits within an int range (strtol returns long) |
|
67 |
- if (errno != 0 || *endptr != '\0' || result < INT_MIN || result > INT_MAX) { |
|
68 |
- return false; // Conversion failed, didn't consume the whole string, or out of int range |
|
69 |
- } |
|
70 |
- |
|
71 |
- value = (int)result; |
|
72 |
- return true; |
|
78 |
+// --- Helper Function: Trim leading/trailing whitespace (Unchanged) --- |
|
79 |
+void trimWhitespace(char *str) { |
|
80 |
+ if (str == NULL || str[0] == '\0') return; |
|
81 |
+ char *start = str; |
|
82 |
+ while (isspace((unsigned char)*start)) start++; |
|
83 |
+ if (*start == '\0') { *str = '\0'; return; } |
|
84 |
+ char *end = str + strlen(str) - 1; |
|
85 |
+ while (end > start && isspace((unsigned char)*end)) end--; |
|
86 |
+ *(end + 1) = '\0'; |
|
87 |
+ if (str != start) memmove(str, start, (end - start) + 2); |
|
73 | 88 |
} |
74 | 89 |
|
75 |
-// --- Function to Process a Single Command Segment (e.g., "S0:90") --- |
|
76 |
-// Modifies 'firstSegmentProcessed' flag to control comma printing in feedback. |
|
77 |
-void processSegment(const String& segment, bool& firstSegmentProcessed) { |
|
78 |
- // --- Flags for Validation --- |
|
79 |
- bool segmentIsValid = true; // Assume valid initially for this segment |
|
80 |
- String errorMessage = ""; |
|
90 |
+// --- Helper Function for Robust Integer Parsing from C-string (Unchanged) --- |
|
91 |
+bool parseCharStrToInt(const char* s, int& value) { |
|
92 |
+ if (s == NULL || s[0] == '\0' || isspace((unsigned char)s[0])) return false; |
|
93 |
+ char* endptr; errno = 0; |
|
94 |
+ long result = strtol(s, &endptr, 10); |
|
95 |
+ if (errno != 0 || *endptr != '\0' || result < INT_MIN || result > INT_MAX) return false; |
|
96 |
+ value = (int)result; |
|
97 |
+ return true; |
|
98 |
+} |
|
99 |
+ |
|
100 |
+// --- Function to Stop Recording --- |
|
101 |
+void stopRecording() { |
|
102 |
+ if (currentState == RECORDING) { |
|
103 |
+ currentState = IDLE; |
|
104 |
+ Serial.print(F("\nRecording stopped. ")); // Use F() |
|
105 |
+ Serial.print(recordedStepsCount); |
|
106 |
+ Serial.println(F(" steps recorded.")); |
|
107 |
+ } |
|
108 |
+} |
|
109 |
+ |
|
110 |
+// --- Function to Process a Single Command Segment (Modified for Recording) --- |
|
111 |
+void processSegmentC(char* segment, bool& firstSegmentProcessed) { |
|
112 |
+ bool segmentIsValid = true; |
|
113 |
+ const char* errorMessage = ""; |
|
81 | 114 |
int servoIndex = -1; |
82 | 115 |
int requestedAngle = 0; |
83 |
- int constrainedAngle = 0; |
|
116 |
+ byte constrainedAngle = 0; // Use byte for angle (0-180) |
|
117 |
+ byte currentServoMin = 0; |
|
118 |
+ byte currentServoMax = 180; |
|
84 | 119 |
|
85 |
- // --- Basic Format Check --- |
|
86 |
- int colonIndex = segment.indexOf(':'); |
|
87 |
- |
|
88 |
- if (!(segment.length() > 2 && segment.startsWith("S"))) { |
|
89 |
- segmentIsValid = false; |
|
90 |
- errorMessage = "[Invalid Segment Format: Doesn't start with 'S' or too short]"; |
|
91 |
- } else if (!(colonIndex > 1 && colonIndex < segment.length() - 1)) { |
|
92 |
- segmentIsValid = false; |
|
93 |
- errorMessage = "[Malformed Segment: Missing or misplaced colon]"; |
|
120 |
+ // Basic Format Check |
|
121 |
+ if (strlen(segment) < 4 || segment[0] != 'S') { |
|
122 |
+ segmentIsValid = false; errorMessage = "[Invalid Segment Format]"; |
|
94 | 123 |
} else { |
95 |
- // --- Segment format looks okay, parse Index and Angle --- |
|
96 |
- String indexStr = segment.substring(1, colonIndex); |
|
97 |
- String angleStr = segment.substring(colonIndex + 1); |
|
98 |
- |
|
99 |
- int parsedIndex; |
|
100 |
- int parsedAngle; |
|
101 |
- |
|
102 |
- bool indexParsedOk = parseStringToInt(indexStr, parsedIndex); |
|
103 |
- bool angleParsedOk = false; // Parsed only if index is valid |
|
104 |
- bool indexInRange = false; // Check only if index parsing was okay |
|
105 |
- |
|
106 |
- if (!indexParsedOk) { |
|
107 |
- segmentIsValid = false; |
|
108 |
- errorMessage = "[Invalid Servo Index: Not a valid number]"; |
|
124 |
+ char* colonPtr = strchr(segment, ':'); |
|
125 |
+ if (colonPtr == NULL || colonPtr == segment + 1 || *(colonPtr + 1) == '\0') { |
|
126 |
+ segmentIsValid = false; errorMessage = "[Malformed Segment]"; |
|
109 | 127 |
} else { |
110 |
- // Index parsed, now check range |
|
111 |
- if (parsedIndex >= 0 && parsedIndex < SERVO_NUM) { |
|
112 |
- indexInRange = true; |
|
113 |
- // Index is valid and in range, try parsing the angle |
|
114 |
- angleParsedOk = parseStringToInt(angleStr, parsedAngle); |
|
115 |
- if (!angleParsedOk) { |
|
116 |
- segmentIsValid = false; |
|
117 |
- errorMessage = "[Invalid Angle: Not a valid number]"; |
|
128 |
+ *colonPtr = '\0'; // Temporarily split |
|
129 |
+ char* indexStr = segment + 1; |
|
130 |
+ char* angleStr = colonPtr + 1; |
|
131 |
+ int parsedIndex, parsedAngle; |
|
132 |
+ bool indexParsedOk = parseCharStrToInt(indexStr, parsedIndex); |
|
133 |
+ bool angleParsedOk = false; |
|
134 |
+ bool indexInRange = false; |
|
135 |
+ |
|
136 |
+ if (!indexParsedOk) { |
|
137 |
+ segmentIsValid = false; errorMessage = "[Invalid Index Num]"; |
|
138 |
+ } else { |
|
139 |
+ if (parsedIndex >= 0 && parsedIndex < SERVO_NUM) { |
|
140 |
+ indexInRange = true; |
|
141 |
+ angleParsedOk = parseCharStrToInt(angleStr, parsedAngle); |
|
142 |
+ if (!angleParsedOk) { |
|
143 |
+ segmentIsValid = false; errorMessage = "[Invalid Angle Num]"; |
|
144 |
+ } |
|
145 |
+ } else { |
|
146 |
+ segmentIsValid = false; errorMessage = "[Index Out of Range]"; |
|
147 |
+ } |
|
148 |
+ } |
|
149 |
+ |
|
150 |
+ if (segmentIsValid) { |
|
151 |
+ servoIndex = parsedIndex; |
|
152 |
+ requestedAngle = parsedAngle; |
|
153 |
+ // Read min/max from PROGMEM for constraining |
|
154 |
+ currentServoMin = pgm_read_byte(&SERVO_MIN[servoIndex]); |
|
155 |
+ currentServoMax = pgm_read_byte(&SERVO_MAX[servoIndex]); |
|
156 |
+ constrainedAngle = constrain(requestedAngle, currentServoMin, currentServoMax); |
|
157 |
+ } |
|
158 |
+ } |
|
159 |
+ } |
|
160 |
+ |
|
161 |
+ // --- Act, Record, and Print Feedback --- |
|
162 |
+ if (!firstSegmentProcessed) Serial.print(F(", ")); |
|
163 |
+ |
|
164 |
+ if (segmentIsValid) { |
|
165 |
+ myServos[servoIndex].write(constrainedAngle); |
|
166 |
+ |
|
167 |
+ // Record if needed |
|
168 |
+ if (currentState == RECORDING) { |
|
169 |
+ if (recordedStepsCount < MAX_RECORDED_STEPS) { |
|
170 |
+ unsigned long now = millis(); |
|
171 |
+ unsigned long offset = now - recordingStartTime; |
|
172 |
+ if (offset > 65535) { |
|
173 |
+ stopRecording(); |
|
174 |
+ Serial.println(F("WARN: Recording stopped due to time limit (>65s).")); |
|
175 |
+ } else { |
|
176 |
+ recordedSequence[recordedStepsCount].timeOffsetMs = (uint16_t)offset; |
|
177 |
+ recordedSequence[recordedStepsCount].servoIndex = (byte)servoIndex; |
|
178 |
+ recordedSequence[recordedStepsCount].servoAngle = constrainedAngle; // Store constrained angle |
|
179 |
+ recordedStepsCount++; |
|
118 | 180 |
} |
119 | 181 |
} else { |
120 |
- segmentIsValid = false; |
|
121 |
- errorMessage = "[Invalid Servo Index: Out of range (0-" + String(SERVO_NUM - 1) + ")]"; |
|
182 |
+ stopRecording(); |
|
183 |
+ Serial.println(F("WARN: Recording stopped, memory full.")); |
|
122 | 184 |
} |
123 | 185 |
} |
124 | 186 |
|
125 |
- // If all checks passed so far, store values and constrain angle |
|
126 |
- if (segmentIsValid) { |
|
127 |
- servoIndex = parsedIndex; |
|
128 |
- requestedAngle = parsedAngle; |
|
129 |
- // Constrain the angle using servo-specific limits |
|
130 |
- constrainedAngle = constrain(requestedAngle, SERVO_MIN[servoIndex], SERVO_MAX[servoIndex]); |
|
131 |
- } |
|
132 |
- } |
|
133 |
- |
|
134 |
- // --- Act based on validation and Print Feedback --- |
|
135 |
- // Print comma separator if not the first valid/invalid segment processed |
|
136 |
- if (!firstSegmentProcessed) Serial.print(", "); |
|
137 |
- |
|
138 |
- if (segmentIsValid) { |
|
139 |
- // *** Robot Direct Control is Here *** |
|
140 |
- myServos[servoIndex].write(constrainedAngle); |
|
141 |
- |
|
142 |
- // Feedback for success |
|
143 |
- Serial.print("S"); Serial.print(servoIndex); Serial.print(":"); Serial.print(constrainedAngle); |
|
187 |
+ // Feedback |
|
188 |
+ Serial.print(F("S")); Serial.print(servoIndex); Serial.print(F(":")); Serial.print(constrainedAngle); |
|
144 | 189 |
if (requestedAngle != constrainedAngle) { |
145 |
- // Show if the angle was constrained |
|
146 |
- Serial.print("(<-"); Serial.print(requestedAngle); Serial.print(")"); |
|
190 |
+ Serial.print(F("(<-")); Serial.print(requestedAngle); Serial.print(F(")")); |
|
147 | 191 |
} |
148 | 192 |
} else { |
149 |
- // Feedback for error |
|
150 |
- Serial.print(errorMessage); Serial.print(" '"); Serial.print(segment); Serial.print("'"); |
|
193 |
+ Serial.print(errorMessage); Serial.print(F(" '")); Serial.print(segment); Serial.print(F("'")); |
|
151 | 194 |
} |
152 |
- // Mark that at least one segment (valid or invalid) has been processed and outputted |
|
153 | 195 |
firstSegmentProcessed = false; |
154 | 196 |
} |
155 | 197 |
|
198 |
+// --- Function to process the raw command buffer --- |
|
199 |
+void processCommand(char* cmd) { |
|
200 |
+ trimWhitespace(cmd); |
|
201 |
+ int cmdLen = strlen(cmd); |
|
202 |
+ if (cmdLen == 0) return; |
|
203 |
+ |
|
204 |
+ // --- Handle Control Commands (RECORD, PLAY, STOP, SEQUENCE) FIRST --- |
|
205 |
+ if (strcmp(cmd, "RECORD") == 0) { |
|
206 |
+ if (currentState == IDLE && !isRunningSequence) { |
|
207 |
+ currentState = RECORDING; |
|
208 |
+ recordedStepsCount = 0; |
|
209 |
+ recordingStartTime = millis(); |
|
210 |
+ Serial.println(F("Recording started... (Send STOP or wait)")); |
|
211 |
+ } else { |
|
212 |
+ Serial.println(F("Error: Cannot start recording now (Busy/Sequence).")); |
|
213 |
+ } |
|
214 |
+ return; // Command handled |
|
215 |
+ } else if (strcmp(cmd, "PLAY") == 0) { |
|
216 |
+ if (currentState == IDLE && !isRunningSequence) { |
|
217 |
+ if (recordedStepsCount > 0) { |
|
218 |
+ currentState = PLAYBACK; |
|
219 |
+ playbackIndex = 0; |
|
220 |
+ playbackStartTime = millis(); |
|
221 |
+ Serial.println(F("Playback started...")); |
|
222 |
+ } else { |
|
223 |
+ Serial.println(F("Error: Nothing recorded to play.")); |
|
224 |
+ } |
|
225 |
+ } else { |
|
226 |
+ Serial.println(F("Error: Cannot start playback now (Busy/Sequence).")); |
|
227 |
+ } |
|
228 |
+ return; // Command handled |
|
229 |
+ } else if (strcmp(cmd, "STOP") == 0) { |
|
230 |
+ bool stoppedSomething = false; |
|
231 |
+ if (currentState == RECORDING) { |
|
232 |
+ stopRecording(); // stopRecording changes state to IDLE |
|
233 |
+ stoppedSomething = true; |
|
234 |
+ } |
|
235 |
+ if (currentState == PLAYBACK) { // Use 'if', not 'else if', in case stopRecording was just called |
|
236 |
+ currentState = IDLE; |
|
237 |
+ Serial.println(F("\nPlayback stopped by command.")); |
|
238 |
+ stoppedSomething = true; |
|
239 |
+ } |
|
240 |
+ if (isRunningSequence) { // Use 'if', check independently |
|
241 |
+ isRunningSequence = false; // Stop sequence execution |
|
242 |
+ currentState = IDLE; // Ensure state is IDLE |
|
243 |
+ Serial.println(F("\nSequence stopped by command.")); |
|
244 |
+ stoppedSomething = true; |
|
245 |
+ } |
|
246 |
+ |
|
247 |
+ if (!stoppedSomething && currentState == IDLE) { // Only print if nothing was actually stopped |
|
248 |
+ Serial.println(F("Status: IDLE (Nothing to stop).")); |
|
249 |
+ } |
|
250 |
+ return; // Command handled |
|
251 |
+ } else if (strcmp(cmd, "SEQUENCE") == 0) { |
|
252 |
+ if (currentState == IDLE && !isRunningSequence) { |
|
253 |
+ isRunningSequence = true; |
|
254 |
+ currentSequenceStateIndex = 0; // Target the first state |
|
255 |
+ sequenceStateStartTime = millis(); |
|
256 |
+ // Record starting positions for interpolation |
|
257 |
+ for (int i = 0; i < SERVO_NUM; i++) { |
|
258 |
+ sequenceStartPos[i] = myServos[i].read(); // Read current angle |
|
259 |
+ } |
|
260 |
+ Serial.println(F("Sequence started...")); |
|
261 |
+ } else { |
|
262 |
+ Serial.println(F("Error: Cannot start sequence now (Busy).")); |
|
263 |
+ } |
|
264 |
+ return; // Command handled |
|
265 |
+ } |
|
266 |
+ |
|
267 |
+ // --- Check for Movement Command Format {Sx:y...} --- |
|
268 |
+ if (cmd[0] == '{' && cmd[cmdLen - 1] == '}') { |
|
269 |
+ // Command has the potential { } structure for movement. |
|
270 |
+ // Now, check if the state *allows* direct servo control commands. |
|
271 |
+ if (currentState == IDLE || currentState == RECORDING) { |
|
272 |
+ // Process the segments. processSegmentC handles recording IF currentState is RECORDING. |
|
273 |
+ cmd[cmdLen - 1] = '\0'; // Null-terminate before '}' |
|
274 |
+ char* content = cmd + 1; // Point after '{' |
|
275 |
+ trimWhitespace(content); |
|
276 |
+ if (content[0] == '\0') { |
|
277 |
+ Serial.println(F("Received empty command: {}")); |
|
278 |
+ return; // Handled (empty command) |
|
279 |
+ } |
|
280 |
+ |
|
281 |
+ Serial.print(F("Processing: {")); |
|
282 |
+ char* segment; char* saveptr; bool firstSegment = true; |
|
283 |
+ for (segment = strtok_r(content, ",", &saveptr); segment != NULL; segment = strtok_r(NULL, ",", &saveptr)) |
|
284 |
+ { |
|
285 |
+ trimWhitespace(segment); |
|
286 |
+ if (segment[0] == '\0') continue; // Skip empty segments like {S1:90,,S2:80} |
|
287 |
+ processSegmentC(segment, firstSegment); // This function moves the servo AND records if needed |
|
288 |
+ } |
|
289 |
+ Serial.println(F("}")); |
|
290 |
+ |
|
291 |
+ } else { |
|
292 |
+ // Current state is PLAYBACK or SEQUENCE - cannot accept direct movement commands. |
|
293 |
+ Serial.println(F("Info: Movement command ignored while PLAYBACK or SEQUENCE running. Use STOP first.")); |
|
294 |
+ } |
|
295 |
+ return; // Command handled (either processed or intentionally ignored based on state) |
|
296 |
+ } |
|
297 |
+ |
|
298 |
+ // --- If the command wasn't a known control word and didn't match {Sx:y...} format --- |
|
299 |
+ Serial.print(F("Error: Unknown command or invalid format '")); Serial.print(cmd); |
|
300 |
+ Serial.println(F("'. Expected {Sx:y,...} or control word.")); |
|
301 |
+} |
|
156 | 302 |
|
157 | 303 |
// --- Arduino Setup Function --- |
158 | 304 |
void setup() { |
159 |
- // Start Serial communication (baud rate should match Serial Monitor setting) |
|
160 | 305 |
Serial.begin(57600); |
161 |
- // Wait for Serial port to connect (needed for native USB like Leonardo, Micro) |
|
162 |
- // Add a timeout to prevent blocking forever if Serial isn't connected |
|
163 | 306 |
unsigned long setupStartTime = millis(); |
164 |
- while (!Serial && (millis() - setupStartTime < 5000)) { // Wait max 5 seconds |
|
165 |
- delay(10); // Small delay while waiting |
|
166 |
- } |
|
307 |
+ while (!Serial && (millis() - setupStartTime < 5000)) { delay(10); } |
|
167 | 308 |
|
168 |
- Serial.println("\n--- Multi Servo Control via Serial ---"); |
|
169 |
- Serial.print("Initializing "); Serial.print(SERVO_NUM); Serial.println(" servos..."); |
|
170 |
- Serial.println("Command format: {S<index>:<angle>,S<index>:<angle>,...}"); |
|
171 |
- Serial.println("Example: {S0:90,S1:45,S5:180}"); |
|
172 |
- Serial.println("Valid servo indices: 0 to " + String(SERVO_NUM - 1)); |
|
173 |
- Serial.println("---------------------------------------"); |
|
309 |
+ Serial.println(F("\n--- Multi Servo Control: C-String + Record/Play/Sequence ---")); |
|
310 |
+ Serial.print(MAX_RECORDED_STEPS); Serial.println(F(" steps max recording.")); |
|
311 |
+ Serial.println(F("Commands: {Sx:y,...} | RECORD | PLAY | STOP | SEQUENCE")); |
|
312 |
+ Serial.println(F("--------------------------------------------------------")); |
|
174 | 313 |
|
175 |
- // Attach each servo to its pin and set initial position |
|
314 |
+ byte pin; // Temporary variable to hold pin from PROGMEM |
|
176 | 315 |
for (int i = 0; i < SERVO_NUM; i++) { |
177 |
- myServos[i].attach(SERVO_PIN[i]); |
|
178 |
- // Set servos to a defined starting position (e.g., minimum) |
|
179 |
- // Consider if middle (90) or another position is a safer start for your setup |
|
180 |
- myServos[i].write( (SERVO_MIN[i]+SERVO_MAX[i])/2 ); |
|
181 |
- delay(20); // Allow time for servo to potentially reach initial position |
|
182 |
- Serial.print("Servo "); Serial.print(i); |
|
183 |
- Serial.print(" attached to pin "); Serial.print(SERVO_PIN[i]); |
|
184 |
- Serial.print(" | Limits: ["); Serial.print(SERVO_MIN[i]); |
|
185 |
- Serial.print(", "); Serial.print(SERVO_MAX[i]); Serial.println("]"); |
|
316 |
+ pin = pgm_read_byte(&SERVO_PIN[i]); // Read pin from PROGMEM |
|
317 |
+ myServos[i].attach(pin); |
|
318 |
+ // Start servos at midpoint |
|
319 |
+ myServos[i].write( (pgm_read_byte(&SERVO_MIN[i]) + pgm_read_byte(&SERVO_MAX[i])) / 2 ); |
|
320 |
+ delay(20); |
|
321 |
+ Serial.print(F("Servo ")); Serial.print(i); |
|
322 |
+ Serial.print(F(" attached to pin ")); Serial.print(pin); |
|
323 |
+ Serial.print(F(" | Limits: [")); Serial.print(pgm_read_byte(&SERVO_MIN[i])); |
|
324 |
+ Serial.print(F(", ")); Serial.print(pgm_read_byte(&SERVO_MAX[i])); Serial.println(F("]")); |
|
186 | 325 |
} |
187 |
- Serial.println("---------------------------------------"); |
|
188 |
- Serial.println("Initialization Complete. Ready for commands."); |
|
326 |
+ Serial.println(F("--------------------------------------------------------")); |
|
327 |
+ Serial.println(F("Initialization Complete. State: IDLE")); |
|
189 | 328 |
} |
190 | 329 |
|
191 | 330 |
// --- Arduino Main Loop --- |
192 | 331 |
void loop() { |
193 |
- // Check if data is available to read from Serial |
|
194 |
- if (Serial.available() > 0) { |
|
195 |
- // Read the incoming string until newline character |
|
196 |
- // NOTE: Using the String class extensively can lead to memory fragmentation |
|
197 |
- // on memory-constrained boards (like Arduino Uno/Nano/Mega) over long periods. |
|
198 |
- // If the device needs to run reliably for extended durations or handles |
|
199 |
- // very large commands, consider switching to C-style char arrays. |
|
200 |
- String command = Serial.readStringUntil('\n'); |
|
201 |
- command.trim(); // Remove leading/trailing whitespace & newline chars |
|
202 | 332 |
|
203 |
- // Ignore empty commands after trimming |
|
204 |
- if (command.length() == 0) { |
|
205 |
- return; // Nothing to do, wait for next loop iteration |
|
333 |
+ // === Sequence Execution Logic === |
|
334 |
+ if (isRunningSequence) { |
|
335 |
+ unsigned long now = millis(); |
|
336 |
+ unsigned long elapsed = now - sequenceStateStartTime; |
|
337 |
+ |
|
338 |
+ if (elapsed >= SEQUENCE_INTERPOLATION_TIME) { |
|
339 |
+ // Time's up for this step, ensure final position is reached |
|
340 |
+ byte targetAngle; |
|
341 |
+ for (int i = 0; i < SERVO_NUM; i++) { |
|
342 |
+ targetAngle = pgm_read_byte(&sequenceData[currentSequenceStateIndex][i]); |
|
343 |
+ // Use constrain just in case, read limits from PROGMEM |
|
344 |
+ targetAngle = constrain(targetAngle, pgm_read_byte(&SERVO_MIN[i]), pgm_read_byte(&SERVO_MAX[i])); |
|
345 |
+ myServos[i].write(targetAngle); |
|
346 |
+ sequenceStartPos[i] = targetAngle; // Update start pos for next potential step |
|
347 |
+ } |
|
348 |
+ |
|
349 |
+ // Move to the next state |
|
350 |
+ currentSequenceStateIndex++; |
|
351 |
+ |
|
352 |
+ // Check if sequence is complete |
|
353 |
+ if (currentSequenceStateIndex >= NUM_SEQUENCE_STATES) { |
|
354 |
+ isRunningSequence = false; |
|
355 |
+ currentState = IDLE; // Ensure state is IDLE |
|
356 |
+ Serial.println(F("\nSequence finished.")); |
|
357 |
+ } else { |
|
358 |
+ // Reset timer for the next interpolation step |
|
359 |
+ sequenceStateStartTime = now; // Use current time to avoid drift |
|
360 |
+ // No need to update start positions here again, done above after write |
|
361 |
+ Serial.print(F("Sequence step ")); Serial.print(currentSequenceStateIndex-1); Serial.println(F(" complete. Starting next.")); |
|
362 |
+ } |
|
363 |
+ } else { |
|
364 |
+ // --- Interpolate --- |
|
365 |
+ float fraction = (float)elapsed / SEQUENCE_INTERPOLATION_TIME; |
|
366 |
+ byte targetAngle; |
|
367 |
+ int startAngle; |
|
368 |
+ int interpolatedAngle; |
|
369 |
+ |
|
370 |
+ for (int i = 0; i < SERVO_NUM; i++) { |
|
371 |
+ // Read target angle for the current destination state from PROGMEM |
|
372 |
+ targetAngle = pgm_read_byte(&sequenceData[currentSequenceStateIndex][i]); |
|
373 |
+ startAngle = sequenceStartPos[i]; |
|
374 |
+ |
|
375 |
+ // Calculate interpolated angle |
|
376 |
+ interpolatedAngle = round(startAngle + (targetAngle - startAngle) * fraction); |
|
377 |
+ |
|
378 |
+ // Constrain (safety, though shouldn't be needed if sequenceData is valid) |
|
379 |
+ interpolatedAngle = constrain(interpolatedAngle, pgm_read_byte(&SERVO_MIN[i]), pgm_read_byte(&SERVO_MAX[i])); |
|
380 |
+ |
|
381 |
+ // Write interpolated position |
|
382 |
+ // Check if different from last write might save some servo jitter/power? Optional. |
|
383 |
+ // if (myServos[i].read() != interpolatedAngle) { // read() might be slow |
|
384 |
+ myServos[i].write(interpolatedAngle); |
|
385 |
+ // } |
|
386 |
+ } |
|
387 |
+ } |
|
388 |
+ } // End Sequence Logic |
|
389 |
+ |
|
390 |
+ // === Playback Logic === |
|
391 |
+ else if (currentState == PLAYBACK) { // Only run if not running sequence |
|
392 |
+ if (playbackIndex < recordedStepsCount) { |
|
393 |
+ RecordedStep& nextStep = recordedSequence[playbackIndex]; |
|
394 |
+ unsigned long targetTime = playbackStartTime + nextStep.timeOffsetMs; |
|
395 |
+ if (millis() >= targetTime) { |
|
396 |
+ myServos[nextStep.servoIndex].write(nextStep.servoAngle); |
|
397 |
+ playbackIndex++; |
|
398 |
+ } |
|
399 |
+ } else { |
|
400 |
+ currentState = IDLE; |
|
401 |
+ Serial.println(F("\nPlayback finished.")); |
|
206 | 402 |
} |
403 |
+ } // End Playback Logic |
|
207 | 404 |
|
208 |
- // --- Validate overall command format FIRST --- |
|
209 |
- bool formatIsValid = command.startsWith("{") && command.endsWith("}"); |
|
405 |
+ // === Recording Check (Memory Limit handled in processSegmentC) === |
|
406 |
+ // Optional: Timeout check could be re-added here if desired |
|
407 |
+ // if (currentState == RECORDING && millis() - recordingStartTime >= RECORDING_TIMEOUT_MS) { ... } |
|
210 | 408 |
|
211 |
- // --- Guard Clause: If format is NOT valid, print error and exit this iteration --- |
|
212 |
- if (!formatIsValid) { |
|
213 |
- Serial.print("Error: Invalid command format '"); |
|
214 |
- Serial.print(command); |
|
215 |
- Serial.println("'. Expected {S<index>:<angle>,...}"); |
|
216 |
- return; // Stop processing this invalid command |
|
217 |
- } |
|
218 | 409 |
|
219 |
- // --- Format IS Valid: Proceed with processing --- |
|
410 |
+ // === Command Processing Logic (Reading from Serial) === |
|
411 |
+ while (Serial.available() > 0 && commandInputIndex < MAX_COMMAND_LEN - 1) { |
|
412 |
+ char receivedChar = Serial.read(); |
|
413 |
+ if (receivedChar == '\n' || receivedChar == '\r') { |
|
414 |
+ if (commandInputIndex > 0) { |
|
415 |
+ commandInputBuffer[commandInputIndex] = '\0'; |
|
416 |
+ processCommand(commandInputBuffer); |
|
417 |
+ } |
|
418 |
+ commandInputIndex = 0; |
|
419 |
+ break; |
|
420 |
+ } else if (isprint(receivedChar)) { |
|
421 |
+ commandInputBuffer[commandInputIndex++] = receivedChar; |
|
422 |
+ } |
|
423 |
+ } |
|
424 |
+ // Buffer overflow check |
|
425 |
+ if (commandInputIndex >= MAX_COMMAND_LEN - 1) { |
|
426 |
+ Serial.println(F("Error: Command buffer overflow. Command discarded.")); |
|
427 |
+ commandInputIndex = 0; |
|
428 |
+ while(Serial.available() > 0 && Serial.read() != '\n'); // Clear rest of serial line |
|
429 |
+ } |
|
220 | 430 |
|
221 |
- // Remove the braces to get the content string |
|
222 |
- String content = command.substring(1, command.length() - 1); |
|
223 |
- content.trim(); // Trim again in case of spaces like { S0:90 } |
|
224 |
- |
|
225 |
- // Handle empty content like {} |
|
226 |
- if (content.length() == 0) { |
|
227 |
- Serial.println("Received empty command: {}"); |
|
228 |
- return; // Nothing to process |
|
229 |
- } |
|
230 |
- |
|
231 |
- // Print command start confirmation |
|
232 |
- Serial.print("Processing: {"); |
|
233 |
- |
|
234 |
- // Process each command segment separated by commas |
|
235 |
- int startIndex = 0; |
|
236 |
- bool firstSegmentOutput = true; // Flag to manage comma printing in feedback |
|
237 |
- |
|
238 |
- while (startIndex < content.length()) { |
|
239 |
- // Extract the next segment based on comma delimiter |
|
240 |
- int commaIndex = content.indexOf(',', startIndex); |
|
241 |
- String currentSegment; |
|
242 |
- |
|
243 |
- if (commaIndex == -1) { |
|
244 |
- // No more commas - this is the last segment |
|
245 |
- currentSegment = content.substring(startIndex); |
|
246 |
- startIndex = content.length(); // Set index to exit loop after this segment |
|
247 |
- } else { |
|
248 |
- // Extract segment before the next comma |
|
249 |
- currentSegment = content.substring(startIndex, commaIndex); |
|
250 |
- startIndex = commaIndex + 1; // Move start index past the comma for next iteration |
|
251 |
- } |
|
252 |
- |
|
253 |
- currentSegment.trim(); // Clean up the individual segment (remove spaces) |
|
254 |
- |
|
255 |
- // Skip empty segments that might result from extra commas (e.g., {S0:90,,S1:90}) |
|
256 |
- if (currentSegment.length() == 0) { |
|
257 |
- continue; // Go to the next iteration of the while loop |
|
258 |
- } |
|
259 |
- |
|
260 |
- // Process the extracted segment using the helper function |
|
261 |
- processSegment(currentSegment, firstSegmentOutput); |
|
262 |
- |
|
263 |
- } // End while loop processing segments |
|
264 |
- |
|
265 |
- Serial.println("}"); // End the feedback line for the processed command |
|
266 |
- |
|
267 |
- } // End if Serial.available() |
|
268 |
- |
|
269 |
- // No delay() needed here in most cases. The loop runs again quickly. |
|
270 |
- // Other non-blocking code (reading sensors, checking buttons) could go here. |
|
271 |
-} |
|
431 |
+} // End main loop |
Add a comment
Delete comment
Once you delete this comment, you won't be able to recover it. Are you sure you want to delete this comment?