├── .gitignore ├── Mower Voltage Divider values.png ├── README.md ├── controller └── controller.ino └── mower └── mower.ino /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea/ -------------------------------------------------------------------------------- /Mower Voltage Divider values.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Petersoj/LawnMower/master/Mower Voltage Divider values.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lawn Mower (Engineering Capstone Project) 2 | This is the repository for the remote-controlled (soon to be autonomous) lawn mower engineering capstone project. It contains the Lawn Mower Arduino code (may be Raspberry Pi Python code in the future) and the Remote Controller Arduino code. -------------------------------------------------------------------------------- /controller/controller.ino: -------------------------------------------------------------------------------- 1 | /* 2 | * April 2018 - Jacob Peterson 3 | * arduino@jacobpeterson.net 4 | */ 5 | /* 6 | * This is the sketch that will run on the the controller Aduino 7 | * to control: wireless transceivers, OLED display, lawn mower 8 | * throttle and turning, and more. 9 | */ 10 | 11 | // Libraries 12 | #include 13 | #include 14 | #include 15 | #include 16 | //#include use later for cool fx :) 17 | #include 18 | 19 | 20 | // Setup Display 21 | //#define OLED_RESET 4 22 | const int OLED_RESET = 4; 23 | Adafruit_SSD1306 display(OLED_RESET); 24 | 25 | #if (SSD1306_LCDHEIGHT != 64) 26 | #error("Height incorrect, please fix Adafruit_SSD1306.h!"); 27 | #endif 28 | 29 | 30 | // Setup Radio 31 | RF24 radio(7, 8); // CE, CSN pins 32 | 33 | // Addresses used by radio 34 | const byte mowerWritingAddress[] = "mower"; 35 | const byte controllerWritingAddress[] = "cntrl"; 36 | 37 | // struct for receiving data from mower 38 | typedef struct { 39 | byte batteryPercentage; 40 | byte currentDraw; // In Amps 41 | } 42 | ReceiveRadioData; 43 | 44 | // struct for transmitting data to mower 45 | typedef struct { 46 | byte throttle; // 0-100 47 | byte turnRight; // 0-255 48 | byte turnLeft; // 0-255 49 | boolean backwards; // Uses right joystick moved all the way down. 50 | } 51 | TransmitRadioData; 52 | 53 | // Construct RadioData 54 | TransmitRadioData transmitRadioData; 55 | ReceiveRadioData receiveRadioData; 56 | 57 | // - Pinout - 58 | // NRF24 pins 59 | // CE - 7 60 | // CSN - 8 61 | // SCK - 13 62 | // MOSI - 11 63 | // MISO - 12 64 | // SSD1306 Display pins (i2c) 65 | // SDA - A4 66 | // SCL - A5 67 | //const byte leftTrimPin = 4; 68 | //const byte rightTrimPin = 5; 69 | //const byte autoTurnRightPin = 6; 70 | //const byte autoTurnLeftPin = 9; 71 | // Analog In pins 72 | const byte throttlePin = A0; 73 | const byte turnPin = A1; 74 | //const byte backwardsPin = A2; 75 | //const byte batteryVoltagePin = A3; 76 | 77 | void setup() { 78 | // Configure pin modes 79 | 80 | 81 | // Configure OLED information screen 82 | display.begin(SSD1306_SWITCHCAPVCC, 0x3C); // initialize with the I2C addr 0x3D 83 | // Show image buffer on the display hardware. 84 | // Since the buffer is intialized with an Adafruit splashscreen 85 | // internally, this will display the splashscreen. 86 | display.display(); 87 | delay(500); 88 | // Clear the buffer. 89 | display.clearDisplay(); 90 | 91 | display.clearDisplay(); 92 | display.setCursor(0, 0); 93 | display.setTextSize(2); 94 | display.print("Throttle: "); 95 | display.setCursor(10, 0); 96 | display.display(); 97 | 98 | // Configure transceiver 99 | radio.begin(); 100 | radio.setChannel(122); 101 | radio.setPALevel(RF24_PA_MAX); 102 | radio.setDataRate(RF24_250KBPS); 103 | // radio.setPayloadSize(max(sizeof(transmitRadioData), sizeof(receiveRadioData))); 104 | radio.openWritingPipe(controllerWritingAddress); 105 | // radio.openReadingPipe(0, mowerWritingAddress); 106 | 107 | radio.stopListening(); // Temporary until transceiving is functional 108 | 109 | Serial.begin(9600); 110 | } 111 | 112 | void loop() { 113 | executeRadioLogic(); 114 | 115 | updateAnalogValues(); 116 | 117 | updateScreen(); 118 | 119 | delay(40); 120 | 121 | Serial.println(transmitRadioData.turnLeft); 122 | Serial.println(transmitRadioData.turnRight); 123 | } 124 | 125 | byte unsuccessfulTxIndex = 0; // Counts unsuccessful writes 126 | void executeRadioLogic() { 127 | // Temporarly only acting as a receiver. 128 | 129 | // Write transmit data 130 | radio.write(&transmitRadioData, sizeof(transmitRadioData)); 131 | 132 | 133 | // TODO Fix transceiving logic 134 | 135 | // This radio logic is primarily acting as the receiver of data 136 | // although we do send necessary data back. 137 | 138 | // if (radio.available()) { // Check if data in rx buffer 139 | // while (radio.available()) { // Get last data if multiple packets in buffer 140 | // radio.read(&receiveRadioData, sizeof(receiveRadioData)); // Read into struct obj 141 | // } 142 | // 143 | // // Close radio buffer and prepare for writing data 144 | // radio.stopListening(); 145 | // 146 | // if (!radio.write(&transmitRadioData, sizeof(transmitRadioData))) { 147 | // unsuccessfulTxIndex++; 148 | // } else { 149 | // unsuccessfulTxIndex = 0; 150 | // } 151 | // 152 | // // Open radio buffer and prepare for reading data 153 | // radio.startListening(); 154 | // 155 | // if (unsuccessfulTxIndex > 3) { // 3 times in a row data tx was unsuccessful 156 | // // TODO inform user of error 157 | // } 158 | // Serial.println(receiveRadioData.currentDraw); 159 | // } 160 | } 161 | 162 | void updateAnalogValues() { 163 | 164 | int analogThrottle = analogRead(throttlePin); 165 | if (analogThrottle < 50) { // Clamp to prevent floating max 166 | analogThrottle = 50; 167 | } else if (analogThrottle > 950) { // Clamp to prevent floating min 168 | analogThrottle = 950; 169 | } 170 | byte finalThrottle = map(analogThrottle, 950, 50, 0, 255); 171 | transmitRadioData.throttle = finalThrottle; 172 | 173 | int analogTurn = analogRead(turnPin); 174 | byte finalTurnRight = 0; 175 | byte finalTurnLeft = 0; 176 | if (analogTurn < 450) { 177 | finalTurnRight = map(analogTurn, 450, 0, 0, 255); 178 | } 179 | if (analogTurn > 650) { 180 | finalTurnLeft = map(analogTurn, 0, 650, 0, 255); 181 | } 182 | 183 | transmitRadioData.turnRight = finalTurnRight; 184 | transmitRadioData.turnLeft = finalTurnLeft; 185 | 186 | // 1023? basically no throttle 187 | // 0? full throttle 188 | 189 | // int analogThrottle = analogRead(A4); 190 | // Serial.println(analogThrottle); 191 | 192 | // Turning analog 193 | // Less resistance is moving right (high number) 194 | // 2.6 middle 195 | // 4.45 max 196 | // 2.6/4.45 = 0.5842696629 197 | 198 | // Throttle Low resistance = 0.59 199 | // Max 4.52 200 | } 201 | 202 | 203 | // Method will update the OLED info screen. 204 | void updateScreen() { 205 | display.print(transmitRadioData.throttle); 206 | display.display(); 207 | } 208 | 209 | 210 | -------------------------------------------------------------------------------- /mower/mower.ino: -------------------------------------------------------------------------------- 1 | /* 2 | * April 2018 - Jacob Peterson 3 | * arduino@jacobpeterson.net 4 | */ 5 | /* 6 | * This is the sketch that will run on the the lawn mower Arduino to control: 7 | * Left/Right Wheel Window Motor Controllers, Blade CIM Motor Controller, 8 | * wireless transceivers, and more. 9 | * 10 | * Motor Controllers used: 13A continuous VNH2SP30 (from china ebay!) 11 | * Radio Transceivers used: NRF24L01+ 12 | * 13 | * 14 | * Features to add to RC version of Lawn Mower: 15 | * - Use CS pins to determine if kill switch was flipped 16 | * - Sending error messages to controller for diagnosis info 17 | * - Remeber path using a 'record' button 18 | */ 19 | 20 | #include 21 | #include 22 | #include 23 | 24 | 25 | // Construct RF24 radio 26 | RF24 radio(7, 8); // CE, CSN pins 27 | 28 | // Addresses used by radio 29 | const byte mowerWritingAddress[] = "mower"; 30 | const byte controllerWritingAddress[] = "cntrl"; 31 | 32 | // struct for transmitting data to controller 33 | typedef struct { 34 | byte batteryPercentage; 35 | byte currentDraw; // In Amps 36 | } 37 | TransmitRadioData; 38 | 39 | // struct for receiving data from controller 40 | typedef struct { 41 | byte throttle; // 0-255 42 | byte turnRight; // 0-255 43 | byte turnLeft; // 0-255 44 | boolean backwards; // Uses right joystick moved all the way down. 45 | } 46 | ReceiveRadioData; 47 | 48 | // Construct RadioData 49 | TransmitRadioData transmitRadioData; 50 | ReceiveRadioData receiveRadioData; 51 | 52 | // - Pinout - unused: 10, A4, A5 53 | // NRF24 pins 54 | // CE - 7 55 | // CSN - 8 56 | // SCK - 13 57 | // MOSI - 11 58 | // MISO - 12 59 | const byte relayPin = 4; 60 | const byte bladePWMPin = 9; // Blade Controller InA pin should connect to 5V 61 | const byte leftPWMPin = 5; 62 | const byte rightPWMPin = 6; 63 | const byte leftDriveInAPin = 2; // LOW for forward 64 | const byte leftDriveInBPin = 3; // HIGH for forward 65 | const byte rightDriveInAPin = 0; // HIGH for forward 66 | const byte rightDriveInBPin = 1; // LOW for forward 67 | // Analog In pins 68 | const byte batteryVoltagePin = A0; 69 | const byte bladeCurrentPin = A1; 70 | const byte leftDriveCurrentPin = A2; 71 | const byte rightDriveCurrentPin = A3; 72 | 73 | const byte loopDelay = 20; // Loop executes 50 times a second 74 | float voltageDividerMaxInput = 5; // Raw divided voltage input of a charged battery. 75 | float voltageDividerMinInput = 3.4; // Raw divided voltage input of a dead battery 76 | // byte throttle = 0; // PWM value of throttle after low pass filter 77 | 78 | // TODO 79 | // Use CS pins to determine if kill switch was flipped 80 | 81 | void setup() { 82 | // Pin modes for relay and Motor Controller In A/B pins 83 | pinMode(0, OUTPUT); 84 | pinMode(1, OUTPUT); 85 | pinMode(2, OUTPUT); 86 | pinMode(3, OUTPUT); 87 | pinMode(4, OUTPUT); 88 | pinMode(5, OUTPUT); 89 | pinMode(6, OUTPUT); 90 | pinMode(9, OUTPUT); 91 | 92 | // Turn on relay pin for latching circuit (to keep power on) 93 | digitalWrite(relayPin, HIGH); 94 | 95 | // Set fromHigh var according to predefined value 96 | // for mapping battery voltage to percentage 97 | voltageDividerMaxInput = (voltageDividerMaxInput / 5) * 1023; 98 | 99 | // Set fromLow var according to predefined value 100 | // for mapping battery voltage to percentage 101 | voltageDividerMinInput = (voltageDividerMinInput / 5) * 1023; 102 | 103 | // Configure transceiver 104 | radio.begin(); 105 | radio.setChannel(122); 106 | radio.setPALevel(RF24_PA_MAX); 107 | radio.setDataRate(RF24_250KBPS); 108 | // radio.setPayloadSize(max(sizeof(transmitRadioData), sizeof(receiveRadioData))); 109 | // radio.openWritingPipe(mowerWritingAddress); 110 | radio.openReadingPipe(0, controllerWritingAddress); 111 | 112 | radio.startListening(); // Temporary until transceiving is functional 113 | } 114 | 115 | void loop() { 116 | 117 | // checkBattery(); // Reads battery voltage, updates, and checks values 118 | // checkCurrent(); // Reads motor current draw, updates, and checks values 119 | 120 | executeRadioLogic(); // Loop radio logic (rx/tx data and check for rx/tx loss) 121 | 122 | // filterThrottleGainValue(); // Loops low pass filter for gradual throttle increase 123 | 124 | driveLogic(); // Updates driving/blade motor values 125 | 126 | delay(loopDelay); // Allow time for rx buffer to fill and values to update/change 127 | } 128 | 129 | // This method powers off everything using the optocoupler relay. 130 | // This method may not power everything off instantly as 131 | // the relay has a 5ms release time. 132 | void poweroff() { 133 | // Disconnect relay NO which creates an open circuit and 134 | // disconnects everything from power. 135 | digitalWrite(relayPin, LOW); 136 | } 137 | 138 | byte unsuccessfulRxIndex = 0; // Counts unsuccessful reads 139 | void executeRadioLogic() { 140 | // Temporarly only acting as a receiver. 141 | 142 | if (radio.available()) { // Check if data in rx buffer 143 | radio.read(&receiveRadioData, sizeof(receiveRadioData)); // Read into struct obj 144 | unsuccessfulRxIndex = 0; // Clear unsuccessful reads 145 | } else { 146 | unsuccessfulRxIndex++; 147 | 148 | if (unsuccessfulRxIndex > 100) { // Check if lost control for more than ~2 seconds 149 | poweroff(); 150 | // Controller radio did not send anything or lost connection and unsuccessful 151 | // data tx/rx threshold was met so power off arduino & circuity completely. 152 | // This is a safety feature as the controller can never go out of range 153 | // and have the lawn mower still operate. Someone must go and turn back 154 | // on the lawn mower as kind of a reminder to stay in range :) 155 | } 156 | } 157 | 158 | 159 | 160 | // TODO Fix transceiving later 161 | 162 | // This radio logic is primarily acting as the transmitter of data 163 | // although we do receive data needed from controller. 164 | 165 | // Close radio buffer and prepare for writing data 166 | // radio.stopListening(); 167 | 168 | // Write transmit data 169 | // if (!radio.write(&transmitRadioData, sizeof(transmitRadioData))) { 170 | // // Not radio acknowledgement was sent back and the RX/TX may be out of range! 171 | // unsuccessfulRxTxIndex++; 172 | // } else { 173 | // unsuccessfulRxTxIndex = 0; 174 | // } 175 | // radio.write(&transmitRadioData, sizeof(transmitRadioData)); 176 | 177 | // Open radio buffer and prepare for reading data 178 | // radio.startListening(); 179 | 180 | // // Loop to allow time for tx radio to send data and fill rx buffer 181 | // unsigned long startingListeningTime = millis(); 182 | // while (!radio.available()) { // Loop until we have data available in radio buffer 183 | // if (millis() - startingListeningTime > 300) { // Listening timeout is 200ms 184 | // unsuccessfulRxIndex++; 185 | // break; 186 | // } 187 | // } 188 | // if (radio.available()) { // Check if data in rx buffer 189 | // radio.read(&receiveRadioData, sizeof(receiveRadioData)); // Read into struct obj 190 | // unsuccessfulRxIndex = 0; // Successful read so reset unsuccessful index 191 | // } 192 | // 193 | // if (unsuccessfulRxTxIndex > 5) { // Threshold met of max ~1 second of dropped rx/tx 194 | // Controller radio did not send anything or lost connection and unsuccessful 195 | // data tx/rx threshold was met so power off arduino & circuity completely. 196 | // This is a safety feature as the controller can never go out of range 197 | // and have the lawn mower still operate. Someone must go and turn back 198 | // on the lawn mower as kind of a reminder to stay in range :) 199 | // poweroff(); 200 | // } 201 | } 202 | 203 | boolean initilized = false; // Value to start motor controller init 204 | void driveLogic() { 205 | if (!initilized) { 206 | initilized = true; 207 | 208 | // Setup right motor controller 209 | digitalWrite(rightDriveInBPin, LOW); // Low at first to prevent braking 210 | digitalWrite(rightDriveInAPin, HIGH); 211 | 212 | // Setup left motor controller 213 | digitalWrite(leftDriveInBPin, HIGH); // Low at first to prevent braking 214 | digitalWrite(leftDriveInAPin, LOW); 215 | } 216 | 217 | // Subtracting values to allow other drive motor to 'push' to the desired direction 218 | int rightPWMValue = receiveRadioData.throttle - receiveRadioData.turnRight; 219 | if (rightPWMValue < 0) { // Clamp low to 0 220 | rightPWMValue = 0; 221 | } 222 | analogWrite(rightPWMPin, rightPWMValue); // Set PWM signal for right motor 223 | 224 | // Subtracting values to allow other drive motor to 'push' to the desired direction 225 | int leftPWMValue = receiveRadioData.throttle - receiveRadioData.turnLeft; 226 | if (leftPWMValue < 0) { // Clamp low to 0 227 | leftPWMValue = 0; 228 | } 229 | analogWrite(leftPWMPin, leftPWMValue); // Set PWM signal for left motor 230 | } 231 | 232 | //boolean rightDriveDirectionChange = true; // True to initialize InA/B 233 | //boolean leftDriveDirectionChange = true; // True to initialize InA/B 234 | //boolean backwardsPreviousValue = false; // Value to check if 'backwards' changed 235 | //void driveLogic() { 236 | // // Check if forward/reverse was changed and change values accordingly 237 | // if (backwardsPreviousValue && !receiveRadioData.backwards) { 238 | // backwardsPreviousValue = receiveRadioData.backwards; 239 | // rightDriveDirectionChange = false; 240 | // leftDriveDirectionChange = false; 241 | // } else if (!backwardsPreviousValue && receiveRadioData.backwards) { 242 | // backwardsPreviousValue = receiveRadioData.backwards; 243 | // rightDriveDirectionChange = true; 244 | // leftDriveDirectionChange = true; 245 | // } 246 | // 247 | // // Right Drive Motor 248 | // if (rightDriveDirectionChange) { 249 | // if (receiveRadioData.backwards & receiveRadioData.throttle == 0) { 250 | // digitalWrite(rightDriveInAPin, LOW); // Low at first to prevent braking 251 | // digitalWrite(rightDriveInBPin, HIGH); 252 | // } else { 253 | // digitalWrite(rightDriveInBPin, LOW); // Low at first to prevent braking 254 | // digitalWrite(rightDriveInAPin, HIGH); 255 | // } 256 | // } 257 | // // Subtracting values to allow other drive motor to 'push' to the desired direction 258 | // int rightPWMValue = receiveRadioData.throttle - receiveRadioData.turnRight; 259 | // if (rightPWMValue < 0) { // Clamp low to 0 260 | // rightPWMValue = 0; 261 | // } 262 | // analogWrite(rightPWMPin, rightPWMValue); // Set PWM signal for right motor 263 | // 264 | // // Left Drive Motor 265 | // if (leftDriveDirectionChange) { 266 | // if (receiveRadioData.backwards & receiveRadioData.throttle == 0) { 267 | // digitalWrite(leftDriveInAPin, LOW); // Low at first to prevent braking 268 | // digitalWrite(leftDriveInBPin, HIGH); 269 | // } else { 270 | // digitalWrite(leftDriveInBPin, LOW); // Low at first to prevent braking 271 | // digitalWrite(leftDriveInAPin, HIGH); 272 | // } 273 | // } 274 | // // Subtracting values to allow other drive motor to 'push' to the desired direction 275 | // int leftPWMValue = receiveRadioData.throttle - receiveRadioData.turnLeft; 276 | // if (leftPWMValue < 0) { // Clamp low to 0 277 | // leftPWMValue = 0; 278 | // } 279 | // analogWrite(leftPWMPin, leftPWMValue); // Set PWM signal for left motor 280 | //} 281 | 282 | byte previousValue = 0; // Previously passed throttle value 283 | short timeDelay = 2000; // 2 Seconds to fully change to actual value 284 | void filterThrottleGainValue() { // A low pass filter to gradually increase throttle 285 | // TODO 286 | } 287 | 288 | void checkCurrent() { 289 | // Current Readings logic - used for checking if there is an 290 | // excessive current draw from one of the motors. If there is, 291 | // it's possible that the motors are drawing their stall current 292 | // and are blocked by something so power off everything (safety first!) 293 | int leftDriveCurrent = readCurrentSense(leftDriveCurrentPin); 294 | int rightDriveCurrent = readCurrentSense(rightDriveCurrentPin); 295 | int bladeCurrent = readCurrentSense(bladeCurrentPin); 296 | if (leftDriveCurrent > 4 || rightDriveCurrent > 4 || bladeCurrent > 5) { 297 | poweroff(); 298 | } 299 | } 300 | 301 | byte batteryReadIndex = 0; // Tracks how many times loop has executed 302 | byte oneSecondsTime = 1000 / loopDelay; // Calc one seconds time from loopDelay 303 | void checkBattery() { 304 | // Battery percentage update 305 | if (batteryReadIndex > oneSecondsTime) { // Only update every second 306 | transmitRadioData.batteryPercentage = getBatteryPercentage(); 307 | batteryReadIndex = 0; 308 | } else { 309 | batteryReadIndex++; 310 | } 311 | 312 | if (transmitRadioData.batteryPercentage < 3) { // Lower than 3% shut off 313 | poweroff(); 314 | } 315 | } 316 | 317 | int getBatteryPercentage() { 318 | // Maps analog input from battery voltage divider to a percentage 319 | float rawVoltage = analogInputToVolts(analogRead(batteryVoltagePin)); 320 | return map(rawVoltage, voltageDividerMinInput, voltageDividerMaxInput, 0.0, 100.0); 321 | } 322 | 323 | // Returns the current (in Amps) using the analog input of the Current Sense 324 | // pins on the Motor Controllers. (This may be inaccurate to some degree) 325 | int readCurrentSense(int currentSenseAnalogPin) { 326 | // Read voltage from CS analog in pin and convert to amps 327 | // CS pin of Motor Controller - 0.13 Volts/Amp 328 | return analogInputToVolts(analogRead(currentSenseAnalogPin)) / 0.13; 329 | } 330 | 331 | void autoTurnClockwise() { 332 | // TODO 333 | } 334 | 335 | void autoTurnCounterClockwise() { 336 | // TODO 337 | } 338 | 339 | 340 | // Utility Functions 341 | 342 | // Converts 0-1023 from analog input to a voltage. 343 | float analogInputToVolts(int input) { 344 | return 5 - (input * (5.0 / 1023.0)); 345 | } 346 | --------------------------------------------------------------------------------