Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
108 commits
Select commit Hold shift + click to select a range
0f84129
UI fixes for button
easytarget Jul 29, 2025
6d05328
webcam part 1
easytarget Jul 29, 2025
73e50cb
finish webcam, daily log instead of hourly
easytarget Jul 30, 2025
e52add5
set correct process name in syslog
easytarget Jul 31, 2025
ec796be
put gpio number in pin log
easytarget Jul 31, 2025
4470af7
ini file consistent format
easytarget Jul 31, 2025
be65f2c
flush console logs
easytarget Aug 2, 2025
07b8b6d
add seperate config item for backup triggers
easytarget Aug 2, 2025
5659d9a
add web backup trigger and log
easytarget Aug 2, 2025
52aecc5
convert to xml backups, use pipe and commandline gzip, logging stuff too
easytarget Aug 2, 2025
30efdb1
re-fix webcam to just open a new page with the URL
easytarget Aug 4, 2025
b810124
use smbus2 and generic bme280 libraries in place of adafruit ones
easytarget Aug 4, 2025
0ad7324
configurable list of links in place of single webcam link
easytarget Aug 5, 2025
e5ca248
consistent footer for log pages
easytarget Aug 6, 2025
81a1cbf
bus and address config settings
easytarget Aug 6, 2025
b5d57df
rename screen options to display
easytarget Aug 8, 2025
60ad99d
swap to luma display drivers
easytarget Aug 8, 2025
b5cf182
remove invert feature, luma does not support it
easytarget Aug 8, 2025
82afc0c
display final, gpio wip.
easytarget Aug 8, 2025
2aa6dc1
screensaver now applied by animator
easytarget Aug 10, 2025
8b8c926
config for gpiod pin monitor
easytarget Aug 10, 2025
1b1269f
gpiod pinreader, WIP
easytarget Aug 13, 2025
f2707f8
Show active pins as bold underlined
easytarget Aug 14, 2025
8fcdf07
dont underline
easytarget Aug 14, 2025
3a40710
New GPIOD pinreader
easytarget Aug 14, 2025
ba17c68
remove debug
easytarget Aug 14, 2025
666bfd3
:Remove button control feature
easytarget Aug 14, 2025
91abed2
fix defaults for recent changes
easytarget Aug 14, 2025
83f9d37
pin numbering explanation
easytarget Aug 14, 2025
325e71d
service file comments
easytarget Aug 14, 2025
d263f0c
comments
easytarget Aug 26, 2025
9822b63
comments
easytarget Aug 26, 2025
558c65c
logging tweak
easytarget Aug 27, 2025
db95b9e
db init log tweak
easytarget Aug 27, 2025
f0af796
test gpiod lib import
easytarget Aug 27, 2025
e966251
WIP: Broken: new pinreader lib
easytarget Sep 2, 2025
738fb07
remove dataclass stuff
easytarget Sep 2, 2025
b25112c
serve pinlist from config
easytarget Sep 2, 2025
fe388cf
new pin monitor, wip, working
easytarget Sep 2, 2025
ec14a0f
use netlist not net_map
easytarget Sep 2, 2025
c994894
output glitch
easytarget Sep 3, 2025
f969136
pinreader info outputs
easytarget Sep 3, 2025
cfe92f9
gpio pins working
easytarget Sep 3, 2025
d20970f
test for usable chip
Sep 3, 2025
4fb6f4d
pin and ping data into web portal
Sep 3, 2025
51da9c2
need pins structure even when failing
easytarget Sep 3, 2025
3c1ef46
tooltip extra
easytarget Sep 3, 2025
1fa18d3
note ping targets to console during startup
Sep 3, 2025
6b2f569
add pin info http config option and embed in title
easytarget Sep 5, 2025
bb059e0
log backup trigger reason
easytarget Sep 8, 2025
a6091f4
pinreader instance as sub-class
easytarget Sep 8, 2025
4086c2e
thread.setDaemon() depreciated
easytarget Sep 9, 2025
0765d2b
pin control wip
easytarget Sep 9, 2025
1f4a198
popper work
easytarget Sep 9, 2025
7a2ac5e
be more specif on errors we ignore
easytarget Sep 9, 2025
15aeb02
unneeded
easytarget Sep 10, 2025
ac37eb2
give more log by default
easytarget Sep 10, 2025
0298582
do better at detecting i2c failures
easytarget Sep 10, 2025
34e2624
nearly there
easytarget Sep 10, 2025
ff452b7
fixed some fluff
easytarget Sep 11, 2025
efac945
stdio feedback on start
easytarget Sep 11, 2025
38de651
rationalise how i2c devices start
easytarget Sep 11, 2025
e5d4494
fix bad logic
easytarget Sep 11, 2025
ac7b2f1
add portal to pinpopper
easytarget Sep 15, 2025
0c67140
auto pin naming
easytarget Sep 15, 2025
7ff782d
writer instead of popper
easytarget Sep 16, 2025
0562de8
config tweaks
easytarget Sep 16, 2025
053d039
gpio output wip
easytarget Sep 16, 2025
2584564
fix import
easytarget Sep 16, 2025
3ff259b
integrate pin setting into gpiohandler
easytarget Sep 18, 2025
6528c8c
pass full gpio to http handler
easytarget Sep 18, 2025
63d7073
tweak how pins show usage
easytarget Sep 18, 2025
4d16a23
simple list for outpins
easytarget Sep 18, 2025
9c20aac
remove outlist and use pin instead of label as argument
easytarget Sep 18, 2025
abeeaa6
remove pin_info option and strip output pin names
easytarget Sep 19, 2025
6d3a51f
tweak pin list again, add pin pages, wip
easytarget Sep 19, 2025
64f168d
pincontrol wip
easytarget Sep 19, 2025
91291d4
add makeInput function to gpio handler
easytarget Sep 19, 2025
7cb6a71
working gpio control! still wip
easytarget Sep 19, 2025
fa80ac4
relative links, strip unused stuff
easytarget Sep 19, 2025
3558487
pin control page fluff, still wip
easytarget Sep 19, 2025
14ddf29
move html part of pin portal to func
easytarget Sep 21, 2025
4266996
pin portal only show valid choices
easytarget Sep 21, 2025
a490c55
make links clearer
easytarget Sep 22, 2025
b09dcf1
gpiod version test earlier
easytarget Sep 22, 2025
9ec91b7
tweaks
easytarget Sep 22, 2025
08458e9
fluff
easytarget Oct 1, 2025
d2bfba5
i2c pin locker
easytarget Oct 2, 2025
d55486e
stop passing bus to luma display
easytarget Oct 2, 2025
05fd119
simplified pinreader
easytarget Oct 2, 2025
b5e6bfa
remove debug
easytarget Oct 2, 2025
0b0daab
update default config for pin lock
easytarget Oct 2, 2025
2df17b5
button handler, wip
easytarget Oct 3, 2025
221f9eb
wip
easytarget Oct 3, 2025
83c48b0
better identifier
easytarget Oct 3, 2025
73ec416
default __main__ handlers
easytarget Oct 3, 2025
167222f
watch buttons nearly there, wip
easytarget Oct 3, 2025
8ed2716
working pin watcher
easytarget Oct 3, 2025
e4d1d9f
be clearer about saver mode
easytarget Oct 5, 2025
89ad323
Button handler complete
easytarget Oct 5, 2025
fcc570c
make debounce compulsory
easytarget Oct 5, 2025
9ed439c
log formatting
easytarget Oct 5, 2025
1ad1ceb
add CIDR based authorisation to pin controls
easytarget Oct 5, 2025
89a49fe
bugfix
easytarget Oct 6, 2025
00db5f9
better action handling
easytarget Oct 6, 2025
1f8c05f
tweak
easytarget Oct 6, 2025
f0cca87
defaults
easytarget Oct 7, 2025
86cb9c8
bugfix
easytarget Oct 29, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
162 changes: 56 additions & 106 deletions SBCEye.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
SBCEye:
Animate the OLED display attached to my OctoPrint server with bme280 and system data
Show, log and graph the environmental, system and gpio data via a web interface
Give me a on/off button + url to control the bench lights via a GPIO pin

