From 3a1945e974e9edfb0091be18552ae715cbdc38e9 Mon Sep 17 00:00:00 2001 From: Michael Lee Date: Thu, 28 Nov 2024 15:27:24 -0500 Subject: [PATCH] Working GPIO Set/Get --- .gitignore | 1 + meta.json | 16 ++++----- reload.sh | 14 ++++++++ requirements.txt | 2 +- setup.sh | 4 +-- src/main.py | 89 +++++++++++++++++++++++++++++++++++++++++------- test.py | 2 +- 7 files changed, 101 insertions(+), 27 deletions(-) create mode 100755 reload.sh diff --git a/.gitignore b/.gitignore index 9bc5490..65c359a 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,7 @@ share/python-wheels/ .installed.cfg *.egg MANIFEST +module.tar.gz # PyInstaller # Usually these files are written by a python script from a template diff --git a/meta.json b/meta.json index 738c2d8..921a23c 100644 --- a/meta.json +++ b/meta.json @@ -1,24 +1,20 @@ { "$schema": "https://dl.viam.dev/module.schema.json", "module_id": "michaellee1019:mcp23017", - "visibility": "public", - "url": "", - "description": "Modular board component: board", + "visibility": "private", + "url": "https://github.com/michaellee1019/mcp23017", + "description": "Provides models for the mcp23017 GPIO expansion chip.", "models": [ { "api": "rdk:component:board", "model": "michaellee1019:mcp23017:board" } ], - "entrypoint": "dist/main", + "entrypoint": "reload.sh", "first_run": "", "build": { - "build": "./build.sh", + "build": "rm -f module.tar.gz && tar czf module.tar.gz requirements.txt src/*.py meta.json setup.sh reload.sh", "setup": "./setup.sh", - "path": "dist/archive.tar.gz", - "arch": [ - "linux/amd64", - "linux/arm64" - ] + "path": "module.tar.gz" } } \ No newline at end of file diff --git a/reload.sh b/reload.sh new file mode 100755 index 0000000..20b2e17 --- /dev/null +++ b/reload.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +# bash safe mode. look at `set --help` to see what these are doing +set -euxo pipefail + +cd $(dirname $0) +MODULE_DIR=$(dirname $0) +VIRTUAL_ENV=$MODULE_DIR/.venv +PYTHON=$VIRTUAL_ENV/bin/python +./setup.sh + +# Be sure to use `exec` so that termination signals reach the python process, +# or handle forwarding termination signals manually +exec $PYTHON src/main.py $@ \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index ded74b0..40fae09 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ viam-sdk==0.34.0 - +adafruit-circuitpython-mcp230xx==2.5.15 typing-extensions diff --git a/setup.sh b/setup.sh index f9dcd7c..5d90e26 100755 --- a/setup.sh +++ b/setup.sh @@ -2,7 +2,7 @@ cd `dirname $0` # Create a virtual environment to run our code -VENV_NAME="venv" +VENV_NAME=".venv" PYTHON="$VENV_NAME/bin/python" ENV_ERROR="This module requires Python >=3.8, pip, and virtualenv to be installed." @@ -19,7 +19,7 @@ if ! python3 -m venv $VENV_NAME >/dev/null 2>&1; then $SUDO apt -qq update >/dev/null fi $SUDO apt install -qqy python3-venv >/dev/null 2>&1 - if ! python3 -m venv $VENV_NAME >/dev/null 2>&1; then + if ! python3 -m .venv $VENV_NAME >/dev/null 2>&1; then echo $ENV_ERROR >&2 exit 1 fi diff --git a/src/main.py b/src/main.py index f2b9cdf..95cd153 100644 --- a/src/main.py +++ b/src/main.py @@ -1,24 +1,56 @@ import asyncio -import sys + from datetime import timedelta -from typing import (Any, ClassVar, Dict, Final, List, Mapping, Optional, - Sequence) +from typing import Any, ClassVar, Dict, List, Mapping, Optional, Sequence from typing_extensions import Self -from viam.components.board import * +from viam.components.board import Board, TickStream from viam.module.module import Module from viam.proto.app.robot import ComponentConfig from viam.proto.common import ResourceName -from viam.proto.component.board import (PowerMode, ReadAnalogReaderResponse, - StreamTicksResponse) +from viam.proto.component.board import PowerMode from viam.resource.base import ResourceBase from viam.resource.easy_resource import EasyResource from viam.resource.types import Model, ModelFamily -from viam.streams import Stream - -class Board(Board, EasyResource): +import digitalio +import board +import busio +from adafruit_mcp230xx.mcp23017 import MCP23017 +from adafruit_mcp230xx.digital_inout import DigitalInOut + +MCP23017_IODIRA = 0x00 +MCP23017_IPOLA = 0x02 +MCP23017_GPINTENA = 0x04 +MCP23017_DEFVALA = 0x06 +MCP23017_INTCONA = 0x08 +MCP23017_IOCONA = 0x0A +MCP23017_GPPUA = 0x0C +MCP23017_INTFA = 0x0E +MCP23017_INTCAPA = 0x10 +MCP23017_GPIOA = 0x12 +MCP23017_OLATA = 0x14 + +MCP23017_IODIRB = 0x01 +MCP23017_IPOLB = 0x03 +MCP23017_GPINTENB = 0x05 +MCP23017_DEFVALB = 0x07 +MCP23017_INTCONB = 0x09 +MCP23017_IOCONB = 0x0B +MCP23017_GPPUB = 0x0D +MCP23017_INTFB = 0x0F +MCP23017_INTCAPB = 0x11 +MCP23017_GPIOB = 0x13 +MCP23017_OLATB = 0x15 + + +class MCP23017Board(Board, EasyResource): MODEL: ClassVar[Model] = Model(ModelFamily("michaellee1019", "mcp23017"), "board") + i2c: busio.I2C = None + i2c_address: int = 0x27 + mcp: MCP23017 = None + pins: List[DigitalInOut] + pullups: List[int] = [] @classmethod def new( @@ -58,6 +90,24 @@ def reconfigure( config (ComponentConfig): The new configuration dependencies (Mapping[ResourceName, ResourceBase]): Any dependencies (both implicit and explicit) """ + # if "i2c_bus" in config.attributes.fields: + # self.i2c_bus = int(config.attributes.fields["i2c_bus"].string_value) + self.i2c = busio.I2C(board.SCL, board.SDA) + if "i2c_address" in config.attributes.fields: + self.i2c_address = int( + config.attributes.fields["i2c_address"].string_value, base=16 + ) + if "pullups" in config.attributes.fields: + self.pullups = [int(x) for x in config.attributes.fields["pullups"].list_value] + + self.mcp = MCP23017(self.i2c, self.i2c_address) + self.pins = [self.mcp.get_pin(i) for i in range(16)] + for i in range(16): + if i in self.pullups: + self.pins[i].pull = digitalio.Pull.UP + else: + self.pins[i].pull = None + return super().reconfigure(config, dependencies) class Analog(Board.Analog): @@ -93,6 +143,11 @@ async def value( raise NotImplementedError() class GPIOPin(Board.GPIOPin): + pin: DigitalInOut = None + + def __init__(self, name: str, pin: DigitalInOut): + self.pin = pin + super().__init__(name) async def set( self, @@ -102,7 +157,9 @@ async def set( timeout: Optional[float] = None, **kwargs ): - raise NotImplementedError() + if self.pin.direction: + self.pin.switch_to_output() + self.pin.value = high async def get( self, @@ -111,7 +168,9 @@ async def get( timeout: Optional[float] = None, **kwargs ) -> bool: - raise NotImplementedError() + if not self.pin.direction: + self.pin.switch_to_input() + return self.pin.value async def get_pwm( self, @@ -158,7 +217,12 @@ async def digital_interrupt_by_name(self, name: str) -> DigitalInterrupt: raise NotImplementedError() async def gpio_pin_by_name(self, name: str) -> GPIOPin: - raise NotImplementedError() + try: + pin_num = int(name) + pin = self.pins[pin_num] + return MCP23017Board.GPIOPin(name, pin) + except ValueError as ex: + raise ValueError("pin name must be an integer between 0-15") from ex async def analog_names(self) -> List[str]: raise NotImplementedError() @@ -188,4 +252,3 @@ async def stream_ticks( if __name__ == "__main__": asyncio.run(Module.run_from_registry()) - diff --git a/test.py b/test.py index a7a2a38..84b3039 100644 --- a/test.py +++ b/test.py @@ -54,7 +54,7 @@ #configue all PinB input bus.write_byte_data(MCP23017_ADDRESS,MCP23017_IODIRB,0xFF) #configue all PinB pullDOWN -bus.write_byte_data(MCP23017_ADDRESS,MCP23017_GPPUB,0x00) +bus.write_byte_data(MCP23017_ADDRESS,MCP23017_GPPUB,0x00) #only for debug