Protobuf prototype
This commit is contained in:
parent
e277a83c5f
commit
9b567b8c6c
119 changed files with 8599 additions and 0 deletions
233
compLib/Api.py
233
compLib/Api.py
|
@ -1,233 +0,0 @@
|
|||
import json
|
||||
import os
|
||||
import time
|
||||
from typing import Dict, Tuple, List
|
||||
|
||||
import requests
|
||||
|
||||
from compLib.LogstashLogging import Logging
|
||||
|
||||
API_URL = os.getenv("API_URL", "http://localhost:5000/") + "api/"
|
||||
|
||||
api_override = os.getenv("API_FORCE", "")
|
||||
|
||||
if api_override != "":
|
||||
print(f"API_URL was set to {API_URL} but was overwritten with {api_override}")
|
||||
API_URL = api_override
|
||||
|
||||
API_URL_GET_DELIVERY = API_URL + "getDelivery"
|
||||
API_URL_GET_MATERIAL = API_URL + "getMaterial"
|
||||
API_URL_GET_GARBAGE = API_URL + "getGarbage"
|
||||
API_URL_GET_LIST_CARGO = API_URL + "listCargo"
|
||||
API_URL_GET_CARGO = API_URL + "getCargo/"
|
||||
API_URL_GET_ROBOT_STATE = API_URL + "getRobotState"
|
||||
|
||||
API_URL_GET_POS = API_URL + "getPos"
|
||||
API_URL_GET_OP = API_URL + "getOp"
|
||||
API_URL_GET_GOAL = API_URL + "getGoal"
|
||||
API_URL_GET_ITEMS = API_URL + "getItems"
|
||||
API_URL_GET_SCORES = API_URL + "getScores"
|
||||
API_URL_GET_METEOROID = API_URL + "getMeteoroids"
|
||||
|
||||
|
||||
class Seeding:
|
||||
"""Class used for communicating with seeding api
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def get_delivery() -> Tuple[Dict, int]:
|
||||
"""Makes the /api/getDelivery call to the api.
|
||||
|
||||
:return: Json Object and status code as returned by the api.
|
||||
:rtype: Tuple[Dict, int]
|
||||
"""
|
||||
res = requests.get(API_URL_GET_DELIVERY)
|
||||
result = json.loads(res.content)
|
||||
Logging.get_logger().debug(f"Seeding.get_delivery = {result}, status code = {res.status_code}")
|
||||
return result, res.status_code
|
||||
|
||||
@staticmethod
|
||||
def get_material() -> Tuple[Dict, int]:
|
||||
"""Makes the /api/getMaterial call to the api.
|
||||
|
||||
:return: Json Object and status code as returned by the api.
|
||||
:rtype: Tuple[Dict, int]
|
||||
"""
|
||||
res = requests.get(API_URL_GET_MATERIAL)
|
||||
result = json.loads(res.content)
|
||||
Logging.get_logger().debug(f"Seeding.get_material = {result}, status code = {res.status_code}")
|
||||
return result, res.status_code
|
||||
|
||||
@staticmethod
|
||||
def get_garbage() -> Tuple[Dict, int]:
|
||||
"""Makes the /api/getGarbage call to the api.
|
||||
|
||||
:return: Json Object and status code as returned by the api.
|
||||
:rtype: Tuple[Dict, int]
|
||||
"""
|
||||
res = requests.get(API_URL_GET_GARBAGE)
|
||||
result = json.loads(res.content)
|
||||
Logging.get_logger().debug(f"Seeding.get_garbage {result}, status code = {res.status_code}")
|
||||
return result, res.status_code
|
||||
|
||||
@staticmethod
|
||||
def list_cargo() -> Tuple[Dict, int]:
|
||||
"""Makes the /api/listCargo call to the api.
|
||||
|
||||
:return: Json Object and status code as returned by the api.
|
||||
:rtype: Tuple[Dict, int]
|
||||
"""
|
||||
res = requests.get(API_URL_GET_LIST_CARGO)
|
||||
result = json.loads(res.content)
|
||||
Logging.get_logger().debug(f"Seeding.list_cargo {result}, status code = {res.status_code}")
|
||||
return result, res.status_code
|
||||
|
||||
@staticmethod
|
||||
def get_cargo(color: str) -> Tuple[Dict, int]:
|
||||
"""Makes the /api/getCargo call to the api.
|
||||
|
||||
:param color: Color parameter which specifies which cargo should be taken. (A string which is either "green", "red", "yellow" or "blue") The function only picks up one package.
|
||||
:return: Json Object and status code as returned by the api.
|
||||
:rtype: Tuple[Dict, int]
|
||||
"""
|
||||
res = requests.get(API_URL_GET_CARGO + color)
|
||||
result = json.loads(res.content)
|
||||
Logging.get_logger().debug(f"Seeding.get_cargo {result}, status code = {res.status_code}")
|
||||
return result, res.status_code
|
||||
|
||||
@staticmethod
|
||||
def get_robot_state() -> Tuple[Dict, int]:
|
||||
res = requests.get(API_URL_GET_ROBOT_STATE)
|
||||
result = json.loads(res.content)
|
||||
Logging.get_logger().debug(f"Seeding.get_robot_state {result}, status code = {res.status_code}")
|
||||
return result, res.status_code
|
||||
|
||||
|
||||
class Position:
|
||||
"""Datastructure for holding a position
|
||||
"""
|
||||
|
||||
def __init__(self, x, y, degrees):
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.degrees = degrees
|
||||
|
||||
def __repr__(self):
|
||||
return f"Position(x={self.x}, y={self.y}, degrees={self.degrees})"
|
||||
|
||||
def __str__(self):
|
||||
return f"Position(x={round(self.x, 5)}, y={round(self.y, 5)}, degrees={round(self.degrees, 5)})"
|
||||
|
||||
|
||||
class DoubleElim:
|
||||
"""Class used for communicating with double elimination api
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def get_position() -> Tuple[Position, int]:
|
||||
"""Makes the /api/getPos call to the api.
|
||||
|
||||
:return: A Position object with robot position
|
||||
:rtype: Tuple[Position, int]
|
||||
"""
|
||||
res = requests.get(API_URL_GET_POS)
|
||||
if res.status_code == 408:
|
||||
Logging.get_logger().error(f"DoubleElim.get_position timeout!")
|
||||
time.sleep(0.01)
|
||||
return DoubleElim.get_position()
|
||||
|
||||
response = json.loads(res.content)
|
||||
Logging.get_logger().debug(f"DoubleElim.get_position = {response}, status code = {res.status_code}")
|
||||
return Position(response["x"], response["y"], response["degrees"]), res.status_code
|
||||
|
||||
@staticmethod
|
||||
def get_opponent() -> Tuple[Position, int]:
|
||||
"""Makes the /api/getOp call to the api.
|
||||
|
||||
:return: A Position object with opponents robot position
|
||||
:rtype: Tuple[Position, int]
|
||||
"""
|
||||
res = requests.get(API_URL_GET_OP)
|
||||
if res.status_code == 408:
|
||||
Logging.get_logger().error(f"DoubleElim.get_opponent timeout!")
|
||||
time.sleep(0.01)
|
||||
return DoubleElim.get_opponent()
|
||||
|
||||
response = json.loads(res.content)
|
||||
Logging.get_logger().debug(f"DoubleElim.get_opponent = x:{response}, status code = {res.status_code}")
|
||||
return Position(response["x"], response["y"], response["degrees"]), res.status_code
|
||||
|
||||
@staticmethod
|
||||
def get_goal() -> Tuple[Position, int]:
|
||||
"""Makes the /api/getGoal call to the api.
|
||||
|
||||
:return: A Position object with x and y coordinates of the goal, rotation is always -1
|
||||
:rtype: Tuple[Position, int]
|
||||
"""
|
||||
res = requests.get(API_URL_GET_GOAL)
|
||||
if res.status_code == 408:
|
||||
Logging.get_logger().error(f"DoubleElim.get_goal timeout!")
|
||||
time.sleep(0.01)
|
||||
return DoubleElim.get_goal()
|
||||
|
||||
response = json.loads(res.content)
|
||||
Logging.get_logger().debug(f"DoubleElim.get_goal = {response}, status code = {res.status_code}")
|
||||
return Position(response["x"], response["y"], -1), res.status_code
|
||||
|
||||
@staticmethod
|
||||
def get_items() -> Tuple[List[Dict], int]:
|
||||
"""Makes the /api/getItems call to the api.
|
||||
|
||||
:return: A list will all items currently on the game field. Items are dictionaries that look like: {"id": 0, "x": 0, "y": 0}
|
||||
:rtype: Tuple[List[Dict], int]
|
||||
"""
|
||||
res = requests.get(API_URL_GET_ITEMS)
|
||||
if res.status_code == 408:
|
||||
Logging.get_logger().error(f"DoubleElim.get_items timeout!")
|
||||
time.sleep(0.01)
|
||||
return DoubleElim.get_items()
|
||||
elif res.status_code == 503:
|
||||
return [], 503
|
||||
|
||||
response = json.loads(res.content)
|
||||
Logging.get_logger().debug(f"DoubleElim.get_items = {response}, status code = {res.status_code}")
|
||||
return response, res.status_code
|
||||
|
||||
@staticmethod
|
||||
def get_scores() -> Tuple[Dict, int]:
|
||||
"""Makes the /api/getScores call to the api.
|
||||
|
||||
:return: A dictionary with all scores included like: {"self":2,"opponent":0}
|
||||
:rtype: Tuple[Dict, int]
|
||||
"""
|
||||
res = requests.get(API_URL_GET_SCORES)
|
||||
if res.status_code == 408:
|
||||
Logging.get_logger().error(f"DoubleElim.get_scores timeout!")
|
||||
time.sleep(0.01)
|
||||
return DoubleElim.get_scores()
|
||||
elif res.status_code == 503:
|
||||
return {"self": 0, "opponent": 0}, 503
|
||||
|
||||
response = json.loads(res.content)
|
||||
Logging.get_logger().debug(f"DoubleElim.get_scores = {response}, status code = {res.status_code}")
|
||||
return response, res.status_code
|
||||
|
||||
@staticmethod
|
||||
def get_meteoroids() -> Tuple[List[Dict], int]:
|
||||
"""Makes the /api/getMeteoroids call to the api.
|
||||
|
||||
:return: A list will all meteoroids currently on the game field. Meteoroids are dictionaries that look like: {"x": 0, "y": 0}
|
||||
:rtype: Tuple[List[Dict], int]
|
||||
"""
|
||||
res = requests.get(API_URL_GET_METEOROID)
|
||||
if res.status_code == 408:
|
||||
Logging.get_logger().error(f"DoubleElim.get_meteoroids timeout!")
|
||||
time.sleep(0.01)
|
||||
return DoubleElim.get_meteoroids()
|
||||
elif res.status_code == 503:
|
||||
return [], 503
|
||||
|
||||
response = json.loads(res.content)
|
||||
Logging.get_logger().debug(f"DoubleElim.get_meteoroids = {response}, status code = {res.status_code}")
|
||||
return response, res.status_code
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
import string
|
||||
|
||||
from compLib.LogstashLogging import logstash_logger
|
||||
from compLib.Spi import Spi, Register
|
||||
|
||||
LINE_COUNT = 4
|
||||
CHARS_PER_LINE = 16
|
||||
|
||||
|
||||
class Display(object):
|
||||
"""Access the display on the robot.
|
||||
The display is split into 4 Rows and 16 Columns. Each function call changes one line at a time.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def write(line: int, text: str):
|
||||
"""Write a string of text to the integrated display.
|
||||
|
||||
:param line: Line to write. Between 1 and 4
|
||||
:param text: Text to write. Up to 16 characters
|
||||
:raises: IndexError
|
||||
"""
|
||||
if len(text) > CHARS_PER_LINE:
|
||||
logstash_logger.error(f"Too many characters specified!")
|
||||
return
|
||||
|
||||
if line <= 0 or line > LINE_COUNT:
|
||||
raise IndexError("Invalid line number specified")
|
||||
|
||||
to_write = [0] * CHARS_PER_LINE
|
||||
for i in range(len(text)):
|
||||
to_write[i] = ord(text[i])
|
||||
|
||||
if line == 1:
|
||||
Spi.write_array(Register.DISPLAY_LINE_1_C0, CHARS_PER_LINE, to_write)
|
||||
elif line == 2:
|
||||
Spi.write_array(Register.DISPLAY_LINE_2_C0, CHARS_PER_LINE, to_write)
|
||||
elif line == 3:
|
||||
Spi.write_array(Register.DISPLAY_LINE_3_C0, CHARS_PER_LINE, to_write)
|
||||
elif line == 4:
|
||||
Spi.write_array(Register.DISPLAY_LINE_4_C0, CHARS_PER_LINE, to_write)
|
||||
|
||||
@staticmethod
|
||||
def clear():
|
||||
"""Clear the display
|
||||
|
||||
"""
|
||||
for i in range(1, LINE_COUNT + 1):
|
||||
Display.write(i, "")
|
|
@ -1,127 +0,0 @@
|
|||
import atexit
|
||||
from enum import Enum
|
||||
|
||||
from compLib.LogstashLogging import Logging
|
||||
from compLib.Spi import Spi, Register
|
||||
|
||||
MOTOR_COUNT = 4
|
||||
|
||||
|
||||
encoder_start_values = [0] * (MOTOR_COUNT + 1)
|
||||
|
||||
|
||||
class Encoder(object):
|
||||
"""Class used to read the encoders
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def handle_wrap(raw_value, port):
|
||||
"""Handle overflow and underflow of int for encoders.
|
||||
|
||||
:param raw_value: Raw value which was read on port
|
||||
:param port: Port, which the motor is connected to. Between 1 and 4
|
||||
:raises: IndexError
|
||||
:return: Current encoder position
|
||||
"""
|
||||
if port <= 0 or port > MOTOR_COUNT:
|
||||
raise IndexError("Invalid encoder port specified!")
|
||||
|
||||
diff = raw_value - encoder_start_values[port]
|
||||
if diff > 2 ** 31:
|
||||
diff -= 2 ** 32
|
||||
elif diff < -2 ** 31:
|
||||
diff += 2 ** 32
|
||||
|
||||
return diff
|
||||
|
||||
@staticmethod
|
||||
def read_raw(port: int) -> int:
|
||||
"""Read raw encoder from a specified port. Will not be reset to 0 at start.
|
||||
|
||||
:param port: Port, which the motor is connected to. Between 1 and 4
|
||||
:raises: IndexError
|
||||
:return: Current encoder position
|
||||
"""
|
||||
if port <= 0 or port > MOTOR_COUNT:
|
||||
raise IndexError("Invalid encoder port specified!")
|
||||
|
||||
raw_value = 0
|
||||
|
||||
if port == 1:
|
||||
raw_value = Spi.read(Register.MOTOR_1_POS_B3, 4)
|
||||
elif port == 2:
|
||||
raw_value = Spi.read(Register.MOTOR_2_POS_B3, 4)
|
||||
elif port == 3:
|
||||
raw_value = Spi.read(Register.MOTOR_3_POS_B3, 4)
|
||||
elif port == 4:
|
||||
raw_value = Spi.read(Register.MOTOR_4_POS_B3, 4)
|
||||
|
||||
return raw_value
|
||||
|
||||
@staticmethod
|
||||
def read_all_raw():
|
||||
"""Read all encoders at once.
|
||||
This is faster than read_raw as it only uses one SPI call for all encoders!
|
||||
|
||||
:return: Tuple of all current raw encoder positions
|
||||
"""
|
||||
|
||||
encoders = Spi.read_array(Register.MOTOR_1_POS_B3, 4 * 4)
|
||||
|
||||
encoder_1 = int.from_bytes(
|
||||
encoders[0:4], byteorder='big', signed=False)
|
||||
encoder_2 = int.from_bytes(
|
||||
encoders[4:8], byteorder='big', signed=False)
|
||||
encoder_3 = int.from_bytes(
|
||||
encoders[8:12], byteorder='big', signed=False)
|
||||
encoder_4 = int.from_bytes(
|
||||
encoders[12:16], byteorder='big', signed=False)
|
||||
|
||||
return (encoder_1, encoder_2, encoder_3, encoder_4)
|
||||
|
||||
@staticmethod
|
||||
def read_all():
|
||||
"""Read all encoders at once.
|
||||
This is faster than read as it only uses one SPI call for all encoders!
|
||||
|
||||
:return: Tuple of all current encoder positions
|
||||
"""
|
||||
encoders = Encoder.read_all_raw()
|
||||
|
||||
return (Encoder.handle_wrap(encoders[0], 1),
|
||||
Encoder.handle_wrap(encoders[1], 2),
|
||||
Encoder.handle_wrap(encoders[2], 3),
|
||||
Encoder.handle_wrap(encoders[3], 4))
|
||||
|
||||
@staticmethod
|
||||
def read(port: int) -> int:
|
||||
"""Read encoder from a specified port
|
||||
|
||||
:param port: Port, which the motor is connected to. Between 1 and 4
|
||||
:raises: IndexError
|
||||
:return: Current encoder position
|
||||
"""
|
||||
if port <= 0 or port > MOTOR_COUNT:
|
||||
raise IndexError("Invalid encoder port specified!")
|
||||
|
||||
return Encoder.handle_wrap(Encoder.read_raw(port), port)
|
||||
|
||||
@staticmethod
|
||||
def clear(port: int):
|
||||
"""Reset encoder position to 0
|
||||
|
||||
:param port: Port, which the motor is connected to. Between 1 and 4
|
||||
:raises: IndexError
|
||||
"""
|
||||
if port <= 0 or port > MOTOR_COUNT:
|
||||
raise IndexError("Invalid encoder port specified!")
|
||||
|
||||
encoder_start_values[port] = Encoder.read_raw(port)
|
||||
|
||||
@staticmethod
|
||||
def clear_all():
|
||||
"""Reset all encoder positions to 0
|
||||
"""
|
||||
|
||||
for i in range(1, MOTOR_COUNT + 1):
|
||||
encoder_start_values[i] = Encoder.read_raw(i)
|
|
@ -1,116 +0,0 @@
|
|||
import datetime
|
||||
import os
|
||||
import socket
|
||||
import threading
|
||||
import time
|
||||
|
||||
import systemd.daemon
|
||||
from compLib.Lock import Lock
|
||||
|
||||
try:
|
||||
from compLib.LogstashLogging import Logging
|
||||
except Exception as e:
|
||||
import logging
|
||||
|
||||
|
||||
class Logger():
|
||||
def __init__(self):
|
||||
self.logger = logging.Logger('compApi background')
|
||||
|
||||
def get_logger(self):
|
||||
return self.logger
|
||||
|
||||
Logging = Logger()
|
||||
print(f"Could not import compLib.LogstashLogging: {str(e)}")
|
||||
Logging.get_logger().error(f"Could not import compLib.LogstashLogging: {str(e)}")
|
||||
|
||||
print("after basic imports")
|
||||
|
||||
RUN_IP_CHECK = False
|
||||
try:
|
||||
from compLib.Display import Display
|
||||
from compLib.Spi import Spi
|
||||
from compLib import __version__
|
||||
RUN_IP_CHECK = True
|
||||
except Exception as e:
|
||||
print(f"Could not import display or spi for ip output {str(e)}")
|
||||
Logging.get_logger().warning(f"Could not import display or spi for ip output {str(e)}")
|
||||
|
||||
print(f"After display and Spi import")
|
||||
|
||||
__run = """raspivid -t 0 -b 5000000 -w 1280 -h 720 -fps 30 -n -o - | gst-launch-1.0 fdsrc ! video/x-h264,width=1280,height=720,framerate=30/1,noise-reduction=1,profile=high,stream-format=byte-stream ! h264parse ! queue ! flvmux streamable=true ! rtmpsink location=\"rtmp://localhost/live/stream\""""
|
||||
|
||||
STREAM_RASPI = False if os.getenv("STREAM_RASPI", "false") == "false" else True
|
||||
IP_OUTPUT = False if os.getenv("IP_OUTPUT", "true") != "true" else True
|
||||
|
||||
|
||||
def get_ip():
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
try:
|
||||
s.connect(('10.255.255.255', 1))
|
||||
IP = s.getsockname()[0]
|
||||
except Exception:
|
||||
IP = 'Not connected'
|
||||
print(f"Error could not query ip: {e}")
|
||||
finally:
|
||||
s.close()
|
||||
return IP
|
||||
|
||||
|
||||
def write_ip_to_screen():
|
||||
while os.getenv("IP_OUTPUT", "true") == "true":
|
||||
try:
|
||||
if not Lock.is_locked():
|
||||
Lock.lock()
|
||||
ip = str(get_ip())
|
||||
print(f"writing {ip} to display")
|
||||
Display.write(2, f"LIB: V{__version__}")
|
||||
Display.write(3, f"FW: V{Spi.get_version()}")
|
||||
Display.write(4, f"IP: {ip}")
|
||||
Display.write(1, datetime.datetime.now().strftime("%b %d %H:%M:%S"))
|
||||
Lock.unlock()
|
||||
|
||||
time.sleep(10)
|
||||
except Exception as e:
|
||||
print(f"Exception in write ip thread: {e}")
|
||||
time.sleep(5)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
systemd.daemon.notify(systemd.daemon.Notification.READY)
|
||||
except:
|
||||
Logging.get_logger().warning("Warning, old systemd version detected")
|
||||
systemd.daemon.notify('READY=1')
|
||||
|
||||
|
||||
ip_output = None
|
||||
if RUN_IP_CHECK and IP_OUTPUT:
|
||||
print("starting ip output")
|
||||
Logging.get_logger().info("starting ip output")
|
||||
Lock.unlock()
|
||||
try:
|
||||
Spi.disable_health_check()
|
||||
ip_output = threading.Thread(target=write_ip_to_screen)
|
||||
ip_output.start()
|
||||
print("starting ip output - DONE")
|
||||
Logging.get_logger().info("starting ip output - DONE")
|
||||
except Exception as e:
|
||||
print(f"could not start ip output -> {str(e)}")
|
||||
Logging.get_logger().error(f"could not start ip output -> {str(e)}")
|
||||
|
||||
if STREAM_RASPI:
|
||||
print("starting gstreamer background process")
|
||||
Logging.get_logger().info("starting gstreamer background process")
|
||||
os.system(__run)
|
||||
print("gstreamer stopped...")
|
||||
Logging.get_logger().error("gstreamer stopped...")
|
||||
else:
|
||||
print("not starting gstreamer background process")
|
||||
Logging.get_logger().info("not starting gstreamer background process")
|
||||
if ip_output is not None:
|
||||
ip_output.join()
|
||||
else:
|
||||
print("ip display output failed to initialize.. sleeping for a day, good night")
|
||||
Logging.get_logger().info("ip display output failed to initialize.. sleeping for a day, good night")
|
||||
time.sleep(60 * 60 * 24)
|
|
@ -1,81 +0,0 @@
|
|||
from compLib.LogstashLogging import Logging
|
||||
from compLib.MetricsLogging import MetricsLogging
|
||||
from compLib.Spi import Spi, Register
|
||||
|
||||
SENSOR_COUNT = 5
|
||||
|
||||
|
||||
class IRSensor(object):
|
||||
"""Access the different IR Sensors of the robot
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def read(sensor: int) -> int:
|
||||
"""Read one infrared sensor
|
||||
|
||||
:param sensor: Which sensor to read. Between 1 and 5
|
||||
:raises: IndexError
|
||||
:return: Sensor value. 10 bit accuracy
|
||||
:rtype: int
|
||||
"""
|
||||
if sensor <= 0 or sensor > SENSOR_COUNT:
|
||||
raise IndexError("Invalid sensor specified!")
|
||||
|
||||
result = 0
|
||||
if sensor == 1:
|
||||
result = Spi.read(Register.IR_1_H, 2)
|
||||
elif sensor == 2:
|
||||
result = Spi.read(Register.IR_2_H, 2)
|
||||
elif sensor == 3:
|
||||
result = Spi.read(Register.IR_3_H, 2)
|
||||
elif sensor == 4:
|
||||
result = Spi.read(Register.IR_4_H, 2)
|
||||
elif sensor == 5:
|
||||
result = Spi.read(Register.IR_5_H, 2)
|
||||
|
||||
MetricsLogging.put("Infrared", result, sensor)
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def read_all():
|
||||
"""Read all IR sensors at once.
|
||||
This is faster than read as it only uses one SPI call for all sensors!
|
||||
|
||||
:return: Tuple of all current ir sensors
|
||||
"""
|
||||
|
||||
sensors = Spi.read_array(Register.IR_1_H, 5 * 2)
|
||||
|
||||
sensor_1 = int.from_bytes(
|
||||
sensors[0:2], byteorder='big', signed=False)
|
||||
sensor_2 = int.from_bytes(
|
||||
sensors[2:4], byteorder='big', signed=False)
|
||||
sensor_3 = int.from_bytes(
|
||||
sensors[4:6], byteorder='big', signed=False)
|
||||
sensor_4 = int.from_bytes(
|
||||
sensors[6:8], byteorder='big', signed=False)
|
||||
sensor_5 = int.from_bytes(
|
||||
sensors[8:10], byteorder='big', signed=False)
|
||||
|
||||
return (sensor_1, sensor_2, sensor_3, sensor_4, sensor_5)
|
||||
|
||||
@staticmethod
|
||||
def set(sensor: int, on: bool):
|
||||
"""Turn on / off a IR emitter
|
||||
|
||||
:param sensor: Which sensor to read. Between 1 and 5
|
||||
:raises: IndexError
|
||||
"""
|
||||
if sensor <= 0 or sensor > SENSOR_COUNT:
|
||||
raise IndexError("Invalid sensor specified!")
|
||||
|
||||
if sensor == 1:
|
||||
Spi.write(Register.IR_1_LED, 1, on)
|
||||
elif sensor == 2:
|
||||
Spi.write(Register.IR_2_LED, 1, on)
|
||||
elif sensor == 3:
|
||||
Spi.write(Register.IR_3_LED, 1, on)
|
||||
elif sensor == 4:
|
||||
Spi.write(Register.IR_4_LED, 1, on)
|
||||
elif sensor == 5:
|
||||
Spi.write(Register.IR_5_LED, 1, on)
|
|
@ -1,25 +0,0 @@
|
|||
from filelock import FileLock, Timeout
|
||||
import atexit
|
||||
|
||||
FILELOCK_PATH = "/root/complib.lock"
|
||||
|
||||
global_lock = FileLock(FILELOCK_PATH, timeout=1)
|
||||
class Lock(object):
|
||||
@staticmethod
|
||||
def lock():
|
||||
global_lock.acquire()
|
||||
|
||||
@staticmethod
|
||||
def unlock():
|
||||
global_lock.release()
|
||||
|
||||
@staticmethod
|
||||
def is_locked():
|
||||
try:
|
||||
global_lock.acquire()
|
||||
global_lock.release()
|
||||
return False
|
||||
except Timeout:
|
||||
return True
|
||||
|
||||
atexit.register(Lock.unlock)
|
|
@ -1,102 +0,0 @@
|
|||
import logging
|
||||
import os
|
||||
import sys
|
||||
import requests
|
||||
|
||||
from logstash_async.transport import HttpTransport
|
||||
from logstash_async.handler import AsynchronousLogstashHandler
|
||||
|
||||
EXTENSIVE_LOGGING = os.getenv("EXTENSIVE_LOGGING", "False")
|
||||
|
||||
if EXTENSIVE_LOGGING == "True":
|
||||
EXTENSIVE_LOGGING = True
|
||||
else:
|
||||
EXTENSIVE_LOGGING = False
|
||||
|
||||
host = 'logstash.robo4you.at'
|
||||
port = 443
|
||||
|
||||
logstash_logger = logging.getLogger('logstash')
|
||||
logstash_logger.setLevel(logging.INFO)
|
||||
|
||||
transport = HttpTransport(
|
||||
host,
|
||||
port,
|
||||
username="robo",
|
||||
password="competition",
|
||||
timeout=60.0,
|
||||
)
|
||||
|
||||
asynchronousLogstashHandler = AsynchronousLogstashHandler(
|
||||
host,
|
||||
port,
|
||||
transport=transport,
|
||||
database_path='logs.db'
|
||||
)
|
||||
|
||||
|
||||
class StreamToLogger(object):
|
||||
"""
|
||||
Fake file-like stream object that redirects writes to a logger instance.
|
||||
"""
|
||||
|
||||
def __init__(self, textio, log_level=logging.INFO):
|
||||
self.logger = logging.getLogger('logstash')
|
||||
self.console = textio
|
||||
self.log_level = log_level
|
||||
self.linebuf = ''
|
||||
|
||||
def write(self, buf):
|
||||
self.console.write(buf)
|
||||
temp_linebuf = self.linebuf + buf
|
||||
for line in buf.splitlines(True):
|
||||
self.linebuf += line
|
||||
|
||||
def flush(self):
|
||||
if self.linebuf != '':
|
||||
self.logger.log(self.log_level, self.linebuf.rstrip())
|
||||
self.linebuf = ''
|
||||
self.console.flush()
|
||||
asynchronousLogstashHandler.flush()
|
||||
|
||||
|
||||
if EXTENSIVE_LOGGING:
|
||||
try:
|
||||
r = requests.get(f"https://{host}:{port}")
|
||||
if r.status_code == 401:
|
||||
logstash_logger.addHandler(asynchronousLogstashHandler)
|
||||
|
||||
so = StreamToLogger(sys.stdout, logging.INFO)
|
||||
sys.stdout = so
|
||||
|
||||
se = StreamToLogger(sys.stderr, logging.ERROR)
|
||||
sys.stderr = se
|
||||
else:
|
||||
print(f"Could not connect to {host} -> ERROR CODE: {r.status_code}!")
|
||||
|
||||
except requests.exceptions.ConnectionError as identifier:
|
||||
print(f"Could not connect to {host}!")
|
||||
print(f"Error loading logger was -> {identifier}")
|
||||
else:
|
||||
print("Extensive logging is disabled! No logs will be sent over network...")
|
||||
|
||||
|
||||
class Logging(object):
|
||||
"""Can be used to manually use the logger or turn up the logging level used for debugging this library.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def get_logger() -> logging.Logger:
|
||||
"""Get the logger object used to communicate with logstash
|
||||
|
||||
:return: Python logger
|
||||
:rtype: logging.Logger
|
||||
"""
|
||||
return logstash_logger
|
||||
|
||||
@staticmethod
|
||||
def set_debug():
|
||||
"""Turns up the logging level of the library to debug. Should be used, when debugging with the
|
||||
Competition organizers
|
||||
"""
|
||||
logstash_logger.setLevel(logging.DEBUG)
|
|
@ -1,97 +0,0 @@
|
|||
from influxdb_client import InfluxDBClient, Point, WritePrecision
|
||||
import socket
|
||||
import uuid
|
||||
import os
|
||||
import multiprocessing
|
||||
import datetime
|
||||
import requests
|
||||
import time
|
||||
|
||||
CONCURRENCY = 1
|
||||
|
||||
HOSTNAME = socket.gethostname()
|
||||
RUN_TRACE = str(uuid.uuid4())
|
||||
|
||||
TOKEN = "aCdxnntw-Zp3agci8Z32lQAR_ep6MhdmWOJG7ObnoikYqe7nKAhFYx1jVGBpipQuId79SC4Jl0J6IBYVqauJyw=="
|
||||
ORG = "robo4you"
|
||||
BUCKET = "compAIR"
|
||||
|
||||
INFLUX_HOST = "https://influxdb.comp-air.at"
|
||||
|
||||
EXTENSIVE_LOGGING = os.getenv("EXTENSIVE_LOGGING", "True")
|
||||
if EXTENSIVE_LOGGING == "True":
|
||||
EXTENSIVE_LOGGING = True
|
||||
else:
|
||||
EXTENSIVE_LOGGING = False
|
||||
|
||||
|
||||
influx_client = InfluxDBClient(url=INFLUX_HOST, token=TOKEN)
|
||||
write_api = influx_client.write_api()
|
||||
|
||||
point_queue = multiprocessing.Queue()
|
||||
workers = []
|
||||
|
||||
class MetricsLogging():
|
||||
"""Used to send metrics / points to a influxdb
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def is_influx_reachable() -> bool:
|
||||
"""Check if we can send metrics to the database
|
||||
|
||||
:return: Is reachable?
|
||||
:rtype: bool
|
||||
"""
|
||||
try:
|
||||
r = requests.get(INFLUX_HOST)
|
||||
if r.status_code == 200:
|
||||
return True
|
||||
else:
|
||||
print(f"Could not connect to {INFLUX_HOST} -> ERROR CODE: {r.status_code}!")
|
||||
return False
|
||||
except requests.exceptions.RequestException as identifier:
|
||||
print(f"Could not connect to {INFLUX_HOST}!")
|
||||
|
||||
@staticmethod
|
||||
def put(sensorType, value, port):
|
||||
"""Put a datapoint into a time-series-database
|
||||
|
||||
:param sensorType: A key used to identify a datapoint
|
||||
:param value: Value measured by the sensor
|
||||
:param port: Port of the sensor which was read
|
||||
"""
|
||||
if EXTENSIVE_LOGGING:
|
||||
point = Point(sensorType) \
|
||||
.tag("host", HOSTNAME) \
|
||||
.tag("runID", RUN_TRACE) \
|
||||
.tag("port", port) \
|
||||
.field("value", value) \
|
||||
.time(datetime.datetime.utcnow(), WritePrecision.MS)
|
||||
point_queue.put_nowait(point)
|
||||
|
||||
@staticmethod
|
||||
def worker():
|
||||
point = object()
|
||||
for job in iter(point_queue.get, point):
|
||||
try:
|
||||
write_api.write(BUCKET, ORG, point)
|
||||
time.sleep(0.1)
|
||||
except Exception as e:
|
||||
pass
|
||||
finally:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def start_workers():
|
||||
global EXTENSIVE_LOGGING
|
||||
if EXTENSIVE_LOGGING:
|
||||
if MetricsLogging.is_influx_reachable():
|
||||
for i in range(CONCURRENCY):
|
||||
worker = multiprocessing.Process(target=MetricsLogging.worker, daemon=True)
|
||||
worker.start()
|
||||
workers.append(worker)
|
||||
else:
|
||||
EXTENSIVE_LOGGING = False
|
||||
|
||||
|
||||
MetricsLogging.start_workers()
|
226
compLib/Motor.py
226
compLib/Motor.py
|
@ -1,226 +0,0 @@
|
|||
import atexit
|
||||
from enum import IntEnum
|
||||
|
||||
from compLib.LogstashLogging import Logging
|
||||
from compLib.MetricsLogging import MetricsLogging
|
||||
from compLib.Spi import Spi, Register
|
||||
|
||||
MOTOR_COUNT = 4
|
||||
MAX_MOTOR_SPEED = 65535
|
||||
MOTOR_PERCENTAGE_MULT = MAX_MOTOR_SPEED / 100.0
|
||||
NEW_MOTOR_CURVE = [0.0, 0.0, 0.5, 60.0, 199.83333333333334, 377.66666666666663, 990.3333333333333, 1860.6666666666665, 2587.0, 3091.6666666666665, 3489.0, 3860.5, 4197.333333333333, 4432.166666666667, 4647.166666666666, 4873.166666666666, 5054.333333333334, 5208.666666666667, 5353.0, 5466.5, 5604.0]
|
||||
MOTOR_CURVE = [0.0, 0.0, 426.5, 692.0, 842.5, 953.5, 1032.5, 1090.5, 1135.5, 1171.0, 1203.5, 1230.0, 1249.5, 1268.0, 1283.0, 1298.5, 1308.0, 1320.0, 1332.0, 1339.5, 1352.5]
|
||||
|
||||
SPEED_LOCK = False
|
||||
SPEED_MULT = 1.0
|
||||
|
||||
|
||||
class MotorMode(IntEnum):
|
||||
COAST = 0,
|
||||
FORWARD = 1,
|
||||
BACKWARD = 2,
|
||||
BREAK = 3
|
||||
|
||||
|
||||
class Motor(object):
|
||||
"""Class used to control the motors
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def pwm(port: int, pwm: int, mode: MotorMode):
|
||||
"""Set specified motor to a specific pwm value and motor mode
|
||||
|
||||
:param port: Port, which the motor is connected to. 1-4 allowed
|
||||
:param pwm: Raw PWM value which the motor should be set to. From 0 to 2^16 - 1
|
||||
:param mode: Motor mode. See enum MotorMode for more info
|
||||
:raises: IndexError
|
||||
"""
|
||||
|
||||
if SPEED_LOCK:
|
||||
return
|
||||
|
||||
if port <= 0 or port > MOTOR_COUNT:
|
||||
raise IndexError("Invalid Motor port specified!")
|
||||
|
||||
if port == 1:
|
||||
Spi.write(Register.MOTOR_1_PWM_H, 2, pwm)
|
||||
Spi.write(Register.PWM_1_CTRL, 1, int(mode))
|
||||
elif port == 2:
|
||||
Spi.write(Register.MOTOR_2_PWM_H, 2, pwm)
|
||||
Spi.write(Register.PWM_2_CTRL, 1, int(mode))
|
||||
elif port == 3:
|
||||
Spi.write(Register.MOTOR_3_PWM_H, 2, pwm)
|
||||
Spi.write(Register.PWM_3_CTRL, 1, int(mode))
|
||||
elif port == 4:
|
||||
Spi.write(Register.MOTOR_4_PWM_H, 2, pwm)
|
||||
Spi.write(Register.PWM_4_CTRL, 1, int(mode))
|
||||
|
||||
@staticmethod
|
||||
def power_raw(port: int, percent: float, log_metric = True):
|
||||
"""Set specified motor to percentage power
|
||||
|
||||
:param port: Port, which the motor is connected to. 1-4
|
||||
:param percent: Percentage of max speed. between -100 and 100
|
||||
:raises: IndexError
|
||||
"""
|
||||
|
||||
if port <= 0 or port > MOTOR_COUNT:
|
||||
raise IndexError("Invalid Motor port specified!")
|
||||
|
||||
if percent < -100 or percent > 100:
|
||||
raise IndexError("Invalid Motor speed specified! Speed is between -100 and 100 percent!")
|
||||
|
||||
if log_metric:
|
||||
MetricsLogging.put("MotorRaw", float(percent), port)
|
||||
|
||||
mode = MotorMode.COAST
|
||||
if percent < 0:
|
||||
percent = abs(percent)
|
||||
mode = MotorMode.BACKWARD
|
||||
elif percent > 0:
|
||||
mode = MotorMode.FORWARD
|
||||
|
||||
percent *= SPEED_MULT
|
||||
pwm = percent * MOTOR_PERCENTAGE_MULT
|
||||
|
||||
Motor.pwm(port, int(pwm), mode)
|
||||
|
||||
@staticmethod
|
||||
def power(port: int, percent: float):
|
||||
"""Set specified motor to percentage power, percentage is linearized
|
||||
so that it's roughly proportional to the speed
|
||||
|
||||
:param port: Port, which the motor is connected to. 1-4
|
||||
:param percent: Percentage of max speed. between -100 and 100
|
||||
:raises: IndexError
|
||||
"""
|
||||
raw_power = raw_power = Motor.__linearizePower(MOTOR_CURVE, abs(percent))
|
||||
if percent < 0:
|
||||
raw_power *= -1
|
||||
|
||||
MetricsLogging.put("Motor", float(percent), port)
|
||||
Motor.power_raw(port, raw_power, False)
|
||||
|
||||
@staticmethod
|
||||
def all_off():
|
||||
"""
|
||||
Turns of all motors
|
||||
"""
|
||||
Logging.get_logger().debug(f"Motor.all_off")
|
||||
|
||||
for i in range(1, MOTOR_COUNT + 1):
|
||||
Motor.active_break(i)
|
||||
|
||||
@staticmethod
|
||||
def active_break(port: int):
|
||||
"""
|
||||
Actively break with a specific motor
|
||||
|
||||
:param port: Port, which the motor is connected to. 1-4
|
||||
"""
|
||||
Motor.pwm(port, 0, MotorMode.BREAK)
|
||||
|
||||
@staticmethod
|
||||
def set_motor_curve(curve):
|
||||
"""
|
||||
Set the global motor curve, must be a float array with exactly 21 elements.
|
||||
[0] = x ticks/s (0% power)
|
||||
[1] = x ticks/s (5% power)
|
||||
[2] = x ticks/s (10% power)
|
||||
...
|
||||
[20] = x ticks/s (100% power)
|
||||
|
||||
:param curve: float array with 21 elements
|
||||
:raises: ValueError
|
||||
"""
|
||||
if (len(curve) != 21):
|
||||
raise ValueError('The motor curve is invalid, check documentation for set_motor_curve()!')
|
||||
|
||||
global MOTOR_CURVE
|
||||
MOTOR_CURVE = curve
|
||||
|
||||
@staticmethod
|
||||
def get_motor_curve():
|
||||
"""
|
||||
Get the currently active motor curve. Check set_motor_curve() for more info.
|
||||
|
||||
:return: current motor curve
|
||||
"""
|
||||
return MOTOR_CURVE
|
||||
|
||||
@staticmethod
|
||||
def __map(x, in_min, in_max, out_min, out_max):
|
||||
"""
|
||||
Linear interpolation. Check https://www.arduino.cc/reference/en/language/functions/math/map/
|
||||
"""
|
||||
x = float(x)
|
||||
in_min = float(in_min)
|
||||
in_max = float(in_max)
|
||||
out_min = float(out_min)
|
||||
out_max = float(out_max)
|
||||
|
||||
return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min
|
||||
|
||||
@staticmethod
|
||||
def __interpolateSpeed(curve, speed):
|
||||
"""
|
||||
Interpolate a speed in the specified motor curve and return
|
||||
the 'power percentage that is needed to reach that speed'
|
||||
"""
|
||||
if speed < min(curve) or speed > max(curve):
|
||||
raise ValueError(f'Speed out of range: {str(speed)} ticks/s')
|
||||
|
||||
for index in range(len(curve) - 1):
|
||||
if speed >= curve[index] and speed <= curve[index + 1] and curve[index] != curve[index + 1]:
|
||||
return Motor.__map(speed, curve[index], curve[index + 1], index * 5, index * 5 + 5)
|
||||
|
||||
return 0
|
||||
|
||||
@staticmethod
|
||||
def __linearizePower(curve, power):
|
||||
"""
|
||||
Takes raw power and returns it corrected so that the motor speed would be roughly linear
|
||||
"""
|
||||
if power < 0 or power > 100:
|
||||
raise ValueError(f'Power out of range: {str(power)}%')
|
||||
|
||||
requiredSpeed = Motor.__map(power, 0, 100, min(curve), max(curve))
|
||||
|
||||
return Motor.__interpolateSpeed(curve, requiredSpeed)
|
||||
|
||||
|
||||
@staticmethod
|
||||
def robot_state_loop():
|
||||
import time
|
||||
from compLib.Api import Seeding
|
||||
|
||||
global SPEED_LOCK
|
||||
global SPEED_MULT
|
||||
|
||||
while True:
|
||||
try:
|
||||
state, status = Seeding.get_robot_state()
|
||||
SPEED_MULT = state["speed"] / 100.0
|
||||
|
||||
if state["left"] != -1 or state["right"] != -1:
|
||||
Motor.power(1, state["right"])
|
||||
Motor.power(4, state["left"])
|
||||
SPEED_LOCK = True
|
||||
time.sleep(state["time"])
|
||||
SPEED_LOCK = False
|
||||
Motor.power(1, 0)
|
||||
Motor.power(4, 0)
|
||||
else:
|
||||
time.sleep(0.25)
|
||||
except:
|
||||
time.sleep(0.25)
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def start_robot_state_loop():
|
||||
from threading import Thread
|
||||
robot_state_thread = Thread(target=Motor.robot_state_loop)
|
||||
robot_state_thread.setDaemon(True)
|
||||
robot_state_thread.start()
|
||||
|
||||
atexit.register(Motor.all_off)
|
101
compLib/Odom.py
101
compLib/Odom.py
|
@ -1,101 +0,0 @@
|
|||
import time
|
||||
import math
|
||||
from compLib.Encoder import Encoder
|
||||
from compLib.Robot import Robot
|
||||
|
||||
last_run = time.time()
|
||||
last_enc_left = 0
|
||||
last_enc_right = 0
|
||||
|
||||
pos_x = 0
|
||||
pos_y = 0
|
||||
orientation = 0
|
||||
|
||||
|
||||
class Odometry():
|
||||
"""DTO used for holding all odometry information. \n
|
||||
Coordinate system: \n
|
||||
X: + Forward; - Backwards \n
|
||||
Y: + Right; - Left \n
|
||||
Orientation: + Right; - Left
|
||||
"""
|
||||
|
||||
def __init__(self, x, y, orientation):
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.orientation = orientation
|
||||
|
||||
def get_x(self) -> float:
|
||||
"""Returns distance driven on x-axis in meters"""
|
||||
return self.x
|
||||
|
||||
def get_y(self) -> float:
|
||||
"""Returns distance driven on y-axis in meters"""
|
||||
return self.y
|
||||
|
||||
def get_orientation(self) -> float:
|
||||
"""Returns degrees turned in radians"""
|
||||
return self.orientation
|
||||
|
||||
def __str__(self):
|
||||
return f"X: {self.x} Y: {self.y} O: {self.orientation}"
|
||||
|
||||
|
||||
class Odom(object):
|
||||
"""Class used to track the movement of the robot in X, Y, Theta (Orientation)
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def get_odom() -> Odometry:
|
||||
"""
|
||||
:return: Current orientation of the robot
|
||||
"""
|
||||
return Odometry(pos_x, pos_y, orientation)
|
||||
|
||||
@staticmethod
|
||||
def clear() -> None:
|
||||
"""
|
||||
Clears the current odometry information and start from X, Y, Orientation set to 0
|
||||
"""
|
||||
global last_run, last_enc_left, last_enc_right, pos_x, pos_y, orientation
|
||||
|
||||
last_run = 0
|
||||
last_enc_left = 0
|
||||
last_enc_right = 0
|
||||
pos_x = 0
|
||||
pos_y = 0
|
||||
orientation = 0
|
||||
|
||||
@staticmethod
|
||||
def update() -> None:
|
||||
"""
|
||||
Updates the current odometry information of the robot, Should be called in a loop with at least 100HZ.
|
||||
Do not clear encoder positions between updates! Must be cleared, when clearing encoders!
|
||||
"""
|
||||
global last_run, last_enc_left, last_enc_right, pos_x, pos_y, orientation
|
||||
|
||||
now = time.time()
|
||||
if last_run + 0.01 < now:
|
||||
last_run = now
|
||||
|
||||
encoder_positions = Encoder.read_all()
|
||||
current_left = encoder_positions[Robot.LEFT_PORT - 1]
|
||||
current_right = -encoder_positions[Robot.RIGHT_PORT - 1]
|
||||
distance_left = (current_left - last_enc_left) / Robot.TICKS_PER_METER
|
||||
distance_right = (current_right - last_enc_right) / Robot.TICKS_PER_METER
|
||||
last_enc_left = current_left
|
||||
last_enc_right = current_right
|
||||
|
||||
distance = (distance_left + distance_right) / 2.0
|
||||
theta = (distance_left - distance_right) / Robot.ARBOR_LENGTH_M
|
||||
|
||||
if distance != 0:
|
||||
distance_x = math.cos(theta) * distance
|
||||
distance_y = -math.sin(theta) * distance
|
||||
|
||||
pos_x = pos_x + (math.cos(orientation) * distance_x -
|
||||
math.sin(orientation) * distance_y)
|
||||
pos_y = pos_y + (math.sin(orientation) * distance_x +
|
||||
math.cos(orientation) * distance_y)
|
||||
if theta != 0:
|
||||
orientation += theta
|
|
@ -1,24 +0,0 @@
|
|||
import RPi.GPIO as GPIO
|
||||
import time
|
||||
|
||||
GPIO.setwarnings(False)
|
||||
RESET_PIN = 23
|
||||
BOOT_PIN = 17
|
||||
|
||||
class Reset:
|
||||
"""Reset the co-processor
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def reset_bot():
|
||||
"""Reset the co-processor
|
||||
"""
|
||||
GPIO.setmode(GPIO.BCM)
|
||||
GPIO.setup(RESET_PIN, GPIO.OUT)
|
||||
GPIO.setup(BOOT_PIN, GPIO.OUT)
|
||||
|
||||
GPIO.output(RESET_PIN, GPIO.LOW)
|
||||
GPIO.output(BOOT_PIN, GPIO.LOW)
|
||||
time.sleep(0.1)
|
||||
GPIO.output(RESET_PIN, GPIO.HIGH)
|
||||
time.sleep(1.5)
|
|
@ -1,24 +0,0 @@
|
|||
import math
|
||||
|
||||
|
||||
class Robot(object):
|
||||
WHEEL_CIRCUMFERENCE_MM = 71.0 * math.pi
|
||||
"""Circumference of a wheel in millimeters"""
|
||||
|
||||
TICKS_PER_TURN = 27.7 * 100.0
|
||||
"""Ticks per 360 degree turn of a wheel"""
|
||||
|
||||
ARBOR_LENGTH_MM = 139.0
|
||||
"""Distance between the two wheels in millimeters"""
|
||||
|
||||
ARBOR_LENGTH_M = ARBOR_LENGTH_MM / 1000.0
|
||||
"""Distance between the two wheels in meters"""
|
||||
|
||||
TICKS_PER_METER = 1000.0 / WHEEL_CIRCUMFERENCE_MM * TICKS_PER_TURN
|
||||
"""Ticks after driving one meter"""
|
||||
|
||||
LEFT_PORT = 4
|
||||
"""Motor port for the left motor"""
|
||||
|
||||
RIGHT_PORT = 1
|
||||
"""Motor port for the right motor"""
|
|
@ -1,63 +0,0 @@
|
|||
from compLib.Spi import Spi, Register
|
||||
|
||||
|
||||
SERVO_COUNT = 8
|
||||
|
||||
MIN_ANGLE = -90.0
|
||||
MAX_ANGLE = 90.0
|
||||
|
||||
|
||||
def map(x, in_min, in_max, out_min, out_max):
|
||||
return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min
|
||||
|
||||
|
||||
class Servo:
|
||||
"""Control the servo ports on the robot
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def set_position(port: int, angle: float, offset: float = 90.0):
|
||||
"""Set position of servo connected to port
|
||||
|
||||
:param port: port between 1 and 8
|
||||
:param angle: Angle of servo
|
||||
"""
|
||||
if port <= 0 or port > SERVO_COUNT:
|
||||
raise IndexError("Invalid Servo port specified!")
|
||||
|
||||
angle = max(min(angle, MAX_ANGLE), MIN_ANGLE)
|
||||
|
||||
mapped_angle = int(map(angle, MIN_ANGLE, MAX_ANGLE, 0, 65535))
|
||||
|
||||
if port == 1:
|
||||
Spi.write(Register.SERVO_1_PWM_H, 2, mapped_angle)
|
||||
Spi.write(Register.PWM_1_CTRL, 1, 4)
|
||||
elif port == 2:
|
||||
Spi.write(Register.SERVO_2_PWM_H, 2, mapped_angle)
|
||||
Spi.write(Register.PWM_1_CTRL, 1, 4)
|
||||
elif port == 3:
|
||||
Spi.write(Register.SERVO_3_PWM_H, 2, mapped_angle)
|
||||
Spi.write(Register.PWM_2_CTRL, 1, 4)
|
||||
elif port == 4:
|
||||
Spi.write(Register.SERVO_4_PWM_H, 2, mapped_angle)
|
||||
Spi.write(Register.PWM_2_CTRL, 1, 4)
|
||||
elif port == 5:
|
||||
Spi.write(Register.SERVO_5_PWM_H, 2, mapped_angle)
|
||||
Spi.write(Register.PWM_3_CTRL, 1, 4)
|
||||
elif port == 6:
|
||||
Spi.write(Register.SERVO_6_PWM_H, 2, mapped_angle)
|
||||
Spi.write(Register.PWM_3_CTRL, 1, 4)
|
||||
elif port == 7:
|
||||
Spi.write(Register.SERVO_7_PWM_H, 2, mapped_angle)
|
||||
Spi.write(Register.PWM_4_CTRL, 1, 4)
|
||||
elif port == 8:
|
||||
Spi.write(Register.SERVO_8_PWM_H, 2, mapped_angle)
|
||||
Spi.write(Register.PWM_4_CTRL, 1, 4)
|
||||
|
||||
@staticmethod
|
||||
def setup_position():
|
||||
"""Set position of servos to the position used during the setup process
|
||||
"""
|
||||
|
||||
for i in range(1, SERVO_COUNT + 1):
|
||||
Servo.set_position(i, 0)
|
222
compLib/Spi.py
222
compLib/Spi.py
|
@ -1,222 +0,0 @@
|
|||
import importlib
|
||||
from threading import Thread
|
||||
import multiprocessing
|
||||
from enum import IntEnum
|
||||
import time
|
||||
import sys
|
||||
import random
|
||||
|
||||
from compLib.LogstashLogging import Logging
|
||||
|
||||
SPI_BUS = 1
|
||||
SPI_DEVICE = 2
|
||||
SPI_SPEED = 4000000
|
||||
SPI_BUFFER_SIZE = 32
|
||||
SPI_HEALTH = True
|
||||
|
||||
# For development purposes
|
||||
spi_found = importlib.util.find_spec("spidev") is not None
|
||||
spi = None
|
||||
spi_lock = multiprocessing.Lock()
|
||||
last_spi_call = time.time()
|
||||
if spi_found:
|
||||
import spidev
|
||||
spi = spidev.SpiDev()
|
||||
spi.open(SPI_BUS, SPI_DEVICE)
|
||||
spi.max_speed_hz = SPI_SPEED
|
||||
spi.mode = 0
|
||||
spi.bits_per_word = 8
|
||||
|
||||
class Register(IntEnum):
|
||||
IDENTIFICATION_MODEL_ID = 1,
|
||||
IDENTIFICATION_MODEL_REV_MAJOR = 2,
|
||||
IDENTIFICATION_MODEL_REV_MINOR = 3,
|
||||
IDENTIFICATION_MODEL_REV_PATCH = 4,
|
||||
|
||||
# Motor encoder positions
|
||||
MOTOR_1_POS_B3 = 10,
|
||||
MOTOR_1_POS_B2 = 11,
|
||||
MOTOR_1_POS_B1 = 12,
|
||||
MOTOR_1_POS_B0 = 13,
|
||||
MOTOR_2_POS_B3 = 14,
|
||||
MOTOR_2_POS_B2 = 15,
|
||||
MOTOR_2_POS_B1 = 16,
|
||||
MOTOR_2_POS_B0 = 17,
|
||||
MOTOR_3_POS_B3 = 18,
|
||||
MOTOR_3_POS_B2 = 19,
|
||||
MOTOR_3_POS_B1 = 20,
|
||||
MOTOR_3_POS_B0 = 21,
|
||||
MOTOR_4_POS_B3 = 22,
|
||||
MOTOR_4_POS_B2 = 23,
|
||||
MOTOR_4_POS_B1 = 24,
|
||||
MOTOR_4_POS_B0 = 25,
|
||||
|
||||
# PWM Control Modes
|
||||
PWM_1_CTRL = 26,
|
||||
PWM_2_CTRL = 27,
|
||||
PWM_3_CTRL = 28,
|
||||
PWM_4_CTRL = 29,
|
||||
|
||||
# Motor pwm speed
|
||||
MOTOR_1_PWM_H = 30,
|
||||
MOTOR_1_PWM_L = 31,
|
||||
MOTOR_2_PWM_H = 32,
|
||||
MOTOR_2_PWM_L = 33,
|
||||
MOTOR_3_PWM_H = 34,
|
||||
MOTOR_3_PWM_L = 35,
|
||||
MOTOR_4_PWM_H = 36,
|
||||
MOTOR_4_PWM_L = 37,
|
||||
|
||||
# Servo goal position
|
||||
SERVO_1_PWM_H = 38,
|
||||
SERVO_1_PWM_L = 39,
|
||||
SERVO_2_PWM_H = 40,
|
||||
SERVO_2_PWM_L = 41,
|
||||
SERVO_3_PWM_H = 42,
|
||||
SERVO_3_PWM_L = 43,
|
||||
SERVO_4_PWM_H = 44,
|
||||
SERVO_4_PWM_L = 45,
|
||||
SERVO_5_PWM_H = 46,
|
||||
SERVO_5_PWM_L = 47,
|
||||
SERVO_6_PWM_H = 48,
|
||||
SERVO_6_PWM_L = 49,
|
||||
SERVO_7_PWM_H = 50,
|
||||
SERVO_7_PWM_L = 51,
|
||||
SERVO_8_PWM_H = 52,
|
||||
SERVO_8_PWM_L = 53,
|
||||
|
||||
# IR Sensor value
|
||||
IR_1_H = 54,
|
||||
IR_1_L = 55,
|
||||
IR_2_H = 56,
|
||||
IR_2_L = 57,
|
||||
IR_3_H = 58,
|
||||
IR_3_L = 59,
|
||||
IR_4_H = 60,
|
||||
IR_4_L = 61,
|
||||
IR_5_H = 62,
|
||||
IR_5_L = 63,
|
||||
IR_1_LED = 64,
|
||||
IR_2_LED = 65,
|
||||
IR_3_LED = 66,
|
||||
IR_4_LED = 67,
|
||||
IR_5_LED = 68,
|
||||
|
||||
# Display registers
|
||||
DISPLAY_LINE_1_C0 = 69,
|
||||
DISPLAY_LINE_2_C0 = 85,
|
||||
DISPLAY_LINE_3_C0 = 101,
|
||||
DISPLAY_LINE_4_C0 = 117
|
||||
|
||||
|
||||
class Spi(object):
|
||||
|
||||
@staticmethod
|
||||
def transfer(tx_buffer: list):
|
||||
if not spi_found:
|
||||
return [] * SPI_BUFFER_SIZE
|
||||
|
||||
write_reg = tx_buffer[1]
|
||||
tx_buffer_copy = tx_buffer.copy()
|
||||
|
||||
with spi_lock:
|
||||
spi.xfer(tx_buffer)
|
||||
rx_buffer = spi.xfer([0] * SPI_BUFFER_SIZE)
|
||||
|
||||
if rx_buffer[1] != write_reg:
|
||||
Logging.get_logger().error(f"Warning! SPI error during read/write of register {write_reg}! Retrying automagically")
|
||||
time.sleep(random.uniform(0.0, 0.1))
|
||||
return Spi.transfer(tx_buffer_copy)
|
||||
|
||||
global last_spi_call
|
||||
last_spi_call = time.time()
|
||||
|
||||
return rx_buffer
|
||||
|
||||
@staticmethod
|
||||
def read(reg: int, length: int):
|
||||
return int.from_bytes(Spi.read_array(reg, length), byteorder='big', signed=False)
|
||||
|
||||
@staticmethod
|
||||
def read_array(reg: int, length: int):
|
||||
if not type(reg) is int:
|
||||
reg = int(reg)
|
||||
|
||||
tx_buf = [0] * SPI_BUFFER_SIZE
|
||||
|
||||
tx_buf[0] = 0
|
||||
tx_buf[1] = reg
|
||||
tx_buf[2] = length
|
||||
|
||||
rx_buf = Spi.transfer(tx_buf)
|
||||
return rx_buf[2:2 + length]
|
||||
|
||||
@staticmethod
|
||||
def write(reg: int, length: int, value: int):
|
||||
if not type(reg) is int:
|
||||
reg = int(reg)
|
||||
|
||||
tx_buf = [0] * SPI_BUFFER_SIZE
|
||||
|
||||
tx_buf[0] = 1
|
||||
tx_buf[1] = reg
|
||||
tx_buf[2] = length
|
||||
|
||||
pos = 3
|
||||
for i in value.to_bytes(length, 'big'):
|
||||
tx_buf[pos] = i
|
||||
pos += 1
|
||||
|
||||
rx_buf = Spi.transfer(tx_buf)
|
||||
return int.from_bytes(rx_buf[2:2 + length], byteorder='big', signed=False)
|
||||
|
||||
@staticmethod
|
||||
def write_array(reg: int, length: int, values):
|
||||
if not type(reg) is int:
|
||||
reg = int(reg)
|
||||
|
||||
tx_buf = [0] * SPI_BUFFER_SIZE
|
||||
|
||||
tx_buf[0] = 1
|
||||
tx_buf[1] = reg
|
||||
tx_buf[2] = length
|
||||
|
||||
pos = 3
|
||||
for i in values:
|
||||
tx_buf[pos] = i
|
||||
pos += 1
|
||||
|
||||
rx_buf = Spi.transfer(tx_buf)
|
||||
return rx_buf
|
||||
|
||||
@staticmethod
|
||||
def health_check():
|
||||
if Spi.read(Register.IDENTIFICATION_MODEL_ID, 1) != 3:
|
||||
Logging.get_logger().error(f"Unable to read Version! Make sure the mainboard is connected!")
|
||||
print("Unable to read Version! Make sure the mainboard is connected!")
|
||||
|
||||
@staticmethod
|
||||
def start_health_check_loop():
|
||||
health_check_thread = Thread(target=Spi.health_check_loop)
|
||||
health_check_thread.setDaemon(True)
|
||||
health_check_thread.start()
|
||||
|
||||
@staticmethod
|
||||
def disable_health_check():
|
||||
global SPI_HEALTH
|
||||
SPI_HEALTH = False
|
||||
|
||||
@staticmethod
|
||||
def health_check_loop():
|
||||
while SPI_HEALTH:
|
||||
if last_spi_call + 0.5 < time.time():
|
||||
Spi.health_check()
|
||||
time.sleep(0.1)
|
||||
|
||||
@staticmethod
|
||||
def get_version():
|
||||
major = Spi.read(Register.IDENTIFICATION_MODEL_REV_MAJOR, 1)
|
||||
minor = Spi.read(Register.IDENTIFICATION_MODEL_REV_MINOR, 1)
|
||||
patch = Spi.read(Register.IDENTIFICATION_MODEL_REV_PATCH, 1)
|
||||
|
||||
return f"{major}.{minor}.{patch}"
|
|
@ -1,207 +0,0 @@
|
|||
import os
|
||||
import queue
|
||||
import socket
|
||||
import threading
|
||||
|
||||
import cv2
|
||||
from flask import Flask, Response
|
||||
|
||||
from compLib.LogstashLogging import Logging
|
||||
|
||||
RTMP_SERVER = os.getenv("RTMP_SERVER", "rtmp://localhost/live/stream")
|
||||
SERVE_VIDEO = os.getenv("SERVER_SRC", "/live")
|
||||
BUILDING_DOCS = os.getenv("BUILDING_DOCS", "false")
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
HTML = """
|
||||
<html>
|
||||
<head>
|
||||
<title>Opencv Output</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Opencv Output</h1>
|
||||
<img src="{{ VIDEO_DST }}">
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
# it would be better to use jinja2 here, but I don't want to blow up the package dependencies...
|
||||
HTML = HTML.replace("{{ VIDEO_DST }}", SERVE_VIDEO)
|
||||
|
||||
|
||||
class __Streaming:
|
||||
"""
|
||||
Class that handles rtmp streaming for opencv.
|
||||
|
||||
DO NOT CREATE AN INSTANCE OF THIS CLASS YOURSELF!
|
||||
|
||||
This is automatically done when importing this module. Use Vision.Streaming which is
|
||||
an instance of this class!
|
||||
|
||||
grab frames -> do your own processing -> publish frame -> view on http server
|
||||
"""
|
||||
|
||||
class __NoBufferVideoCapture:
|
||||
def __init__(self, cam):
|
||||
self.cap = cv2.VideoCapture(cam)
|
||||
self.cap.set(3, 640)
|
||||
self.cap.set(4, 480)
|
||||
self.q = queue.Queue(maxsize=5)
|
||||
t = threading.Thread(target=self._reader)
|
||||
t.daemon = True
|
||||
t.start()
|
||||
|
||||
def _reader(self):
|
||||
while True:
|
||||
ret, frame = self.cap.read()
|
||||
if not ret:
|
||||
break
|
||||
if not self.q.empty():
|
||||
try:
|
||||
self.q.get_nowait()
|
||||
except queue.Empty:
|
||||
pass
|
||||
self.q.put(frame)
|
||||
|
||||
def read(self):
|
||||
return self.q.get()
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
Create instance of __Streaming class
|
||||
|
||||
This is done implicitly when importing the vision module and will only fail if you would
|
||||
create an object of this class. (There can (SHOULD!) only be one VideCapture)
|
||||
"""
|
||||
Logging.get_logger().info("capturing rtmp stream is disabled in this version")
|
||||
#self.__camera_stream = cv2.VideoCapture(RTMP_SERVER)
|
||||
#self.__camera_stream = cv2.VideoCapture(0)
|
||||
#self.__camera_stream.set(3, 640)
|
||||
#self.__camera_stream.set(4, 480)
|
||||
self.__camera_stream = self.__NoBufferVideoCapture(0)
|
||||
self.__newest_frame = None
|
||||
self.__lock = threading.Lock()
|
||||
Logging.get_logger().info("Initialized vision")
|
||||
|
||||
def get_frame(self):
|
||||
"""
|
||||
Grab the newest frame from the rtmp stream.
|
||||
|
||||
:return: An opencv frame
|
||||
"""
|
||||
#ret, img16 = self.__camera_stream.read()
|
||||
img16 = self.__camera_stream.read()
|
||||
return img16
|
||||
|
||||
def publish_frame(self, image):
|
||||
"""
|
||||
Publish an opencv frame to the http webserver.
|
||||
|
||||
:param image: Opencv frame that will be published
|
||||
:return: None
|
||||
"""
|
||||
with self.__lock:
|
||||
if image is not None:
|
||||
self.__newest_frame = image.copy()
|
||||
|
||||
def _newest_frame_generator(self):
|
||||
"""
|
||||
Private generator which is called directly from flask server.
|
||||
|
||||
:return: Yields image/jpeg encoded frames published from publish_frame function.
|
||||
"""
|
||||
while True:
|
||||
# use a buffer frame to copy the newest frame with lock and then freeing it immediately
|
||||
buffer_frame = None
|
||||
with self.__lock:
|
||||
if self.__newest_frame is None:
|
||||
continue
|
||||
|
||||
buffer_frame = self.__newest_frame.copy()
|
||||
|
||||
# encode frame for jpeg stream
|
||||
(flag, encoded_image) = cv2.imencode(".jpg", buffer_frame)
|
||||
|
||||
# if there was an error try again with the next frame
|
||||
if not flag:
|
||||
continue
|
||||
|
||||
# else yield encoded frame with mimetype image/jpeg
|
||||
yield (b'--frame\r\n' b'Content-Type: image/jpeg\r\n\r\n' +
|
||||
bytearray(encoded_image) + b'\r\n')
|
||||
|
||||
|
||||
Streaming = None
|
||||
if BUILDING_DOCS == "false":
|
||||
# instantiate private class __Streaming
|
||||
Streaming = __Streaming()
|
||||
Logging.get_logger().info("created instance of streaming class")
|
||||
|
||||
|
||||
@app.route("/live")
|
||||
def __video_feed():
|
||||
"""
|
||||
Define route for serving jpeg stream.
|
||||
|
||||
:return: Return the response generated along with the specific media.
|
||||
"""
|
||||
return Response(Streaming._newest_frame_generator(),
|
||||
mimetype="multipart/x-mixed-replace; boundary=frame")
|
||||
|
||||
|
||||
@app.route("/")
|
||||
def __index():
|
||||
"""
|
||||
Define route for serving a static http site to view the stream.
|
||||
|
||||
:return: Static html page
|
||||
"""
|
||||
return HTML
|
||||
|
||||
|
||||
def get_ip():
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
try:
|
||||
s.connect(('10.255.255.255', 1))
|
||||
IP = s.getsockname()[0]
|
||||
except Exception as e:
|
||||
IP = '127.0.0.1'
|
||||
print(f"Error could not query ip: {e}")
|
||||
finally:
|
||||
s.close()
|
||||
return IP
|
||||
|
||||
|
||||
def __start_flask():
|
||||
"""
|
||||
Function for running flask server in a thread.
|
||||
|
||||
:return:
|
||||
"""
|
||||
Logging.get_logger().info("starting flask server")
|
||||
app.run(host="0.0.0.0", port=9898, debug=True, threaded=True, use_reloader=False)
|
||||
ip = get_ip()
|
||||
Logging.get_logger().info(f"Vision stream started and can be viewed on: {ip}:9898")
|
||||
print(f"\033[92mVision stream started and can be viewed on: {ip}:9898\033[0m")
|
||||
|
||||
|
||||
if BUILDING_DOCS == "false":
|
||||
# start flask service in the background
|
||||
__webserver_thread = threading.Thread(target=__start_flask)
|
||||
__webserver_thread.start()
|
||||
|
||||
# for debugging and testing start processing frames and detecting a 6 by 9 calibration chessboard
|
||||
if __name__ == '__main__' and BUILDING_DOCS == "false":
|
||||
while True:
|
||||
frame = Streaming.get_frame()
|
||||
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001)
|
||||
|
||||
# processing
|
||||
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
|
||||
|
||||
# find the chessboard corners
|
||||
ret, corners = cv2.findChessboardCorners(gray, (6, 9), None)
|
||||
|
||||
cv2.drawChessboardCorners(frame, (6, 9), corners, ret)
|
||||
Streaming.publish_frame(frame)
|
|
@ -1,50 +0,0 @@
|
|||
__version__ = "0.4.1-1"
|
||||
|
||||
import sys
|
||||
import importlib
|
||||
import compLib.LogstashLogging
|
||||
import compLib.MetricsLogging
|
||||
from compLib.Lock import Lock
|
||||
|
||||
apt_found = importlib.util.find_spec("apt") is not None
|
||||
spi_found = importlib.util.find_spec("spidev") is not None
|
||||
|
||||
|
||||
if apt_found:
|
||||
import apt
|
||||
|
||||
try:
|
||||
__versions = apt.Cache()["python3-complib"].versions
|
||||
if len(__versions) != 1:
|
||||
print(f"Starting compLib! \033[91mVersion: {__version__} is outdated\033[0m\n"
|
||||
f"\033[92m[!] run the command 'sudo apt update && sudo apt install python3-complib' to install the newest version\033[0m")
|
||||
else:
|
||||
print(f"Starting compLib! \033[92mVersion: {__version__} is up to date\033[0m")
|
||||
except Exception as e:
|
||||
compLib.LogstashLogging.Logging.get_logger().error(f"error during checking apt package version -> {str(e)}")
|
||||
print(f"\033[91merror during checking apt package version -> {str(e)}\033[0m\n")
|
||||
else:
|
||||
print("apt is not installed! This is for local development only!")
|
||||
|
||||
if Lock.is_locked():
|
||||
print("Another program using compLib is still running! Delete /root/complib.lock if this is an error. Exiting...")
|
||||
sys.exit()
|
||||
else:
|
||||
Lock.lock()
|
||||
|
||||
if spi_found:
|
||||
from compLib.Spi import Spi
|
||||
from compLib.Reset import Reset
|
||||
from compLib.Encoder import Encoder
|
||||
from compLib.Motor import Motor
|
||||
import logging
|
||||
|
||||
print(f"\033[34mInitializing chipmunk board...\033[0m")
|
||||
Reset.reset_bot()
|
||||
Spi.health_check()
|
||||
Spi.start_health_check_loop()
|
||||
Motor.start_robot_state_loop()
|
||||
Encoder.clear_all()
|
||||
print(f"\033[34mReady\033[0m\n")
|
||||
else:
|
||||
print("spidev is not installed! This is for local development only!")
|
Reference in a new issue