diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..b0471c3c99380974e808a3985a9a145886dd92a2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Department of Automatic Control, Lund University + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/__pycache__/Omnibot.cpython-310.pyc b/__pycache__/Omnibot.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..07df5f4bb8d582d36a2ff6892b6165d2280c37f7 Binary files /dev/null and b/__pycache__/Omnibot.cpython-310.pyc differ diff --git a/dist/omnibot_client-0.0.1-py3-none-any.whl b/dist/omnibot_client-0.0.1-py3-none-any.whl new file mode 100644 index 0000000000000000000000000000000000000000..af5823726089324bb369e8e7479102449be92426 Binary files /dev/null and b/dist/omnibot_client-0.0.1-py3-none-any.whl differ diff --git a/dist/omnibot_client-0.0.1.tar.gz b/dist/omnibot_client-0.0.1.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..41e7273a4e6402e3f1ea24c2309233b41c8b84db Binary files /dev/null and b/dist/omnibot_client-0.0.1.tar.gz differ diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000000000000000000000000000000000000..ccba303102d602b093c27b389b54e99e5348ee7a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,22 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "omnibot_client" +version = "0.0.1" +authors = [ + { name="Felix Agner", email="felix.agner@control.lth.se" }, +] +description = "A python package for connecting to omnibots via TCP." +readme = "README.md" +requires-python = ">=3.7" +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", +] + +[project.urls] +"Homepage" = "https://gitlab.control.lth.se/processes/omnibot/omnibotclient.py" +"Bug Tracker" = "https://gitlab.control.lth.se/processes/omnibot/omnibotclient.py/issues" diff --git a/src/omnibot_client/ControllerTest.py b/src/omnibot_client/ControllerTest.py new file mode 100644 index 0000000000000000000000000000000000000000..3e81f5062633b9be6d5230c189e6ad99a6fe466a --- /dev/null +++ b/src/omnibot_client/ControllerTest.py @@ -0,0 +1,120 @@ +# general +import numpy as np +import sys +from time import time, sleep + +# threads +import threading + +# Controller +from inputs import get_gamepad + +from Omnibot import OmniBot + +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" + """ + 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 + + + +def run_omnibot_with_controller(HOST,verbose=False): + """ + Connect to an omnibot at ip HOST and proceed to steer it with a controller and print current position of omnibot. + """ + + # 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 = 6 # 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 OmniBot(HOST,verbose=verbose) as bot: + 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): + bot.set_speed(i,round(v)) + + print('x:'+bot.get_x()) + print('y:'+bot.get_y()) + + sleep(max(0,t0+ts-time())) + + +if __name__ == '__main__': + + # Server settings + HOST = "localhost" + + 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] + + print(f"HOST: \t {HOST}") + run_omnibot_with_controller(HOST) \ No newline at end of file diff --git a/src/omnibot_client/Dummybot.py b/src/omnibot_client/Dummybot.py new file mode 100644 index 0000000000000000000000000000000000000000..d710439bcd009c88bf0a9bbb0fa8fc11ba382731 --- /dev/null +++ b/src/omnibot_client/Dummybot.py @@ -0,0 +1,40 @@ +# general +import sys +from time import time, sleep + +from Omnibot import OmniBot + + +def run_dummybot(HOST,verbose=True): + """ + Runs a dummybot at host HOST that rotates a bit while printing x and y coordinates. + """ + + ts = 0.1 # sampling time + tstart = time() + + with OmniBot(HOST,verbose=verbose) as bot: + print("Connected") + + while time() < tstart + 5: + t0 = time() + + bot.set_speed([]) + + + print('x:'+bot.get_x()) + print('y:'+bot.get_y()) + + sleep(max(0,t0+ts-time())) + +if __name__ == '__main__': + # Server settings + HOST = "localhost" + + 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] + + print(f"HOST: \t {HOST}") + run_dummybot(HOST) \ No newline at end of file diff --git a/src/omnibot_client/Omnibot.py b/src/omnibot_client/Omnibot.py new file mode 100644 index 0000000000000000000000000000000000000000..4482668bd9924abd3ed30d7429a38e7a9de31485 --- /dev/null +++ b/src/omnibot_client/Omnibot.py @@ -0,0 +1,159 @@ +import socket + +class OmniBot(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) (default 9998) + verbose: choose level of chit-chat (boolean) + """ + def __init__(self,HOST,PORT=9998,verbose=False): + self.HOST = HOST + self.PORT = PORT + self.verbose=verbose + self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + + def __enter__(self): + """ + Boot up the socket and connect to the omnibot server + """ + if self.verbose: + print("Connecting to HOST: " + str(self.HOST) + ", PORT: " + str(self.PORT)) + + self.sock.connect((self.HOST, self.PORT)) # Connect to the robot + if self.verbose: + print("Connection successful.") + + + 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.sock.close() + + def send_and_receive(self,message): + """Send a message to the omnibot and return the answer that the omnibot gave.""" + # Send the package + self.sock.sendall(bytes(message,'utf-8')) + # Receive confirmation + ret=self.sock.recv(1024).decode('utf-8') + + return ret + + def set_speed(self,i,v): + """ + Set the speed of servo i to v + + + Communicates with messages on the form "wivvvv" + where w stands for "write"self. + i stands for index of desired servo + vvvv stands for requested target speed for the servo + """ + + package = 'w'+str(i)+str(v) + if self.verbose: + print("Sending " + package) + + + ret = self.send_and_receive(package) + + if ret[0] == "e": + raise Exception(ret) + elif self.verbose: + print(ret) + + def set_speeds(self,v): + """ + Set the speed of the servos to the values in vector v + """ + for (i,vi) in enumerate(v): + self.set_speed(i,vi) + + def get_x(self): + """Get x coordinate + + Sends a message "rx" + where "r" stands for "read" and "x" stands for "x" + """ + if self.verbose: + print("Requesting x") + + ret = self.send_and_receive("rx") + if ret[0] == "e": + raise Exception(ret) + + if self.verbose: + print("x: "+ret) + + return float(ret) + + def get_y(self): + """Get y coordinate + + Sends a message "ry" + where "r" stands for "read" and "y" stands for "y" + """ + if self.verbose: + print("Requesting y") + + ret = self.send_and_receive("ry") + if ret[0] == "e": + raise Exception(ret) + + if self.verbose: + print("y: "+ret) + + return float(ret) + + def get_z(self): + """Get z coordinate + + Sends a message "rz" + where "r" stands for "read" and "z" stands for "z" + """ + if self.verbose: + print("Requesting z") + + ret = self.send_and_receive("rz") + if ret[0] == "e": + raise Exception(ret) + + if self.verbose: + print("z: "+ret) + + return float(ret) + + def get_theta(self): + """Get x coordinate + + Sends a message "rtheta" + where "r" stands for "read" and "theta" stands for "theta" + """ + if self.verbose: + print("Requesting theta") + + ret = self.send_and_receive("rtheta") + if ret[0] == "e": + raise Exception(ret) + + if self.verbose: + print("theta: "+ret) + + return float(ret) + + + def get_state(self): + """"Get state vector of [x y theta]^T.""" + + x = self.get_x() + y = self.get_y() + theta = self.get_theta() + + return [x,y,theta] diff --git a/src/omnibot_client/__init__.py b/src/omnibot_client/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/test/gitkeep b/test/gitkeep new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391