diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..f5840d3 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,31 @@ +name: Lint + +on: +- push +- workflow_dispatch + +jobs: + lint: + runs-on: ubuntu-latest + name: Lint + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Set up Python + uses: actions/setup-python@v4 + with: + cache: 'pip' + python-version: "3.12" + - name: 'Install requirements' + run: | + python -m venv .venv + source .venv/bin/activate + pip install -r requirements.txt + - name: 'Modify Actions PATH' + run: echo "$PWD/.venv/bin" >> $GITHUB_PATH + - name: 'Lint: Pyright' + uses: jakebailey/pyright-action@v2 + with: + version: PATH + - name: 'Lint: flake8' + uses: py-actions/flake8@v2.3.0 diff --git a/.gitignore b/.gitignore index 0face57..a4f0da8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +.mypy-cache +.vscode .venv __pycache__ docs/firmware diff --git a/README.md b/README.md index 145f857..e71d680 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,25 @@ koristi za buduće letnje projekte koji se bave izradom robota. U ovom repozitorijumu se može naći kod i potrebna dokumentacija za Miloja. +## Doprinošenje +[![Lint](https://github.com/pfe-rs/lk-s-2024-miloje-api/actions/workflows/lint.yml/badge.svg)](https://github.com/pfe-rs/lk-s-2024-miloje-api/actions/workflows/lint.yml) + +Ovaj repozitorijum koristi dva lintera: + +- [flake8](https://flake8.pycqa.org/), koji proverava da li stil Python koda + prati [PEP 8](https://pep8.org/), i +- [Pyright](https://microsoft.github.io/pyright/), koji proverava dinamičke i + statičke tipove korišćene u Python kodu i određuje da li taj kod ima smisla sa + tim tipovima u vidu. + +Kako biste doprineli kod na glavnu granu, potrebno je da vaš kod prođe proveru +oba lintera. Kako biste tokom razvoja bili sigurni da poštujete pravila ova dva +lintera, možete instalirati +[Pyright](https://marketplace.visualstudio.com/items?itemName=ms-pyright.pyright) +i [Flake8](https://marketplace.visualstudio.com/items?itemName=ms-python.flake8) +ekstenzije za [Visual Studio Code](https://code.visualstudio.com/), koje će vam +automatski prijaviti ukoliko vaš kod ima ove greške. + ## Istorija Ovaj projekat su radili: diff --git a/api/actuators/actuator.py b/api/actuators/actuator.py new file mode 100644 index 0000000..851f5aa --- /dev/null +++ b/api/actuators/actuator.py @@ -0,0 +1,10 @@ +from abc import ABC + +from communication.communication import Communication + + +class Actuator(ABC): + """Base Actuator class""" + def __init__(self, communication: Communication, actuator_id: int): + self.communication = communication + self.id = actuator_id diff --git a/api/actuators/servo.py b/api/actuators/servo.py new file mode 100644 index 0000000..86d9035 --- /dev/null +++ b/api/actuators/servo.py @@ -0,0 +1,7 @@ +from actuators.actuator import Actuator + + +class Servo(Actuator): + """Class for controlling a servo motor""" + def set_position(self, angle): + self.communication.send(f"{self.id} {angle}") diff --git a/api/actuators/stepper.py b/api/actuators/stepper.py new file mode 100644 index 0000000..64aedb2 --- /dev/null +++ b/api/actuators/stepper.py @@ -0,0 +1,10 @@ +from actuators.actuator import Actuator + + +class Stepper(Actuator): + """Class for controlling a stepper motor""" + def clockwise(self, speed: int, distance: int): + self.communication.send(f"{self.id} F {round(speed * 7.87)} {round(distance * 7.87)}") + + def anticlockwise(self, speed: int, distance: int): + self.communication.send(f"{self.id} B {round(speed * 7.87)} {round(distance * 7.87)}") diff --git a/api/actuators/wheels.py b/api/actuators/wheels.py new file mode 100644 index 0000000..a9e0539 --- /dev/null +++ b/api/actuators/wheels.py @@ -0,0 +1,27 @@ +from actuators.stepper import Stepper + + +class Wheels: + def __init__(self, stepper1: Stepper, stepper2: Stepper): + self.stepper1 = stepper1 + self.stepper2 = stepper2 + + def forward(self, speed: int, distance: int): + """Move MILOJE forward""" + self.stepper1.clockwise(speed, distance) + self.stepper2.anticlockwise(speed, distance) + + def backward(self, speed: int, distance: int): + """Move MILOJE backward""" + self.stepper1.anticlockwise(speed, distance) + self.stepper2.clockwise(speed, distance) + + def spin_clockwise(self, speed: int, angle: int): + """Spin MILOJE in the clockwise direction""" + self.stepper1.anticlockwise(speed, (2*angle)//8) + self.stepper2.anticlockwise(speed, (2*angle)//8) + + def spin_anticlockwise(self, speed: int, angle: int): + """Spin MILOJE in the anticlockwise direction""" + self.stepper1.clockwise(speed, (2*angle)//8) + self.stepper2.clockwise(speed, (2*angle)//8) diff --git a/api/buzzer.py b/api/buzzer.py new file mode 100644 index 0000000..ea5953c --- /dev/null +++ b/api/buzzer.py @@ -0,0 +1,3 @@ +class Buzzer: + def __init__(self, communication, index): + ... diff --git a/api/communication/communication.py b/api/communication/communication.py new file mode 100644 index 0000000..1f600ed --- /dev/null +++ b/api/communication/communication.py @@ -0,0 +1,13 @@ +from abc import ABC, abstractmethod + + +class Communication(ABC): + @abstractmethod + def send(self, data): + """Send data to the communication channel""" + pass + + @abstractmethod + def receive(self) -> str: + """Receive data from the communication channel""" + return "" diff --git a/api/communication/uart.py b/api/communication/uart.py new file mode 100644 index 0000000..a519fb3 --- /dev/null +++ b/api/communication/uart.py @@ -0,0 +1,33 @@ +import time + +import serial +from communication.communication import Communication + + +class UART(Communication): + def __init__(self, port: str, baud_rate: int, timeout: int | None = None): + self.baud_rate = baud_rate + self.port = port + self.conn = serial.Serial( + port=self.port, + baudrate=self.baud_rate, + timeout=timeout + ) + time.sleep(1) + + def send(self, data: str): + """Send data to the communication channel""" + self.conn.write(data.encode() + b"\n") + + def receive(self) -> str: + """Receive data from the communication channel""" + data = self.conn.read_all() + if data is not None: + data = data.decode() + else: + while data is None or len(data) == 0: + time.sleep(0.5) + data = self.conn.read_all() + if data is not None: + data = data.decode() + return data.strip() diff --git a/api/examples/example1.py b/api/examples/example1.py new file mode 100644 index 0000000..245ffa6 --- /dev/null +++ b/api/examples/example1.py @@ -0,0 +1,20 @@ +from communication.uart import UART +from miloje import Miloje + +if __name__ == "__main__": + miloje = Miloje(UART("/dev/rfcomm0", 9600, 1)) + + wheels = miloje.get_wheels() + head = miloje.get_head() + + if wheels: + while True: + query = input("? ") + if query == "w": + wheels.forward(500, 212) + elif query == "s": + wheels.backward(500, 212) + elif query == "a": + wheels.spin_anticlockwise(300, 30) + elif query == "d": + wheels.spin_clockwise(300, 30) diff --git a/api/head.py b/api/head.py new file mode 100644 index 0000000..1cca705 --- /dev/null +++ b/api/head.py @@ -0,0 +1,18 @@ +from actuators.servo import Servo +from sensors.ultrasonic import UltraSonic + + +class Head: + def __init__(self, servo1: Servo, servo2: Servo, ultra_sonic: UltraSonic): + self.servo1 = servo1 + self.servo2 = servo2 + self.ultra_sonic = ultra_sonic + + def horizontal_angle(self, angle: int): + self.servo1.set_position(angle) + + def vertical_angle(self, angle: int): + self.servo2.set_position(angle) + + def ultrasonic_read(self) -> float: + return self.ultra_sonic.get_data() diff --git a/api/miloje.py b/api/miloje.py new file mode 100644 index 0000000..1f8f82d --- /dev/null +++ b/api/miloje.py @@ -0,0 +1,59 @@ +from typing import List + +from actuators.servo import Servo +from actuators.stepper import Stepper +from actuators.wheels import Wheels +from buzzer import Buzzer +from communication.communication import Communication +from head import Head +from sensors.battery import Battery +from sensors.ultrasonic import UltraSonic + +CMD_CAPABILITIES = "C 1" +CAPABILITY_TYPES = { + "M": Stepper, + "S": Servo, + "B": Battery, + "U": UltraSonic, + "Z": Buzzer +} + + +class Miloje: + def __init__(self, communication: Communication): + self.communication = communication + self.capabilities = {} + self.wheels = None + self.head = None + + self.communication.send(CMD_CAPABILITIES) + data = self.communication.receive() + for index, char in enumerate(data): + if char not in self.capabilities and char.isalpha(): + self.capabilities[char] = [] + self.capabilities[char].append(CAPABILITY_TYPES[char](self.communication, index)) + + try: + self.wheels = Wheels(self.capabilities["M"][0], + self.capabilities["M"][1]) + except IndexError: + self.wheels = None + + try: + self.head = Head(self.capabilities["S"][0], + self.capabilities["S"][1], + self.capabilities["U"][0]) + except IndexError: + self.head = None + + def get_steppers(self) -> List[Stepper]: + return self.capabilities["M"] + + def get_servos(self) -> List[Servo]: + return self.capabilities["S"] + + def get_head(self) -> Head | None: + return self.head + + def get_wheels(self) -> Wheels | None: + return self.wheels diff --git a/api/sensors/battery.py b/api/sensors/battery.py new file mode 100644 index 0000000..a30203a --- /dev/null +++ b/api/sensors/battery.py @@ -0,0 +1,8 @@ +from sensors.sensor import Sensor + + +class Battery(Sensor): + def get_data(self) -> float: + """Get value (voltage) from the battery""" + self.communication.send(f"{self.id}") + return float(self.communication.receive()) diff --git a/api/sensors/sensor.py b/api/sensors/sensor.py new file mode 100644 index 0000000..14b3743 --- /dev/null +++ b/api/sensors/sensor.py @@ -0,0 +1,13 @@ +from abc import ABC, abstractmethod + +from communication.communication import Communication + + +class Sensor(ABC): + def __init__(self, communication: Communication, sensor_id: int): + self.communication = communication + self.id = sensor_id + + @abstractmethod + def get_data(self) -> str | float: + return "" diff --git a/api/sensors/ultrasonic.py b/api/sensors/ultrasonic.py new file mode 100644 index 0000000..dad7627 --- /dev/null +++ b/api/sensors/ultrasonic.py @@ -0,0 +1,8 @@ +from sensors.sensor import Sensor + + +class UltraSonic(Sensor): + def get_data(self) -> float: + """Get value (distance) from the ultrasonic sensor""" + self.communication.send(f"{self.id}") + return float(self.communication.receive()) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..0853f4d --- /dev/null +++ b/requirements.txt @@ -0,0 +1,17 @@ +flake8==7.1.0 +flake8-builtins==2.5.0 +# flake8-docstrings==1.7.0 +flake8-isort==6.1.1 +flake8-pep3101==2.1.0 +flake8-quotes==3.4.0 +flake8-string-format==0.3.0 +flake8-tidy-imports==4.10.0 +isort==5.13.2 +mccabe==0.7.0 +nodeenv==1.9.1 +pep8-naming==0.14.1 +pycodestyle==2.12.0 +pyflakes==3.2.0 +pyright==1.1.373 +pyserial==3.5 +types-keyboard==0.13.2.20240310 diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..4f79292 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,6 @@ +[flake8] +exclude = + .venv + __pycache__ +max-line-length = 120 +inline-quotes = "