├── README.md ├── cars ├── acceltest │ └── main.go ├── autonomous │ └── main.go ├── camtest │ └── main.go ├── hello │ ├── README.md │ └── main.go ├── joycar │ └── main.go ├── keyboardcar │ ├── README.md │ └── main.go ├── oledtest │ └── main.go └── servotest │ └── main.go ├── diagram.txt ├── images ├── arch.png ├── gophercar-fpv.gif └── gophercon2018.gif └── runner.sh /README.md: -------------------------------------------------------------------------------- 1 | # Gophercar 2 | 3 | ![Gophercar FPV](https://github.com/hybridgroup/gophercar/blob/master/images/gophercar-fpv.gif?raw=true) 4 | 5 | Gophercar is a DIY platform for self-driving miniature cars like Donkeycar ([http://www.donkeycar.com/](http://www.donkeycar.com/)), but is written in the Go programming language. The idea is to make Gophercar able to run on any of the supported Donkeycar cars/hardware without any modification. 6 | 7 | ## How it works 8 | 9 | ![Arch](https://github.com/hybridgroup/gophercar/blob/master/images/arch.png?raw=true) 10 | 11 | ## Car and Controller Hardware 12 | 13 | - Exceed Short Course Truck (https://www.amazon.com/Exceed-Racing-Desert-Course-2-4ghz/dp/9269802094) 14 | - Donkeycar chassis kit(https://squareup.com/store/donkeycar/item/desert-monster-short-course-truck-or-blaze-partial-kit) 15 | - Raspberry Pi 3 Model B+ 16 | - Raspberry Pi wide-angle camera (included in Donkeycar kit) 17 | - PCA9685 I2C servo driver (included in Donkeycar kit) 18 | - SSD1306 I2C OLED display (optional) 19 | - MPU6050 I2C Accelerometer/Gyroscope (optional) 20 | 21 | ## Car OS Software 22 | 23 | The following needs to be installed on a bootable SD card for the Raspi: 24 | 25 | - Raspbian Stretch OS 26 | - Go v1.10+ 27 | - OpenCV 3.4.2 28 | - SDL2 v2.0.8+ 29 | - Movidius NCS SDK (optional) 30 | 31 | The following OS features must be enabled: 32 | 33 | - I2C 34 | - Camera 35 | 36 | You will also need to update the kernel on the Raspbian Pi to v4.14+ 37 | 38 | sudo rpi-update 39 | 40 | ## Current workflow 41 | 42 | - Edit your car in a sub-directory of the `cars` directory. 43 | - To transfer your code to the car, compile it on the car, and then run it: 44 | ./runner.sh hello 192.168.1.42 45 | 46 | This copies the code to the Raspberry Pi, compiles it on the Pi, and then executes it. 47 | 48 | ## Future workflow 49 | 50 | - Install the Gophercar Docker container to cross-compiling for Raspian easier, due to using binary libaries such as OpenCV/GoCV 51 | - Compile the code to run on your car 52 | - Copy the compiled executable to your car's controller using scp 53 | - Execute the car code on the car controller 54 | - Drive! 55 | 56 | ## Cars 57 | 58 | The `cars` directory will contain various car controller programs. Choose one to compile and put on your car controller. 59 | 60 | ![Gophercon 2018](https://github.com/hybridgroup/gophercar/blob/master/images/gophercon2018.gif?raw=true) -------------------------------------------------------------------------------- /cars/acceltest/main.go: -------------------------------------------------------------------------------- 1 | // this does not really do anything yet except connect to all of the various devices 2 | package main 3 | 4 | import ( 5 | "fmt" 6 | "time" 7 | 8 | "gobot.io/x/gobot" 9 | "gobot.io/x/gobot/drivers/i2c" 10 | "gobot.io/x/gobot/platforms/raspi" 11 | ) 12 | 13 | var ( 14 | r *raspi.Adaptor 15 | mpu6050 *i2c.MPU6050Driver 16 | ) 17 | 18 | func main() { 19 | r = raspi.NewAdaptor() 20 | mpu6050 = i2c.NewMPU6050Driver(r) 21 | 22 | work := func() { 23 | gobot.Every(100*time.Millisecond, func() { 24 | handleAccel() 25 | }) 26 | } 27 | 28 | robot := gobot.NewRobot("gophercar", 29 | []gobot.Connection{r}, 30 | []gobot.Device{mpu6050}, 31 | work, 32 | ) 33 | 34 | robot.Start() 35 | } 36 | 37 | func handleAccel() { 38 | mpu6050.GetData() 39 | 40 | fmt.Println("Accelerometer", mpu6050.Accelerometer) 41 | fmt.Println("Gyroscope", mpu6050.Gyroscope) 42 | fmt.Println("Temperature", mpu6050.Temperature) 43 | } 44 | -------------------------------------------------------------------------------- /cars/autonomous/main.go: -------------------------------------------------------------------------------- 1 | // What it does: 2 | // 3 | // This is the code from the self-driving car that completed the course at Gophercon 2018. 4 | // Written by @erutherford and friends, but then modified by @deadprogram 5 | // 6 | // This example also streams MJPEG video from the car's camera. 7 | // Once running point your browser to the hostname/port you passed in the 8 | // command line (for example http://localhost:8080) and you should see 9 | // the live video stream. 10 | // 11 | // How to run: 12 | // 13 | // autonomous [camera ID] [host:port] 14 | // 15 | // go get -u github.com/hybridgroup/mjpeg 16 | // sudo modprobe bcm2835-v4l2 17 | // go run ./cars/autonomous/main.go 0 0.0.0.0:8080 18 | // 19 | 20 | package main 21 | 22 | import ( 23 | "fmt" 24 | "log" 25 | "math" 26 | "net/http" 27 | "os" 28 | "strconv" 29 | "sync/atomic" 30 | "time" 31 | 32 | "image" 33 | "image/color" 34 | 35 | "github.com/fogleman/gg" 36 | "github.com/hybridgroup/mjpeg" 37 | "gobot.io/x/gobot" 38 | "gobot.io/x/gobot/drivers/i2c" 39 | "gobot.io/x/gobot/platforms/raspi" 40 | "gocv.io/x/gocv" 41 | ) 42 | 43 | var ( 44 | // cv related 45 | deviceID int 46 | err error 47 | webcam *gocv.VideoCapture 48 | stream *mjpeg.Stream 49 | 50 | // car related 51 | r *raspi.Adaptor 52 | pca9685 *i2c.PCA9685Driver 53 | mpu6050 *i2c.MPU6050Driver 54 | 55 | ctx *gg.Context 56 | 57 | // self-driving 58 | steering, throttle atomic.Value 59 | 60 | throttleZero = 350 61 | ) 62 | 63 | func main() { 64 | if len(os.Args) < 4 { 65 | fmt.Println("How to run:\n\tmjpeg-streamer [camera ID] [host:port] [throttle]") 66 | return 67 | } 68 | 69 | // parse args 70 | deviceID := os.Args[1] 71 | host := os.Args[2] 72 | t, _ := strconv.ParseFloat(os.Args[3], 64) 73 | 74 | steering.Store(float64(0.0)) 75 | throttle.Store(float64(0.0)) 76 | 77 | r = raspi.NewAdaptor() 78 | pca9685 = i2c.NewPCA9685Driver(r) 79 | mpu6050 = i2c.NewMPU6050Driver(r) 80 | 81 | work := func() { 82 | // init the PWM controller 83 | pca9685.SetPWMFreq(60) 84 | 85 | // init the ESC controller for throttle zero 86 | pca9685.SetPWM(0, 0, uint16(throttleZero)) 87 | time.Sleep(300 * time.Millisecond) 88 | throttle.Store(t) 89 | 90 | gobot.Every(100*time.Millisecond, func() { 91 | handleSteering() 92 | handleThrottle() 93 | }) 94 | } 95 | 96 | robot := gobot.NewRobot("gophercar", 97 | []gobot.Connection{r}, 98 | []gobot.Device{pca9685, mpu6050}, 99 | work, 100 | ) 101 | 102 | // open webcam 103 | webcam, err = gocv.OpenVideoCapture(deviceID) 104 | if err != nil { 105 | fmt.Printf("Error opening capture device: %v\n", deviceID) 106 | return 107 | } 108 | defer webcam.Close() 109 | 110 | // create the mjpeg stream 111 | stream = mjpeg.NewStream() 112 | 113 | // start capturing 114 | go capture() 115 | 116 | fmt.Println("Capturing. Point your browser to " + host) 117 | 118 | // start http server 119 | http.Handle("/", stream) 120 | go func() { log.Fatal(http.ListenAndServe(host, nil)) }() 121 | 122 | robot.Start() 123 | } 124 | 125 | // capture video and process it to perform autonomous driving. 126 | func capture() { 127 | img := gocv.NewMat() 128 | defer img.Close() 129 | 130 | if ok := webcam.Read(&img); ok { 131 | gocv.IMWrite("/tmp/img.jpg", img) 132 | } 133 | for { 134 | if ok := webcam.Read(&img); !ok { 135 | fmt.Printf("Device closed: %v\n", deviceID) 136 | return 137 | } 138 | if img.Empty() { 139 | continue 140 | } 141 | 142 | img, rawSteering := processVision(img) 143 | applySteeringCurve(rawSteering) 144 | 145 | buf, _ := gocv.IMEncode(".jpg", img) 146 | stream.UpdateJPEG(buf) 147 | } 148 | } 149 | 150 | func applySteeringCurve(raw float64) { 151 | steering.Store(float64(raw) * -7) 152 | } 153 | 154 | func handleSteering() { 155 | steeringVal := getSteeringPulse(steering.Load().(float64)) 156 | pca9685.SetPWM(1, 0, uint16(steeringVal)) 157 | } 158 | 159 | func handleThrottle() { 160 | throttleVal := getThrottlePulse(throttle.Load().(float64)) 161 | pca9685.SetPWM(0, 0, uint16(throttleVal)) 162 | } 163 | 164 | // adjusts the steering from -1.0 (hard left) <-> 1.0 (hardright) to the correct 165 | // pwm pulse values. 166 | func getSteeringPulse(val float64) float64 { 167 | return gobot.Rescale(val, -1, 1, 290, 490) 168 | } 169 | 170 | // adjusts the throttle from -1.0 (hard back) <-> 1.0 (hard forward) to the correct 171 | // pwm pulse values. 172 | func getThrottlePulse(val float64) int { 173 | if val > 0 { 174 | return int(gobot.Rescale(val, 0, 1, 350, 300)) 175 | } 176 | return int(gobot.Rescale(val, -1, 0, 490, 350)) 177 | } 178 | 179 | func round(x, unit float64) float64 { 180 | return math.Round(x/unit) * unit 181 | } 182 | 183 | // processVision processes each frame and returns a Mat with the modified image frame showing the analysis results, 184 | // along with the correct steering direction to keep the car on the track. 185 | func processVision(original gocv.Mat) (gocv.Mat, float64) { 186 | bwImg := gocv.NewMat() 187 | defer bwImg.Close() 188 | blurredImg := gocv.NewMat() 189 | defer blurredImg.Close() 190 | thresholdImg := gocv.NewMat() 191 | defer thresholdImg.Close() 192 | erodedImg := gocv.NewMat() 193 | defer erodedImg.Close() 194 | outputImg := gocv.NewMat() 195 | defer outputImg.Close() 196 | 197 | dim := original.Size() 198 | cropHeight := int(float64(dim[0]) * 0.4) 199 | region := original.Region(image.Rectangle{image.Point{0, cropHeight}, image.Point{dim[1], dim[0]}}) 200 | 201 | gocv.CvtColor(region, &bwImg, gocv.ColorBGRToGray) 202 | gocv.GaussianBlur(bwImg, &blurredImg, image.Point{X: 5, Y: 5}, float64(5), float64(5), gocv.BorderDefault) 203 | gocv.Threshold(blurredImg, &thresholdImg, float32(100), float32(255), gocv.ThresholdBinary) 204 | 205 | gocv.Erode(thresholdImg, &erodedImg, gocv.GetStructuringElement(gocv.MorphRect, image.Point{X: 6, Y: 6})) 206 | gocv.Dilate(erodedImg, &outputImg, gocv.GetStructuringElement(gocv.MorphRect, image.Point{X: 6, Y: 6})) 207 | 208 | contours := gocv.FindContours(outputImg, gocv.RetrievalList, gocv.ChainApproxNone) 209 | maxArea := float64(0) 210 | maxContour := 0 211 | 212 | for idx, contour := range contours { 213 | area := gocv.ContourArea(contour) 214 | if area > maxArea { 215 | maxArea = area 216 | maxContour = idx 217 | } 218 | } 219 | 220 | line := gocv.NewMatWithSize(region.Rows(), region.Cols(), gocv.MatTypeCV8U) 221 | gocv.FillPoly(&line, contours[maxContour:maxContour+1], color.RGBA{R: 255, G: 255, B: 255, A: 255}) 222 | M := gocv.Moments(line, true) 223 | 224 | cx := M["m10"] / M["m00"] 225 | 226 | gocv.DrawContours(®ion, contours, maxContour, color.RGBA{R: 255, A: 255}, 3) 227 | dim = region.Size() 228 | centerX := dim[1] / 2 229 | gocv.Line(®ion, image.Point{X: centerX, Y: 0}, image.Point{X: centerX, Y: dim[0]}, color.RGBA{B: 255, A: 255}, 1) 230 | gocv.Circle(®ion, image.Point{X: int(cx), Y: dim[0] / 2}, 1, color.RGBA{G: 255, A: 255}, 2) 231 | 232 | steer := cx/float64(centerX) - 0.5 233 | 234 | return region, steer 235 | } 236 | -------------------------------------------------------------------------------- /cars/camtest/main.go: -------------------------------------------------------------------------------- 1 | // What it does: 2 | // 3 | // This example opens a video capture device, then streams MJPEG from it. 4 | // Once running point your browser to the hostname/port you passed in the 5 | // command line (for example http://localhost:8080) and you should see 6 | // the live video stream. 7 | // 8 | // How to run: 9 | // 10 | // mjpeg-streamer [camera ID] [host:port] 11 | // 12 | // go get -u github.com/hybridgroup/mjpeg 13 | // sudo modprobe bcm2835-v4l2 14 | // go run ./cmd/mjpeg-streamer/main.go 0 0.0.0.0:8080 15 | // 16 | // +build example 17 | 18 | package main 19 | 20 | import ( 21 | "fmt" 22 | "log" 23 | "net/http" 24 | "os" 25 | 26 | "github.com/hybridgroup/mjpeg" 27 | "gocv.io/x/gocv" 28 | ) 29 | 30 | var ( 31 | deviceID int 32 | err error 33 | webcam *gocv.VideoCapture 34 | stream *mjpeg.Stream 35 | ) 36 | 37 | func main() { 38 | if len(os.Args) < 3 { 39 | fmt.Println("How to run:\n\tmjpeg-streamer [camera ID] [host:port]") 40 | return 41 | } 42 | 43 | // parse args 44 | deviceID := os.Args[1] 45 | host := os.Args[2] 46 | 47 | // open webcam 48 | webcam, err = gocv.OpenVideoCapture(deviceID) 49 | if err != nil { 50 | fmt.Printf("Error opening capture device: %v\n", deviceID) 51 | return 52 | } 53 | defer webcam.Close() 54 | 55 | // create the mjpeg stream 56 | stream = mjpeg.NewStream() 57 | 58 | // start capturing 59 | go mjpegCapture() 60 | 61 | fmt.Println("Capturing. Point your browser to " + host) 62 | 63 | // start http server 64 | http.Handle("/", stream) 65 | log.Fatal(http.ListenAndServe(host, nil)) 66 | } 67 | 68 | func mjpegCapture() { 69 | img := gocv.NewMat() 70 | defer img.Close() 71 | 72 | for { 73 | if ok := webcam.Read(&img); !ok { 74 | fmt.Printf("Device closed: %v\n", deviceID) 75 | return 76 | } 77 | if img.Empty() { 78 | continue 79 | } 80 | 81 | buf, _ := gocv.IMEncode(".jpg", img) 82 | stream.UpdateJPEG(buf) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /cars/hello/README.md: -------------------------------------------------------------------------------- 1 | # Hello 2 | 3 | This car does not really do anything except connect to all of the various devices. 4 | -------------------------------------------------------------------------------- /cars/hello/main.go: -------------------------------------------------------------------------------- 1 | // this does not really do anything yet except connect to all of the various devices 2 | package main 3 | 4 | import ( 5 | "math" 6 | "time" 7 | 8 | "github.com/fogleman/gg" 9 | "gobot.io/x/gobot" 10 | "gobot.io/x/gobot/drivers/i2c" 11 | "gobot.io/x/gobot/platforms/raspi" 12 | ) 13 | 14 | var ( 15 | r *raspi.Adaptor 16 | pca9685 *i2c.PCA9685Driver 17 | oled *i2c.SSD1306Driver 18 | mpu6050 *i2c.MPU6050Driver 19 | 20 | ctx *gg.Context 21 | ) 22 | 23 | var ( 24 | steering = 0.0 25 | steeringDirection = "right" 26 | throttle = 0.0 27 | throttleDirection = "up" 28 | 29 | throttleZero = 350 30 | ) 31 | 32 | func main() { 33 | r = raspi.NewAdaptor() 34 | pca9685 = i2c.NewPCA9685Driver(r) 35 | oled = i2c.NewSSD1306Driver(r) 36 | mpu6050 = i2c.NewMPU6050Driver(r) 37 | 38 | ctx = gg.NewContext(oled.Buffer.Width, oled.Buffer.Height) 39 | 40 | work := func() { 41 | gobot.Every(1*time.Second, func() { 42 | handleOLED() 43 | }) 44 | 45 | gobot.Every(100*time.Millisecond, func() { 46 | handleAccel() 47 | }) 48 | 49 | // init the PWM controller 50 | pca9685.SetPWMFreq(60) 51 | 52 | // init the ESC controller for throttle zero 53 | pca9685.SetPWM(0, 0, uint16(throttleZero)) 54 | 55 | gobot.Every(1*time.Second, func() { 56 | handleSteering() 57 | handleThrottle() 58 | }) 59 | } 60 | 61 | robot := gobot.NewRobot("gophercar", 62 | []gobot.Connection{r}, 63 | []gobot.Device{pca9685, oled, mpu6050}, 64 | work, 65 | ) 66 | 67 | robot.Start() 68 | } 69 | 70 | func handleOLED() { 71 | ctx.SetRGB(0, 0, 0) 72 | ctx.Clear() 73 | ctx.SetRGB(1, 1, 1) 74 | ctx.DrawStringAnchored(time.Now().Format("15:04:05"), 0, 0, 0, 1) 75 | oled.ShowImage(ctx.Image()) 76 | } 77 | 78 | func handleAccel() { 79 | mpu6050.GetData() 80 | 81 | // fmt.Println("Accelerometer", mpu6050.Accelerometer) 82 | // fmt.Println("Gyroscope", mpu6050.Gyroscope) 83 | // fmt.Println("Temperature", mpu6050.Temperature) 84 | } 85 | 86 | func handleSteering() { 87 | if steering >= 1 && steeringDirection == "right" { 88 | steeringDirection = "left" 89 | } 90 | if steering <= -1 && steeringDirection == "left" { 91 | steeringDirection = "right" 92 | } 93 | 94 | if steeringDirection == "right" { 95 | steering += 0.1 96 | } 97 | if steeringDirection == "left" { 98 | steering -= 0.1 99 | } 100 | 101 | steeringVal := getSteeringPulse(steering) 102 | pca9685.SetPWM(1, 0, uint16(steeringVal)) 103 | } 104 | 105 | func handleThrottle() { 106 | if round(throttle, 0.05) >= 1 && throttleDirection == "up" { 107 | throttleDirection = "down" 108 | } 109 | if round(throttle, 0.05) <= -1 && throttleDirection == "down" { 110 | throttleDirection = "up" 111 | } 112 | 113 | if throttleDirection == "up" { 114 | throttle += 0.1 115 | } 116 | if throttleDirection == "down" { 117 | throttle -= 0.1 118 | } 119 | 120 | throttleVal := getThrottlePulse(throttle) 121 | pca9685.SetPWM(0, 0, uint16(throttleVal)) 122 | } 123 | 124 | // adjusts the steering from -1.0 (hard left) <-> 1.0 (hardright) to the correct 125 | // pwm pulse values. 126 | func getSteeringPulse(val float64) float64 { 127 | return gobot.Rescale(val, -1, 1, 290, 490) 128 | } 129 | 130 | // adjusts the throttle from -1.0 (hard back) <-> 1.0 (hard forward) to the correct 131 | // pwm pulse values. 132 | func getThrottlePulse(val float64) int { 133 | if val > 0 { 134 | return int(gobot.Rescale(val, 0, 1, 350, 300)) 135 | } 136 | return int(gobot.Rescale(val, -1, 0, 490, 350)) 137 | } 138 | 139 | func round(x, unit float64) float64 { 140 | return math.Round(x/unit) * unit 141 | } 142 | -------------------------------------------------------------------------------- /cars/joycar/main.go: -------------------------------------------------------------------------------- 1 | // drive with your ds3 controller 2 | // 3 | // controls: 4 | // left stick - throttle 5 | // right stick - steering 6 | // 7 | package main 8 | 9 | import ( 10 | "fmt" 11 | "math" 12 | "sync/atomic" 13 | "time" 14 | 15 | "github.com/fogleman/gg" 16 | "gobot.io/x/gobot" 17 | "gobot.io/x/gobot/drivers/i2c" 18 | "gobot.io/x/gobot/platforms/joystick" 19 | "gobot.io/x/gobot/platforms/raspi" 20 | ) 21 | 22 | var ( 23 | r *raspi.Adaptor 24 | pca9685 *i2c.PCA9685Driver 25 | oled *i2c.SSD1306Driver 26 | mpu6050 *i2c.MPU6050Driver 27 | 28 | ctx *gg.Context 29 | 30 | throttleZero = 350 31 | throttlePower = 0.25 32 | steering = 0.0 33 | 34 | // joystick 35 | leftX, leftY, rightX, rightY atomic.Value 36 | ) 37 | 38 | type pair struct { 39 | x float64 40 | y float64 41 | } 42 | 43 | func main() { 44 | r = raspi.NewAdaptor() 45 | pca9685 = i2c.NewPCA9685Driver(r) 46 | oled = i2c.NewSSD1306Driver(r) 47 | mpu6050 = i2c.NewMPU6050Driver(r) 48 | 49 | joystickAdaptor := joystick.NewAdaptor() 50 | stick := joystick.NewDriver(joystickAdaptor, "dualshock3") 51 | 52 | ctx = gg.NewContext(oled.Buffer.Width, oled.Buffer.Height) 53 | 54 | work := func() { 55 | leftX.Store(float64(0.0)) 56 | leftY.Store(float64(0.0)) 57 | rightX.Store(float64(0.0)) 58 | rightY.Store(float64(0.0)) 59 | 60 | gobot.Every(1*time.Second, func() { 61 | handleOLED() 62 | }) 63 | 64 | gobot.Every(100*time.Millisecond, func() { 65 | handleAccel() 66 | }) 67 | 68 | // init the PWM controller 69 | pca9685.SetPWMFreq(60) 70 | 71 | // init the ESC controller for throttle zero 72 | pca9685.SetPWM(0, 0, uint16(throttleZero)) 73 | 74 | stick.On(joystick.LeftX, func(data interface{}) { 75 | val := float64(data.(int16)) 76 | leftX.Store(val) 77 | }) 78 | 79 | stick.On(joystick.LeftY, func(data interface{}) { 80 | val := float64(data.(int16)) 81 | leftY.Store(val) 82 | }) 83 | 84 | stick.On(joystick.RightX, func(data interface{}) { 85 | val := float64(data.(int16)) 86 | rightX.Store(val) 87 | }) 88 | 89 | stick.On(joystick.RightY, func(data interface{}) { 90 | val := float64(data.(int16)) 91 | rightY.Store(val) 92 | }) 93 | 94 | gobot.Every(10*time.Millisecond, func() { 95 | // right stick is steering 96 | rightStick := getRightStick() 97 | 98 | switch { 99 | case rightStick.x > 10: 100 | setSteering(gobot.Rescale(rightStick.x, -32767.0, 32767.0, -1.0, 1.0)) 101 | case rightStick.x < -10: 102 | setSteering(gobot.Rescale(rightStick.x, -32767.0, 32767.0, -1.0, 1.0)) 103 | default: 104 | setSteering(0) 105 | } 106 | }) 107 | 108 | gobot.Every(10*time.Millisecond, func() { 109 | leftStick := getLeftStick() 110 | // left stick is throttle 111 | 112 | switch { 113 | case leftStick.y < -10: 114 | setThrottle(gobot.Rescale(leftStick.y, -32767.0, 32767.0, -1.0, 1.0)) 115 | case leftStick.y > 10: 116 | setThrottle(gobot.Rescale(leftStick.y, -32767.0, 32767.0, -1.0, 1.0)) 117 | default: 118 | setThrottle(0) 119 | } 120 | }) 121 | } 122 | 123 | robot := gobot.NewRobot("gophercar", 124 | []gobot.Connection{r, joystickAdaptor}, 125 | []gobot.Device{pca9685, oled, mpu6050, stick}, 126 | work, 127 | ) 128 | 129 | robot.Start() 130 | } 131 | 132 | func handleOLED() { 133 | ctx.SetRGB(0, 0, 0) 134 | ctx.Clear() 135 | ctx.SetRGB(1, 1, 1) 136 | ctx.DrawStringAnchored(time.Now().Format("15:04:05"), 0, 0, 0, 1) 137 | 138 | ctx.DrawStringAnchored(fmt.Sprint("Steering: ", steering), 0, 32, 0, 1) 139 | oled.ShowImage(ctx.Image()) 140 | } 141 | 142 | func handleAccel() { 143 | mpu6050.GetData() 144 | 145 | // fmt.Println("Accelerometer", mpu6050.Accelerometer) 146 | // fmt.Println("Gyroscope", mpu6050.Gyroscope) 147 | // fmt.Println("Temperature", mpu6050.Temperature) 148 | } 149 | 150 | func setSteering(steering float64) { 151 | steeringVal := getSteeringPulse(steering) 152 | pca9685.SetPWM(1, 0, uint16(steeringVal)) 153 | } 154 | 155 | func setThrottle(throttle float64) { 156 | throttleVal := getThrottlePulse(throttle) 157 | pca9685.SetPWM(0, 0, uint16(throttleVal)) 158 | } 159 | 160 | // adjusts the steering from -1.0 (hard left) <-> 1.0 (hardright) to the correct 161 | // pwm pulse values. 162 | func getSteeringPulse(val float64) float64 { 163 | return gobot.Rescale(val, -1, 1, 290, 490) 164 | } 165 | 166 | // adjusts the throttle from -1.0 (hard back) <-> 1.0 (hard forward) to the correct 167 | // pwm pulse values. 168 | func getThrottlePulse(val float64) int { 169 | if val > 0 { 170 | return int(gobot.Rescale(val, 0, 1, 350, 300)) 171 | } 172 | return int(gobot.Rescale(val, -1, 0, 490, 350)) 173 | } 174 | 175 | func getLeftStick() pair { 176 | s := pair{x: 0, y: 0} 177 | s.x = leftX.Load().(float64) 178 | s.y = leftY.Load().(float64) 179 | return s 180 | } 181 | 182 | func getRightStick() pair { 183 | s := pair{x: 0, y: 0} 184 | s.x = rightX.Load().(float64) 185 | s.y = rightY.Load().(float64) 186 | return s 187 | } 188 | 189 | func round(x, unit float64) float64 { 190 | return math.Round(x/unit) * unit 191 | } 192 | -------------------------------------------------------------------------------- /cars/keyboardcar/README.md: -------------------------------------------------------------------------------- 1 | # Keyboard car 2 | 3 | This car can be driven with your keyboard. SSH into the car, and run it. 4 | 5 | ## controls 6 | - up arrow - forward 7 | - down arrow - backward 8 | - right arrow - turn right 9 | - left arrow - turn left 10 | -------------------------------------------------------------------------------- /cars/keyboardcar/main.go: -------------------------------------------------------------------------------- 1 | // drive with your keyboard! 2 | // 3 | // controls: 4 | // up arrow - forward 5 | // down arrow - backward 6 | // right arrow - turn right 7 | // left arrow - turn left 8 | // 9 | package main 10 | 11 | import ( 12 | "fmt" 13 | "math" 14 | "time" 15 | 16 | "github.com/fogleman/gg" 17 | "gobot.io/x/gobot" 18 | "gobot.io/x/gobot/drivers/i2c" 19 | "gobot.io/x/gobot/platforms/keyboard" 20 | "gobot.io/x/gobot/platforms/raspi" 21 | ) 22 | 23 | var ( 24 | r *raspi.Adaptor 25 | pca9685 *i2c.PCA9685Driver 26 | oled *i2c.SSD1306Driver 27 | mpu6050 *i2c.MPU6050Driver 28 | 29 | ctx *gg.Context 30 | 31 | throttleZero = 350 32 | throttlePower = 0.25 33 | steering = 0.0 34 | ) 35 | 36 | func main() { 37 | r = raspi.NewAdaptor() 38 | pca9685 = i2c.NewPCA9685Driver(r) 39 | oled = i2c.NewSSD1306Driver(r) 40 | mpu6050 = i2c.NewMPU6050Driver(r) 41 | keys := keyboard.NewDriver() 42 | 43 | ctx = gg.NewContext(oled.Buffer.Width, oled.Buffer.Height) 44 | 45 | work := func() { 46 | gobot.Every(1*time.Second, func() { 47 | handleOLED() 48 | }) 49 | 50 | gobot.Every(100*time.Millisecond, func() { 51 | handleAccel() 52 | }) 53 | 54 | // init the PWM controller 55 | pca9685.SetPWMFreq(60) 56 | 57 | // init the ESC controller for throttle zero 58 | pca9685.SetPWM(0, 0, uint16(throttleZero)) 59 | 60 | keys.On(keyboard.Key, func(data interface{}) { 61 | key := data.(keyboard.KeyEvent) 62 | 63 | switch key.Key { 64 | case keyboard.ArrowUp: 65 | setThrottle(throttlePower) 66 | 67 | gobot.After(1*time.Second, func() { 68 | setThrottle(0) 69 | }) 70 | case keyboard.ArrowDown: 71 | setThrottle(-1 * throttlePower) 72 | 73 | gobot.After(1*time.Second, func() { 74 | setThrottle(0) 75 | }) 76 | case keyboard.ArrowRight: 77 | if steering < 1.0 { 78 | steering = round(steering+0.1, 0.05) 79 | } 80 | 81 | setSteering(steering) 82 | case keyboard.ArrowLeft: 83 | if round(steering, 0.05) > -1.0 { 84 | steering = round(steering-0.1, 0.05) 85 | } 86 | 87 | setSteering(steering) 88 | } 89 | }) 90 | } 91 | 92 | robot := gobot.NewRobot("gophercar", 93 | []gobot.Connection{r}, 94 | []gobot.Device{pca9685, oled, mpu6050, keys}, 95 | work, 96 | ) 97 | 98 | robot.Start() 99 | } 100 | 101 | func handleOLED() { 102 | ctx.SetRGB(0, 0, 0) 103 | ctx.Clear() 104 | ctx.SetRGB(1, 1, 1) 105 | ctx.DrawStringAnchored(time.Now().Format("15:04:05"), 0, 0, 0, 1) 106 | 107 | ctx.DrawStringAnchored(fmt.Sprint("Steering: ", steering), 0, 32, 0, 1) 108 | oled.ShowImage(ctx.Image()) 109 | } 110 | 111 | func handleAccel() { 112 | mpu6050.GetData() 113 | 114 | // fmt.Println("Accelerometer", mpu6050.Accelerometer) 115 | // fmt.Println("Gyroscope", mpu6050.Gyroscope) 116 | // fmt.Println("Temperature", mpu6050.Temperature) 117 | } 118 | 119 | func setSteering(steering float64) { 120 | steeringVal := getSteeringPulse(steering) 121 | pca9685.SetPWM(1, 0, uint16(steeringVal)) 122 | } 123 | 124 | func setThrottle(throttle float64) { 125 | throttleVal := getThrottlePulse(throttle) 126 | pca9685.SetPWM(0, 0, uint16(throttleVal)) 127 | } 128 | 129 | // adjusts the steering from -1.0 (hard left) <-> 1.0 (hardright) to the correct 130 | // pwm pulse values. 131 | func getSteeringPulse(val float64) float64 { 132 | return gobot.Rescale(val, -1, 1, 290, 490) 133 | } 134 | 135 | // adjusts the throttle from -1.0 (hard back) <-> 1.0 (hard forward) to the correct 136 | // pwm pulse values. 137 | func getThrottlePulse(val float64) int { 138 | if val > 0 { 139 | return int(gobot.Rescale(val, 0, 1, 350, 300)) 140 | } 141 | return int(gobot.Rescale(val, -1, 0, 490, 350)) 142 | } 143 | 144 | func round(x, unit float64) float64 { 145 | return math.Round(x/unit) * unit 146 | } 147 | -------------------------------------------------------------------------------- /cars/oledtest/main.go: -------------------------------------------------------------------------------- 1 | // this does not really do anything yet except connect to all of the various devices 2 | package main 3 | 4 | import ( 5 | "time" 6 | 7 | "github.com/fogleman/gg" 8 | "gobot.io/x/gobot" 9 | "gobot.io/x/gobot/drivers/i2c" 10 | "gobot.io/x/gobot/platforms/raspi" 11 | ) 12 | 13 | var ( 14 | r *raspi.Adaptor 15 | oled *i2c.SSD1306Driver 16 | 17 | ctx *gg.Context 18 | ) 19 | 20 | func main() { 21 | r = raspi.NewAdaptor() 22 | oled = i2c.NewSSD1306Driver(r) 23 | 24 | ctx = gg.NewContext(oled.Buffer.Width, oled.Buffer.Height) 25 | 26 | work := func() { 27 | gobot.Every(1*time.Second, func() { 28 | handleOLED() 29 | }) 30 | } 31 | 32 | robot := gobot.NewRobot("gophercar", 33 | []gobot.Connection{r}, 34 | []gobot.Device{oled}, 35 | work, 36 | ) 37 | 38 | robot.Start() 39 | } 40 | 41 | func handleOLED() { 42 | ctx.SetRGB(0, 0, 0) 43 | ctx.Clear() 44 | ctx.SetRGB(1, 1, 1) 45 | ctx.DrawStringAnchored(time.Now().Format("15:04:05"), 0, 0, 0, 1) 46 | oled.ShowImage(ctx.Image()) 47 | } 48 | -------------------------------------------------------------------------------- /cars/servotest/main.go: -------------------------------------------------------------------------------- 1 | // this does not really do anything yet except connect to all of the various devices 2 | package main 3 | 4 | import ( 5 | "fmt" 6 | "time" 7 | 8 | "gobot.io/x/gobot" 9 | "gobot.io/x/gobot/drivers/gpio" 10 | "gobot.io/x/gobot/drivers/i2c" 11 | "gobot.io/x/gobot/platforms/raspi" 12 | ) 13 | 14 | var ( 15 | r *raspi.Adaptor 16 | pca9685 *i2c.PCA9685Driver 17 | ) 18 | 19 | func main() { 20 | r = raspi.NewAdaptor() 21 | pca9685 = i2c.NewPCA9685Driver(r) 22 | 23 | // just here as placeholder for the real steering or throttle 24 | servo := gpio.NewServoDriver(pca9685, "1") 25 | 26 | work := func() { 27 | pca9685.SetPWMFreq(60) 28 | i := 10 29 | direction := 1 30 | 31 | gobot.Every(1*time.Second, func() { 32 | fmt.Println("Turning", i) 33 | servo.Move(uint8(i)) 34 | if i > 150 { 35 | direction = -1 36 | } 37 | if i < 10 { 38 | direction = 1 39 | } 40 | i += 10 * direction 41 | }) 42 | } 43 | 44 | robot := gobot.NewRobot("gophercar", 45 | []gobot.Connection{r}, 46 | []gobot.Device{pca9685, servo}, 47 | work, 48 | ) 49 | 50 | robot.Start() 51 | } 52 | -------------------------------------------------------------------------------- /diagram.txt: -------------------------------------------------------------------------------- 1 | graph TD 2 | M[Motor controller] --> Main 3 | V[Video camera] --> Main 4 | S[Sensors] --> Main 5 | Main(Gophercar) --> |commands| B(API server) 6 | Main --> W(Web server) 7 | Main --> |video stream| C(MJPEG server) 8 | Main --- AI(Self-driving) 9 | Main --- J(Joystick) 10 | C -->|mjpeg| D[Web page] 11 | W -->|http| D 12 | -------------------------------------------------------------------------------- /images/arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hybridgroup/gophercar/5e1697aeb49e6a55c90b35cd0bf48a3d0a7229d7/images/arch.png -------------------------------------------------------------------------------- /images/gophercar-fpv.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hybridgroup/gophercar/5e1697aeb49e6a55c90b35cd0bf48a3d0a7229d7/images/gophercar-fpv.gif -------------------------------------------------------------------------------- /images/gophercon2018.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hybridgroup/gophercar/5e1697aeb49e6a55c90b35cd0bf48a3d0a7229d7/images/gophercon2018.gif -------------------------------------------------------------------------------- /runner.sh: -------------------------------------------------------------------------------- 1 | [ $# -eq 0 ] && { echo "Usage: $0 [carname] [ipaddress]"; exit 1; } 2 | 3 | echo "Copying..." 4 | scp ./cars/$1/main.go pi@$2:/home/pi/gophercar/cars/$1/main.go 5 | ssh pi@$2 ARG1=$1 ARG2=$2 'bash -s' <<'ENDSSH' 6 | echo "Compiling..." 7 | export PATH=$PATH:/usr/local/go/bin 8 | GOARCH=arm GOOS=linux go build -o ./gophercar/build/$ARG1 ./gophercar/cars/$ARG1/main.go 9 | echo "Running..." 10 | ./gophercar/build/$ARG1 11 | ENDSSH 12 | --------------------------------------------------------------------------------