Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added support for a physical button and led strip #26

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
Open
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,6 @@ dist
*.egg*
.DS_Store
*.zip
temp
update.sh
.vscode/settings.json
58 changes: 50 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# OctoLight
A simple plugin that adds a button to the navigation bar for toggleing a GPIO pin on the Raspberry Pi.
A simple plugin that adds a button to the navigation bar for toggling a GPIO pin on the Raspberry Pi.

![WebUI interface](img/screenshoot.png)

Expand All @@ -12,18 +12,60 @@ or manually using this URL:
## Configuration
![Settings panel](img/settings.png)

Curently, you can configure two settings:
Currently, you can configure theses settings:
- `Light PIN`: The pin on the Raspberry Pi that the button controls.
- Default value: 13
- The pin number is saved in the **board layout naming** scheme (gray labels on the pinout image below).
- **!! IMPORTANT !!** The Raspberry Pi can only controll the **GPIO** pins (orange labels on the pinout image below)
- Default value: 19
- The pin number is saved in the **BCM layout naming** scheme (orange labels on the pinout image below).
- **!! IMPORTANT !!** The Raspberry Pi can only control th **GPIO** pins (orange labels on the pinout image below)
![Raspberry Pi GPIO](img/rpi_gpio.png)

- `Inverted output`: If true, the output will be inverted
- Usage: if you have a light, that is turned off when voltage is applied to the pin (wired in negative logic), you should turn on this option, so the light isn't on when you reboot your Raspberry Pi.