!! DISPLAY, BME280 and GPIO functionality is CURRENTLY Raspberry PI only!
- Needs to be made generic for other architectures
Expand Down Expand Up @@ -41,27 +40,30 @@
import sys
import logging
import random
import schedule
import psutil
from datetime import timedelta
from logging.handlers import RotatingFileHandler
from atexit import register
from signal import signal, SIGTERM, SIGINT, SIGHUP
from multiprocessing import Process, Queue
import schedule
import psutil
from pathlib import Path

# Local classes
from load_config import Settings
from robin import Robin
from httpserver import serve_http
from netreader import Netreader
from pinreader import Pinreader
from bus_drivers import i2c_setup
from gpiohandler import GPIOHandler
from i2c_bus import i2c_setup
from bme_sensor import bme_setup
from oled_display import oled_setup

# Re-nice to reduce blocking of other processes
os.nice(10)

# The setting class will also process the arguments
settings = Settings()
settings = Settings(appname=Path(sys.argv[0]).stem)

# Let the console know we are starting
print("Starting SBCEye")
Expand All @@ -78,7 +80,7 @@
datefmt=settings.short_format,
handlers=[handler])

# Older scheduler versions can log debug to 'INFO' not 'DEBUG', change threshold.
# Older scheduler versions might log debug at wrong level, change threshold.
schedule_logger = logging.getLogger('schedule')
schedule_logger.setLevel(level=logging.WARN)

