mirror of
https://github.com/akuker/RASCSI.git
synced 2024-12-13 17:30:46 +00:00
Initial version of the Control Board UI (#687)
Initial version of the Control Board UI (#687)
This commit is contained in:
parent
f5f5c002aa
commit
cd0da558c3
221
easyinstall.sh
221
easyinstall.sh
@ -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\""
|
||||
|
@ -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
|
||||
|
56
python/ctrlboard/README.md
Normal file
56
python/ctrlboard/README.md
Normal 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)
|
0
python/ctrlboard/__init__.py
Normal file
0
python/ctrlboard/__init__.py
Normal file
11
python/ctrlboard/requirements.txt
Normal file
11
python/ctrlboard/requirements.txt
Normal 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
|
97
python/ctrlboard/resources/DejaVu Fonts License.txt
Normal file
97
python/ctrlboard/resources/DejaVu Fonts License.txt
Normal 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.
|
BIN
python/ctrlboard/resources/DejaVuSansMono-Bold.ttf
Normal file
BIN
python/ctrlboard/resources/DejaVuSansMono-Bold.ttf
Normal file
Binary file not shown.
BIN
python/ctrlboard/resources/splash_start_32.bmp
Normal file
BIN
python/ctrlboard/resources/splash_start_32.bmp
Normal file
Binary file not shown.
After Width: | Height: | Size: 574 B |
BIN
python/ctrlboard/resources/splash_start_64.bmp
Normal file
BIN
python/ctrlboard/resources/splash_start_64.bmp
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
14
python/ctrlboard/service-infra/rascsi-ctrlboard.service
Normal file
14
python/ctrlboard/service-infra/rascsi-ctrlboard.service
Normal 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
|
0
python/ctrlboard/src/__init__.py
Normal file
0
python/ctrlboard/src/__init__.py
Normal file
35
python/ctrlboard/src/config.py
Normal file
35
python/ctrlboard/src/config.py
Normal 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
|
@ -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 + "!")
|
@ -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)
|
@ -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
|
@ -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"
|
0
python/ctrlboard/src/ctrlboard_hw/__init__.py
Normal file
0
python/ctrlboard/src/ctrlboard_hw/__init__.py
Normal file
227
python/ctrlboard/src/ctrlboard_hw/ctrlboard_hw.py
Normal file
227
python/ctrlboard/src/ctrlboard_hw/ctrlboard_hw.py
Normal 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()
|
24
python/ctrlboard/src/ctrlboard_hw/ctrlboard_hw_constants.py
Normal file
24
python/ctrlboard/src/ctrlboard_hw/ctrlboard_hw_constants.py
Normal 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"
|
66
python/ctrlboard/src/ctrlboard_hw/encoder.py
Normal file
66
python/ctrlboard/src/ctrlboard_hw/encoder.py
Normal 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
|
17
python/ctrlboard/src/ctrlboard_hw/hardware_button.py
Normal file
17
python/ctrlboard/src/ctrlboard_hw/hardware_button.py
Normal 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)
|
64
python/ctrlboard/src/ctrlboard_hw/pca9554multiplexer.py
Normal file
64
python/ctrlboard/src/ctrlboard_hw/pca9554multiplexer.py
Normal 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
|
185
python/ctrlboard/src/ctrlboard_menu_builder.py
Normal file
185
python/ctrlboard/src/ctrlboard_menu_builder.py
Normal 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:
|
||||