- Usage: if you have a light, that is turned off when voltage is applied to the pin (wired in negative logic), you
should turn on this option, so the light isn't on when you reboot your Raspberry Pi.
- `Enable physical button`: If true, you will be able to turn on and off the light from a physical button wired to
the Raspberry Pi GPIO
- Default value: False
- `Button PIN`: The pin on the Raspberry Pi to read value from for the physical button.
- Default value: 2
- `Led Strip`: if checked, enable the support for a led strip on the `Light PIN`
- Default value: False
- Before using this option, you should follow the [guide bellow](#setup-spi)
- Usage: theoretically it should work on a PWM, PCM or SPI pin but it has only been tested on the SPI pin.
- `Number of leds`: Number of leds on the led strip.
- Default value: 20

## Setup SPI
**Only needed if you use the LED strip option**
1. Add the pi user to the gpio group.
```shell
sudo adduser pi gpio
```
2. Enable SPI. The plugin uses SPI to drive the LEDs, which is disabled by default and needs to be turned on.
- `Adds dtparam=spi=on to /boot/config.txt`
3. Increase SPI buffer size. Whilst the plugin will work without this, it will only work well with a handful of LEDs.
- `Adds spidev.bufsize=32768 to the end of /boot/cmdline.txt`
4. Set compatible clock frequency Raspberry Pi 3 or earlier only, not required for a Pi 4 The Pi 3's default internal
clock frequency is not compatible with SPI, so it needs to be set to 250 to be compatible.
- `Adds core_freq=250 to /boot/config.txt`
5. Set a minimum clock frequency Raspberry Pi 4 only On a Raspberry Pi 4, the clock frequency is dynamic and can change
when the pi is 'idle' vs. 'working', which causes LEDs to flicker, change colour, or stop working completely. By setting
a minimum the same as the max, we stop this dynamic clocking.
- `Adds core_freq_min=500 to /boot/config.txt`
6. reboot the pi
```shell
sudo reboot
```

_[source](https://cp2004.gitbook.io/ws281x-led-status/guides/setup-guide-1/spi-setup)_

## TO DO
- [x] Update interface if Light is turned on or off
- [ ] Use wizard to setup the led strip library like on [OctoPrint-WS281x_LED_Status](https://github.com/cp2004/OctoPrint-WS281x_LED_Status)

Maybe in the distant future:
### Maybe in the distant future:
- [ ] Turn off on finish print

## Thanks

Thanks to [cp2004](https://github.com/cp2004) for its documentation to setup the SPI interface to be used by the
[rpi_ws281x](https://github.com/jgarff/rpi_ws281x) library
162 changes: 118 additions & 44 deletions octoprint_octolight/__init__.py
Original file line number Diff line number Diff line change
@@ -1,31 +1,56 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, unicode_literals


import octoprint.plugin
from octoprint.events import Events
import flask

import board
from rpi_ws281x import Color, PixelStrip, ws
import RPi.GPIO as GPIO

GPIO.setmode(GPIO.BOARD)

GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)

# constants
BUTTON_PIN = "button_pin"
LIGHT_PIN = "light_pin"
INVERTED_OUTPUT = "inverted_output"
PREVIOUS_BUTTON_PIN = "previous_button_pin"
IS_LED_STRIP = "is_led_strip"
NB_LEDS = "nb_leds"

LED_DMA = 10 # DMA channel to use for generating signal (try 10)
LED_BRIGHTNESS = 255 # Set to 0 for darkest and 255 for brightest
LED_CHANNEL = 0
LED_STRIP = ws.WS2812_STRIP
LED_FREQ_HZ = 800000 # LED signal frequency in hertz (usually 800khz)


class OctoLightPlugin(
octoprint.plugin.AssetPlugin,
octoprint.plugin.StartupPlugin,
octoprint.plugin.ShutdownPlugin,
octoprint.plugin.TemplatePlugin,
octoprint.plugin.SimpleApiPlugin,
octoprint.plugin.SettingsPlugin,
octoprint.plugin.EventHandlerPlugin,
octoprint.plugin.RestartNeedingPlugin
):

# variables
light_state = False
pixels = None

def get_settings_defaults(self):
return dict(
light_pin = 13,
inverted_output = False
light_pin=19,
button_pin=2,
previous_button_pin=2,
inverted_output=False,
is_led_strip=False,
nb_leds=20,
)

def get_template_configs(self):
Expand All @@ -39,69 +64,96 @@ def get_assets(self):
# core UI here.
return dict(
js=["js/octolight.js"],
css=["css/octolight.css"],
css=["css/octolight.css"]
#less=["less/octolight.less"]
)

def on_after_startup(self):
self.light_state = False
self._logger.info("--------------------------------------------")
self._logger.info("OctoLight started, listening for GET request")
self._logger.info("Light pin: {}, inverted_input: {}".format(
self._settings.get(["light_pin"]),
self._settings.get(["inverted_output"])
self._logger.debug("GPIO Mode: {}".format(
GPIO.getmode()
))
self._logger.info("Light pin: {}, inverted_input: {}, button pin: {}".format(
self._settings.get([LIGHT_PIN]),
self._settings.get([INVERTED_OUTPUT]),
self._settings.get([BUTTON_PIN])
))
self._logger.info("--------------------------------------------")

# Setting the default state of pin
GPIO.setup(int(self._settings.get(["light_pin"])), GPIO.OUT)
if bool(self._settings.get(["inverted_output"])):
GPIO.output(int(self._settings.get(["light_pin"])), GPIO.HIGH)
else:
GPIO.output(int(self._settings.get(["light_pin"])), GPIO.LOW)
# Setting the default state of the light pin
self.setup_pin()
if not bool(self._settings.get([IS_LED_STRIP])):
if bool(self._settings.get([INVERTED_OUTPUT])):
GPIO.output(int(self._settings.get([LIGHT_PIN])), GPIO.HIGH)
else:
GPIO.output(int(self._settings.get([LIGHT_PIN])), GPIO.LOW)

self._plugin_manager.send_plugin_message(self._identifier, dict(isLightOn=self.light_state))

#Because light is set to ff on startup we don't need to retrieve the current state
"""
r = self.light_state = GPIO.input(int(self._settings.get(["light_pin"])))
if r==1:
self.light_state = False
else:
self.light_state = True
# Enabling watch to the default button pin
self.enable_watch_button(self._settings.get([BUTTON_PIN]))

self._logger.info("After Startup. Light state: {}".format(
self.light_state
))
"""
def on_api_get(self, request):
self._logger.info("Got request. Light state: {}".format(
self.light_state
))

if self._settings.get([BUTTON_PIN]) != self._settings.get([PREVIOUS_BUTTON_PIN]):
# stop watching on the previous pin
GPIO.remove_event_detect(
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also here we need to wrap with int()

self._settings.get([PREVIOUS_BUTTON_PIN]))
# enable watching on the new pin
self.enable_watch_button(self._settings.get([BUTTON_PIN]))
self._settings.set([PREVIOUS_BUTTON_PIN],
self._settings.get([BUTTON_PIN]))

self.setup_pin()

self.change_light_state(None)

return flask.jsonify(status="ok")

def on_event(self, event, payload):
self._plugin_manager.send_plugin_message(self._identifier, dict(isLightOn=self.light_state))
if event == Events.CLIENT_OPENED:
return

def on_api_get(self, request):
# Sets the GPIO every time, if user changed it in the settings.
GPIO.setup(int(self._settings.get(["light_pin"])), GPIO.OUT)

def enable_watch_button(self, button):
self._logger.info("watching events on pin : {}".format(
button
))
GPIO.setup(button, GPIO.IN, pull_up_down=GPIO.PUD_UP)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@emouty you need to wrap button in this line and below (or in L96 and 108) with int()

otherwise an exception is thrown

octoprint.server.api - ERROR - Error calling SimpleApiPlugin octolight
Traceback (most recent call last):
  File "/home/pi/oprint/lib/python3.7/site-packages/octoprint/server/api/__init__.py", line 94, in pluginData
    response = api_plugin.on_api_get(request)
  File "/home/pi/oprint/lib/python3.7/site-packages/octoprint/util/__init__.py", line 1737, in wrapper
    return f(*args, **kwargs)
  File "/home/pi/oprint/lib/python3.7/site-packages/octoprint_octolight/__init__.py", line 108, in on_api_get
    self.enable_watch_button(self._settings.get([BUTTON_PIN]))
  File "/home/pi/oprint/lib/python3.7/site-packages/octoprint_octolight/__init__.py", line 128, in enable_watch_button
    GPIO.setup(button, GPIO.IN, pull_up_down=GPIO.PUD_UP)
ValueError: Channel must be an integer or list/tuple of integers

GPIO.add_event_detect(button, GPIO.FALLING,
callback=self.change_light_state, bouncetime=200)

def change_light_state(self, channel):
self.light_state = not self.light_state

# Sets the light state depending on the inverted output setting (XOR)
if self.light_state ^ self._settings.get(["inverted_output"]):
GPIO.output(int(self._settings.get(["light_pin"])), GPIO.HIGH)
if bool(self._settings.get([IS_LED_STRIP])):
if self.light_state:
self._logger.debug(
"Led strip mode is enabled, will turn on the led strip")
self.colorWipe(self.pixels, Color(255, 255, 255))
else:
self._logger.debug(
"Led strip mode is enabled, will turn off the led strip")
self.colorWipe(self.pixels, Color(0, 0, 0))
elif self.light_state ^ self._settings.get([INVERTED_OUTPUT]):
GPIO.output(int(self._settings.get([LIGHT_PIN])), GPIO.HIGH)
else:
GPIO.output(int(self._settings.get(["light_pin"])), GPIO.LOW)
GPIO.output(int(self._settings.get([LIGHT_PIN])), GPIO.LOW)

self._logger.info("Got request. Light state: {}".format(
self._logger.debug("Light state switched to : {}".format(
self.light_state
))

# message the ui to change the button color
self._plugin_manager.send_plugin_message(self._identifier, dict(isLightOn=self.light_state))


return flask.jsonify(status="ok")

def on_event(self, event, payload):
if event == Events.CLIENT_OPENED:
self._plugin_manager.send_plugin_message(self._identifier, dict(isLightOn=self.light_state))
return

def get_update_information(self):
return dict(
octolight=dict(
Expand All @@ -111,16 +163,38 @@ def get_update_information(self):
type="github_release",
current=self._plugin_version,

user="gigibu5",
user="emouty",
repo="OctoLight",
pip="https://github.com/gigibu5/OctoLight/archive/{target}.zip"
pip="https://github.com/emouty/OctoLight/archive/{target}.zip"
)
)

def on_shutdown(self):
# release GPIO pin on shutdown
GPIO.cleanup()

# Define functions which animate LEDs in various ways.
def colorWipe(self, strip, color):
"""Wipe color across display a pixel at a time."""
for i in range(strip.numPixels()):
strip.setPixelColor(i, color)
strip.show()

def setup_pin(self):
if bool(self._settings.get([IS_LED_STRIP])):

# enabling led strip on selected pin
self.pixels = PixelStrip(int(self._settings.get([NB_LEDS])), int(self._settings.get(
[LIGHT_PIN])), LED_FREQ_HZ, LED_DMA, bool(self._settings.get([INVERTED_OUTPUT])), LED_BRIGHTNESS, LED_CHANNEL, LED_STRIP)
self.pixels.begin()
else:
# Sets the GPIO every time, if user changed it in the settings.
GPIO.setup(int(self._settings.get([LIGHT_PIN])), GPIO.OUT)

__plugin_pythoncompat__ = ">=2.7,<4"
__plugin_implementation__ = OctoLightPlugin()

__plugin_hooks__ = {
"octoprint.plugin.softwareupdate.check_config":
__plugin_implementation__.get_update_information
}
__plugin_implementation__.get_update_information
}
21 changes: 21 additions & 0 deletions octoprint_octolight/templates/octolight_settings.jinja2
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<form class="form-horizontal">
<h3>OctoLight settings</h3>
<div class="control-group">
<p>If LED strip has been selected ensure that the pin selected is either a PWM, PCM or SPI pin</p>
<label class="control-label">{{ _('Light PIN') }}</label>
<div class="controls">
<input id="light_pin-input" type="number" min="1" max="40" class="input-small" data-bind="value: settings.plugins.octolight.light_pin">
Expand All @@ -12,5 +13,25 @@
{{ _('Inverted output') }}
</label>
</div>
<div class="controls">
<label class="checkbox">
<input type="checkbox" data-bind="checked: settings.plugins.octolight.is_button_enabled">
{{ _('Enable physical button') }}
</label>
</div>
<label class="control-label">{{ _('Button PIN') }}</label>
<div class="controls">
<input type="number" min="1" max="40" class="input-small" data-bind="value: settings.plugins.octolight.button_pin">
</div>
<div class="controls">
<label class="checkbox">
<input type="checkbox" data-bind="checked: settings.plugins.octolight.is_led_strip">
{{ _('Led strip') }}
</label>
</div>
<label class="control-label">{{ _('Number of leds') }}</label>
<div class="controls">
<input type="number" min="1" class="input-small" data-bind="value: settings.plugins.octolight.nb_leds">
</div>
</div>
</form>
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
plugin_name = "OctoLight"

# The plugin's version. Can be overwritten within OctoPrint's internal data via __plugin_version__ in the plugin module
plugin_version = "0.1.2"
plugin_version = "0.2.0"

# The plugin's description. Can be overwritten within OctoPrint's internal data via __plugin_description__ in the plugin
# module
Expand All @@ -33,7 +33,7 @@
plugin_license = "AGPLv3"

# Any additional requirements besides OctoPrint should be listed here
plugin_requires = ["RPi.GPIO"]
plugin_requires = ["RPi.GPIO","rpi_ws281x"]

### --------------------------------------------------------------------------------------------------------------------
### More advanced options that you usually shouldn't have to touch follow after this point
Expand Down