Expand All @@ -105,26 +107,18 @@
#
# Import, setup and return hardware drivers, or 'None' if setup fails

disp, bme280 = i2c_setup(settings.have_screen, settings.have_sensor)

if disp:
disp.contrast(settings.display_contrast)
disp.invert(settings.display_invert)
disp.fill(0) # Blank asap in case we are showing garbage
disp.show()

if settings.button_out > 0:
try:
from RPi import GPIO
except ImportError as e:
print(e)
print("ERROR: button & pin control requirements not met, features disabled")
settings.button_out = 0
i2c, bus_lock = i2c_setup(settings)

#
# Local Classes, Globals

display_queue = None # will be set during
# We override the dictionary class so that every time an item is
# modified it sends a a message to the display queue.
# This allows the display to run in a seperate process while keeping
# it's local data copy updated in real-time.
# The queue is initially disabled (type: None), and assigned as a
# queue object only if the display is enabled and detected.
display_queue = None
class TheData(dict):
'''Override the dictionary class to also send data to the queue for the display'''
def __setitem__(self, item, value):
Expand All @@ -136,7 +130,7 @@ def __delitem__(self, item):
display_queue.put([item], None)
super().__delitem__(item)

# Use a (custom overridden) dictionary to store current readings
# Use this overridden dictionary to store current readings
data = TheData({})

# Counters used for incremental data need pre-populating
Expand All @@ -152,44 +146,8 @@ def __delitem__(self, item):
#
# Local functions

def button_control(action="toggle"):
'''Set the controlled pin to a specified state'''
if settings.button_out > 0:
ret = f'{settings.button_name} '
pin = settings.button_out
if action.lower() in ['toggle','invert','button']:
GPIO.output(pin, not GPIO.input(pin))
ret += 'Toggled: '
elif action.lower() in [settings.pin_state_names[1].lower(),'on','true']:
GPIO.output(pin,True)
ret += 'Switched: '
elif action.lower() in [settings.pin_state_names[0].lower(),'off','false']:
GPIO.output(pin,False)
ret += 'Switched: '
elif action.lower() in ['random','easter']:
GPIO.output(pin,random.choice([True, False]))
ret += 'Randomly Switched: '
else:
ret += ': '
state = GPIO.input(pin)
ret += settings.pin_state_names[state]
else:
state = False
ret = 'Not supported, no output pin defined'
pins.update_pins()
return (ret, state)

def button_interrupt(*_):
'''give a short delay, then re-read input to provide a minimum hold-down time
and suppress false triggers from other gpio operations'''
time.sleep(settings.button_hold)
if GPIO.input(settings.button_pin):
logging.info('Button pressed')
button_control()

def update_system():
'''Get current environmental and system data, called on a schedule
'''
'''Get current environmental and system data, called on a schedule'''
data['sys-temp'] = psutil.sensors_temperatures()[cpu_thermal_device][0].current
data['sys-load'] = psutil.getloadavg()[0]
data["sys-freq"] = psutil.cpu_freq().current
Expand All @@ -211,12 +169,12 @@ def update_system():
counter["sys-cpu-int"] = int_count

def update_sensors():
'''Get current environmental sensor data
'''
if bme280:
data['env-temp'] = bme280.temperature
data['env-humi'] = bme280.relative_humidity
data['env-pres'] = bme280.pressure
'''Get current environmental sensor data'''
if bme:
bme.update_sensor()
data['env-temp'] = bme.temperature
data['env-humi'] = bme.humidity
data['env-pres'] = bme.pressure
# Failed pressure measurements really foul up the graph, skip
if data['env-pres'] == 0:
data['env-pres'] = 'U'
Expand All @@ -228,22 +186,22 @@ def update_data():
net.update(data)
rrd.update(data)

def hourly():
def daily():
'''Remind everybody we are alive'''
myself = os.path.basename(__file__)
timestamp = time.strftime(settings.long_format)
uptime = timedelta(seconds=int(time.time() - psutil.boot_time()))
logging.info(f'{settings.name} :: up {uptime}')
print(f'{myself} :: {timestamp} :: {settings.name} :: up {uptime}')
logging.info(f'{settings.name} :: system uptime {uptime}')
print(f'{myself} :: {timestamp} :: {settings.name} :: system uptime {uptime}',flush=True)

def handle_signal(sig, *_):
'''Handle common signals'''
if DISPLAY:
# clean up the screen process
# clean up the display process
DISPLAY.join()
if sig == SIGHUP:
handle_restart()
elif sig == SIGINT and settings.debug:
elif sig == SIGINT and settings.debug_sigint:
handle_restart()
else:
# calling sys.exit() will invoke handle_exit()
Expand All @@ -252,60 +210,52 @@ def handle_signal(sig, *_):
def handle_restart():
'''In-Place safe restart (re-reads config)'''
logging.info('Safe Restarting')
print('Restart\n')
print('Restart\n',flush=True)
rrd.write_updates()
if bus_lock:
bus_lock.release()
os.execv(sys.executable, ['python'] + sys.argv)

def handle_exit():
'''Ensure we write ipending data to the RRD database as we exit'''
rrd.write_updates()
if bus_lock:
bus_lock.release()
logging.info('Exiting')
print('Graceful Exit\n')
print('Graceful Exit\n',flush=True)


# The fun starts here:
if __name__ == '__main__':

