Initial version of the Control Board UI (#687)

Initial version of the Control Board UI (#687)
This commit is contained in:
Benjamin Zeiss 2022-02-25 21:03:36 +01:00 committed by GitHub
parent f5f5c002aa
commit cd0da558c3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 2518 additions and 5 deletions

View File

@ -52,6 +52,7 @@ VIRTUAL_DRIVER_PATH="$HOME/images"
CFG_PATH="$HOME/.config/rascsi"
WEB_INSTALL_PATH="$BASE/python/web"
OLED_INSTALL_PATH="$BASE/python/oled"
CTRLBOARD_INSTALL_PATH="$BASE/python/ctrlboard"
PYTHON_COMMON_PATH="$BASE/python/common"
SYSTEMD_PATH="/etc/systemd/system"
HFS_FORMAT=/usr/bin/hformat
@ -314,6 +315,17 @@ function stopRaScsiScreen() {
fi
}
# Stops the rascsi-ctrlboard service if it is running
function stopRaScsiCtrlBoard() {
if [[ -f "$SYSTEMD_PATH/rascsi-ctrlboard.service" ]]; then
SERVICE_RASCSI_CTRLBOARD_RUNNING=0
sudo systemctl is-active --quiet rascsi-ctrlboard.service >/dev/null 2>&1 || SERVICE_RASCSI_CTRLBOARD_RUNNING=$?
if [[ SERVICE_RASCSI_CTRLBOARD_RUNNING -eq 0 ]]; then
sudo systemctl stop rascsi-ctrlboard.service
fi
fi
}
# disables and removes the old monitor_rascsi service
function disableOldRaScsiMonitorService() {
if [ -f "$SYSTEMD_PATH/monitor_rascsi.service" ]; then
@ -332,6 +344,40 @@ function disableOldRaScsiMonitorService() {
fi
}
# disables the rascsi-oled service
function disableRaScsiOledService() {
if [ -f "$SYSTEMD_PATH/rascsi-oled.service" ]; then
SERVICE_RASCSI_OLED_RUNNING=0
sudo systemctl is-active --quiet rascsi-oled.service >/dev/null 2>&1 || SERVICE_RASCSI_OLED_RUNNING=$?
if [[ $SERVICE_RASCSI_OLED_RUNNING -eq 0 ]]; then
sudo systemctl stop rascsi-oled.service
fi
SERVICE_RASCSI_OLED_ENABLED=0
sudo systemctl is-enabled --quiet rascsi-oled.service >/dev/null 2>&1 || SERVICE_RASCSI_OLED_ENABLED=$?
if [[ $SERVICE_RASCSI_OLED_ENABLED -eq 0 ]]; then
sudo systemctl disable rascsi-oled.service
fi
fi
}
# disables the rascsi-ctrlboard service
function disableRaScsiCtrlBoardService() {
if [ -f "$SYSTEMD_PATH/rascsi-ctrlboard.service" ]; then
SERVICE_RASCSI_CTRLBOARD_RUNNING=0
sudo systemctl is-active --quiet rascsi-ctrlboard.service >/dev/null 2>&1 || SERVICE_RASCSI_CTRLBOARD_RUNNING=$?
if [[ $SERVICE_RASCSI_CTRLBOARD_RUNNING -eq 0 ]]; then
sudo systemctl stop rascsi-ctrlboard.service
fi
SERVICE_RASCSI_CTRLBOARD_ENABLED=0
sudo systemctl is-enabled --quiet rascsi-ctrlboard.service >/dev/null 2>&1 || SERVICE_RASCSI_CTRLBOARD_ENABLED=$?
if [[ $SERVICE_RASCSI_CTRLBOARD_ENABLED -eq 0 ]]; then
sudo systemctl disable rascsi-ctrlboard.service
fi
fi
}
# Stops the macproxy service if it is running
function stopMacproxy() {
if [ -f "$SYSTEMD_PATH/macproxy.service" ]; then
@ -339,7 +385,7 @@ function stopMacproxy() {
fi
}
# Starts the rascsi-oled service if installed
# Checks whether the rascsi-oled service is installed
function isRaScsiScreenInstalled() {
SERVICE_RASCSI_OLED_ENABLED=0
if [[ -f "$SYSTEMD_PATH/rascsi-oled.service" ]]; then
@ -353,7 +399,19 @@ function isRaScsiScreenInstalled() {
echo $SERVICE_RASCSI_OLED_ENABLED
}
# Starts the rascsi-oled service if installed
# Checks whether the rascsi-ctrlboard service is installed
function isRaScsiCtrlBoardInstalled() {
SERVICE_RASCSI_CTRLBOARD_ENABLED=0
if [[ -f "$SYSTEMD_PATH/rascsi-ctrlboard.service" ]]; then
sudo systemctl is-enabled --quiet rascsi-ctrlboard.service >/dev/null 2>&1 || SERVICE_RASCSI_CTRLBOARD_ENABLED=$?
else
SERVICE_RASCSI_CTRLBOARD_ENABLED=1
fi
echo $SERVICE_RASCSI_CTRLBOARD_ENABLED
}
# Checks whether the rascsi-oled service is running
function isRaScsiScreenRunning() {
SERVICE_RASCSI_OLED_RUNNING=0
if [[ -f "$SYSTEMD_PATH/rascsi-oled.service" ]]; then
@ -367,6 +425,19 @@ function isRaScsiScreenRunning() {
echo $SERVICE_RASCSI_OLED_RUNNING
}
# Checks whether the rascsi-oled service is running
function isRaScsiCtrlBoardRunning() {
SERVICE_RASCSI_CTRLBOARD_RUNNING=0
if [[ -f "$SYSTEMD_PATH/rascsi-ctrlboard.service" ]]; then
sudo systemctl is-active --quiet rascsi-ctrlboard.service >/dev/null 2>&1 || SERVICE_RASCSI_CTRLBOARD_RUNNING=$?
else
SERVICE_RASCSI_CTRLBOARD_RUNNING=1
fi
echo $SERVICE_RASCSI_CTRLBOARD_RUNNING
}
# Starts the rascsi-oled service if installed
function startRaScsiScreen() {
if [[ $(isRaScsiScreenInstalled) -eq 0 ]] && [[ $(isRaScsiScreenRunning) -ne 1 ]]; then
@ -375,6 +446,14 @@ function startRaScsiScreen() {
fi
}
# Starts the rascsi-ctrlboard service if installed
function startRaScsiCtrlBoard() {
if [[ $(isRaScsiCtrlBoardInstalled) -eq 0 ]] && [[ $(isRaScsiCtrlBoardRunning) -ne 1 ]]; then
sudo systemctl start rascsi-ctrlboard.service
showRaScsiCtrlBoardStatus
fi
}
# Starts the macproxy service if installed
function startMacproxy() {
if [ -f "$SYSTEMD_PATH/macproxy.service" ]; then
@ -398,6 +477,11 @@ function showRaScsiScreenStatus() {
systemctl status rascsi-oled | tee
}
# Shows status for the rascsi-ctrlboard service
function showRaScsiCtrlBoardStatus() {
systemctl status rascsi-ctrlboard | tee
}
# Shows status for the macproxy service
function showMacproxyStatus() {
systemctl status macproxy | tee
@ -828,6 +912,7 @@ function installRaScsiScreen() {
fi
stopRaScsiScreen
disableRaScsiCtrlBoardService
updateRaScsiGit
sudo apt-get update && sudo apt-get install libjpeg-dev libpng-dev libopenjp2-7-dev i2c-tools raspi-config -y </dev/null
@ -875,6 +960,111 @@ function installRaScsiScreen() {
sudo systemctl start rascsi-oled
}
# updates configuration files and installs packages needed for the CtrlBoard script
function installRaScsiCtrlBoard() {
echo "IMPORTANT: This configuration requires a RaSCSI Control Board connected to your RaSCSI board."
echo "See wiki for more information: https://github.com/akuker/RASCSI/wiki/RaSCSI-Control-Board"
echo ""
echo "Choose screen rotation:"
echo " 1) 0 degrees"
echo " 2) 180 degrees (default)"
read REPLY
if [ "$REPLY" == "1" ]; then
echo "Proceeding with 0 degrees rotation."
ROTATION="0"
else
echo "Proceeding with 180 degrees rotation."
ROTATION="180"
fi
if [ -z "$TOKEN" ]; then
echo ""
echo "Did you protect your RaSCSI installation with a token password? [y/N]"
read -r REPLY
if [ "$REPLY" == "y" ] || [ "$REPLY" == "Y" ]; then
echo -n "Enter the password that you configured with RaSCSI at the time of installation: "
read -r TOKEN
fi
fi
stopRaScsiCtrlBoard
updateRaScsiGit
sudo apt-get update && sudo apt-get install libjpeg-dev libpng-dev libopenjp2-7-dev i2c-tools raspi-config -y </dev/null
# install numpy via apt to avoid compilation
sudo apt-get install python3-numpy -y </dev/null
# enable i2c
if [[ $(grep -c "^dtparam=i2c_arm=on" /boot/config.txt) -ge 1 ]]; then
echo "NOTE: I2C support seems to have been configured already."
REBOOT=0
else
sudo raspi-config nonint do_i2c 0 </dev/null
echo "Modified the Raspberry Pi boot configuration to enable I2C."
echo "A reboot will be required for the change to take effect."
REBOOT=1
fi
# determine target baudrate
PI_MODEL=$(/usr/bin/tr -d '\0' < /proc/device-tree/model)
TARGET_I2C_BAUDRATE=100000
if [[ ${PI_MODEL} =~ "Raspberry Pi 4" ]]; then
echo "Detected: Raspberry Pi 4"
TARGET_I2C_BAUDRATE=1000000
elif [[ ${PI_MODEL} =~ "Raspberry Pi 3" ]] || [[ ${PI_MODEL} =~ "Raspberry Pi Zero 2" ]]; then
echo "Detected: Raspberry Pi 3 or Zero 2"
TARGET_I2C_BAUDRATE=400000
else
echo "No Raspberry Pi 4, Pi 3 or Pi Zero 2 detected. Falling back on low i2c baudrate."
echo "Transition animations will be disabled."
fi
# adjust i2c baudrate according to the raspberry pi model detection
GREP_PARAM="^dtparam=i2c_arm=on,i2c_arm_baudrate=${TARGET_I2C_BAUDRATE}$"
ADJUST_BAUDRATE=$(grep -c "${GREP_PARAM}" /boot/config.txt)
if [[ $ADJUST_BAUDRATE -eq 0 ]]; then
echo "Adjusting I2C baudrate in /boot/config.txt"
sudo sed -i "s/dtparam=i2c_arm=on.*/dtparam=i2c_arm=on,i2c_arm_baudrate=${TARGET_I2C_BAUDRATE}/g" /boot/config.txt
REBOOT=1
else
echo "I2C baudrate already correct in /boot/config.txt"
fi
echo "Installing the rascsi-ctrlboard.service configuration..."
sudo cp -f "$CTRLBOARD_INSTALL_PATH/service-infra/rascsi-ctrlboard.service" "$SYSTEMD_PATH/rascsi-ctrlboard.service"
sudo sed -i /^ExecStart=/d "$SYSTEMD_PATH/rascsi-ctrlboard.service"
if [ ! -z "$TOKEN" ]; then
sudo sed -i "8 i ExecStart=$CTRLBOARD_INSTALL_PATH/start.sh --rotation=$ROTATION --password=$TOKEN" "$SYSTEMD_PATH/rascsi-ctrlboard.service"
sudo chmod 600 "$SYSTEMD_PATH/rascsi-ctrlboard.service"
echo "Granted access to the RaSCSI Control Board UI with the password that you configured for RaSCSI."
else
sudo sed -i "8 i ExecStart=$CTRLBOARD_INSTALL_PATH/start.sh --rotation=$ROTATION" "$SYSTEMD_PATH/rascsi-ctrlboard.service"
fi
sudo systemctl daemon-reload
# ensure that the old monitor_rascsi or rascsi-oled service is disabled and removed before the new one is installed
disableOldRaScsiMonitorService
disableRaScsiOledService
sudo systemctl daemon-reload
sudo systemctl enable rascsi-ctrlboard
if [ $REBOOT -eq 1 ]; then
echo ""
echo "The rascsi-ctrlboard service will start on the next Pi boot."
echo "Press Enter to reboot or CTRL-C to exit"
read
echo "Rebooting..."
sleep 3
sudo reboot
fi
sudo systemctl start rascsi-ctrlboard
}
# Prints a notification if the rascsi.service file was backed up
function notifyBackup {
if "$SYSTEMD_BACKUP"; then
@ -932,10 +1122,14 @@ function runChoice() {
if [[ $(isRaScsiScreenInstalled) -eq 0 ]]; then
echo "Detected rascsi oled service; will run the installation steps for the OLED monitor."
installRaScsiScreen
elif [[ $(isRaScsiCtrlBoardInstalled) -eq 0 ]]; then
echo "Detected rascsi control board service; will run the installation steps for the control board ui."
installRaScsiCtrlBoard
fi
installRaScsiWebInterface
installWebInterfaceService
showRaScsiScreenStatus
showRaScsiCtrlBoardStatus
showRaScsiStatus
showRaScsiWebStatus
notifyBackup
@ -966,8 +1160,12 @@ function runChoice() {
if [[ $(isRaScsiScreenInstalled) -eq 0 ]]; then
echo "Detected rascsi oled service; will run the installation steps for the OLED monitor."
installRaScsiScreen
elif [[ $(isRaScsiCtrlBoardInstalled) -eq 0 ]]; then
echo "Detected rascsi control board service; will run the installation steps for the control board ui."
installRaScsiCtrlBoard
fi
showRaScsiScreenStatus
showRaScsiCtrlBoardStatus
showRaScsiStatus
notifyBackup
echo "Installing / Updating RaSCSI Service (${CONNECT_TYPE:-FULLSPEC}) - Complete!"
@ -1083,6 +1281,19 @@ function runChoice() {
echo "Enabling authentication for the RaSCSI Web Interface - Complete!"
echo "Use the credentials for user '$USER' to log in to the Web Interface."
;;
13)
echo "Installing / Updating RaSCSI Control Board UI"
echo "This script will make the following changes to your system:"
echo "- Install additional packages with apt-get"
echo "- Add and modify systemd services"
echo "- Stop and disable the RaSCSI OLED service if it is running"
echo "- Modify the Raspberry Pi boot configuration (may require a reboot)"
sudoCheck
preparePythonCommon
installRaScsiCtrlBoard
showRaScsiCtrlBoardStatus
echo "Installing / Updating RaSCSI Control Board UI - Complete!"
;;
-h|--help|h|help)
showMenu
;;
@ -1096,7 +1307,7 @@ function runChoice() {
function readChoice() {
choice=-1
until [ $choice -ge "0" ] && [ $choice -le "12" ]; do
until [ $choice -ge "0" ] && [ $choice -le "13" ]; do
echo -n "Enter your choice (0-12) or CTRL-C to exit: "
read -r choice
done
@ -1126,6 +1337,8 @@ function showMenu() {
echo " 10) compile and install RaSCSI stand-alone"
echo " 11) configure the RaSCSI Web Interface stand-alone"
echo " 12) enable authentication for the RaSCSI Web Interface"
echo "EXPERIMENTAL FEATURES"
echo " 13) install or update RaSCSI Control Board UI (requires hardware)"
}
# parse arguments passed to the script
@ -1148,7 +1361,7 @@ while [ "$1" != "" ]; do
;;
esac
case $VALUE in
FULLSPEC | STANDARD | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12)
FULLSPEC | STANDARD | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13)
;;
*)
echo "ERROR: unknown option \"$VALUE\""

View File

@ -15,7 +15,7 @@ ignore-patterns=
# Python code to execute, usually for sys.path manipulation such as
# pygtk.require().
init-hook=import sys; sys.path.append("common/src"); sys.path.append("web/src"); sys.path.append("oled/src");
init-hook=import sys; sys.path.append("common/src"); sys.path.append("web/src"); sys.path.append("oled/src"); sys.path.append("ctrlboard/src");
# venv hook for pylint
# Requires pylint-venv package:
# $ pip install pylint-venv

View File

@ -0,0 +1,56 @@
# RaSCSI Control Board UI
## Run as standalone script for development / troubleshooting
```bash
# Make a virtual env named venv
$ python3 -m venv venv
# Use that virtual env in this shell
$ source venv/bin/activate
# Install requirements
$ pip3 install -r requirements.txt
$ python3 src/main.py
```
### Parameters
The script parameters can be shown with
```
python src/main.py --help
```
or
```
start.sh --help
```
Example:
```
$ python3 src/main.py --rotation=0 --transitions=0
```
## Run the start.sh script standalone
The start.sh script can also be run standalone, and will handle the venv creation/updating for you. It takes the same command line parameters in the following format:
```
$ ./start.sh --rotation=0 --transitions=0
```
### I2C baudrate and transitions
The available bandwidth for the display through I2C is limited. The I2C baudrate is automatically adjusted in
easy_install.sh and start.sh to maximize the possible baudrate, however, depending on the Raspberry Pi model in use, we found that for some
models enabling the transitions does not work very well. As a result, we have implemented a model detection for
the raspberry pi model and enable transitions only for the following pi models:
- Raspberry Pi 4 models
- Raspberry Pi 3 models
- Raspberry Pi Zero 2 models
The model detection can be overriden by adding a --transitions parameter to start.sh.
## Credits
### DejaVuSansMono-Bold.ttf
* Source: https://dejavu-fonts.github.io
* Distributed under DejaVu Fonts Lience (see DejaVu Fonts License.txt for full text)
### splash_start_\*.bmp, splash_stop_\*.bmp
* Drawn by Daniel Markstedt
* Distributed under BSD 3-Clause by permission from author (see LICENSE for full text)

View File

View File

@ -0,0 +1,11 @@
#adafruit-circuitpython-busdevice==5.1.1
#adafruit-circuitpython-framebuf==1.4.8
#adafruit-circuitpython-ssd1306==2.12.3
luma-oled==3.8.1
Pillow==9.0.0
RPi.GPIO==0.7.0
protobuf==3.19.3
unidecode==1.3.2
smbus==1.1.post2
# installed via apt to avoid lengthy compilation
#numpy==1.21.5

View File

@ -0,0 +1,97 @@
Fonts are (c) Bitstream (see below). DejaVu changes are in public domain.
Glyphs imported from Arev fonts are (c) Tavmjong Bah (see below)
Bitstream Vera Fonts Copyright
------------------------------
Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera is
a trademark of Bitstream, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of the fonts accompanying this license ("Fonts") and associated
documentation files (the "Font Software"), to reproduce and distribute the
Font Software, including without limitation the rights to use, copy, merge,
publish, distribute, and/or sell copies of the Font Software, and to permit
persons to whom the Font Software is furnished to do so, subject to the
following conditions:
The above copyright and trademark notices and this permission notice shall
be included in all copies of one or more of the Font Software typefaces.
The Font Software may be modified, altered, or added to, and in particular
the designs of glyphs or characters in the Fonts may be modified and
additional glyphs or characters may be added to the Fonts, only if the fonts
are renamed to names not containing either the words "Bitstream" or the word
"Vera".
This License becomes null and void to the extent applicable to Fonts or Font
Software that has been modified and is distributed under the "Bitstream
Vera" names.
The Font Software may be sold as part of a larger software package but no
copy of one or more of the Font Software typefaces may be sold by itself.
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT,
TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME
FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING
ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE
FONT SOFTWARE.
Except as contained in this notice, the names of Gnome, the Gnome
Foundation, and Bitstream Inc., shall not be used in advertising or
otherwise to promote the sale, use or other dealings in this Font Software
without prior written authorization from the Gnome Foundation or Bitstream
Inc., respectively. For further information, contact: fonts at gnome dot
org.
Arev Fonts Copyright
------------------------------
Copyright (c) 2006 by Tavmjong Bah. All Rights Reserved.
Permission is hereby granted, free of charge, to any person obtaining
a copy of the fonts accompanying this license ("Fonts") and
associated documentation files (the "Font Software"), to reproduce
and distribute the modifications to the Bitstream Vera Font Software,
including without limitation the rights to use, copy, merge, publish,
distribute, and/or sell copies of the Font Software, and to permit
persons to whom the Font Software is furnished to do so, subject to
the following conditions:
The above copyright and trademark notices and this permission notice
shall be included in all copies of one or more of the Font Software
typefaces.
The Font Software may be modified, altered, or added to, and in
particular the designs of glyphs or characters in the Fonts may be
modified and additional glyphs or characters may be added to the
Fonts, only if the fonts are renamed to names not containing either
the words "Tavmjong Bah" or the word "Arev".
This License becomes null and void to the extent applicable to Fonts
or Font Software that has been modified and is distributed under the
"Tavmjong Bah Arev" names.
The Font Software may be sold as part of a larger software package but
no copy of one or more of the Font Software typefaces may be sold by
itself.
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL
TAVMJONG BAH BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.
Except as contained in this notice, the name of Tavmjong Bah shall not
be used in advertising or otherwise to promote the sale, use or other
dealings in this Font Software without prior written authorization
from Tavmjong Bah. For further information, contact: tavmjong @ free
. fr.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 574 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,14 @@
[Unit]
Description=RaSCSI Control Board service
After=network.target rascsi.service
[Service]
Type=simple
Restart=always
RestartSec=2s
ExecStart=/home/pi/RASCSI/python/ctrlboard/start.sh
ExecStop=/bin/pkill --signal 2 -f "python3 src/main.py"
SyslogIdentifier=RASCSICTRLB
[Install]
WantedBy=multi-user.target

View File

View File

@ -0,0 +1,35 @@
"""
Module for central RaSCSI control board configuration parameters
"""
from ctrlboard_hw.ctrlboard_hw_constants import CtrlBoardHardwareConstants
# pylint: disable=too-few-public-methods
class CtrlboardConfig:
"""Class for central RaSCSI control board configuration parameters"""
ROTATION = 0
WIDTH = 128
HEIGHT = 64
LINES = 8
TOKEN = ""
BORDER = 5
TRANSITIONS = 1
DISPLAY_I2C_ADDRESS = CtrlBoardHardwareConstants.DISPLAY_I2C_ADDRESS
PCA9554_I2C_ADDRESS = CtrlBoardHardwareConstants.PCA9554_I2C_ADDRESS
MENU_REFRESH_INTERVAL = 6
LOG_LEVEL = 30 # Warning
RASCSI_HOST = "localhost"
RASCSI_PORT = "6868"
def __str__(self):
result = "rotation: " + str(self.ROTATION) + "\n"
result += "width: " + str(self.WIDTH) + "\n"
result += "height: " + str(self.HEIGHT) + "\n"
result += "lines: " + str(self.LINES) + "\n"
result += "border: " + str(self.BORDER) + "\n"
result += "rascsi host: " + str(self.RASCSI_HOST) + "\n"
result += "rascsi port: " + str(self.RASCSI_PORT) + "\n"
result += "transitions: " + str(self.TRANSITIONS) + "\n"
return result

View File

@ -0,0 +1,275 @@
"""Module for interfacing between the menu controller and the RaSCSI Control Board hardware"""
import logging
from typing import Optional
from ctrlboard_event_handler.rascsi_profile_cycler import RascsiProfileCycler
from ctrlboard_event_handler.rascsi_shutdown_cycler import RascsiShutdownCycler
from ctrlboard_menu_builder import CtrlBoardMenuBuilder
from ctrlboard_hw.ctrlboard_hw_constants import CtrlBoardHardwareConstants
from ctrlboard_hw.hardware_button import HardwareButton
from ctrlboard_hw.encoder import Encoder
from observer import Observer
from rascsi.file_cmds import FileCmds
from rascsi.ractl_cmds import RaCtlCmds
from rascsi.socket_cmds import SocketCmds
from rascsi_menu_controller import RascsiMenuController
# pylint: disable=too-many-instance-attributes
class CtrlBoardMenuUpdateEventHandler(Observer):
"""Class interfacing the menu controller the RaSCSI Control Board hardware."""
def __init__(self, menu_controller: RascsiMenuController, sock_cmd: SocketCmds,
ractl_cmd: RaCtlCmds):
self.message = None
self._menu_controller = menu_controller
self._menu_renderer_config = self._menu_controller.get_menu_renderer().get_config()
self.sock_cmd = sock_cmd
self.ractl_cmd = ractl_cmd
self.context_stack = []
self.rascsi_profile_cycler: Optional[RascsiProfileCycler] = None
self.rascsi_shutdown_cycler: Optional[RascsiShutdownCycler] = None
def update(self, updated_object):
if isinstance(updated_object, HardwareButton):
if updated_object.name == CtrlBoardHardwareConstants.ROTARY_BUTTON:
menu = self._menu_controller.get_active_menu()
info_object = menu.get_current_info_object()
self.route_rotary_button_handler(info_object)
self._menu_controller.get_menu_renderer().render()
else: # button pressed
if updated_object.name == "Bt1":
self.handle_button1()
elif updated_object.name == "Bt2":
self.handle_button2()
if isinstance(updated_object, Encoder):
active_menu = self._menu_controller.get_active_menu()
if updated_object.direction == 1:
if active_menu.item_selection + 1 < len(active_menu.entries):
self._menu_controller.get_active_menu().item_selection += 1
if updated_object.direction == -1:
if active_menu.item_selection - 1 >= 0:
active_menu.item_selection -= 1
else:
active_menu.item_selection = 0
self._menu_controller.get_menu_renderer().render()
def update_events(self):
"""Method handling non-blocking event handling for the cycle buttons."""
if self.rascsi_profile_cycler is not None:
result = self.rascsi_profile_cycler.update()
if result is not None:
self.rascsi_profile_cycler = None
self.context_stack = []
self._menu_controller.segue(result)
if self.rascsi_shutdown_cycler is not None:
self.rascsi_shutdown_cycler.empty_messages = False
result = self.rascsi_shutdown_cycler.update()
if result == "return":
self.rascsi_shutdown_cycler = None
def handle_button1(self):
"""Method for handling the first cycle button (cycle profiles)"""
if self.rascsi_profile_cycler is None:
self.rascsi_profile_cycler = RascsiProfileCycler(self._menu_controller, self.sock_cmd,
self.ractl_cmd, return_entry=True)
else:
self.rascsi_profile_cycler.cycle()
def handle_button2(self):
"""Method for handling the second cycle button (cycle shutdown)"""
if self.rascsi_shutdown_cycler is None:
self.rascsi_shutdown_cycler = RascsiShutdownCycler(self._menu_controller, self.sock_cmd,
self.ractl_cmd)
else:
self.rascsi_shutdown_cycler.cycle()
def route_rotary_button_handler(self, info_object):
"""Method for handling the rotary button press for the menu navigation"""
if info_object is None:
return
context = info_object["context"]
action = info_object["action"]
handler_function_name = "handle_" + context + "_" + action
try:
handler_function = getattr(self, handler_function_name)
if handler_function is not None:
handler_function(info_object)
except AttributeError:
log = logging.getLogger(__name__)
log.error("Handler function [%s] not found or returned an error. Skipping.",
str(handler_function_name))
# noinspection PyUnusedLocal
# pylint: disable=unused-argument
def handle_scsi_id_menu_openactionmenu(self, info_object):
"""Method handles the rotary button press with the scsi list to open the action menu."""
context_object = self._menu_controller.get_active_menu().get_current_info_object()
self.context_stack.append(context_object)
self._menu_controller.segue(CtrlBoardMenuBuilder.ACTION_MENU, context_object=context_object,
transition_attributes=self._menu_renderer_config.
transition_attributes_left)
# noinspection PyUnusedLocal
# pylint: disable=unused-argument
def handle_action_menu_return(self, info_object):
"""Method handles the rotary button press to return from the
action menu to the scsi list."""
self.context_stack.pop()
self._menu_controller.segue(CtrlBoardMenuBuilder.SCSI_ID_MENU,
transition_attributes=self._menu_renderer_config.
transition_attributes_right)
# noinspection PyUnusedLocal
# pylint: disable=unused-argument
def handle_action_menu_slot_attachinsert(self, info_object):
"""Method handles the rotary button press on attach in the action menu."""
context_object = self._menu_controller.get_active_menu().context_object
self.context_stack.append(context_object)
self._menu_controller.segue(CtrlBoardMenuBuilder.IMAGES_MENU, context_object=context_object,
transition_attributes=self._menu_renderer_config.
transition_attributes_left)
# noinspection PyUnusedLocal
def handle_action_menu_slot_detacheject(self, info_object):
"""Method handles the rotary button press on detach in the action menu."""
context_object = self._menu_controller.get_active_menu().context_object
self.detach_eject_scsi_id()
self.context_stack = []
self._menu_controller.segue(CtrlBoardMenuBuilder.SCSI_ID_MENU,
context_object=context_object,
transition_attributes=self._menu_renderer_config.
transition_attributes_right)
# noinspection PyUnusedLocal
def handle_action_menu_slot_info(self, info_object):
"""Method handles the rotary button press on 'Info' in the action menu."""
context_object = self._menu_controller.get_active_menu().context_object
self.context_stack.append(context_object)
self._menu_controller.segue(CtrlBoardMenuBuilder.DEVICEINFO_MENU,
transition_attributes=self._menu_renderer_config.
transition_attributes_left,
context_object=context_object)
# noinspection PyUnusedLocal
def handle_device_info_menu_return(self, info_object):
"""Method handles the rotary button press on 'Return' in the info menu."""
self.context_stack.pop()
context_object = self._menu_controller.get_active_menu().context_object
self._menu_controller.segue(CtrlBoardMenuBuilder.ACTION_MENU,
transition_attributes=self._menu_renderer_config.
transition_attributes_right,
context_object=context_object)
# noinspection PyUnusedLocal
def handle_action_menu_loadprofile(self, info_object):
"""Method handles the rotary button press on 'Load Profile' in the action menu."""
context_object = self._menu_controller.get_active_menu().context_object
self.context_stack.append(context_object)
self._menu_controller.segue(CtrlBoardMenuBuilder.PROFILES_MENU,
transition_attributes=self._menu_renderer_config.
transition_attributes_left)
# noinspection PyUnusedLocal
def handle_profiles_menu_loadprofile(self, info_object):
"""Method handles the rotary button press in the profile selection menu
for selecting a profile to load."""
if info_object is not None and "name" in info_object:
file_cmd = FileCmds(sock_cmd=self.sock_cmd, ractl=self.ractl_cmd)
result = file_cmd.read_config(file_name=info_object["name"])
if result["status"] is True:
self._menu_controller.show_message("Profile loaded!")
else:
self._menu_controller.show_message("Loading failed!")
self.context_stack = []
self._menu_controller.segue(CtrlBoardMenuBuilder.SCSI_ID_MENU,
transition_attributes=self._menu_renderer_config.
transition_attributes_right)
# noinspection PyUnusedLocal
def handle_action_menu_shutdown(self, info_object):
"""Method handles the rotary button press on 'Shutdown' in the action menu."""
self.ractl_cmd.shutdown_pi("system")
self._menu_controller.show_message("Shutting down!", 150)
self._menu_controller.segue(CtrlBoardMenuBuilder.SCSI_ID_MENU,
transition_attributes=self._menu_renderer_config.
transition_attributes_right)
# noinspection PyUnusedLocal
def handle_images_menu_return(self, info_object):
"""Method handles the rotary button press on 'Return' in the image selection menu
(through attach/insert)."""
context_object = self.context_stack.pop()
self._menu_controller.segue(CtrlBoardMenuBuilder.ACTION_MENU,
context_object=context_object,
transition_attributes=self._menu_renderer_config.
transition_attributes_right)
def handle_images_menu_image_attachinsert(self, info_object):
"""Method handles the rotary button press on an image in the image selection menu
(through attach/insert)"""
context_object = self._menu_controller.get_active_menu().context_object
self.attach_insert_scsi_id(info_object)
self.context_stack = []
self._menu_controller.segue(CtrlBoardMenuBuilder.SCSI_ID_MENU,
context_object=context_object,
transition_attributes=self._menu_renderer_config.
transition_attributes_right)
def attach_insert_scsi_id(self, info_object):
"""Helper method to attach/insert an image on a scsi id given through the menu context"""
image_name = info_object["name"]
device_type = info_object["device_type"]
context_object = self._menu_controller.get_active_menu().context_object
scsi_id = context_object["scsi_id"]
params = {"file": image_name}
result = self.ractl_cmd.attach_device(scsi_id=scsi_id,
device_type=device_type,
params=params)
if result["status"] is False:
self._menu_controller.show_message("Attach failed!")
else:
self.show_id_action_message(scsi_id, "attached")
def detach_eject_scsi_id(self):
"""Helper method to detach/eject an image on a scsi id given through the menu context"""
context_object = self._menu_controller.get_active_menu().context_object
scsi_id = context_object["scsi_id"]
device_info = self.ractl_cmd.list_devices(scsi_id)
if not device_info["device_list"]:
return
device_type = device_info["device_list"][0]["device_type"]
image = device_info["device_list"][0]["image"]
if device_type in ("SAHD", "SCHD", "SCBR", "SCDP", "SCLP", "SCHS"):
result = self.ractl_cmd.detach_by_id(scsi_id)
if result["status"] is True:
self.show_id_action_message(scsi_id, "detached")
else:
self._menu_controller.show_message("Detach failed!")
elif device_type in ("SCRM", "SCMO", "SCCD"):
if image:
result = self.ractl_cmd.eject_by_id(scsi_id)
if result["status"] is True:
self.show_id_action_message(scsi_id, "ejected")
else:
self._menu_controller.show_message("Eject failed!")
else:
result = self.ractl_cmd.detach_by_id(scsi_id)
if result["status"] is True:
self.show_id_action_message(scsi_id, "detached")
else:
self._menu_controller.show_message("Detach failed!")
else:
log = logging.getLogger(__name__)
log.info("Device type '%s' currently unsupported for detach/eject!", str(device_type))
def show_id_action_message(self, scsi_id, action: str):
"""Helper method for displaying an action message in the case of an exception."""
self._menu_controller.show_message("ID " + str(scsi_id) + " " + action + "!")

View File

@ -0,0 +1,15 @@
"""Module for test printing events when buttons from the RaSCSI Control Board are pressed"""
import observer
from ctrlboard_hw.hardware_button import HardwareButton
from ctrlboard_hw.encoder import Encoder
# pylint: disable=too-few-public-methods
class CtrlBoardPrintEventHandler(observer.Observer):
"""Class implements a basic event handler that prints button presses from the RaSCSI
Control Board hardware."""
def update(self, updated_object):
if isinstance(updated_object, HardwareButton):
print(updated_object.name + " has been pressed!")
if isinstance(updated_object, Encoder):
print(updated_object.pos)

View File

@ -0,0 +1,24 @@
"""Module providing the profile cycler class for the RaSCSI Control Board UI"""
from ctrlboard_menu_builder import CtrlBoardMenuBuilder
from menu.cycler import Cycler
class RascsiProfileCycler(Cycler):
"""Class implementing the profile cycler for the RaSCSI Control Baord UI"""
def populate_cycle_entries(self):
cycle_entries = self.file_cmd.list_config_files()
return cycle_entries
def perform_selected_entry_action(self, selected_entry):
result = self.file_cmd.read_config(selected_entry)
self._menu_controller.show_timed_mini_message("")
if result["status"] is True:
return CtrlBoardMenuBuilder.SCSI_ID_MENU
self._menu_controller.show_message("Failed!")
return CtrlBoardMenuBuilder.SCSI_ID_MENU
def perform_return_action(self):
return CtrlBoardMenuBuilder.SCSI_ID_MENU

View File

@ -0,0 +1,31 @@
"""Module providing the shutdown cycler for the RaSCSI Control Board UI """
from menu.cycler import Cycler
class RascsiShutdownCycler(Cycler):
"""Class implementing the shutdown cycler for the RaSCSI Control Board UI"""
def __init__(self, menu_controller, sock_cmd, ractl_cmd):
super().__init__(menu_controller, sock_cmd, ractl_cmd, return_entry=True,
empty_messages=False)
self.executed_once = False
def populate_cycle_entries(self):
cycle_entries = ["Shutdown"]
return cycle_entries
def perform_selected_entry_action(self, selected_entry):
if self.executed_once is False:
self.executed_once = True
self._menu_controller.show_timed_message("Shutting down...")
self.ractl_cmd.shutdown_pi("system")
return "shutdown"
return None
def perform_return_action(self):
self._menu_controller.show_timed_mini_message("")
self._menu_controller.show_timed_message("")
return "return"

View File

@ -0,0 +1,227 @@
"""Module providing the interface to the RaSCSI Control Board hardware"""
# noinspection PyUnresolvedReferences
import logging
import RPi.GPIO as GPIO
import numpy
import smbus
from ctrlboard_hw import pca9554multiplexer
from ctrlboard_hw.hardware_button import HardwareButton
from ctrlboard_hw.ctrlboard_hw_constants import CtrlBoardHardwareConstants
from ctrlboard_hw.encoder import Encoder
from ctrlboard_hw.pca9554multiplexer import PCA9554Multiplexer
from observable import Observable
# pylint: disable=too-many-instance-attributes
class CtrlBoardHardware(Observable):
"""Class implements the RaSCSI Control Board hardware and provides an interface to it."""
def __init__(self, display_i2c_address, pca9554_i2c_address):
self.display_i2c_address = display_i2c_address
self.pca9554_i2c_address = pca9554_i2c_address
self.rascsi_controlboard_detected = self.detect_rascsi_controlboard()
log = logging.getLogger(__name__)
log.info("RaSCSI Control Board detected: %s", str(self.rascsi_controlboard_detected))
self.display_detected = self.detect_display()
log.info("Display detected: %s", str(self.display_detected))
if self.rascsi_controlboard_detected is False:
return
self.pos = 0
self.pca_driver = pca9554multiplexer.PCA9554Multiplexer(self.pca9554_i2c_address)
# setup pca9554
self.pca_driver.write_configuration_register_port(CtrlBoardHardwareConstants.
PCA9554_PIN_ENC_A,
PCA9554Multiplexer.PIN_ENABLED_AS_INPUT)
self.pca_driver.write_configuration_register_port(CtrlBoardHardwareConstants.
PCA9554_PIN_ENC_B,
PCA9554Multiplexer.PIN_ENABLED_AS_INPUT)
self.pca_driver.write_configuration_register_port(CtrlBoardHardwareConstants.
PCA9554_PIN_BUTTON_1,
PCA9554Multiplexer.PIN_ENABLED_AS_INPUT)
self.pca_driver.write_configuration_register_port(CtrlBoardHardwareConstants.
PCA9554_PIN_BUTTON_2,
PCA9554Multiplexer.PIN_ENABLED_AS_INPUT)
self.pca_driver.write_configuration_register_port(CtrlBoardHardwareConstants.
PCA9554_PIN_BUTTON_ROTARY,
PCA9554Multiplexer.PIN_ENABLED_AS_INPUT)
self.pca_driver.write_configuration_register_port(CtrlBoardHardwareConstants.
PCA9554_PIN_LED_1,
PCA9554Multiplexer.PIN_ENABLED_AS_OUTPUT)
self.pca_driver.write_configuration_register_port(CtrlBoardHardwareConstants.
PCA9554_PIN_LED_2,
PCA9554Multiplexer.PIN_ENABLED_AS_OUTPUT)
self.input_register_buffer = numpy.uint32(0)
# pylint: disable=no-member
GPIO.setmode(GPIO.BCM)
GPIO.setup(CtrlBoardHardwareConstants.PI_PIN_INTERRUPT, GPIO.IN)
GPIO.add_event_detect(CtrlBoardHardwareConstants.PI_PIN_INTERRUPT, GPIO.FALLING,
callback=self.button_pressed_callback)
# configure button of the rotary encoder
self.rotary_button = HardwareButton(self.pca_driver,
CtrlBoardHardwareConstants.PCA9554_PIN_BUTTON_ROTARY)
self.rotary_button.state = True
self.rotary_button.name = CtrlBoardHardwareConstants.ROTARY_BUTTON
# configure button 1
self.button1 = HardwareButton(self.pca_driver,
CtrlBoardHardwareConstants.PCA9554_PIN_BUTTON_1)
self.button1.state = True
self.button1.name = CtrlBoardHardwareConstants.BUTTON_1
# configure button 2
self.button2 = HardwareButton(self.pca_driver,
CtrlBoardHardwareConstants.PCA9554_PIN_BUTTON_2)
self.button2.state = True
self.button2.name = CtrlBoardHardwareConstants.BUTTON_2
# configure rotary encoder pin a
self.rotary_a = HardwareButton(self.pca_driver,
CtrlBoardHardwareConstants.PCA9554_PIN_ENC_A)
self.rotary_a.state = True
self.rotary_a.directionalTransition = False
self.rotary_a.name = CtrlBoardHardwareConstants.ROTARY_A
# configure rotary encoder pin b
self.rotary_b = HardwareButton(self.pca_driver,
CtrlBoardHardwareConstants.PCA9554_PIN_ENC_B)
self.rotary_b.state = True
self.rotary_b.directionalTransition = False
self.rotary_b.name = CtrlBoardHardwareConstants.ROTARY_B
# configure encoder object
self.rotary = Encoder(self.rotary_a, self.rotary_b)
self.rotary.pos_prev = 0
self.rotary.name = CtrlBoardHardwareConstants.ROTARY
# noinspection PyUnusedLocal
# pylint: disable=unused-argument
def button_pressed_callback(self, channel):
"""Method is called when a button is pressed and reads the corresponding input register."""
input_register = self.pca_driver.read_input_register()
self.input_register_buffer <<= 8
self.input_register_buffer |= input_register
def check_button_press(self, button):
"""Checks whether the button state has changed."""
if button.state_interrupt is True:
return
value = button.state_interrupt
if value != button.state and value is False:
button.state = False
self.notify(button)
button.state = True
button.state_interrupt = True
def check_rotary_encoder(self, rotary):
"""Checks whether the rotary state has changed."""
rotary.update()
if self.rotary.pos_prev != self.rotary.pos:
self.notify(rotary)
self.rotary.pos_prev = self.rotary.pos
# noinspection PyMethodMayBeStatic
@staticmethod
def button_value(input_register_buffer, bit):
"""Method reads the button value from a specific bit in the input register."""
tmp = input_register_buffer
bitmask = 1 << bit
tmp &= bitmask
tmp >>= bit
return tmp
# noinspection PyMethodMayBeStatic
@staticmethod
def button_value_shifted_list(input_register_buffer, bit):
"""Helper method for dealing with multiple buffered input registers"""
input_register_buffer_length = int(len(format(input_register_buffer, 'b'))/8)
shiftval = (input_register_buffer_length-1)*8
tmp = input_register_buffer >> shiftval
bitmask = 1 << bit
tmp &= bitmask
tmp >>= bit
return tmp
def process_events(self):
"""Non-blocking event processor for hardware events (button presses etc.)"""
input_register_buffer_length = int(len(format(self.input_register_buffer, 'b'))/8)
if input_register_buffer_length <= 1:
return
input_register_buffer = self.input_register_buffer
self.input_register_buffer = 0
for i in range(0, input_register_buffer_length):
shiftval = (input_register_buffer_length-1-i)*8
input_register = (input_register_buffer >> shiftval) & 0b11111111
rot_a = self.button_value(input_register, 0)
rot_b = self.button_value(input_register, 1)
button_rotary = self.button_value(input_register, 5)
button_1 = self.button_value(input_register, 2)
button_2 = self.button_value(input_register, 3)
if button_1 == 0:
self.button1.state_interrupt = bool(button_1)
if button_2 == 0:
self.button2.state_interrupt = bool(button_2)
if button_rotary == 0:
self.rotary_button.state_interrupt = bool(button_rotary)
if rot_a == 0:
self.rotary.enc_a.state_interrupt = bool(rot_a)
if rot_b == 0:
self.rotary.enc_b.state_interrupt = bool(rot_b)
self.check_button_press(self.rotary_button)
self.check_button_press(self.button1)
self.check_button_press(self.button2)
self.check_rotary_encoder(self.rotary)
self.rotary.state = 0b11
self.input_register_buffer = 0
@staticmethod
def detect_i2c_devices(_bus):
"""Method finds addresses on the i2c bus"""
detected_i2c_addresses = []
for _address in range(128):
if 2 < _address < 120:
try:
_bus.read_byte(_address)
address = '%02x' % _address
detected_i2c_addresses.append(int(address, base=16))
except IOError: # simply skip unsuccessful i2c probes
pass
return detected_i2c_addresses
def detect_rascsi_controlboard(self):
"""Detects whether the RaSCSI Control Board is attached by checking whether
the expected i2c addresses are detected."""
# pylint: disable=c-extension-no-member
i2c_addresses = self.detect_i2c_devices(smbus.SMBus(1))
return bool((int(self.display_i2c_address) in i2c_addresses and
(int(self.pca9554_i2c_address) in i2c_addresses)))
def detect_display(self):
"""Detects whether an i2c display is connected to the RaSCSI hat."""
# pylint: disable=c-extension-no-member
i2c_addresses = self.detect_i2c_devices(smbus.SMBus(1))
return bool(int(self.display_i2c_address) in i2c_addresses)
@staticmethod
def cleanup():
"""Resets pin and interrupt settings on the pins."""
# pylint: disable=no-member
GPIO.cleanup()

View File

@ -0,0 +1,24 @@
"""Module containing the RaSCSI Control Board hardware constants"""
# pylint: disable=too-few-public-methods
class CtrlBoardHardwareConstants:
"""Class containing the RaSCSI Control Board hardware constants"""
DISPLAY_I2C_ADDRESS = 0x3c
PCA9554_I2C_ADDRESS = 0x3f
PCA9554_PIN_ENC_A = 0
PCA9554_PIN_ENC_B = 1
PCA9554_PIN_BUTTON_1 = 2
PCA9554_PIN_BUTTON_2 = 3
PCA9554_PIN_BUTTON_ROTARY = 5
PCA9554_PIN_LED_1 = 6
PCA9554_PIN_LED_2 = 7
PI_PIN_INTERRUPT = 9 # BCM
BUTTON_1 = "Bt1"
BUTTON_2 = "Bt2"
ROTARY_A = "RotA"
ROTARY_B = "RotB"
ROTARY_BUTTON = "RotBtn"
ROTARY = "Rot"

View File

@ -0,0 +1,66 @@
"""Module containing an implementation for reading the rotary encoder directions through
the i2c multiplexer + interrupt"""
from ctrlboard_hw.hardware_button import HardwareButton
class Encoder:
"""Class implementing a detection mechanism to detect the rotary encoder directions
through the i2c multiplexer + interrupt"""
def __init__(self, enc_a: HardwareButton, enc_b: HardwareButton):
self.enc_a = enc_a
self.enc_b = enc_b
self.pos = 0
self.state = 0b0011
self.direction = 0
def update(self):
"""Updates the internal attributes wrt. to the encoder position and direction."""
self.update2()
def update2(self):
"""Primary method for detecting the direction"""
value_enc_a = self.enc_a.state_interrupt
value_enc_b = self.enc_b.state_interrupt
self.direction = 0
state = self.state & 0b0011
if value_enc_a:
state |= 0b0100
if value_enc_b:
state |= 0b1000
if state == 0b1011:
self.pos += 1
self.direction = 1
if state == 0b0111:
self.pos -= 1
self.direction = -1
self.state = state >> 2
self.enc_a.state_interrupt = True
self.enc_b.state_interrupt = True
def update1(self):
"""Secondary, less well working method to detect the direction"""
if self.enc_a.state_interrupt is True and self.enc_b.state_interrupt is True:
return
if self.enc_a.state_interrupt is False and self.enc_b.state_interrupt is False:
self.enc_a.state_interrupt = True
self.enc_b.state_interrupt = True
return
self.direction = 0
if self.enc_a.state_interrupt is False:
self.pos += 1
self.direction = 1
elif self.enc_a.state_interrupt is True:
self.pos -= 1
self.direction = -1
self.enc_a.state_interrupt = True
self.enc_b.state_interrupt = True

View File

@ -0,0 +1,17 @@
"""Module containing an abstraction for the hardware button through the i2c multiplexer"""
# pylint: disable=too-few-public-methods
class HardwareButton:
"""Class implementing a hardware button interface that uses the i2c multiplexer"""
def __init__(self, pca_driver, pin):
self.pca_driver = pca_driver
self.pin = pin
self.state = True
self.state_interrupt = True
self.name = "n/a"
def read(self):
"""Reads the configured port of the i2c multiplexer"""
return self.pca_driver.read_input_register_port(self.pin)

View File

@ -0,0 +1,64 @@
"""
Module for interfacting with the pca9554 multiplexer
"""
# pylint: disable=c-extension-no-member
import logging
import smbus
class PCA9554Multiplexer:
"""Class interfacing with the pca9554 multiplexer"""
PIN_ENABLED_AS_OUTPUT = 0
PIN_ENABLED_AS_INPUT = 1
def __init__(self, i2c_address):
"""Constructor for the pc9554 multiplexer interface class"""
self.i2c_address = i2c_address
try:
self.i2c_bus = smbus.SMBus(1)
if self.read_input_register() is None:
logging.error("PCA9554 initialization test on specified i2c address %s failed",
self.i2c_address)
self.i2c_bus = None
except IOError:
logging.error("Could not open the i2c bus.")
self.i2c_bus = None
def write_configuration_register_port(self, port_bit, bit_value):
"""Reconfigures the configuration register. Updates the specified
port bit with bit_value. Returns true if successful, false otherwise."""
try:
if (0 <= port_bit <= 8) and (0 <= bit_value <= 1):
configuration_register = self.i2c_bus.read_byte_data(self.i2c_address, 3)
if bit_value:
updated_configuration_register = configuration_register | (1 << port_bit)
else:
updated_configuration_register = configuration_register & (0xFF -
(1 << port_bit))
self.i2c_bus.write_byte_data(self.i2c_address, 3, updated_configuration_register)
return True
return False
except IOError:
return False
def read_input_register(self):
"""Reads the complete 8 bit input port register from pca9554"""
try:
return self.i2c_bus.read_byte_data(self.i2c_address, 0)
except IOError:
return None
def read_input_register_port(self, port_bit):
"""Reads the input port register and returns the logic level of one specific port in the
argument"""
try:
if 0 <= port_bit <= 8:
input_register = self.i2c_bus.read_byte_data(self.i2c_address, 0)
return (input_register >> port_bit) & 1
return None
except IOError:
return None

View File

@ -0,0 +1,185 @@
"""Module for building the control board UI specific menus"""
import logging
from menu.menu import Menu
from menu.menu_builder import MenuBuilder
from rascsi.file_cmds import FileCmds
from rascsi.ractl_cmds import RaCtlCmds
class CtrlBoardMenuBuilder(MenuBuilder):
"""Class fgor building the control board UI specific menus"""
SCSI_ID_MENU = "scsi_id_menu"
ACTION_MENU = "action_menu"
IMAGES_MENU = "images_menu"
PROFILES_MENU = "profiles_menu"
DEVICEINFO_MENU = "device_info_menu"
ACTION_OPENACTIONMENU = "openactionmenu"
ACTION_RETURN = "return"
ACTION_SLOT_ATTACHINSERT = "slot_attachinsert"
ACTION_SLOT_DETACHEJECT = "slot_detacheject"
ACTION_SLOT_INFO = "slot_info"
ACTION_SHUTDOWN = "shutdown"
ACTION_LOADPROFILE = "loadprofile"
ACTION_IMAGE_ATTACHINSERT = "image_attachinsert"
def __init__(self, ractl_cmd: RaCtlCmds):
super().__init__()
self._rascsi_client = ractl_cmd
self.file_cmd = FileCmds(sock_cmd=ractl_cmd.sock_cmd, ractl=ractl_cmd,
token=ractl_cmd.token, locale=ractl_cmd.locale)
def build(self, name: str, context_object=None) -> Menu:
if name == CtrlBoardMenuBuilder.SCSI_ID_MENU:
return self.create_scsi_id_list_menu(context_object)
if name == CtrlBoardMenuBuilder.ACTION_MENU:
return self.create_action_menu(context_object)
if name == CtrlBoardMenuBuilder.IMAGES_MENU:
return self.create_images_menu(context_object)
if name == CtrlBoardMenuBuilder.PROFILES_MENU:
return self.create_profiles_menu(context_object)
if name == CtrlBoardMenuBuilder.DEVICEINFO_MENU:
return self.create_device_info_menu(context_object)
log = logging.getLogger(__name__)
log.warning("Provided menu name [%s] cannot be built!", name)
return self.create_scsi_id_list_menu(context_object)
# pylint: disable=unused-argument
def create_scsi_id_list_menu(self, context_object=None):
"""Method creates the menu displaying the 7 scsi slots"""
devices = self._rascsi_client.list_devices()
reserved_ids = self._rascsi_client.get_reserved_ids()
devices_by_id = {}
for device in devices["device_list"]:
devices_by_id[int(device["id"])] = device
menu = Menu(CtrlBoardMenuBuilder.SCSI_ID_MENU)
if reserved_ids["status"] is False:
menu.add_entry("No scsi ids reserved")
for scsi_id in range(0, 8):
device = None
if devices_by_id.get(scsi_id) is not None: