diff --git a/MikesServoMapper.py b/MikesServoMapper.py index 23480cf..a391007 100644 --- a/MikesServoMapper.py +++ b/MikesServoMapper.py @@ -1,13 +1,31 @@ +from adafruit_servokit import ServoKit + + +import getch import logging +import multiprocessing +import os import pprint import sys +import time +import threading +import tty import yaml class MikesServoMapper: + __BASE_I2C_ADDRESS = 0x40 + __CHANNELS_COUNT = 16 + + __DEFAULT_SERVO_DEGREES = 180 + __DEFAULT_JIGGLE_DURATION = 2 + __DEFAULT_JIGGLE_SLICES = 50 + + __ESCAPE_KEY = chr(27) + def __init__(self, config_file: str, names): # noinspection PyTypeChecker @@ -21,15 +39,17 @@ class MikesServoMapper: self.__config = None self.load_config(config_file) self.pull_config_names() - self.__logger.info("Names: %s" % (pprint.pformat(self.__names))) + + self.__mappings = {} def init_logging(self): self.__logger = logging.Logger("Mikes Servo Mapper") - self.__logger_formatter = logging.Formatter(fmt="Hi poop") + self.__logger_formatter = logging.Formatter(fmt="[%(asctime)s][%(name)s] %(message)s") stream_handler = logging.StreamHandler(sys.stdout) + stream_handler.setFormatter(self.__logger_formatter) self.__logger.addHandler(stream_handler) @@ -69,4 +89,171 @@ class MikesServoMapper: self.__names.append(name) self.__names.sort() self.__logger.info("Names after pulling from config: %s" % (self.__names,)) - + + def set_name_mapping(self, name, channel): + + self.__mappings[name] = channel + + def get_name_mapping(self, name): + + if name in self.__mappings: + return self.__mappings[name] + + return None + + def determine_i2c_address(self): + + return self.__BASE_I2C_ADDRESS + + def run(self): + + self.__logger.info("Running!") + + while True: + + self.__logger.info("") + self.__logger.info("Please choose a mode: ") + self.__logger.info("1. Create mappings") + self.__logger.info("2. Test current mappings") + self.__logger.info("3. Write mappings to file") + self.__logger.info("Q. Quit") + user_choice = input("====> ") + + if user_choice == "q" or user_choice == "Q": + self.__logger.info("Quitting!") + break + + if user_choice == "1": + self.do_mappings() + elif user_choice == "2": + self.test_mappings() + elif user_choice == "3": + self.write_mappings() + else: + self.__logger.warning("Invalid choice: %s" % user_choice) + + def do_mappings(self): + + self.__logger.info("Begin mapping mode !") + + i2c_address = self.determine_i2c_address() + servo_kit = ServoKit( + address=i2c_address, + channels=self.__CHANNELS_COUNT + ) + + # + while True: + + # Print all current mappings + self.__logger.info("") + self.__logger.info("Current Mappings:") + menu_number_to_name = {} + for name_index in range(len(self.__names)): + + name = self.__names[name_index] + name_number = name_index + 1 + menu_number_to_name[str(name_number)] = name + + self.__logger.info( + "%s. %s ==> %s" + % (name_number, name, self.get_name_mapping(name=name)) + ) + + self.__logger.info("") + self.__logger.info("Please enter a number to change the corresponding mapping, or Q to quit.") + user_input = input("==========> ") + if user_input == "Q" or user_input == "q": + self.__logger.info("Quitting mapping mode") + break + elif user_input in menu_number_to_name: + name = menu_number_to_name[user_input] + channel = self.run_one_mapping( + servo_kit=servo_kit, + name=name, + default_channel=self.get_name_mapping(name) + ) + self.set_name_mapping(name=name, channel=channel) + else: + self.__logger.warning("Invalid input: %s" % user_input) + + def run_one_mapping(self, servo_kit, name, default_channel=None): + + selected_channel = default_channel + + while True: + + self.__logger.info("") + self.__logger.info("Mapping channel for: %s" % (name,)) + self.__logger.info( + "Press a key between 0-9 and A-F to try a channel." + " Press the space bar when you've found the correct channel, or escape to abort." + ) + self.__logger.info("Currently selected channel: %s" % selected_channel) + + key = getch.getch().lower() + if key == self.__ESCAPE_KEY: + self.__logger.info("Aborting") + selected_channel = None + break + elif key == " ": + self.__logger.info("Selected channel: %s" % selected_channel) + break + else: + + try: + channel = int(key, 16) + selected_channel = channel + self.jiggle_channel(servo_kit=servo_kit, channel=channel) + except ValueError: + self.__logger.warning("Invalid input!: %s" % (key,)) + time.sleep(1) + + return selected_channel + + def test_mappings(self): + + self.__logger.info("Testing mappings!") + + for name in self.__mappings.keys(): + + channel = self.get_name_mapping(name=name) + self.__logger.info("Jiggling mapping: %s ==> %s" % (name, channel)) + + time.sleep(1) + + self.__logger.info("Done testing mappings") + + def jiggle_channel(self, servo_kit, channel): + + duration = self.__DEFAULT_JIGGLE_DURATION + + degrees_per_slice = self.__DEFAULT_SERVO_DEGREES / self.__DEFAULT_JIGGLE_SLICES + seconds_per_slice = duration / self.__DEFAULT_JIGGLE_SLICES + + self.__logger.info( + "Jiggling servo on channel #%s using %s slices over %s seconds" + % (channel, self.__DEFAULT_JIGGLE_SLICES, duration) + ) + + servo = servo_kit.servo[channel] + + # Jiggle + for slice_index in range(self.__DEFAULT_JIGGLE_SLICES): + + angle = 0 + (degrees_per_slice * slice_index) + servo.angle = angle + time.sleep(seconds_per_slice) + + # Center + servo.angle = 90 + + def write_mappings(self): + + output_file_path = os.path.join( + "output", + "servo-mappings.yml" + ) + + with open(output_file_path, 'w') as f: + yaml.dump(self.__mappings, f, default_flow_style=False) diff --git a/Pipfile b/Pipfile index d45bb49..fc97a79 100644 --- a/Pipfile +++ b/Pipfile @@ -5,6 +5,8 @@ name = "pypi" [packages] pyaml = "*" +adafruit-circuitpython-servokit = "*" +getch = "*" [dev-packages] diff --git a/Pipfile.lock b/Pipfile.lock index e08355f..6a3be26 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "1f84f63565324f6ff4b4071af61c776885d170eed424917d7adb74b2b97517e3" + "sha256": "82ef81fca28479231fa9cc4c64186b3ca5b9a84ed6de11c07c74fa837224d85a" }, "pipfile-spec": 6, "requires": { @@ -16,6 +16,63 @@ ] }, "default": { + "adafruit-blinka": { + "hashes": [ + "sha256:7f6b2361e066033cfbace159bf85c26f50ef18a3c889b068529e29891caf1a10" + ], + "version": "==5.2.3" + }, + "adafruit-circuitpython-busdevice": { + "hashes": [ + "sha256:7709c75c22e37f3af1b161be26c7b9925bafd68a0d7c5526637a3f838fcb6627" + ], + "version": "==4.3.2" + }, + "adafruit-circuitpython-motor": { + "hashes": [ + "sha256:9324290afa07faec1c7d9ca35f084150f0bf8169dc12599bda8fc4c615d0806e" + ], + "version": "==3.1.2" + }, + "adafruit-circuitpython-pca9685": { + "hashes": [ + "sha256:0f8e8c4cdb4ef1828ac9e8bc870017f2f5ef1cccc906172e06d81062859bc34e" + ], + "version": "==3.3.2" + }, + "adafruit-circuitpython-register": { + "hashes": [ + "sha256:5e7232fd7ffb3f0aac2dc0de631097c629e836a8845aa0e028e8dc1c364ea064" + ], + "version": "==1.9.0" + }, + "adafruit-circuitpython-servokit": { + "hashes": [ + "sha256:396c93f07b62ff5599691261ead0f0731d1a22dae8e394afbd372a0842fc886c" + ], + "index": "pypi", + "version": "==1.3.0" + }, + "adafruit-platformdetect": { + "hashes": [ + "sha256:01f07606319f4e463889ee129fa1c62b4538d754cb1f89cf530b1aef91253729" + ], + "version": "==2.14.1" + }, + "adafruit-pureio": { + "hashes": [ + "sha256:ebab172823f7249e02a644844a64e6dc2e4b3ded38ba42099068fd3e96623cfb" + ], + "version": "==1.1.5" + }, + "getch": { + "hashes": [ + "sha256:a6c22717c10051ce65b8fb7bddb171af705b1175e694a73be956990f6089d8b1", + "sha256:be451438f7a2b389f96753aea39b6ed2540a390f1b9a12badcbc110cf9a5ce7f" + ], + "index": "pypi", + "version": "==1.0" + }, "pyaml": { "hashes": [ "sha256:29a5c2a68660a799103d6949167bd6c7953d031449d08802386372de1db6ad71", @@ -24,6 +81,26 @@ "index": "pypi", "version": "==20.4.0" }, + "pyftdi": { + "hashes": [ + "sha256:02926258d5dfd28452a3d4d7c2f6d5bab722133b2885bde8b9e28bd2ccc095ca", + "sha256:6cacb8fe28491ee6d00b8d45e18f73ea539b31bcd785f5d8c80a60322297c007" + ], + "version": "==0.51.2" + }, + "pyserial": { + "hashes": [ + "sha256:6e2d401fdee0eab996cf734e67773a0143b932772ca8b42451440cfed942c627", + "sha256:e0770fadba80c31013896c7e6ef703f72e7834965954a78e71a3049488d4d7d8" + ], + "version": "==3.4" + }, + "pyusb": { + "hashes": [ + "sha256:4e9b72cc4a4205ca64fbf1f3fff39a335512166c151ad103e55c8223ac147362" + ], + "version": "==1.0.2" + }, "pyyaml": { "hashes": [ "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97", @@ -39,6 +116,28 @@ "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a" ], "version": "==5.3.1" + }, + "rpi-ws281x": { + "hashes": [ + "sha256:461153b0ae29f28ebf7fbca781a92759a5f622d5478d3e61b0475fb0a7da6fb3", + "sha256:6cfb8c0c429dee312b2173ea3968c3da2a3625ce57bbf580ee9f49c3edaf4504", + "sha256:7175e708d6085bc02a9d0b8227797d697e34fd00787030ae5f119fe2f4f90889" + ], + "version": "==4.2.4" + }, + "rpi.gpio": { + "hashes": [ + "sha256:7424bc6c205466764f30f666c18187a0824077daf20b295c42f08aea2cb87d3f" + ], + "version": "==0.7.0" + }, + "sysv-ipc": { + "hashes": [ + "sha256:2b2b8491764845d06a9d5731ebe67f12fbe0c35967ac7f2bbe55ff24cb302405", + "sha256:8eff10dd17789ddf21b422ce46ae0f6420088902a88e4296cb805cf2fde8b4dc", + "sha256:a67fc5b7a3dd5fc9c50776de0c65a44eb776d29bb88364dedb1fb1149e53a1f1" + ], + "version": "==1.0.1" } }, "develop": {} diff --git a/README.md b/README.md index 27bbf15..0f64dc2 100644 --- a/README.md +++ b/README.md @@ -7,13 +7,22 @@ Written and tested using the Adafruit I2C servo driver board: [PCA9685](https:// ## Requiremments -* ```pipenv``` +### Python Requirements -## Installation +Python's requirements are handled by pipenv, which you can install like so: -cd to this repo's directory and install pip dependencies with: +```bash +sudo apt install pipenv +``` +or +```bash +sudo dnf install pipenv +``` -```pipenv install``` +Once installed, you can have pipenv install all python requirements like so: + +1. cd to this repo's directory +2. Execute the command: ```pipenv install``` ## Execution @@ -23,6 +32,30 @@ cd to this repo's directory and execute using: ## Command Line Arguments -TODO +### ***--name*** (Specify one or more mapping names) +You can specify desired mapping names by adding the ***--name*** argument, as many times as you wish: + +```bash +$ pipenv run python3 main.py --name Leg --name Arm +``` + +### ***--config*** (Specify an input config file) + +You can specify a yaml configuration file to load with this argument, like so: + +```bash +$ pipenv run python3 main.py --config /path/to/config.yaml +``` + +So far the config file is only good for storing desired names to be mapped. Here's an example: + +```yaml + +names: + - Manny + - Moe + - Jack + +```