From 9462799bc197d2f1aa75368d91fa109b72057ed4 Mon Sep 17 00:00:00 2001 From: Spencer Gautreaux <37717452+Gautreaux@users.noreply.github.com> Date: Sat, 22 Aug 2020 11:45:47 -0500 Subject: [PATCH] creating the library framework --- Examples/Examples.md | 2 +- Examples/WithMotors/WithMotors.md | 2 +- Examples/WithMotors/downloadScript.sh | 10 +- Library/About.md | 21 +++++ Library/abstractMotorInterface.py | 37 ++++++++ Library/adaMotorInterface.py | 49 ++++++++++ Library/connectionBase.js | 55 +++++++++++ Library/joystick.css | 5 + Library/joystickBase.html | 25 +++++ Library/joysticksController.js | 6 ++ Library/joysticksGeneral.js | 9 ++ Library/server.py | 130 ++++++++++++++++++++++++++ Library/singleJoy.sh | 1 + Library/slidersIndex.html | 37 ++++++++ Library/slidersScript.js | 103 ++++++++++++++++++++ Library/statusBar.js | 27 ++++++ Library/statusConnection.js | 20 ++++ Library/styles.css | 74 +++++++++++++++ Library/virtualMotorInterface.py | 41 ++++++++ 19 files changed, 647 insertions(+), 7 deletions(-) create mode 100644 Library/About.md create mode 100644 Library/abstractMotorInterface.py create mode 100644 Library/adaMotorInterface.py create mode 100644 Library/connectionBase.js create mode 100644 Library/joystick.css create mode 100644 Library/joystickBase.html create mode 100644 Library/joysticksController.js create mode 100644 Library/joysticksGeneral.js create mode 100644 Library/server.py create mode 100644 Library/singleJoy.sh create mode 100644 Library/slidersIndex.html create mode 100644 Library/slidersScript.js create mode 100644 Library/statusBar.js create mode 100644 Library/statusConnection.js create mode 100644 Library/styles.css create mode 100644 Library/virtualMotorInterface.py diff --git a/Examples/Examples.md b/Examples/Examples.md index 60fc49c..1c24476 100644 --- a/Examples/Examples.md +++ b/Examples/Examples.md @@ -1,6 +1,6 @@ # Examples -This folder contains a series of minimum functional code examples that should allow for a quick start to development. While it is likely possible to start at the final one and understand the code, it may be easier to progress through them in order. The intended order is `Fundamentals` > `Skeleton Code` > `With Motors` +This folder contains a series of minimum functional code examples that should allow for a quick background for further development. The intended order is `Fundamentals` > `Skeleton Code` > `With Motors`. For more complete examples, see the `Library` folder. Sample 3d Models are collected on the [CADExamples](CADExamples.md) page. diff --git a/Examples/WithMotors/WithMotors.md b/Examples/WithMotors/WithMotors.md index f463651..0ec0707 100644 --- a/Examples/WithMotors/WithMotors.md +++ b/Examples/WithMotors/WithMotors.md @@ -7,7 +7,7 @@ This version adds the ability to control the motors via the Adafruit motor shiel To download these files, a simple script has been provided. Starting in the `var/www/html` directory, make sure that it is empty using `rm -rf var/www/html/*`. Note, this command will **DELETE EVERYTHING** in that directory. Next, download the script via ``` -wget https://raw.githubusercontent.com/Gautreaux/PiSetup/master/Examples/WithMotors/downloadScript.sh +wget https://raw.githubusercontent.com/Aggie-Robotics/PiSetup/master/Examples/WithMotors/downloadScript.sh ``` then, run the script to download the remaining files: diff --git a/Examples/WithMotors/downloadScript.sh b/Examples/WithMotors/downloadScript.sh index 641cf48..985b472 100644 --- a/Examples/WithMotors/downloadScript.sh +++ b/Examples/WithMotors/downloadScript.sh @@ -1,8 +1,8 @@ -wget https://raw.githubusercontent.com/Gautreaux/PiSetup/master/Examples/WithMotors/index.html -wget https://raw.githubusercontent.com/Gautreaux/PiSetup/master/Examples/WithMotors/motorInterface.py -wget https://raw.githubusercontent.com/Gautreaux/PiSetup/master/Examples/WithMotors/script.js -wget https://raw.githubusercontent.com/Gautreaux/PiSetup/master/Examples/WithMotors/server.py -wget https://raw.githubusercontent.com/Gautreaux/PiSetup/master/Examples/WithMotors/styles.css +wget https://raw.githubusercontent.com/Aggie-Robotics/PiSetup/master/Examples/WithMotors/index.html +wget https://raw.githubusercontent.com/Aggie-Robotics/PiSetup/master/Examples/WithMotors/motorInterface.py +wget https://raw.githubusercontent.com/Aggie-Robotics/PiSetup/master/Examples/WithMotors/script.js +wget https://raw.githubusercontent.com/Aggie-Robotics/PiSetup/master/Examples/WithMotors/server.py +wget https://raw.githubusercontent.com/Aggie-Robotics/PiSetup/master/Examples/WithMotors/styles.css chmod u+r+x index.html chmod u+r+x motorInterface.py diff --git a/Library/About.md b/Library/About.md new file mode 100644 index 0000000..ed132bf --- /dev/null +++ b/Library/About.md @@ -0,0 +1,21 @@ +# Library + +This folder contains sample/started code to assist with the development of your system. Download scripts are provided for grouped components. + +# Scripts + +Use the scripts to collect code by: + +1. Navigate to folder +1. (Optional) empty the folder `rm -rf *` + 1. WARNING: this will delete everything +1. Download the script: `wget https://raw.githubusercontent.com/Aggie-Robotics/PiSetup/master/Examples/WithMotors/.sh` +1. Allow the script permission to run: `chmod u+r+x .sh` + +1. Run the script: `./.sh` + +## Single Joystick + +Name: `singleJoy.sh` + +A simple controller with a single joystick. Script contains a full python, javascript, and html (index.html) ready to deploy. Also implements the virtual motor interface. \ No newline at end of file diff --git a/Library/abstractMotorInterface.py b/Library/abstractMotorInterface.py new file mode 100644 index 0000000..5a41d5f --- /dev/null +++ b/Library/abstractMotorInterface.py @@ -0,0 +1,37 @@ +from abc import ABC, abstractmethod + +#abstract class for a motor interface +class MotorInterface(ABC): + #constructor + def __init__(self): + super().__init__() + + #destructor + @abstractmethod + def __del__(self): + raise NotImplementedError + + #return the value associated with the key + #i.e. return the throttle of the motor with that index + @abstractmethod + def __getitem__(self, key): + raise NotImplementedError + + #set the value associated with the key + #i.e. set the value of the throttle for the motor at index i + # the throttle value should be in the range [-1,1] inclusive or None + @abstractmethod + def __setitem__(self, key, value): + raise NotImplementedError + + #stop all the motors + @abstractmethod + def stop(self): + pass + + #what is the string representation of the motors + def __str__(self): + return "abstract MotorInterface" + + + diff --git a/Library/adaMotorInterface.py b/Library/adaMotorInterface.py new file mode 100644 index 0000000..af47657 --- /dev/null +++ b/Library/adaMotorInterface.py @@ -0,0 +1,49 @@ +from abstractMotorInterface import MotorInterface + +# interface for the adafruit motor library via motor interface +class AdaMotorInterface(MotorInterface): + kit = None #shared across all instances of AdaMotorInterface + + def __init__(self): + try: + kit == None + except UnboundLocalError: + #do not load the interface until needed + from adafruit_motorkit import MotorKit + kit = MotorKit() + + self.motors = [kit.motor1, kit.motor2, kit.motor3, kit.motor4] + self.stop() + + def __del__(self): + self.stop() + + def __getitem__(self, key): + return self.motors[key].throttle + + def __setitem__(self, key, value): + if(value != None and (value < -1 or value > 1)): + raise ValueError(f"Illegal throttle Value. Should be float in inclusive range [-1,1] got {value}") + self.motors[key].throttle = value + + def stop(self): + #stop all the motors + for i in range(len(self.motors)): + self.motors[i].throttle = None + + def __str__(self): + s = "[" + for i in range(len(self.motors)): + try: + t = self.motors[i].throttle + if(t > 0): + s += f" {t:4.2f}" #4.2f formats how the number is printed + elif(t < 0): + s += f"{t:4.2f}" + else: + s += " 0" + except TypeError: + s += " None" + if(i != len(self.motors)-1): + s += "," + return s + "]" \ No newline at end of file diff --git a/Library/connectionBase.js b/Library/connectionBase.js new file mode 100644 index 0000000..7c1c6ef --- /dev/null +++ b/Library/connectionBase.js @@ -0,0 +1,55 @@ +class ConnectionBase { + constructor(host, port) { + this.host = host + this.port = port + } + + connect() { + let s = "ws://" + this.host + ":" + this.port; + console.log("Client started. Tying connection on: " + s); + + //create the socket object + this.socket = new WebSocket(s); + + //add the callbacks as necessary + this.socket.onopen = this.onSocketOpen; //called when connection established + this.socket.onerror = this.onSocketError; //called when error + this.socket.onmessage = this.onSocketReceive; //called each message received + this.socket.onclose = this.onSocketClose; //called each close + } + + //called when the socket establishes a connection to the server + onSocketOpen(event) { + console.log("Socket Opened"); + } + + //called on a socket error + // most commonly, this is due to connection timeout + //most errors also cause the socket to close, + // calling onclose callback after the onerror callback + onSocketError(event) { + console.log("Socket Error"); + } + + //called when the socket is closed + onSocketClose(event) { + console.log("Socket Closed"); + //now that the socket is invalid, remove it + this.socket = null; + } + + //called when the socket receives data + onSocketReceive(event) { + console.log("Message Received '" + event.data + "'"); + } + + //sends the input parameter into the socket + send(message) { + if (this.socket == null) { + console.log("Cannot send message, socket is not connected."); + } + else { + this.socket.send(message); + } + } +} \ No newline at end of file diff --git a/Library/joystick.css b/Library/joystick.css new file mode 100644 index 0000000..24cca82 --- /dev/null +++ b/Library/joystick.css @@ -0,0 +1,5 @@ +#joystick{ + height: 50px; + width: 50%; + background-color: red; +} \ No newline at end of file diff --git a/Library/joystickBase.html b/Library/joystickBase.html new file mode 100644 index 0000000..8362da3 --- /dev/null +++ b/Library/joystickBase.html @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + +
+
+ Open the console: +
+ ctrl+shift+j on chrome + \ No newline at end of file diff --git a/Library/joysticksController.js b/Library/joysticksController.js new file mode 100644 index 0000000..3fd3a2d --- /dev/null +++ b/Library/joysticksController.js @@ -0,0 +1,6 @@ +class Joystick{ + constructor(element) { + this.element = element + return this + } +} \ No newline at end of file diff --git a/Library/joysticksGeneral.js b/Library/joysticksGeneral.js new file mode 100644 index 0000000..328c7ac --- /dev/null +++ b/Library/joysticksGeneral.js @@ -0,0 +1,9 @@ +function initFunction(){ + let j = document.getElementById("joystick") + let joystick = new Joystick(j) + + let s = document.getElementById("statusBar"); + let statusBar = new StatusBar(s) + connection = new StatusConnection(myHost, myPort, statusBar); + +} \ No newline at end of file diff --git a/Library/server.py b/Library/server.py new file mode 100644 index 0000000..14a8e7c --- /dev/null +++ b/Library/server.py @@ -0,0 +1,130 @@ +import asyncio +import websockets +import sys +from signal import SIGINT, SIGTERM +from virtualMotorInterface import VirtualMotorInterface +from adaMotorInterface import AdaMotorInterface +from time import sleep + +DEFAULT_PORT = "8765" +DEFAULT_HOST = "192.168.4.1" + + +#function builder utilizing lambda capture +# allows the inner function access to the args of the outer function +def buildConnectionHandler(interface): + #called once per connection + async def connectionHandler(websocket, path): + try: + print("New connection received.") + async for message in websocket: + try: + assert(message[0] == "M") + i = int(message[1]) + assert(i in [0,1,2,3]) #should preempt the IndexError + if(message[2:] == "None"): + v = None + else: + v = float(message[2:]) + interface[i] = v + # print(interface) #print the updated interface values + except (ValueError, AssertionError, IndexError): + #fall back onto the echo functionality + print(f"Received new message '{message}'") + await websocket.send(f"ECHO:{message}") + + #this part is executed after the websocket is closed by the client + #if the socket is closed by the server (i.e. ctrl-c) this is not executed + print("Connection lost.") + interface.stop() #stop the motors (for safety) + print(interface) + except websockets.exceptions.ConnectionClosedError: + #when the websocket is closed in some abnormal fashion + print("Connection closed abnormally.") + interface.stop() + return connectionHandler + + +#if the python module has been launched as the main one +if __name__ == "__main__": + #first, we allow for command line arguments formatted such: + # server.py + # where hostname and port are optional + try: + port = sys.argv[2] + except IndexError: + #The user did not provide an port argument + port = DEFAULT_PORT + + try: + host = sys.argv[1] + except IndexError: + host = DEFAULT_HOST + + try: + motorInterfaceType = int(sys.argv[3]) + except (IndexError, ValueError): + motorInterfaceType = 0 + + #next, write the host:port combo to a file that js can read. + try: + with open("hostPort.js", 'w') as fileOut: + fileOut.write(f"myHost = '{host}';") + fileOut.write(f"myPort = '{port}';") + #with open() as syntax automatically closes the file on exit + except (FileNotFoundError, FileExistsError, OSError) as e: + print("Something went wrong trying to write the hostport.js file. Type:" + str(type(e))) + + print(f"Starting Server at {host}:{port}") + + if(motorInterfaceType == 1): + print("Using the AdaMotorInterface") + interface = AdaMotorInterface() + else: + print("Using the VirtualMotorInterface") + interface = VirtualMotorInterface() + + #This outer while loop "solves" the race condition. + #More on this at the end of the file. + failCtr = 5 + while(True): + #start the server + server = websockets.serve(buildConnectionHandler(interface), host, port) + + try: + #since the connection is asynchronous, we need to hold the program until its finished + # under normal circumstances, this means we wait forever + asyncio.get_event_loop().run_until_complete(server) + print("server started successfully.") + failCtr = -1 + asyncio.get_event_loop().run_forever() + #any code down here would not be reachable until the server closes its socket + # under normal circumstances, this means this code is unreachable + + except KeyboardInterrupt: + #the interrupt was fired (ctrl-c), time to exit + #note, the interrupt wont happen till the next async event happens + print("Exiting via KeyboardInterrupt") + exit(-1) + except OSError as e: + failCtr+=1 + if(failCtr > 5): + print("The server startup failed too many times, allowing exception to fire.") + print("\tThis may be a result of too many packages slowing down the loading of the network interface.") + raise e #allow the error to fire. + else: + sleep(5) + +#About the race condition: +#The pi has to load packages that give it the ability to accept incoming connections +#These packages take time to load (both on the pi and within the actual wifi chip) +#In other threads, the pi continues loading other packages +#One of these other packages is the rc.local script +#Which launches this python program in the background +#It is a frequent occurrence that rc.local happens before the wifi fully loads +#Which causes a bind failure and an exception + +#In the long term, it would be better to register the server as a service that is dependent on wifi +#Thus, the kernel will not launch it until the wifi is ready, preventing error +#Futhermore, in the case of a crash, the kernel should be able to relaunch the program + diff --git a/Library/singleJoy.sh b/Library/singleJoy.sh new file mode 100644 index 0000000..b448783 --- /dev/null +++ b/Library/singleJoy.sh @@ -0,0 +1 @@ +ECHO Not Implemented \ No newline at end of file diff --git a/Library/slidersIndex.html b/Library/slidersIndex.html new file mode 100644 index 0000000..86c309c --- /dev/null +++ b/Library/slidersIndex.html @@ -0,0 +1,37 @@ + + + + + + + + + +
No Connection Attempted
+
+
+ + +
+
+
--
+
+
+
+
+
+ + + + + +
+
+ +
+
+
+ Open the console: +
+ ctrl+shift+j on chrome + \ No newline at end of file diff --git a/Library/slidersScript.js b/Library/slidersScript.js new file mode 100644 index 0000000..d1275f6 --- /dev/null +++ b/Library/slidersScript.js @@ -0,0 +1,103 @@ +mySocket = null; //global variable for storing the socket +sliderElements = null; //global variable storing the slider elements + +//called once on page load +function initFunction() { + s = "ws://" + myHost + ":" + myPort; + console.log("Client started. Tying connection on: " + s) + + setStatus("Socket Connecting", "yellow") + + //create the socket object + const socket = new WebSocket(s) + + //add the callbacks as necessary + socket.onopen = onSocketOpen; //called when connection established + socket.onerror = onSocketError; //called when error + socket.onmessage = onSocketReceive; //called each message received + socket.onclose = onSocketClose; //called each close + + //by caching this search, the web-app should be slightly faster + // not that thats going to matter in this context + sliderElements = document.getElementsByClassName("verticalSlider") +} + +//called when the socket establishes a connection to the server +function onSocketOpen(event) { + setStatus("Socket Opened", "lime") + console.log("Socket Opened"); + + //now that the socket is properly opened, capture it + mySocket = this; //this refers to the socket object +} + +//called on a socket error +// most commonly, this is due to connection timeout +//most errors also cause the socket to close, +// calling onclose callback after the onerror callback +function onSocketError(event) { + setStatus("Socket Error", "#f53636"); //set background to a soft red color + console.log("Socket Error"); +} + +//called when the socket is closed +function onSocketClose(event) { + setStatus("Socket Closed", "#f53636"); //set background to a soft red color + console.log("Socket Closed"); + + //now that the socket is invalid, remove it + mySocket = null; +} + +//called when the socket receives data +function onSocketReceive(event) { + console.log("Message Received '" + event.data + "'"); + + //update the output to reflect the value + document.getElementById("outputBar").innerHTML = event.data; +} + +//sends the input parameter into the socket +function send(message) { + if (mySocket == null) { + console.log("Cannot send message, socket is not connected.") + } + else { + mySocket.send(message); + } +} + +function buttonSend(){ + //get the contents in the input field and send it to the server + send(document.getElementById("inputField").value); +} + +//set the status bar to contain the message and color specified +function setStatus(message, color){ + element = document.getElementById("statusBar"); + element.style.backgroundColor = color; + element.innerHTML = message; +} + +//called whenever a slider value changes +// paramter is the id of the calling slider +function sliderUpdate(sliderIndex){ + // value = sliderElements[sliderIndex].value + // console.log(value) + + send("M" + sliderIndex + sliderElements[sliderIndex].value); +} + +//stops all the motors +function buttonStop(){ + send("M00"); + send("M10"); + send("M20"); + send("M30"); + + //update the sliders to reflect this change + sliderElements[0].value = 0 + sliderElements[1].value = 0 + sliderElements[2].value = 0 + sliderElements[3].value = 0 +} \ No newline at end of file diff --git a/Library/statusBar.js b/Library/statusBar.js new file mode 100644 index 0000000..ef6acd2 --- /dev/null +++ b/Library/statusBar.js @@ -0,0 +1,27 @@ +const StatusEnum = { + CONNECTED: 1, + DISCONNECTED: 2, +}; +Object.freeze(StatusEnum) + +class StatusBar { + constructor(element) { + this.element = element + this.element.innerHTML = "Status Unknown" + return this + } + + setStatus(status) { + if(status == StatusEnum.CONNECTED) { + this.element.innerHTML = "Connected"; + this.element.style.background = "green"; + } + else if(status == StatusEnum.DISCONNECTED) { + this.element.innerHTML = "Disconnected"; + this.element.style.background = "red"; + } + else { + console.log("Unrecognized status: " + status); + } + } +} \ No newline at end of file diff --git a/Library/statusConnection.js b/Library/statusConnection.js new file mode 100644 index 0000000..e3e5284 --- /dev/null +++ b/Library/statusConnection.js @@ -0,0 +1,20 @@ +class StatusConnection extends ConnectionBase { + + constructor(host, port, statusObj) { + super(host, port); + this.statusObj = statusObj; + + this.onSocketOpen = function(event){ + console.log("Socket Opened"); + statusObj.setStatus(StatusEnum.CONNECTED); + } + + this.onSocketClose = function(event){ + console.log("Socket Closed"); + statusObj.setStatus(StatusEnum.DISCONNECTED); + } + + this.connect() + } + +} \ No newline at end of file diff --git a/Library/styles.css b/Library/styles.css new file mode 100644 index 0000000..d9a896c --- /dev/null +++ b/Library/styles.css @@ -0,0 +1,74 @@ +#statusBar, #outputBar, #stopButtonDiv{ + display: inline-block; + width: 100%; + height: 10vh; + background-color: white; + border: 2px solid black; + font-size: 8vh; +} + +#inputBar{ + display: inline-block; + width: 100%; + height: 10vh; + background-color: white; + border: 2px solid black; +} + +#sendButton, #inputField{ + height: 100%; + border: none; + background-color: lightgray; +} + +body{ + height: 100%; +} + +#sliderBox{ + display: inline-block; + position: relative; + height: 50vh; + width: 100%; +} + +.verticalSlider{ + /* As sliders are only horizontal, this makes it vertical */ + -webkit-transform: rotate(-90deg); + -moz-transform: rotate(-90deg); + -o-transform: rotate(-90deg); + -ms-transform: rotate(-90deg); + transform: rotate(-90deg); + width: 50vh; + height: 25vw; + position: absolute; + top: calc(25vh - 12.5vw); /*See Markdown for explanation*/ + left:-25vh; + margin: none; +} + +#slider0{ + left: calc(12.5vw - 25vh); /*See Markdown for explanation*/ +} + +#slider1{ + left: calc(37.5vw - 25vh); /*See Markdown for explanation*/ +} + +#slider2{ + left: calc(62.5vw - 25vh); /*See Markdown for explanation*/ +} + +#slider3{ + left: calc(87.5vw - 25vh); /*See Markdown for explanation*/ +} + +#stopButton{ + border: 5px white dashed; + color: black; + background-color: red; + padding: none; + text-align: center; + font-size: 8vh; + width: 100%; +} \ No newline at end of file diff --git a/Library/virtualMotorInterface.py b/Library/virtualMotorInterface.py new file mode 100644 index 0000000..9f629a1 --- /dev/null +++ b/Library/virtualMotorInterface.py @@ -0,0 +1,41 @@ +from abstractMotorInterface import MotorInterface + +#interface for a virtual motor for debugging when motors are not available +class VirtualMotorInterface(MotorInterface): + def __init__(self): + self.motors = [None]*4 + self.stop() #not technically necessary + + def __del__(self): + self.stop() + + def __getitem__(self, key): + return self.motors[key] + + def __setitem__(self, key, value): + if(value != None and (value < -1 or value > 1)): + raise ValueError(f"Illegal throttle Value. Should be float in inclusive range [-1,1] got {value}") + self.motors[key] = value + print(self) + + def stop(self): + #stop all the motors + for i in range(len(self.motors)): + self.motors[i] = None + + def __str__(self): + s = "[" + for i in range(len(self.motors)): + try: + t = self.motors[i] + if(t > 0): + s += f" {t:4.2f}" #4.2f formats how the number is printed + elif(t < 0): + s += f"{t:4.2f}" + else: + s += " 0" + except TypeError: + s += " None" + if(i != len(self.motors)-1): + s += "," + return s + "]" \ No newline at end of file