# Log sensor status
if bme280:
# Environmental sensor
bme = bme_setup(i2c, settings) if i2c else None
if bme:
logging.info('Environmental sensor configured and enabled')
elif settings.have_sensor:
logging.warning('Environmental data configured but no sensor detected: '\
logging.warning('Environmental data configured but no sensor available: '\
'Environment status and logging disabled')

# Set button interrupt and output if we have a button and a pin to control
if settings.button_out > 0:
GPIO.setmode(GPIO.BCM) # Use BCM GPIO numbering
GPIO.setup(settings.button_out, GPIO.OUT)
logging.info(f'Controllable pin ({settings.button_name}) enabled')
if settings.button_pin > 0:
GPIO.setup(settings.button_pin, GPIO.IN)
# Set up the button pin interrupt
GPIO.add_event_detect(settings.button_pin,
GPIO.RISING, button_interrupt,
bouncetime = int(settings.button_hold * 2000))
logging.info('Button enabled')
if len(settings.button_url) > 0:
logging.info(f'Web Button enabled on: /{settings.button_url}')
print(f'Controllable pin ({settings.button_name}) configured and enabled; '\
f'(pin={settings.button_pin}, url="{settings.button_url})"')

# Display animation setup
disp = oled_setup(settings) if i2c else None
if disp:
# display initialisation does a 'clear()' and 'show()'
disp.contrast(settings.display_contrast)

from animator import animate
display_queue = Queue()
DISPLAY = Process(target=animate, args=(settings, disp, display_queue),
name='sbceye_animator')
DISPLAY.start()
else:
DISPLAY = None
if settings.have_screen:
if settings.have_display:
logging.warning('Display configured but did not initialise properly: '\
'Display features disabled')

print('Performing initial data update', end='')
if settings.net_map:
print(f' may take up to {settings.net_timeout}s if ping targets are down')
if settings.netlist:
print(f' (may take up to {settings.net_timeout}s if ping targets are down)')
else:
print()

Expand All @@ -315,17 +265,17 @@ def handle_exit():
# Populate initial sensor data
update_sensors()

# Network (ping) monitoring
net = Netreader((settings.net_map, settings.net_timeout), data)
# GPIO pin monitoring
gpio = GPIOHandler(settings.pinlist, settings.buttons, data, settings.identifier)

# GPIO Pin monitoring
pins = Pinreader((settings.pin_map, settings.pin_state_names), data)
# Network (ping) monitoring
net = Netreader((settings.netlist, settings.net_timeout), data)

# RRD init now that the data{} structure is populated
rrd = Robin(settings, data)

# Start the web server, it will fork into a seperate thread and run continually
serve_http(settings, rrd, data, (button_control,))
serve_http(settings, rrd, gpio, data)

# Exit handlers (needed for rrd cache write on shutdown)
signal(SIGTERM, handle_signal)
Expand All @@ -334,11 +284,11 @@ def handle_exit():
register(handle_exit)

# Schedule pin monitoring, database updates and logging events
if settings.log_hourly:
schedule.every().hour.at(":00").do(hourly)
schedule.every(settings.data_interval).seconds.do(update_data)
if len(settings.pin_map.keys()) > 0:
schedule.every(settings.pin_interval).seconds.do(pins.update_pins)
if gpio.available:
schedule.every(settings.pin_interval).seconds.do(gpio.update)
if settings.log_daily:
schedule.every().day.at("00:00").do(daily)

# We got this far... time to start the show
logging.info("Init complete, starting schedules and entering service loop")
Expand Down
10 changes: 10 additions & 0 deletions SBCEye.service
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
# (as root) Place this file in /etc/systemd/service/
# run 'systemctl reload-daemon' then
# 'systemctl enable --now SBCEye.service
#
# Note: assumes that SBCEye has it's own # user account ('eye')
# with a homedir at '/home/eye', the SBCEye repo checked out as `/home/eye/SBCEye'
# and that a virtual environment exists at '/home/eye/SBCEye/env'
# with the required libraries installed (see docs)

[Unit]
Description=SBCEye monitoring script
After=multi-user.target
Expand All @@ -7,6 +16,7 @@ Type=simple
Restart=always
WorkingDirectory=/home/eye/SBCEye/
ExecStart=/home/eye/SBCEye/env/bin/python /home/eye/SBCEye/SBCEye.py
SyslogIdentifier=SBCEye
User=eye
Group=eye

Expand Down
Loading