diff --git a/code/tcp-server/ServoClient.py b/code/tcp-server/ServoClient.py new file mode 100644 index 0000000000000000000000000000000000000000..40048e6467cdf890b1a6c72290c626960eef898e --- /dev/null +++ b/code/tcp-server/ServoClient.py @@ -0,0 +1,63 @@ +# general +from time import sleep + +#server +import socket + +class ServoClient(object): + """ + A class that implements a servo-client which is capable of sending servo speed-settings + to an omnibot. + + HOST: Host name of server (string) + PORT: Port to connect to (int) + verbose: choose level of chit-chat (boolean) + """ + def __init__(self,HOST,PORT,verbose=False): + self.HOST = HOST + self.PORT = PORT + self.verbose=verbose + self.s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + + def __enter__(self): + """ + Boot up the socket and connect to the servo server + """ + if self.verbose: + print("Connecting to HOST: " + str(self.HOST) + ", PORT: " + str(self.PORT)) + + self.s.connect((self.HOST, self.PORT)) # Connect to the robot + if self.verbose: + print("Connection successful.") + + sleep(0.1)# sleep to let servos boot + return self + + def __exit__(self, exc_type, exc_value, exc_tb): + """ + Close down the socket after closing the client + """ + if self.verbose: + print("Closing down socket") + self.s.close() + + def get_max_velocity(self): + """TODO: function for retreving max velocity of servos""" + pass + + def set_speed(self,i,v): + """ + Set the speed of servo i to v + """ + package = str(i)+":"+str(v) + if self.verbose: + print("Sending " + package) + + # Send the package + self.s.sendall(bytes(package,'utf-8')) + # Receive confirmation + ret=self.s.recv(1024).decode('utf-8') + if ret[0] == "e": + raise Exception(ret) + elif self.verbose: + print(ret) diff --git a/code/tcp-server/omnibot_controller.py b/code/tcp-server/omnibot_controller.py new file mode 100644 index 0000000000000000000000000000000000000000..2184949d47a2bf6b50e3181f03dac154fac081af --- /dev/null +++ b/code/tcp-server/omnibot_controller.py @@ -0,0 +1,126 @@ +# Import package for communicating with omnibot +from ServoClient import ServoClient + +# general +import numpy as np +import sys +from time import time, sleep + +# threads +import threading +import logging + +# Controller +from inputs import get_gamepad + + +class ControllerState: + """ + Class that logs the controller state + """ + def __init__(self): + """ Initialize the controller """ + self.x = 128 + self.y = 128 + self.r = 128 + self.on = True + + def set_x(self,x): + self.x = x + def set_y(self,y): + self.y = y + def set_r(self,r): + self.r = r + def state_data(self): + return 'x'+str(self.x)+'y'+str(self.y)+'r'+str(self.r) + def set_state(self,data): + s = str(data) + ind = [s.find(i) for i in ['x','y','r']] + self.set_x(int(s[ind[0]+1:ind[1]])) + self.set_y(int(s[ind[1]+1:ind[2]])) + self.set_r(int(s[ind[2]+1:-1])) + +def log_controller(state): + """ + Function that continuously listens to any event from the controller, and logs the change in + the controller state "state" + """ + logging.info("Starting controller logging") + while 1: + events = get_gamepad() + for event in events: + if event.code == "ABS_X": + state.set_x(event.state) + elif event.code == "ABS_Y": + state.set_y(event.state) + elif event.code == "ABS_Z": + state.set_r(event.state) + elif event.code == "BTN_PINKIE" and event.state == 1: + # The "BTN_PINKIE" (right index trigger) + # is currently used as an off-button. + state.on = False + + logging.info("Stopping controller logging") + + +if __name__ == '__main__': + + # Logger format + format = "%(asctime)s: %(message)s" + logging.basicConfig(format=format, level=logging.INFO,datefmt="%H:%M:%S") + + # Server settings + HOST, PORT = "localhost", 9998 + + if len(sys.argv) > 1: + # If an input is given to the script, it will be interpreted as the intended + # IP-address. Baseline is localhost. + HOST = sys.argv[1] + + logging.info(f"HOST: \t {HOST}") + logging.info(f"PORT: \t {PORT}") + + + # Robot parameters + R = 0.16 # Distance between center of robot and wheels + a1 = 2*np.pi/3 # Angle between x axis and first wheel + a2 = 4*np.pi/3 # Angle between x axis and second wheel + r = 0.028*0.45/18 # Wheel radius. Has been fudge-factored because the actual velocity of the wheels did not align with the set-points. + + + vel_factor = 4 # Fudge factor that changes the relative velocity of the bot + + def phidot(xdot,ang): + """Returns reference velocities for the wheels, given current system state and reference velocities""" + M = -1/r*np.array([[-np.sin(ang), np.cos(ang), R ],[-np.sin(ang+a1), np.cos(ang+a1), R],[-np.sin(ang+a2), np.cos(ang+a2), R]]) + return M.dot(xdot) + + # Start loggin controller + state = ControllerState() + t = threading.Thread(target=log_controller, args=(state,), daemon=True) + t.start() + + ts = 0.1 # sampling time + + with ServoClient(HOST,PORT,verbose=True) as s: + + no_error = True + while state.on and no_error: + t0 = time() + + # Get gamepad updates. + # Use some scaling to turn the controller state values into + # reasonable velocities. + w = (128-state.r)*np.pi/1000 + vy = (128-state.x)/1500 + vx = (128-state.y)/1500 + dphi = vel_factor*phidot([vx,vy,w],0.0) + + for i,v in enumerate(dphi): + s.set_speed(i,int(np.round(v))) + + sleep(max(0,t0+ts-time())) + + + + diff --git a/code/tcp-server/servo_server.py b/code/tcp-server/servo_server.py new file mode 100644 index 0000000000000000000000000000000000000000..1429f250e0c79fd729e6fbbe9bcc1c47475f8cd2 --- /dev/null +++ b/code/tcp-server/servo_server.py @@ -0,0 +1,92 @@ +import numpy as np +import sys +from time import time, sleep +import logging + +# Dynamixel packages +from dynamixel.model.xm430_w210_t_r import XM430_W210_T_R +import dynamixel.channel +VEL_MAX = 1022 #maximum velocity set-point (max possible value is 1022) + +#server +import socket + +def load_servos(): + """ + Servo load function, based on the dynamixel implementation of Anders Blomdell. + """ + + # The speed and channel numbers can be found and checked via the dynamixel software "dynamixel wizard". + # The device can be found by e.g. lsusb, and is currently adapted for the three-color robots. + channel = dynamixel.channel.Channel(speed=57600,device='/dev/ttyACM0') + servos = [ XM430_W210_T_R(channel, 1), + XM430_W210_T_R(channel, 2), + XM430_W210_T_R(channel, 3) ] + clear_servos(servos) + + return servos + +def clear_servos(servos): + """ + Clear whatchdog error and write maximum velocity to the servos.z + """ + for s in servos: + s.torque_enable.write(0) + print(s.model_number.read(), s.id.read()) + s.velocity_limit.write(VEL_MAX+1) # +1 for padding because it seemed to struggle with negative values + s.operating_mode.write(1) + s.bus_watchdog.write(0) # Clear old watchdog error + s.bus_watchdog.write(100) # 2 second timeout + s.torque_enable.write(1) + +if __name__ == "__main__": + + # Set IP (HOST) and port number (PORT) + # "" means listening to any available IP + HOST, PORT = "", 9998 + + # Define loggin format + format = "%(asctime)s: %(message)s" + logging.basicConfig(format=format, level=logging.INFO,datefmt="%H:%M:%S") + + # Create socket s + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.bind((HOST, PORT)) # Bind the socket to specified port and ip + s.listen(1) # Start listening for connections + logging.info("Waiting for connection") + while True: + conn, addr = s.accept() # Wait for connection + + with conn: # Connection received + logging.info(f"Connection with {addr}") + logging.info("Loading servos") + servos = load_servos() # Load the servos + + while True: + data = conn.recv(1024) + if not data: + logging.info("Empty input, stopping connection") + break + + try: + inp = data.decode('utf-8') + logging.info(f"Received {inp}") + i = int(inp[0]) + v = int(inp[2:]) + + # Check if received velocity is out of range. If so, return + # info that velocity was changed + if np.abs(v) > VEL_MAX: + conn.sendall(b"i:velocity") + v = VEL_MAX*np.sign(v) + else: + conn.sendall(data) + servos[i].goal_velocity.write(v) + + except Exception as e: + # If an exception is encountered, send it to the client. + logging.info(f"Exception encountered: {e}") + message = "e:"+str(e) + conn.sendall(message.encode('utf-8')) + + \ No newline at end of file diff --git a/code/tcp-server/servo_server_launcher.sh b/code/tcp-server/servo_server_launcher.sh new file mode 100644 index 0000000000000000000000000000000000000000..3f5395f9ab9cf4a8aec423df2599d531e1c16ca1 --- /dev/null +++ b/code/tcp-server/servo_server_launcher.sh @@ -0,0 +1,6 @@ +#!/bin/sh +# servo_server_launcher.sh +# This script navigates to the ServoServer directory and then launches the server. + +cd ~/ServoServer +sudo python servo_server.py \ No newline at end of file