├── README.md ├── README.md~ ├── gopro.py ├── gopro.py~ ├── remote.py ├── remote.py~ ├── v-black.jpg └── v-white.jpg /README.md: -------------------------------------------------------------------------------- 1 | GoPro Remote 2 | ============ 3 | 4 | This is an attempt to reverse engineer the GoPro wifi remote protocol. 5 | 6 | The GoPro (Hero 3 and Hero 2 with wifi backpack) has two wifi modes. "App mode" and "Wifi RC". In "app mode" the GoPro becomes a wifi host, and commands are sent using http requests. The list of commands can be found at https://github.com/KonradIT/goprowifihack . A python interface for them can be found at https://github.com/joshvillbrandt/GoProController . 7 | 8 | As yet I have not been able to find similar resources for the "Wifi RC" mode. This is an attempt to create such a place. 9 | 10 | Current Functionality and Example 11 | --------------------------------- 12 | 13 | In "Wifi RC" mode the Wifi remote becomes the wifi host and the GoPro becomes the client. The communication happens directly via the UDP protocol. 14 | 15 | To connect to the remote, let the Wifi Remote look for cameras by holding the top button and pressing the bottom one. You should see a camera with arrows underneath. While the remote is in this state it broadcasts its wifi network. Connect to this network from your computer. Then start the gopro class. (If the remote is not looking for a camera it does not broadcast the network. You will have to be able to connect to a hidden wifi network.) 16 | 17 | At the moment it only simulates a GoPro listening to commands from the remote, and responds like a GoPro would. When a "start recording" command (SH) is received the time is saved in `self.start_time`. When a "stop recording" command is received the time is saved in `self.stop_time` and the double is written to a line in the csv file defined in `self.filename`. 18 | 19 | If the computer is the only thing connected to the remote it will display an image sent by the computer. Thanks Michael for providing a suitable image. 20 | 21 | This example starts a thread which listens to start and stop recording commands and records them to a csv file. 22 | 23 | import gopro 24 | import time 25 | gp = gopro.gopro() 26 | gp.start() 27 | time.sleep(10) 28 | gp.stop() 29 | 30 | 31 | This was used in the Bridgestone Bikebooth project. (Link to follow soon) 32 | 33 | Next Steps 34 | ---------- 35 | 36 | A next step would be to simulate the wifi remote to control the GoPros from a computer. This would allow a computer to send simultaneous commands to many GoPros. The limitations of this method is that it is not possible to create live stream from the GoPros or change the settings of the GoPros as far as I am aware. 37 | 38 | The class should be expanded to make it more modular. 39 | 40 | A full catalogue of the commands should be compiled with what the right responses should be, as well as how the state variable changes with these commands (see below). 41 | 42 | How the remote works 43 | -------------------- 44 | 45 | The remote sends a series of UPD commands to all the cameras connected to it. It waits for a response from each one. It also broadcasts a state (st) command via the broadcast channel. The wifi remote waits until all the state responses are the same. 46 | 47 | The UDP packet has the following form in hex: 00 00 XX 00 00 00 00 48 | Where XX are ASCII characters. The ASCII characters explain what the command is for. Many of the meanings can be gathered from the links above, but there are some new ones. 49 | 50 | How to Help 51 | ----------- 52 | 53 | Connect to a remote's network. Connect a GoPro to the networks as well. Open wireshark (or tcp dump) to listen to the traffic. Descyfer the commands and responses. As well as the changes in the state response. 54 | 55 | -------------------------------------------------------------------------------- /README.md~: -------------------------------------------------------------------------------- 1 | GoPro Remote 2 | ============ 3 | 4 | This is an attempt to reverse engineer the GoPro wifi remote protocol. 5 | 6 | The GoPro (Hero 3 and Hero 2 with wifi backpack) has two wifi modes. "App mode" and "Wifi RC". In "app mode" the GoPro becomes a wifi host, and commands are sent using http requests. The list of commands can be found at https://github.com/KonradIT/goprowifihack . A python interface for them can be found at https://github.com/joshvillbrandt/GoProController . 7 | 8 | As yet I have not been able to find similar resources for the "Wifi RC" mode. This is an attempt to create such a place. 9 | 10 | Current Functionality and Example 11 | --------------------------------- 12 | 13 | In "Wifi RC" mode the Wifi remote becomes the wifi host and the GoPro becomes the client. The communication happens directly via the UDP protocol. 14 | 15 | To connect to the remote, let the Wifi Remote look for cameras by holding the top button and pressing the bottom one. You should see a camera with arrows underneath. While the remote is in this state it broadcasts its wifi network. Connect to this network from your computer. Then start the gopro class. (If the remote is not looking for a camera it does not broadcast the network. You will have to be able to connect to a hidden wifi network.) 16 | 17 | At the moment it only simulates a GoPro listening to commands from the remote, and responds like a GoPro would. When a "start recording" command (SH) is recieved the time is saved in `self.start_time`. When a "stop recording" command is recieved the time is saved in `self.stop_time` and the double is written to a line in the csv file defined in `self.filename`. 18 | 19 | If the computer is the only thing connected to the remote it will display an image sent by the computer. Thanks Michael for providing a suitable image. 20 | 21 | This example starts a thread which listens to start and stop recoding commands and records them to a csv file. 22 | 23 | import gopro 24 | import time 25 | gp = gopro.gopro() 26 | gp.start() 27 | time.sleep(10) 28 | gp.stop() 29 | 30 | 31 | This was used in the Bridgestone Bikebooth project. (Link to follow soon) 32 | 33 | Next Steps 34 | ---------- 35 | 36 | A next step would be to simulate the wifi remote to control the GoPros from a computer. This would alow a computer to send simultanious commands to many GoPros. The limitations of this method is that it is not possible to create live stream from the GoPros or change the settings of the gopros as far as I am aware. 37 | 38 | The class should be expanded to make it more modular. 39 | 40 | A full catalogue of the commands should be compiled with what the right responses should be, as well as how the state variable changes with these commands (see below). 41 | 42 | How the remote works 43 | -------------------- 44 | 45 | The remote sends a series of UPD commands to all the cameras connected to it. It waits for a response from each one. It also broadcasts a state (st) command via the broadcast channel. The wifi remote waits until all the state responses are the same. 46 | 47 | The UDP packet has the following form in hex: 00 00 XX 00 00 00 00 48 | Where XX are ASCII characters. The ASCII characters explain what the command is for. Many of the meanings can be gathered from the links above, but there are some new ones. 49 | 50 | How to Help 51 | ----------- 52 | 53 | Connect to a remote's network. Connect a GoPro to the networks as well. Open wireshark (or tcp dump) to listen to the trafic. Decifer the commands and responces. As well as the changes in the state response. 54 | 55 | -------------------------------------------------------------------------------- /gopro.py: -------------------------------------------------------------------------------- 1 | ''' 2 | module containing the (virtual) gopro class and some usefull functions. 3 | ''' 4 | 5 | import socket 6 | from PIL import Image 7 | import datetime 8 | from threading import Thread 9 | import csv 10 | import time 11 | from numpy import array as nparray 12 | import os 13 | 14 | curdir=os.path.dirname(__file__) 15 | 16 | 17 | HOST = '' # Symbolic name meaning all available interfaces 18 | PORT = 8484 # Arbitrary non-privileged port 19 | AREA = 4800 # pixel area of gopro lcd 20 | WIDTH = 64 # pixels 21 | HEIGHT = 75 # pixels 22 | 23 | class gopro: 24 | """ 25 | This class defines a gopro object. It will connect to a udp stream 26 | from the gopro wifi remote. It keeps track of the states and triggers 27 | the write_start function when record is started and write_stop when 28 | it is stopped. Run start() and stop() functions after initiation. 29 | Inputs: 30 | Optional: 31 | all default reply and stated values. 32 | """ 33 | def __init__(self, filename='record_times.csv', image=os.path.join(curdir,'v-white.jpg'), 34 | CM=[0,1], OO=[0], wt=[0], se=[0 for i in range(31)], 35 | cv=[1], pw=[0,1], st=[0,0,0,0,0], SH=[0,0]): 36 | self.values = { 37 | 'cv':cv, 38 | 'pw':pw, 39 | 'st':st, 40 | 'CM':CM, 41 | 'OO':OO, 42 | 'wt':wt, 43 | 'se':se, 44 | 'SH':SH 45 | } 46 | self.set_image(image); 47 | self.running = False 48 | self.filename = filename 49 | self.start_time = 0; 50 | self.stop_time = 0; 51 | 52 | def set_image(self,image): 53 | """ 54 | Converts image to an array that will be printed on the lcd 55 | screen when this gopro is the only one connected. (save into the lc 56 | property of values) 57 | Arguments: 58 | image: path to a x image 59 | """ 60 | self.values['lc'] = [0] + get_image_data(image) 61 | 62 | def start(self): 63 | """ 64 | Starts thread that creates udp connection and opens a csv file 65 | defined in initiation. 66 | """ 67 | # create udp socket 68 | try : 69 | self.s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 70 | print 'Socket created' 71 | except socket.error, msg : 72 | print 'Failed to create socket. Error Code : ' + str(msg[0]) + ' Message ' + msg[1] 73 | exit() 74 | # bind to host 75 | try: 76 | self.s.bind((HOST, PORT)) 77 | except socket.error , msg: 78 | print 'Bind failed. Error Code : '+str(msg[0])+' Message '+msg[1] 79 | exit() 80 | # open file for writing and creat csv writter 81 | self.file = open(self.filename, 'wb') 82 | self.writer = csv.writer(self.file) 83 | # start thread 84 | self.running = True 85 | self.thread = Thread(target=self.run) 86 | self.thread.daemon = True 87 | self.thread.start() 88 | 89 | def run(self): 90 | """ 91 | The function the thread runs in start. 92 | """ 93 | while self.running: 94 | try: 95 | # receive data from client (data, addr) 96 | d = self.s.recvfrom(1024) 97 | data = d[0] 98 | addr = d[1] 99 | 100 | if not data: 101 | print "No udp data => stopped" 102 | break 103 | 104 | 105 | 106 | # make reply 107 | datarray = [ord(c) for c in data] 108 | #cmd = datarray[11:13] 109 | cmd = data[11:13] 110 | #print cmdstr 111 | 112 | # print recieved data 113 | # print get_hex_str(datarray) 114 | 115 | # change things before reply 116 | 117 | if cmd == 'CM': 118 | # if mode is changed, change status 119 | self.values['st'] = [0,datarray[13],0,0,0] 120 | 121 | 122 | if cmd == 'PW': 123 | # change power state 124 | #self.values['pw'] = [0,datarray[13]] 125 | self.values['PW'] = datarray[13:] 126 | 127 | if cmd == 'SH': 128 | # change shutter state 129 | #self.values['pw'] = [0,datarray[13]] 130 | #self.values['SH'] = datarray[13:] 131 | self.values['st'][2] = datarray[13] 132 | self.values['st'][4] = datarray[13] 133 | if datarray[13] == 1: 134 | self.start_time = datetime.datetime.now() 135 | print "Start" 136 | else: 137 | self.stop_time = datetime.datetime.now() 138 | self.writer.writerow([str(self.start_time),str(self.stop_time)]) 139 | print "Stop" 140 | 141 | datarray[13:] = self.values[cmd] 142 | 143 | reply = str(bytearray(datarray)) 144 | 145 | # do things after reply is made 146 | 147 | if cmd == 'cv': 148 | # after ok is sent send actual camera version next time 149 | self.values['cv'] = [0,2,1,0x0c] 150 | self.values['cv'] += [ ord(c) for c in 'HD3.03.03.00.Hero3-BlackEdition' ] 151 | 152 | 153 | self.s.sendto(reply , addr) 154 | 155 | # print sent data 156 | # print get_hex_str(datarray) 157 | 158 | except Exception as e: 159 | print e 160 | self.s.close() 161 | self.file.close() 162 | break 163 | 164 | def stop(self): 165 | """ 166 | Stops thread and closes socket and file. 167 | """ 168 | self.running = False 169 | time.sleep(0.5) 170 | self.s.close() 171 | self.file.close() 172 | return not self.thread.is_alive() 173 | 174 | 175 | def get_image_data(image): 176 | """ 177 | Puts converts image to an array, whose bit reprisentation reprisents 178 | the back and white values of the image. (binary data is captured in 179 | reverse. 180 | Arguments: 181 | image: path to a x image 182 | """ 183 | v = Image.open(image) 184 | vrgb = v.getdata() 185 | 186 | vb= "" 187 | for i in range(len(vrgb)): 188 | if vrgb[-i][0] < 100: 189 | vb += '1' 190 | else: 191 | vb += '0' 192 | 193 | a = [ int(vb[i:i+8],2) for i in range(0, len(vb), 8) ] 194 | return a 195 | 196 | def get_int_array(hexstr): 197 | """ 198 | returns an array of integers resulting from a string such as: 199 | 00:00:01:3A 200 | """ 201 | return [int(c,16) for c in hexstr.split(':')] 202 | 203 | def get_hex_str(intarray): 204 | """ 205 | Accepts array of integers and outputs a string of the form 00:01:3D 206 | """ 207 | return ':'.join('%02x'%i for i in intarray) 208 | 209 | 210 | def show_image(aa): 211 | """ 212 | Displays the image defined by an integer array, as it will show on the 213 | remote lcd. 214 | """ 215 | astr = [] 216 | for i in aa: 217 | b = bin(i)[2:] 218 | astr += [255]*(8-len(b)) 219 | for c in b: 220 | if c=='0': 221 | astr += [255] 222 | else: 223 | astr += [0] 224 | 225 | 226 | aim = [] 227 | for i in range(HEIGHT-1): 228 | col = astr[WIDTH*(HEIGHT-1-i):WIDTH*(HEIGHT-1-i+1)] 229 | aim.append(col) 230 | 231 | aim = np.array(aim) 232 | img = Image.fromarray(aim) 233 | img.show() 234 | 235 | 236 | -------------------------------------------------------------------------------- /gopro.py~: -------------------------------------------------------------------------------- 1 | ''' 2 | module containing the (virtual) gopro class and some usefull functions. 3 | ''' 4 | 5 | import socket 6 | from PIL import Image 7 | import datetime 8 | from threading import Thread 9 | import csv 10 | import time 11 | from numpy import array as nparray 12 | import os 13 | 14 | curdir=os.path.dirname(__file__) 15 | 16 | 17 | HOST = '' # Symbolic name meaning all available interfaces 18 | PORT = 8484 # Arbitrary non-privileged port 19 | AREA = 4800 # pixel area of gopro lcd 20 | WIDTH = 64 # pixels 21 | HEIGHT = 75 # pixels 22 | 23 | class gopro: 24 | """ 25 | This class defines a gopro object. It will connect to a udp stream 26 | from the gopro wifi remote. It keeps track of the states and triggers 27 | the write_start function when record is started and write_stop when 28 | it is stopped. Run start() and stop() functions after initiation. 29 | Inputs: 30 | Optional: 31 | all default reply and stated values. 32 | """ 33 | def __init__(self, filename='record_times.csv', image='v-white.jpg', 34 | CM=[0,1], OO=[0], wt=[0], se=[0 for i in range(31)], 35 | cv=[1], pw=[0,1], st=[0,0,0,0,0], SH=[0,0]): 36 | self.values = { 37 | 'cv':cv, 38 | 'pw':pw, 39 | 'st':st, 40 | 'CM':CM, 41 | 'OO':OO, 42 | 'wt':wt, 43 | 'se':se, 44 | 'SH':SH 45 | } 46 | self.set_image(image); 47 | self.running = False 48 | self.filename = filename 49 | self.start_time = 0; 50 | self.stop_time = 0; 51 | 52 | def set_image(self,image): 53 | """ 54 | Converts image to an array that will be printed on the lcd 55 | screen when this gopro is the only one connected. (save into the lc 56 | property of values) 57 | Arguments: 58 | image: path to a x image 59 | """ 60 | self.values['lc'] = [0] + get_image_data(image) 61 | 62 | def start(self): 63 | """ 64 | Starts thread that creates udp connection and opens a csv file 65 | defined in initiation. 66 | """ 67 | # create udp socket 68 | try : 69 | self.s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 70 | print 'Socket created' 71 | except socket.error, msg : 72 | print 'Failed to create socket. Error Code : ' + str(msg[0]) + ' Message ' + msg[1] 73 | exit() 74 | # bind to host 75 | try: 76 | self.s.bind((HOST, PORT)) 77 | except socket.error , msg: 78 | print 'Bind failed. Error Code : '+str(msg[0])+' Message '+msg[1] 79 | exit() 80 | # open file for writing and creat csv writter 81 | self.file = open(self.filename, 'wb') 82 | self.writer = csv.writer(self.file) 83 | # start thread 84 | self.running = True 85 | self.thread = Thread(target=self.run) 86 | self.thread.daemon = True 87 | self.thread.start() 88 | 89 | def run(self): 90 | """ 91 | The function the thread runs in start. 92 | """ 93 | while self.running: 94 | try: 95 | # receive data from client (data, addr) 96 | d = self.s.recvfrom(1024) 97 | data = d[0] 98 | addr = d[1] 99 | 100 | if not data: 101 | print "No udp data => stopped" 102 | break 103 | 104 | 105 | 106 | # make reply 107 | datarray = [ord(c) for c in data] 108 | #cmd = datarray[11:13] 109 | cmd = data[11:13] 110 | #print cmdstr 111 | 112 | # print recieved data 113 | # print get_hex_str(datarray) 114 | 115 | # change things before reply 116 | 117 | if cmd == 'CM': 118 | # if mode is changed, change status 119 | self.values['st'] = [0,datarray[13],0,0,0] 120 | 121 | 122 | if cmd == 'PW': 123 | # change power state 124 | #self.values['pw'] = [0,datarray[13]] 125 | self.values['PW'] = datarray[13:] 126 | 127 | if cmd == 'SH': 128 | # change shutter state 129 | #self.values['pw'] = [0,datarray[13]] 130 | #self.values['SH'] = datarray[13:] 131 | self.values['st'][2] = datarray[13] 132 | self.values['st'][4] = datarray[13] 133 | if datarray[13] == 1: 134 | self.start_time = datetime.datetime.now() 135 | print "Start" 136 | else: 137 | self.stop_time = datetime.datetime.now() 138 | self.writer.writerow([str(self.start_time),str(self.stop_time)]) 139 | print "Stop" 140 | 141 | datarray[13:] = self.values[cmd] 142 | 143 | reply = str(bytearray(datarray)) 144 | 145 | # do things after reply is made 146 | 147 | if cmd == 'cv': 148 | # after ok is sent send actual camera version next time 149 | self.values['cv'] = [0,2,1,0x0c] 150 | self.values['cv'] += [ ord(c) for c in 'HD3.03.03.00.Hero3-BlackEdition' ] 151 | 152 | 153 | self.s.sendto(reply , addr) 154 | 155 | # print sent data 156 | # print get_hex_str(datarray) 157 | 158 | except Exception as e: 159 | print e 160 | self.s.close() 161 | self.file.close() 162 | break 163 | 164 | def stop(self): 165 | """ 166 | Stops thread and closes socket and file. 167 | """ 168 | self.running = False 169 | time.sleep(0.5) 170 | self.s.close() 171 | self.file.close() 172 | return not self.thread.is_alive() 173 | 174 | 175 | def get_image_data(image): 176 | """ 177 | Puts converts image to an array, whose bit reprisentation reprisents 178 | the back and white values of the image. (binary data is captured in 179 | reverse. 180 | Arguments: 181 | image: path to a x image 182 | """ 183 | v = Image.open(image) 184 | vrgb = v.getdata() 185 | 186 | vb= "" 187 | for i in range(len(vrgb)): 188 | if vrgb[-i][0] < 100: 189 | vb += '1' 190 | else: 191 | vb += '0' 192 | 193 | a = [ int(vb[i:i+8],2) for i in range(0, len(vb), 8) ] 194 | return a 195 | 196 | def get_int_array(hexstr): 197 | """ 198 | returns an array of integers resulting from a string such as: 199 | 00:00:01:3A 200 | """ 201 | return [int(c,16) for c in hexstr.split(':')] 202 | 203 | def get_hex_str(intarray): 204 | """ 205 | Accepts array of integers and outputs a string of the form 00:01:3D 206 | """ 207 | return ':'.join('%02x'%i for i in intarray) 208 | 209 | 210 | def show_image(aa): 211 | """ 212 | Displays the image defined by an integer array, as it will show on the 213 | remote lcd. 214 | """ 215 | astr = [] 216 | for i in aa: 217 | b = bin(i)[2:] 218 | astr += [255]*(8-len(b)) 219 | for c in b: 220 | if c=='0': 221 | astr += [255] 222 | else: 223 | astr += [0] 224 | 225 | 226 | aim = [] 227 | for i in range(HEIGHT-1): 228 | col = astr[WIDTH*(HEIGHT-1-i):WIDTH*(HEIGHT-1-i+1)] 229 | aim.append(col) 230 | 231 | aim = np.array(aim) 232 | img = Image.fromarray(aim) 233 | img.show() 234 | 235 | 236 | -------------------------------------------------------------------------------- /remote.py: -------------------------------------------------------------------------------- 1 | #TODO 2 | -------------------------------------------------------------------------------- /remote.py~: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theContentMint/GoProRemote/dbd734ed18100ce3b77222f945c638c3a823e0ea/remote.py~ -------------------------------------------------------------------------------- /v-black.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theContentMint/GoProRemote/dbd734ed18100ce3b77222f945c638c3a823e0ea/v-black.jpg -------------------------------------------------------------------------------- /v-white.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theContentMint/GoProRemote/dbd734ed18100ce3b77222f945c638c3a823e0ea/v-white.jpg --------------------------------------------------------------------------------