initial
This commit is contained in:
commit
dd54f14917
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
/venv
|
||||||
|
/.servo-state
|
||||||
|
/.idea
|
28
README.md
Normal file
28
README.md
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# opi-mg996r
|
||||||
|
|
||||||
|
This is a Python implementation of MG996R servo support for Orange Pi boards (H3 specifically, others untested).
|
||||||
|
It does not use hardware PWM but instead emulates PWM with primitive `time.sleep()` calls, so don't expect any real-time
|
||||||
|
accuracy. And, well, it's Python, after all.
|
||||||
|
|
||||||
|
But it does its job and MG996R servo works just fine!
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
- Clone the repo.
|
||||||
|
- Install dependencies (see [here](requirements.txt)).
|
||||||
|
- Check out [example.py](example.py) for an example.
|
||||||
|
|
||||||
|
You can also just use it from command line like this:
|
||||||
|
```
|
||||||
|
./example.py --deg 0
|
||||||
|
./example.py --deg 180
|
||||||
|
./example.py --deg 90
|
||||||
|
```
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
The softpwm implementation was taken the [orangepwm](https://github.com/evergreen-it-dev/orangepwm) project.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
44
example.py
Executable file
44
example.py
Executable file
@ -0,0 +1,44 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import os, logging
|
||||||
|
from argparse import ArgumentParser
|
||||||
|
from mg996r import MG996R
|
||||||
|
|
||||||
|
default_degree = 360
|
||||||
|
state_file = '.servo-state'
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# set logging level
|
||||||
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
|
||||||
|
# parse arguments
|
||||||
|
parser = ArgumentParser()
|
||||||
|
parser.add_argument('--deg', type=int, required=True)
|
||||||
|
parser.add_argument('--pin', type=str, default='PA6',
|
||||||
|
help='GPIO pin to use')
|
||||||
|
parser.add_argument('--reset', action='store_true',
|
||||||
|
help=f'Use clean default state (degree = {default_degree})')
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
start_degree = default_degree
|
||||||
|
|
||||||
|
# restore previous degree from a file
|
||||||
|
if not args.reset:
|
||||||
|
try:
|
||||||
|
if os.path.exists(state_file):
|
||||||
|
with open(state_file, 'r') as f:
|
||||||
|
start_degree = int(f.read())
|
||||||
|
if not 0 <= start_degree <= 360:
|
||||||
|
raise ValueError(f'invalid degree value in {state_file}')
|
||||||
|
except (IOError, ValueError) as e:
|
||||||
|
logging.exception(e)
|
||||||
|
start_degree = default_degree
|
||||||
|
|
||||||
|
servo = MG996R(args.pin, start_degree)
|
||||||
|
servo.move(args.deg)
|
||||||
|
|
||||||
|
# save degree to a file
|
||||||
|
try:
|
||||||
|
with open(state_file, 'w') as f:
|
||||||
|
f.write(str(args.deg))
|
||||||
|
except IOError as e:
|
||||||
|
logging.exception(e)
|
28
mg996r.py
Normal file
28
mg996r.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
from time import sleep
|
||||||
|
from orangepwm import OrangePwm
|
||||||
|
from pyA20.gpio import gpio
|
||||||
|
from pyA20.gpio import port
|
||||||
|
|
||||||
|
|
||||||
|
class MG996R:
|
||||||
|
def __init__(self, pin: str, prev: int = 360):
|
||||||
|
self.pwm = OrangePwm(50, getattr(port, pin))
|
||||||
|
self.prev = prev
|
||||||
|
gpio.init()
|
||||||
|
|
||||||
|
logging.debug(f'Initialized MG996R class with starting degree of {prev}')
|
||||||
|
|
||||||
|
def move(self, deg, delay: int = 0):
|
||||||
|
distance = abs(self.prev - deg)
|
||||||
|
sleep_time = distance / 60 * .5 + delay
|
||||||
|
|
||||||
|
duty = deg / 18 + 2
|
||||||
|
logging.debug(f'Moving to {deg} degrees, duty {duty:.0f}%, sleeping for {sleep_time:.1f} sec.')
|
||||||
|
|
||||||
|
self.prev = deg
|
||||||
|
|
||||||
|
self.pwm.start(duty)
|
||||||
|
sleep(sleep_time + delay)
|
||||||
|
self.pwm.stop()
|
83
orangepwm.py
Normal file
83
orangepwm.py
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
from pyA20.gpio import gpio as GPIO
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
class OrangePwm(threading.Thread):
|
||||||
|
def __init__(self, frequency, gpioPin, gpioScheme=0):
|
||||||
|
"""
|
||||||
|
Init the OrangePwm instance. Expected parameters are :
|
||||||
|
- frequency : the frequency in Hz for the PWM pattern. A correct value may be 100.
|
||||||
|
- gpioPin : the gpio.port which will act as PWM ouput
|
||||||
|
- gpioScheme : saved for compatibility with PiZyPWM code
|
||||||
|
"""
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self.baseTime = 1.0 / frequency
|
||||||
|
self.maxCycle = 100.0
|
||||||
|
self.sliceTime = self.baseTime / self.maxCycle
|
||||||
|
self.gpioPin = gpioPin
|
||||||
|
self.terminated = False
|
||||||
|
self.toTerminate = False
|
||||||
|
# GPIO.setmode(gpioScheme)
|
||||||
|
|
||||||
|
def start(self, dutyCycle):
|
||||||
|
"""
|
||||||
|
Start PWM output. Expected parameter is :
|
||||||
|
- dutyCycle : percentage of a single pattern to set HIGH output on the GPIO pin
|
||||||
|
|
||||||
|
Example : with a frequency of 1 Hz, and a duty cycle set to 25, GPIO pin will
|
||||||
|
stay HIGH for 1*(25/100) seconds on HIGH output, and 1*(75/100) seconds on LOW output.
|
||||||
|
"""
|
||||||
|
self.dutyCycle = dutyCycle
|
||||||
|
GPIO.setcfg(self.gpioPin, GPIO.OUTPUT)
|
||||||
|
self.thread = threading.Thread(None, self.run, None, (), {})
|
||||||
|
self.thread.start()
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"""
|
||||||
|
Run the PWM pattern into a background thread. This function should not be called outside of this class.
|
||||||
|
"""
|
||||||
|
while self.toTerminate == False:
|
||||||
|
if self.dutyCycle > 0:
|
||||||
|
GPIO.output(self.gpioPin, GPIO.HIGH)
|
||||||
|
time.sleep(self.dutyCycle * self.sliceTime)
|
||||||
|
|
||||||
|
if self.dutyCycle < self.maxCycle:
|
||||||
|
GPIO.output(self.gpioPin, GPIO.LOW)
|
||||||
|
time.sleep((self.maxCycle - self.dutyCycle) * self.sliceTime)
|
||||||
|
|
||||||
|
self.terminated = True
|
||||||
|
|
||||||
|
def changeDutyCycle(self, dutyCycle):
|
||||||
|
"""
|
||||||
|
Change the duration of HIGH output of the pattern. Expected parameter is :
|
||||||
|
- dutyCycle : percentage of a single pattern to set HIGH output on the GPIO pin
|
||||||
|
|
||||||
|
Example : with a frequency of 1 Hz, and a duty cycle set to 25, GPIO pin will
|
||||||
|
stay HIGH for 1*(25/100) seconds on HIGH output, and 1*(75/100) seconds on LOW output.
|
||||||
|
"""
|
||||||
|
self.dutyCycle = dutyCycle
|
||||||
|
|
||||||
|
def changeFrequency(self, frequency):
|
||||||
|
"""
|
||||||
|
Change the frequency of the PWM pattern. Expected parameter is :
|
||||||
|
- frequency : the frequency in Hz for the PWM pattern. A correct value may be 100.
|
||||||
|
|
||||||
|
Example : with a frequency of 1 Hz, and a duty cycle set to 25, GPIO pin will
|
||||||
|
stay HIGH for 1*(25/100) seconds on HIGH output, and 1*(75/100) seconds on LOW output.
|
||||||
|
"""
|
||||||
|
self.baseTime = 1.0 / frequency
|
||||||
|
self.sliceTime = self.baseTime / self.maxCycle
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
"""
|
||||||
|
Stops PWM output.
|
||||||
|
"""
|
||||||
|
self.toTerminate = True
|
||||||
|
while self.terminated == False:
|
||||||
|
# Just wait
|
||||||
|
time.sleep(0.01)
|
||||||
|
|
||||||
|
GPIO.output(self.gpioPin, GPIO.LOW)
|
||||||
|
GPIO.setcfg(self.gpioPin, GPIO.INPUT)
|
1
requirements.txt
Normal file
1
requirements.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
git+git://github.com/duxingkei33/orangepi_PC_gpio_pyH3@master
|
Loading…
x
Reference in New Issue
Block a user