Merge branch 'develop' into bug_335_cache_fix_selectable

This commit is contained in:
RaSCSI User 2022-03-14 01:34:07 +00:00
commit 97f857b3b7
93 changed files with 3593 additions and 1462 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,113 @@ 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 python3-cbor2 -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
set +e
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
set -e
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 +1124,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 +1162,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 +1283,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 +1309,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 +1339,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 +1363,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
@ -140,8 +140,7 @@ disable=print-statement,
xreadlines-attribute,
deprecated-sys-function,
exception-escape,
comprehension-escape,
import-outside-toplevel
comprehension-escape
# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option

View File

@ -4,8 +4,16 @@ Module for methods reading from and writing to the file system
import os
import logging
from pathlib import PurePath
import asyncio
from pathlib import PurePath
from zipfile import ZipFile, is_zipfile
from re import escape, findall
from time import time
from subprocess import run, CalledProcessError
from json import dump, load
import requests
import rascsi_interface_pb2 as proto
from rascsi.common_settings import CFG_DIR, CONFIG_FILE_SUFFIX, PROPERTIES_SUFFIX, RESERVATIONS
from rascsi.ractl_cmds import RaCtlCmds
@ -79,7 +87,6 @@ class FileCmds:
prop_data = self.list_files(PROPERTIES_SUFFIX, CFG_DIR)
prop_files = [PurePath(x[0]).stem for x in prop_data]
from zipfile import ZipFile, is_zipfile
server_info = self.ractl.get_server_info()
files = []
for file in result.image_files_info.image_files:
@ -225,12 +232,11 @@ class FileCmds:
members contains all of the full paths to each of the zip archive members
Returns (dict) with (boolean) status and (list of str) msg
"""
from asyncio import run
server_info = self.ractl.get_server_info()
prop_flag = False
if not member:
unzip_proc = run(self.run_async(
unzip_proc = asyncio.run(self.run_async(
f"unzip -d {server_info['image_dir']} -n -j "
f"{server_info['image_dir']}/{file_name}"
))
@ -241,14 +247,13 @@ class FileCmds:
self.rename_file(f"{server_info['image_dir']}/{name}", f"{CFG_DIR}/{name}")
prop_flag = True
else:
from re import escape
member = escape(member)
unzip_proc = run(self.run_async(
unzip_proc = asyncio.run(self.run_async(
f"unzip -d {server_info['image_dir']} -n -j "
f"{server_info['image_dir']}/{file_name} {member}"
))
# Attempt to unzip a properties file in the same archive dir
unzip_prop = run(self.run_async(
unzip_prop = asyncio.run(self.run_async(
f"unzip -d {CFG_DIR} -n -j "
f"{server_info['image_dir']}/{file_name} {member}.{PROPERTIES_SUFFIX}"
))
@ -258,7 +263,6 @@ class FileCmds:
logging.warning("Unzipping failed: %s", unzip_proc["stderr"])
return {"status": False, "msg": unzip_proc["stderr"]}
from re import findall
unzipped = findall(
"(?:inflating|extracting):(.+)\n",
unzip_proc["stdout"]
@ -270,8 +274,6 @@ class FileCmds:
Takes (str) url and one or more (str) *iso_args
Returns (dict) with (bool) status and (str) msg
"""
from time import time
from subprocess import run, CalledProcessError
server_info = self.ractl.get_server_info()
@ -287,7 +289,6 @@ class FileCmds:
if not req_proc["status"]:
return {"status": False, "msg": req_proc["msg"]}
from zipfile import is_zipfile, ZipFile
if is_zipfile(tmp_full_path):
if "XtraStuf.mac" in str(ZipFile(tmp_full_path).namelist()):
logging.info("MacZip file format detected. Will not unzip to retain resource fork.")
@ -339,7 +340,6 @@ class FileCmds:
Takes (str) url, (str) save_dir, (str) file_name
Returns (dict) with (bool) status and (str) msg
"""
import requests
logging.info("Making a request to download %s", url)
try:
@ -371,7 +371,6 @@ class FileCmds:
Takes (str) file_name
Returns (dict) with (bool) status and (str) msg
"""
from json import dump
file_name = f"{CFG_DIR}/{file_name}"
try:
with open(file_name, "w", encoding="ISO-8859-1") as json_file:
@ -433,7 +432,6 @@ class FileCmds:
Takes (str) file_name
Returns (dict) with (bool) status and (str) msg
"""
from json import load
file_name = f"{CFG_DIR}/{file_name}"
try:
with open(file_name, encoding="ISO-8859-1") as json_file:
@ -512,7 +510,6 @@ class FileCmds:
Takes file name base (str) and (list of dicts) conf as arguments
Returns (dict) with (bool) status and (str) msg
"""
from json import dump
file_path = f"{CFG_DIR}/{file_name}"
try:
with open(file_path, "w") as json_file:
@ -548,7 +545,6 @@ class FileCmds:
Takes (str) file_path as argument.
Returns (dict) with (bool) status, (str) msg, (dict) conf
"""
from json import load
try:
with open(file_path) as json_file:
conf = load(json_file)

View File

@ -162,7 +162,7 @@ class RaCtlCmds:
def get_disk_device_types(self):
"""
Returns a (list) of (str) of four letter device acronyms
Returns a (list) of (str) of four letter device acronyms
that take image files as arguments.
"""
device_types = self.get_device_types()

View File

@ -3,7 +3,10 @@ Module for sending and receiving data over a socket connection with the RaSCSI b
"""
import logging
import socket
from time import sleep
from struct import pack, unpack
from rascsi.exceptions import (EmptySocketChunkException,
InvalidProtobufResponse,
FailedSocketConnectionException)
@ -27,7 +30,6 @@ class SocketCmds:
tries = 20
error_msg = ""
import socket
while counter < tries:
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
@ -56,7 +58,6 @@ class SocketCmds:
Tries to extract and interpret the protobuf header to get response size.
Reads data from socket in 2048 bytes chunks until all data is received.
"""
from struct import pack, unpack
# Sending the magic word "RASCSI" to authenticate with the server
sock.send(b"RASCSI")

View File

@ -0,0 +1,169 @@
"""
Module with methods that interact with the Pi system
"""
import subprocess
import logging
from subprocess import run
from shutil import disk_usage
from re import findall, match
from socket import socket, gethostname, AF_INET, SOCK_DGRAM
class SysCmds:
"""
Class for commands sent to the Pi's Linux system.
"""
@staticmethod
def running_env():
"""
Returns (str) git and (str) env
git contains the git hash of the checked out code
env is the various system information where this app is running
"""
try:
ra_git_version = (
subprocess.run(
["git", "rev-parse", "HEAD"],
capture_output=True,
check=True,
)
.stdout.decode("utf-8")
.strip()
)
except subprocess.CalledProcessError as error:
logging.warning("Executed shell command: %s", " ".join(error.cmd))
logging.warning("Got error: %s", error.stderr.decode("utf-8"))
ra_git_version = ""
try:
pi_version = (
subprocess.run(
["uname", "-a"],
capture_output=True,
check=True,
)
.stdout.decode("utf-8")
.strip()
)
except subprocess.CalledProcessError as error:
logging.warning("Executed shell command: %s", " ".join(error.cmd))
logging.warning("Got error: %s", error.stderr.decode("utf-8"))
pi_version = "Unknown"
return {"git": ra_git_version, "env": pi_version}
@staticmethod
def running_proc(daemon):
"""
Takes (str) daemon
Returns (int) proc, which is the number of processes currently running
"""
try:
processes = (
subprocess.run(
["ps", "aux"],
capture_output=True,
check=True,
)
.stdout.decode("utf-8")
.strip()
)
except subprocess.CalledProcessError as error:
logging.warning("Executed shell command: %s", " ".join(error.cmd))
logging.warning("Got error: %s", error.stderr.decode("utf-8"))
processes = ""
matching_processes = findall(daemon, processes)
return len(matching_processes)
@staticmethod
def is_bridge_setup():
"""
Returns (bool) True if the rascsi_bridge network interface exists
"""
try:
bridges = (
subprocess.run(
["brctl", "show"],
capture_output=True,
check=True,
)
.stdout.decode("utf-8")
.strip()
)
except subprocess.CalledProcessError as error:
logging.warning("Executed shell command: %s", " ".join(error.cmd))
logging.warning("Got error: %s", error.stderr.decode("utf-8"))
bridges = ""
if "rascsi_bridge" in bridges:
return True
return False
@staticmethod
def disk_space():
"""
Returns a (dict) with (int) total (int) used (int) free
This is the disk space information of the volume where this app is running
"""
total, used, free = disk_usage(__file__)
return {"total": total, "used": used, "free": free}
@staticmethod
def introspect_file(file_path, re_term):
"""
Takes a (str) file_path and (str) re_term in regex format
Will introspect file_path for the existance of re_term
and return True if found, False if not found
"""
try:
ifile = open(file_path, "r", encoding="ISO-8859-1")
except (IOError, ValueError, EOFError, TypeError) as error:
logging.error(str(error))
return False
for line in ifile:
if match(re_term, line):
return True
return False
# pylint: disable=broad-except
@staticmethod
def get_ip_and_host():
"""
Use a mock socket connection to identify the Pi's hostname and IP address
"""
host = gethostname()
sock = socket(AF_INET, SOCK_DGRAM)
try:
# mock ip address; doesn't have to be reachable
sock.connect(('10.255.255.255', 1))
ip_addr = sock.getsockname()[0]
except Exception:
ip_addr = False
finally:
sock.close()
return ip_addr, host
@staticmethod
def get_logs(lines, scope):
"""
Takes (int) lines and (str) scope.
If scope is a None equivalent, all syslogs are returned.
Returns either the decoded log output, or the stderr output of journalctl.
"""
line_param = []
scope_param = []
if lines:
line_param = ["-n", lines]
if scope:
scope_param = ["-u", scope]
process = run(
["journalctl"] + line_param + scope_param,
capture_output=True,
check=True,
)
if process.returncode == 0:
return process.returncode, process.stdout.decode("utf-8")
return process.returncode, process.stderr.decode("utf-8")

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:
device = devices_by_id[scsi_id]
file = "-"
device_type = ""
if str(scsi_id) in reserved_ids["ids"]:
file = "[Reserved]"
elif device is not None:
file = str(device["file"])
device_type = str(device["device_type"])
menu_str = str(scsi_id) + ":"
if device_type == "SCDP":
menu_str += "Daynaport"
elif device_type == "SCBR":
menu_str += "X68000 Host Bridge"
elif device_type == "SCLP":
menu_str += "SCSI Printer"
elif device_type == "SCHS":
menu_str += "Host Services"
else:
if file == "":
menu_str += "(empty)"
else:
menu_str += file
if device_type != "":
menu_str += " [" + device_type + "]"
menu.add_entry(menu_str, {"context": self.SCSI_ID_MENU,
"action": self.ACTION_OPENACTIONMENU,
"scsi_id": scsi_id})
return menu
# noinspection PyMethodMayBeStatic
def create_action_menu(self, context_object=None):
"""Method creates the action submenu with action that can be performed on a scsi slot"""
menu = Menu(CtrlBoardMenuBuilder.ACTION_MENU)
menu.add_entry("Return", {"context": self.ACTION_MENU,
"action": self.ACTION_RETURN})
menu.add_entry("Attach/Insert", {"context": self.ACTION_MENU,
"action": self.ACTION_SLOT_ATTACHINSERT})
menu.add_entry("Detach/Eject", {"context": self.ACTION_MENU,
"action": self.ACTION_SLOT_DETACHEJECT})
menu.add_entry("Info", {"context": self.ACTION_MENU,
"action": self.ACTION_SLOT_INFO})
menu.add_entry("Load Profile", {"context": self.ACTION_MENU,
"action": self.ACTION_LOADPROFILE})
menu.add_entry("Shutdown", {"context": self.ACTION_MENU,
"action": self.ACTION_SHUTDOWN})
return menu
def create_images_menu(self, context_object=None):
"""Creates a sub menu showing all the available images"""
menu = Menu(CtrlBoardMenuBuilder.IMAGES_MENU)
images_info = self.file_cmd.list_images()
menu.add_entry("Return", {"context": self.IMAGES_MENU, "action": self.ACTION_RETURN})
images = images_info["files"]
sorted_images = sorted(images, key=lambda d: d['name'])
for image in sorted_images:
image_str = image["name"] + " [" + image["detected_type"] + "]"
image_context = {"context": self.IMAGES_MENU, "name": str(image["name"]),
"device_type": str(image["detected_type"]),
"action": self.ACTION_IMAGE_ATTACHINSERT}
menu.add_entry(image_str, image_context)
return menu
def create_profiles_menu(self, context_object=None):
"""Creates a sub menu showing all the available profiles"""
menu = Menu(CtrlBoardMenuBuilder.PROFILES_MENU)
menu.add_entry("Return", {"context": self.IMAGES_MENU, "action": self.ACTION_RETURN})
config_files = self.file_cmd.list_config_files()
for config_file in config_files:
menu.add_entry(str(config_file),
{"context": self.PROFILES_MENU, "name": str(config_file),
"action": self.ACTION_LOADPROFILE})
return menu
def create_device_info_menu(self, context_object=None):
"""Create a menu displaying information of an image in a scsi slot"""
menu = Menu(CtrlBoardMenuBuilder.DEVICEINFO_MENU)
menu.add_entry("Return", {"context": self.DEVICEINFO_MENU, "action": self.ACTION_RETURN})
device_info = self._rascsi_client.list_devices(context_object["scsi_id"])
if not device_info["device_list"]:
return menu
scsi_id = context_object["scsi_id"]
file = device_info["device_list"][0]["file"]
status = device_info["device_list"][0]["status"]
if not status:
status = "Read/Write"
lun = device_info["device_list"][0]["unit"]
device_type = device_info["device_list"][0]["device_type"]
if "parameters" in device_info["device_list"][0]:
parameters = device_info["device_list"][0]["parameters"]
else:
parameters = "{}"
menu.add_entry("ID : " + str(scsi_id))
menu.add_entry("LUN : " + str(lun))
menu.add_entry("File : " + str(file))
menu.add_entry("Type : " + str(device_type))
menu.add_entry("R/RW : " + str(status))
menu.add_entry("Prms : " + str(parameters))
menu.add_entry("Vndr : " + str(device_info["device_list"][0]["vendor"]))
menu.add_entry("Prdct: " + str(device_info["device_list"][0]["product"]))
menu.add_entry("Rvisn: " + str(device_info["device_list"][0]["revision"]))
menu.add_entry("Blksz: " + str(device_info["device_list"][0]["block_size"]))
menu.add_entry("Imgsz: " + str(device_info["device_list"][0]["size"]))
return menu
def get_rascsi_client(self):
"""Returns an instance of the rascsi client"""
return self._rascsi_client

View File

@ -0,0 +1,188 @@
"""Module is the entry point for the RaSCSI Control Board UI"""
import argparse
import sys
import logging
from config import CtrlboardConfig
from ctrlboard_hw.ctrlboard_hw import CtrlBoardHardware
from ctrlboard_hw.ctrlboard_hw_constants import CtrlBoardHardwareConstants
from ctrlboard_event_handler.ctrlboard_menu_update_event_handler \
import CtrlBoardMenuUpdateEventHandler
from ctrlboard_menu_builder import CtrlBoardMenuBuilder
from menu.menu_renderer_config import MenuRendererConfig
from menu.menu_renderer_luma_oled import MenuRendererLumaOled
from rascsi.exceptions import (EmptySocketChunkException,
InvalidProtobufResponse,
FailedSocketConnectionException)
from rascsi.ractl_cmds import RaCtlCmds
from rascsi.socket_cmds import SocketCmds
from rascsi_menu_controller import RascsiMenuController
def parse_config():
"""Parses the command line parameters and configured the RaSCSI Control Board UI accordingly"""
config = CtrlboardConfig()
cmdline_args_parser = argparse.ArgumentParser(description='RaSCSI ctrlboard service')
cmdline_args_parser.add_argument(
"--rotation",
type=int,
choices=[0, 90, 180, 270],
default=180,
action="store",
help="The rotation of the screen buffer in degrees. Default: 180",
)
cmdline_args_parser.add_argument(
"--height",
type=int,
choices=[64],
default=64,
action="store",
help="The pixel height of the screen buffer. Default: 64",
)
cmdline_args_parser.add_argument(
"--rascsi-host",
type=str,
default="localhost",
action="store",
help="RaSCSI host. Default: localhost",
)
cmdline_args_parser.add_argument(
"--rascsi-port",
type=int,
default=6868,
action="store",
help="RaSCSI port. Default: 6868",
)
cmdline_args_parser.add_argument(
"--password",
type=str,
default="",
action="store",
help="Token password string for authenticating with RaSCSI",
)
cmdline_args_parser.add_argument(
"--loglevel",
type=int,
choices=[0, 10, 30, 40, 50],
default=logging.WARN,
action="store",
help="Loglevel. Valid values: 0 (notset), 10 (debug), 30 (warning), "
"40 (error), 50 (critical). Default: Warning",
)
cmdline_args_parser.add_argument(
"--transitions",
type=int,
choices=[0, 1],
default=1,
action="store",
help="Transition animations. Valid values: 0 (disabled), 1 (enabled). Default: 1",
)
args = cmdline_args_parser.parse_args()
config.ROTATION = args.rotation
if args.height == 64:
config.HEIGHT = 64
config.LINES = 8
elif args.height == 32:
config.HEIGHT = 32
config.LINES = 4
config.TOKEN = args.password
config.WIDTH = 128
config.BORDER = 5
config.RASCSI_HOST = args.rascsi_host
config.RASCSI_PORT = args.rascsi_port
config.LOG_LEVEL = args.loglevel
config.TRANSITIONS = bool(args.transitions)
return config
def check_rascsi_connection(ractl_cmd):
"""Checks whether a RaSCSI connection exists by polling the RaSCSI server info.
Returns true if connection works, false if connection fails."""
try:
info = ractl_cmd.get_reserved_ids()
return bool(info["status"] is True)
except FailedSocketConnectionException:
log = logging.getLogger(__name__)
log.error("Could not establish connection. Stopping service")
exit(1)
def main():
"""Main function for the RaSCSI Control Board UI"""
config = parse_config()
log_format = "%(asctime)s:%(name)s:%(levelname)s - %(message)s"
logging.basicConfig(stream=sys.stdout,
format=log_format,
level=config.LOG_LEVEL)
log = logging.getLogger(__name__)
log.debug("RaSCSI ctrlboard service started.")
ctrlboard_hw = CtrlBoardHardware(display_i2c_address=config.DISPLAY_I2C_ADDRESS,
pca9554_i2c_address=config.PCA9554_I2C_ADDRESS)
# for now, we require the complete rascsi ctrlboard hardware.
# Oled only will be supported as well at some later point in time.
if ctrlboard_hw.rascsi_controlboard_detected is False:
log.error("Ctrlboard hardware not detected. Stopping service")
exit(1)
sock_cmd = None
ractl_cmd = None
try:
sock_cmd = SocketCmds(host=config.RASCSI_HOST, port=config.RASCSI_PORT)
ractl_cmd = RaCtlCmds(sock_cmd=sock_cmd, token=config.TOKEN)
except EmptySocketChunkException:
log.error("Retrieved empty data from RaSCSI. Stopping service")
exit(1)
except InvalidProtobufResponse:
log.error("Retrieved unexpected data from RaSCSI. Stopping service")
exit(1)
if check_rascsi_connection(ractl_cmd) is False:
log.error("Communication with RaSCSI failed. Please check if password token must be set "
"and whether is set correctly.")
exit(1)
menu_renderer_config = MenuRendererConfig()
if config.TRANSITIONS is False:
menu_renderer_config.transition = None
menu_renderer_config.i2c_address = CtrlBoardHardwareConstants.DISPLAY_I2C_ADDRESS
menu_renderer_config.rotation = config.ROTATION
menu_builder = CtrlBoardMenuBuilder(ractl_cmd)
menu_controller = RascsiMenuController(config.MENU_REFRESH_INTERVAL, menu_builder=menu_builder,
menu_renderer=MenuRendererLumaOled(menu_renderer_config),
menu_renderer_config=menu_renderer_config)
menu_controller.add(CtrlBoardMenuBuilder.SCSI_ID_MENU)
menu_controller.add(CtrlBoardMenuBuilder.ACTION_MENU)
menu_controller.show_splash_screen(f"resources/splash_start_64.bmp")
menu_update_event_handler = CtrlBoardMenuUpdateEventHandler(menu_controller,
sock_cmd=sock_cmd,
ractl_cmd=ractl_cmd)
ctrlboard_hw.attach(menu_update_event_handler)
menu_controller.set_active_menu(CtrlBoardMenuBuilder.SCSI_ID_MENU)
while True:
# pylint: disable=broad-except
try:
ctrlboard_hw.process_events()
menu_update_event_handler.update_events()
menu_controller.update()
except KeyboardInterrupt:
ctrlboard_hw.cleanup()
break
except Exception as ex:
print(ex)
if __name__ == '__main__':
main()

View File

View File

@ -0,0 +1,26 @@
"""Module implementing a blank screensaver"""
from menu.screensaver import ScreenSaver
class BlankScreenSaver(ScreenSaver):
"""Class implementing a blank screen safer that simply blanks the screen after a
configured activation delay"""
def __init__(self, activation_delay, menu_renderer):
super().__init__(activation_delay, menu_renderer)
self._initial_draw_call = None
def draw_screensaver(self):
if self._initial_draw_call is True:
self.menu_renderer.blank_screen()
else:
self._initial_draw_call = False
def check_timer(self):
already_enabled = False
if self.enabled is True:
already_enabled = True
super().check_timer()
if self.enabled is True and already_enabled is False: # new switch to screensaver
self._initial_draw_call = True

View File

@ -0,0 +1,82 @@
"""Module that implements a button cycling functionality"""
from abc import abstractmethod
from menu.timer import Timer
from rascsi.file_cmds import FileCmds
class Cycler:
"""Class implementing button cycling functionality. Message is shown at the center of
the screen where repeated button presses cycle through the available selection
possibilities. Inactivity (cycle_timeout) actives cycle entry last shown on the screen."""
def __init__(self, menu_controller, sock_cmd, ractl_cmd,
cycle_timeout=3, return_string="Return ->",
return_entry=True, empty_messages=True):
self._cycle_profile_timer_flag = Timer(activation_delay=cycle_timeout)
self._menu_controller = menu_controller
self.sock_cmd = sock_cmd
self.ractl_cmd = ractl_cmd
self.file_cmd = FileCmds(sock_cmd=self.sock_cmd, ractl=self.ractl_cmd)
self.cycle_entries = self.populate_cycle_entries()
self.return_string = return_string
self.return_entry = return_entry
self.empty_messages = empty_messages
if self.return_entry is True:
self.cycle_entries.insert(0, self.return_string)
self.selected_config_file_index = 0
self.message = str(self.cycle_entries[self.selected_config_file_index])
self.update()
@abstractmethod
def populate_cycle_entries(self):
"""Returns a list of entries to cycle"""
@abstractmethod
def perform_selected_entry_action(self, selected_entry):
"""Performs an action on the selected cycle menu entry"""
@abstractmethod
def perform_return_action(self):
"""Perform the return action, i.e., when no selection is chosen"""
def update(self):
""" Returns True if object has completed its task and can be deleted """
if self._cycle_profile_timer_flag is None:
return None
self._cycle_profile_timer_flag.check_timer()
if self.message is not None:
self._menu_controller.show_timed_mini_message(self.message)
self._menu_controller.get_menu_renderer().render()
if self._cycle_profile_timer_flag.enabled is False: # timer is running
return None
selected_cycle_entry = str(self.cycle_entries[self.selected_config_file_index])
if self.return_entry is True:
if selected_cycle_entry != self.return_string:
if self.empty_messages is True:
self._menu_controller.show_timed_mini_message("")
self._menu_controller.show_timed_message("")
return self.perform_selected_entry_action(selected_cycle_entry)
self._menu_controller.show_timed_mini_message("")
self._menu_controller.show_timed_message("")
return self.perform_return_action()
return self.perform_selected_entry_action(selected_cycle_entry)
def cycle(self):
"""Cycles between entries in the cycle menu"""
if self._cycle_profile_timer_flag is None:
return
self.selected_config_file_index += 1
if self.selected_config_file_index > len(self.cycle_entries) - 1:
self.selected_config_file_index = 0
self.message = str(self.cycle_entries[self.selected_config_file_index])
self._cycle_profile_timer_flag.reset_timer()

View File

@ -0,0 +1,30 @@
"""Module for creating a menu"""
from typing import List
class Menu:
"""Class implement the Menu class"""
def __init__(self, name: str):
self.entries: List = []
self.item_selection = 0
self.name = name
self.context_object = None
def add_entry(self, text, data_object=None):
"""Adds an entry to a menu"""
entry = {"text": text, "data_object": data_object}
self.entries.append(entry)
def get_current_text(self):
"""Returns the text content of the currently selected text in the menu."""
return self.entries[self.item_selection]['text']
def get_current_info_object(self):
"""Returns the data object to the currently selected menu item"""
return self.entries[self.item_selection]['data_object']
def __repr__(self):
print("entries: " + str(self.entries))
print("item_selection: " + str(self.item_selection))
print("name: " + self.name)
print("context object: " + str(self.context_object))

View File

@ -0,0 +1,14 @@
"""Module for creating menus"""
from abc import ABC, abstractmethod
from menu.menu import Menu
# pylint: disable=too-few-public-methods
class MenuBuilder(ABC):
"""Base class for menu builders"""
def __init__(self):
pass
@abstractmethod
def build(self, name: str, context_object=None) -> Menu:
"""builds a menu and gives it a name and a context object"""

View File

@ -0,0 +1,145 @@
"""Module providing the menu controller."""
import time
import importlib
from typing import Dict, Optional
from PIL import Image
from menu.menu import Menu
from menu.menu_builder import MenuBuilder
from menu.menu_renderer_config import MenuRendererConfig
from menu.menu_renderer_luma_oled import MenuRendererLumaOled
from menu.transition import Transition
class MenuController:
"""Class providing the menu controller. The menu controller is a central class
that controls the menu and its associated rendering to a screen."""
def __init__(self, menu_builder: MenuBuilder, menu_renderer=None, menu_renderer_config=None):
self._menus: Dict[str, Menu] = {}
self._active_menu: Optional[Menu] = None
self._menu_renderer = menu_renderer
self._menu_builder: MenuBuilder = menu_builder
self._menu_renderer_config: Optional[MenuRendererConfig]
if menu_renderer_config is None:
self._menu_renderer_config = MenuRendererConfig()
else:
self._menu_renderer_config = menu_renderer_config
if menu_renderer is None: # default to LumaOled renderer if nothing else is stated
self._menu_renderer = MenuRendererLumaOled(self._menu_renderer_config)
else:
self._menu_renderer = menu_renderer
self._transition: Optional[Transition] = None
if self._menu_renderer_config.transition is None:
self._transition = None
return
try:
module = importlib.import_module("menu.transition")
try:
transition_class = getattr(module, self._menu_renderer_config.transition)
if transition_class is not None:
self._transition = transition_class(self._menu_renderer.disp)
except AttributeError:
pass
except ImportError:
print("transition module does not exist. Falling back to default.")
self._transition = None
def add(self, name: str, context_object=None):
"""Adds a menu to the menu collection internal to the controller by name.
The associated class menu builder builds the menu by name."""
self._menus[name] = self._menu_builder.build(name)
if context_object is not None:
self._menus[name].context_object = context_object
def set_active_menu(self, name: str, display_on_device=True):
"""Activates a menu from the controller internal menu collection by name."""
self._active_menu = self._menus[name]
self._menu_renderer.set_menu(self._active_menu)
self._menu_renderer.render(display_on_device)
def refresh(self, name: str, context_object=None):
"""Refreshes a menu by name (by calling the menu builder again to rebuild the menu)."""
item_selection = None
if self._menus.get(name) is not None:
item_selection = self._menus[name].item_selection
self._menus[name] = self._menu_builder.build(name, context_object)
if context_object is not None:
self._menus[name].context_object = context_object
if item_selection is not None:
self._menus[name].item_selection = item_selection
def get_menu(self, name: str):
"""Returns the controller internal menu collection"""
return self._menus[name]
def get_active_menu(self):
"""Returns the currently activated menu"""
return self._active_menu
def get_menu_renderer_config(self):
"""Returns the menu renderer configuration"""
return self._menu_renderer_config
def get_menu_renderer(self):
"""Returns the menu renderer for this menu controller"""
return self._menu_renderer
def segue(self, name, context_object=None, transition_attributes=None):
"""Transitions one menu into the other with all associated actions such
as transition animations."""
self.get_active_menu().context_object = None
self.refresh(name, context_object)
if self._transition is not None and transition_attributes is not None:
source_image = self._menu_renderer.image.copy()
transition_menu = self.get_menu(name)
self._menu_renderer.set_menu(transition_menu)
target_image = self._menu_renderer.render(display_on_device=False)
transition_attributes["transition_speed"] = self._menu_renderer_config.transition_speed
self._transition.perform(source_image, target_image, transition_attributes)
self.set_active_menu(name)
def show_message(self, message: str, sleep=1):
"""Displays a blocking message on the screen that stays for sleep seconds"""
self.get_menu_renderer().message = message
self.get_menu_renderer().render()
time.sleep(sleep)
self.get_menu_renderer().message = ""
def show_timed_message(self, message: str):
"""Shows a message on the screen. The timed message is non-blocking for the main loop and
simply redraws the message on screen if necessary."""
self.get_menu_renderer().message = message
self.get_menu_renderer().render()
def show_mini_message(self, message: str, sleep=1):
"""The mini message is a message on the screen that only coveres the center portion
of the screen, i.e., the remaining content is still visible on the screen while the mini
message is shown in the middle. This version is blocking and stays for sleep seconds."""
self.get_menu_renderer().mini_message = message
self.get_menu_renderer().render()
time.sleep(sleep)
self.get_menu_renderer().mini_message = ""
def show_timed_mini_message(self, message: str):
"""The mini message is a message on the screen that only coveres the center portion of
the screen, i.e., the remaining content is still visible on the screen while the mini
message is shown in the middle. This version is non-blocking for the main loop and
simply redraws the mini message on screen if necessary."""
self.get_menu_renderer().mini_message = message
self.get_menu_renderer().render()
def show_splash_screen(self, filename, sleep=2):
"""Shows a splash screen for a given number of seconds."""
image = Image.open(filename).convert("1")
self.get_menu_renderer().update_display_image(image)
time.sleep(sleep)
def update(self):
"""Updates the menu / draws the screen if necessary."""
self._menu_renderer.update()

View File

@ -0,0 +1,254 @@
"""Module provides the abstract menu renderer class"""
import time
import math
import itertools
from abc import ABC, abstractmethod
from pydoc import locate
from typing import Optional
from PIL import Image
from PIL import ImageDraw
from PIL import ImageFont
from menu.menu import Menu
from menu.menu_renderer_config import MenuRendererConfig
from menu.screensaver import ScreenSaver
class MenuRenderer(ABC):
"""The abstract menu renderer class provides the base for concrete menu
renderer classes that implement functionality based on conrete hardware or available APIs."""
def __init__(self, config: MenuRendererConfig):
self.message = ""
self.mini_message = ""
self._menu = None
self._config = config
self.disp = self.display_init()
self.image = Image.new('1', (self.disp.width, self.disp.height))
self.draw = ImageDraw.Draw(self.image)
self.font = ImageFont.truetype(config.font_path, size=config.font_size)
# just a sample text to work with the font height
_, self.font_height = self.font.getsize("ABCabc")
self.cursor_position = 0
self.frame_start_row = 0
self.render_timestamp = None
# effectively a small state machine that deals with the scrolling
self._perform_scrolling_stage = 0
self._x_scrolling = 0
self._current_line_horizontal_overlap = None
self._stage_timestamp: Optional[int] = None
screensaver = locate(self._config.screensaver)
# noinspection PyCallingNonCallable
self.screensaver: ScreenSaver = screensaver(self._config.screensaver_delay, self)
@abstractmethod
def display_init(self):
"""Method initializes the displays for usage."""
@abstractmethod
def display_clear(self):
"""Methods clears the screen. Possible hardware clear call if necessary."""
@abstractmethod
def blank_screen(self):
"""Method blanks the screen. Based on drawing a blank rectangle."""
@abstractmethod
def update_display_image(self, image):
"""Method displays an image using PIL."""
@abstractmethod
def update_display(self):
"""Method updates the display."""
def set_config(self, config: MenuRendererConfig):
"""Configures the menu renderer with a generic menu renderer configuration."""
self._config = config
def get_config(self):
"""Returns the menu renderer configuration."""
return self._config
def set_menu(self, menu: Menu):
"""Method sets the menu that the menu renderer should draw."""
self._menu = menu
def rows_per_screen(self):
"""Calculates the number of rows per screen based on the configured font size."""
rows = self.disp.height / self.font_height
return math.floor(rows)
def draw_row(self, row_number: int, text: str, selected: bool):
"""Draws a single row of the menu."""
x_pos = 0
y_pos = row_number*self.font_height
if selected:
selection_extension = 0
if row_number < self.rows_per_screen():
selection_extension = self._config.row_selection_pixel_extension
self.draw.rectangle((x_pos, y_pos, self.disp.width,
y_pos+self._config.font_size+selection_extension),
outline=0, fill=255)
# in stage 1, we initialize scrolling for the currently selected line
if self._perform_scrolling_stage == 1:
self.setup_horizontal_scrolling(text)
self._perform_scrolling_stage = 2 # don't repeat once the scrolling has started
# in stage 2, we know the details and can thus perform the scrolling to the left
if self._perform_scrolling_stage == 2:
if self._current_line_horizontal_overlap+self._x_scrolling > 0:
self._x_scrolling -= 1
if self._current_line_horizontal_overlap+self._x_scrolling == 0:
self._stage_timestamp = int(time.time())
self._perform_scrolling_stage = 3
# in stage 3, we wait for a little when we have scrolled to the end of the text
if self._perform_scrolling_stage == 3:
current_time = int(time.time())
time_diff = current_time - self._stage_timestamp
if time_diff >= int(self._config.scroll_line_end_delay):
self._stage_timestamp = None
self._perform_scrolling_stage = 4
# in stage 4, we scroll back to the right
if self._perform_scrolling_stage == 4:
if self._current_line_horizontal_overlap+self._x_scrolling >= 0:
self._x_scrolling += 1
if (self._current_line_horizontal_overlap +
self._x_scrolling) == self._current_line_horizontal_overlap:
self._stage_timestamp = int(time.time())
self._perform_scrolling_stage = 5
# in stage 5, we wait again for a little while before we start again
if self._perform_scrolling_stage == 5:
current_time = int(time.time())
time_diff = current_time - self._stage_timestamp
if time_diff >= int(self._config.scroll_line_end_delay):
self._stage_timestamp = None
self._perform_scrolling_stage = 2
self.draw.text((x_pos+self._x_scrolling, y_pos), text, font=self.font,
spacing=0, stroke_fill=0, fill=0)
else:
self.draw.text((x_pos, y_pos), text, font=self.font, spacing=0, stroke_fill=0, fill=255)
def draw_fullsceen_message(self, text: str):
"""Draws a fullscreen message, i.e., a full-screen message."""
font_width, font_height = self.font.getsize(text)
centered_width = (self.disp.width - font_width) / 2
centered_height = (self.disp.height - font_height) / 2
self.draw.rectangle((0, 0, self.disp.width, self.disp.height), outline=0, fill=255)
self.draw.text((centered_width, centered_height), text, align="center", font=self.font,
stroke_fill=0, fill=0, textsize=20)
def draw_mini_message(self, text: str):
"""Draws a fullscreen message, i.e., a message covering only the center portion of
the screen. The remaining areas stay visible."""
font_width, _ = self.font.getsize(text)
centered_width = (self.disp.width - font_width) / 2
centered_height = (self.disp.height - self.font_height) / 2
self.draw.rectangle((0, centered_height-4, self.disp.width,
centered_height+self.font_height+4), outline=0, fill=255)
self.draw.text((centered_width, centered_height), text, align="center", font=self.font,
stroke_fill=0, fill=0, textsize=20)
def draw_menu(self):
"""Method draws the menu set to the class instance."""
if self._menu.item_selection >= self.frame_start_row + self.rows_per_screen():
if self._config.scroll_behavior == "page":
self.frame_start_row = self.frame_start_row + (round(self.rows_per_screen()/2)) + 1
if self.frame_start_row > len(self._menu.entries) - self.rows_per_screen():
self.frame_start_row = len(self._menu.entries) - self.rows_per_screen()
else: # extend as default behavior
self.frame_start_row = self._menu.item_selection + 1 - self.rows_per_screen()
if self._menu.item_selection < self.frame_start_row:
if self._config.scroll_behavior == "page":
self.frame_start_row = self.frame_start_row - (round(self.rows_per_screen()/2)) - 1
if self.frame_start_row < 0:
self.frame_start_row = 0
else: # extend as default behavior
self.frame_start_row = self._menu.item_selection
self.draw_menu_frame(self.frame_start_row, self.frame_start_row+self.rows_per_screen())
def draw_menu_frame(self, frame_start_row: int, frame_end_row: int):
"""Draws row frame_start_row to frame_end_row of the class instance menu, i.e., it
draws a given frame of the complete menu that fits the screen."""
self.draw.rectangle((0, 0, self.disp.width, self.disp.height), outline=0, fill=0)
row_on_screen = 0
row_in_menuitems = frame_start_row
for menu_entry in itertools.islice(self._menu.entries, frame_start_row, frame_end_row):
if row_in_menuitems == self._menu.item_selection:
self.draw_row(row_on_screen, menu_entry["text"], True)
else:
self.draw_row(row_on_screen, menu_entry["text"], False)
row_in_menuitems += 1
row_on_screen += 1
if row_on_screen >= self.rows_per_screen():
break
def render(self, display_on_device=True):
"""Method renders the menu."""
if display_on_device is True:
self.screensaver.reset_timer()
self._perform_scrolling_stage = 0
self._current_line_horizontal_overlap = None
self._x_scrolling = 0
if self._menu is None:
self.draw_fullsceen_message("No menu set!")
self.disp.image(self.image)
if display_on_device is True:
self.disp.show()
return None
self.display_clear()
if self.message != "":
self.draw_fullsceen_message(self.message)
elif self.mini_message != "":
self.draw_mini_message(self.mini_message)
else:
self.draw_menu()
if display_on_device is True:
self.update_display_image(self.image)
self.update_display()
self.render_timestamp = int(time.time())
return self.image
def setup_horizontal_scrolling(self, text):
"""Configure horizontal scrolling based on the configured screen dimensions."""
font_width, _ = self.font.getsize(text)
self._current_line_horizontal_overlap = font_width - self.disp.width
def update(self):
"""Method updates the menu drawing within the main loop in a non-blocking manner.
Also updates the current entry scrolling if activated."""
if self._config.scroll_line is False:
return
self.screensaver.check_timer()
if self.screensaver.enabled is True:
self.screensaver.draw()
return
current_time = int(time.time())
time_diff = current_time - self.render_timestamp
if time_diff >= self._config.scroll_delay:
if self._perform_scrolling_stage == 0:
self._perform_scrolling_stage = 1
self.draw_menu()
self.update_display_image(self.image)

View File

@ -0,0 +1,34 @@
"""Module providing the Adafruit SSD1306 menu renderer class"""
# pylint: disable=import-error
from board import SCL, SDA
import busio
import adafruit_ssd1306
from menu.menu_renderer import MenuRenderer
class MenuRendererAdafruitSSD1306(MenuRenderer):
"""Class implementing a menu renderer using the Adafruit SSD1306 library"""
def display_init(self):
i2c = busio.I2C(SCL, SDA)
self.disp = adafruit_ssd1306.SSD1306_I2C(self._config.width, self._config.height, i2c,
addr=self._config.i2c_address)
self.disp.rotation = self._config.get_mapped_rotation()
self.disp.fill(0)
self.disp.show()
return self.disp
def update_display_image(self, image):
self.disp.image(self.image)
self.disp.show()
def update_display(self):
self.disp.show()
def display_clear(self):
self.disp.fill(0)
def blank_screen(self):
self.disp.fill(0)
self.disp.show()

View File

@ -0,0 +1,39 @@
"""Module for configuring menu renderer instances"""
# pylint: disable=too-many-instance-attributes, too-few-public-methods
class MenuRendererConfig:
"""Class for configuring menu renderer instances. Provides configuration options
such as width, height, i2c address, font, transitions, etc."""
_rotation_mapper = {
0: 0,
90: 1,
180: 2,
270: 3
}
def __init__(self):
self.width = 128
self.height = 64
self.i2c_address = 0x3c
self.i2c_port = 1
self.display_type = "ssd1306" # luma-oled supported devices, "sh1106", "ssd1306", ...
self.font_path = 'resources/DejaVuSansMono-Bold.ttf'
self.font_size = 12
self.row_selection_pixel_extension = 2
self.scroll_behavior = "page" # "extend" or "page"
self.transition = "PushTransition" # "PushTransition" or "None
self.transition_attributes_left = {"direction": "push_left"}
self.transition_attributes_right = {"direction": "push_right"}
self.transition_speed = 10
self.scroll_line = True
self.scroll_delay = 3
self.scroll_line_end_delay = 2
self.screensaver = "menu.blank_screensaver.BlankScreenSaver"
self.screensaver_delay = 25
self.rotation = 0 # 0, 180
def get_mapped_rotation(self):
"""Converts human-readable rotation value to the one expected
by the luma and adafruit libraries"""
return self._rotation_mapper[self.rotation]

View File

@ -0,0 +1,34 @@
"""Module providing the luma oled menu renderer class"""
from luma.core.interface.serial import i2c
from menu.menu_renderer import MenuRenderer
class MenuRendererLumaOled(MenuRenderer):
"""Class implementing the luma oled menu renderer"""
def display_init(self):
serial = i2c(port=self._config.i2c_port, address=self._config.i2c_address)
import luma.oled.device
device = getattr(luma.oled.device, self._config.display_type)
self.disp = device(serial_interface=serial, width=self._config.width,
height=self._config.height,
rotate=self._config.get_mapped_rotation())
self.disp.clear()
self.disp.show()
return self.disp
def update_display_image(self, image):
self.disp.display(image)
def update_display(self):
self.disp.display(self.image)
def display_clear(self):
pass
def blank_screen(self):
self.disp.clear()
self.draw.rectangle((0, 0, self.disp.width, self.disp.height), outline=0, fill=0)
self.disp.show()

View File

@ -0,0 +1,33 @@
"""Module providing the menu screensaver class"""
from abc import abstractmethod
from menu.timer import Timer
class ScreenSaver:
"""Class implementing the menu screensaver"""
def __init__(self, activation_delay, menu_renderer):
self.enabled = False
self.menu_renderer = menu_renderer
self.screensaver_activation_delay = activation_delay
self.timer_flag = Timer(self.screensaver_activation_delay)
def draw(self):
"""Draws the screen saver in a non-blocking way if enabled."""
if self.enabled is True:
self.draw_screensaver()
@abstractmethod
def draw_screensaver(self):
"""Draws the screen saver. Must be implemented in subclasses."""
def check_timer(self):
"""Checks if the screen saver should be enabled given the configured
activation delay."""
self.timer_flag.check_timer()
self.enabled = self.timer_flag.enabled
def reset_timer(self):
"""Resets the screen saver timer if an activitiy happend."""
self.timer_flag.reset_timer()
self.enabled = self.timer_flag.enabled

View File

@ -0,0 +1,24 @@
"""Module providing a timer class"""
import time
class Timer:
"""Class implementing a timer class. Takes an activation delay and
sets a flag if the activation delay exprires."""
def __init__(self, activation_delay):
self.start_timestamp = int(time.time())
self.activation_delay = activation_delay
self.enabled = False
def check_timer(self):
"""Checks the timer whether it has reached the activation delay."""
current_timestamp = int(time.time())
timestamp_diff = current_timestamp - self.start_timestamp
if timestamp_diff >= self.activation_delay:
self.enabled = True
def reset_timer(self):
"""Resets the timer and starts from the beginning."""
self.start_timestamp = int(time.time())
self.enabled = False

View File

@ -0,0 +1,65 @@
"""Module providing implementations for menu transitions."""
from abc import abstractmethod
from PIL import Image
# pylint: disable=too-few-public-methods
class Transition:
"""Class provides the interface for menu transitions. Must be subclassed."""
def __init__(self, disp):
self.disp = disp
@abstractmethod
def perform(self, start_image: Image, end_image: Image, transition_attributes=None) -> None:
"""Signature for an abstract transition. Must be implemented in subclasses."""
class PushTransition(Transition):
"""Class implementing a push left/right transition."""
PUSH_LEFT_TRANSITION = "push_left"
PUSH_RIGHT_TRANSITION = "push_right"
def __init__(self, disp):
super().__init__(disp)
self.transition_attributes = None
def perform(self, start_image: Image, end_image: Image, transition_attributes=None):
"""Performs a push transition to the left or right depending on the
transition attribute 'direction'."""
direction = {}
self.transition_attributes = transition_attributes
if transition_attributes is not None and transition_attributes != {}:
direction = transition_attributes["direction"]
transition_image = Image.new('1', (self.disp.width, self.disp.height))
if direction == PushTransition.PUSH_LEFT_TRANSITION:
self.perform_push_left(end_image, start_image, transition_image)
elif direction == PushTransition.PUSH_RIGHT_TRANSITION:
self.perform_push_right(end_image, start_image, transition_image)
else:
self.disp.image(end_image)
self.disp.show()
def perform_push_left(self, end_image, start_image, transition_image):
"""Implements a push left transition. Is called by perform depending on the transition
attribute 'direction'."""
for x_pos in range(0, 128, self.transition_attributes["transition_speed"]):
left_region = start_image.crop((x_pos, 0, 128, 64))
right_region = end_image.crop((0, 0, x_pos, 64))
transition_image.paste(left_region, (0, 0, 128 - x_pos, 64))
transition_image.paste(right_region, (128 - x_pos, 0, 128, 64))
self.disp.display(transition_image)
self.disp.display(end_image)
def perform_push_right(self, end_image, start_image, transition_image):
"""Implements a push right transition. Is called by perform depending on the transition
attribute 'direction'."""
for x_pos in range(0, 128, self.transition_attributes["transition_speed"]):
left_region = start_image.crop((0, 0, 128-x_pos, 64))
right_region = end_image.crop((128-x_pos, 0, 128, 64))
transition_image.paste(left_region, (x_pos, 0, 128, 64))
transition_image.paste(right_region, (0, 0, x_pos, 64))
self.disp.display(transition_image)
self.disp.display(end_image)

View File

@ -0,0 +1,21 @@
"""Module for Observable part of the Observer pattern functionality"""
from typing import List
from observer import Observer
class Observable:
"""Class implementing the Observable pattern"""
_observers: List[Observer] = []
def attach(self, observer: Observer):
"""Attaches an observer to an obserable object"""
self._observers.append(observer)
def detach(self, observer: Observer):
"""detaches an observer from an observable object"""
self._observers.remove(observer)
def notify(self, updated_object):
"""Notifies all observers with a given object parameter"""
for observer in self._observers:
observer.update(updated_object)

View File

@ -0,0 +1,10 @@
"""Module implementing the Observer part of the Observer pattern"""
from abc import ABC, abstractmethod
# pylint: disable=too-few-public-methods
class Observer(ABC):
"""Class implementing an abserver"""
@abstractmethod
def update(self, updated_object) -> None:
"""Abstract method for updating an observer. Needs to be implemented by subclasses."""

View File

@ -0,0 +1,32 @@
"""Module implementing the RaSCSI Control Board UI specific menu controller"""
from ctrlboard_menu_builder import CtrlBoardMenuBuilder
from menu.menu_builder import MenuBuilder
from menu.menu_controller import MenuController
from menu.menu_renderer import MenuRenderer
from menu.timer import Timer
class RascsiMenuController(MenuController):
"""Class implementing a RaSCSI Control Board UI specific menu controller"""
def __init__(self, refresh_interval, menu_builder: MenuBuilder,
menu_renderer=None, menu_renderer_config=None):
super().__init__(menu_builder, menu_renderer, menu_renderer_config)
self._refresh_interval = refresh_interval
self._menu_renderer: MenuRenderer = menu_renderer
self._scsi_list_refresh_timer_flag = Timer(self._refresh_interval)
def segue(self, name, context_object=None, transition_attributes=None):
super().segue(name, context_object, transition_attributes)
self._scsi_list_refresh_timer_flag.reset_timer()
def update(self):
super().update()
if self.get_active_menu().name == CtrlBoardMenuBuilder.SCSI_ID_MENU:
self._scsi_list_refresh_timer_flag.check_timer()
if self._scsi_list_refresh_timer_flag.enabled is True:
self._scsi_list_refresh_timer_flag.reset_timer()
self.refresh(name=CtrlBoardMenuBuilder.SCSI_ID_MENU)
if self._menu_renderer.screensaver.enabled is False:
self.set_active_menu(CtrlBoardMenuBuilder.SCSI_ID_MENU, display_on_device=False)

128
python/ctrlboard/start.sh Executable file
View File

@ -0,0 +1,128 @@
#!/usr/bin/env bash
set -e
# set -x # Uncomment to Debug
PI_MODEL=$(/usr/bin/tr -d '\0' < /proc/device-tree/model)
cd "$(dirname "$0")"
# verify packages installed
ERROR=0
if ! command -v dpkg -l i2c-tools &> /dev/null ; then
echo "i2c-tools could not be found"
echo "Run 'sudo apt install i2c-tools' to fix."
ERROR=1
fi
if ! command -v python3 &> /dev/null ; then
echo "python3 could not be found"
echo "Run 'sudo apt install python3' to fix."
ERROR=1
fi
if ! python3 -m venv --help &> /dev/null ; then
echo "venv could not be found"
echo "Run 'sudo apt install python3-venv' to fix."
ERROR=1
fi
# Dep to build Pillow
if ! dpkg -l python3-dev &> /dev/null; then
echo "python3-dev could not be found"
echo "Run 'sudo apt install python3-dev' to fix."
ERROR=1
fi
if ! dpkg -l libjpeg-dev &> /dev/null; then
echo "libjpeg-dev could not be found"
echo "Run 'sudo apt install libjpeg-dev' to fix."
ERROR=1
fi
if ! dpkg -l libpng-dev &> /dev/null; then
echo "libpng-dev could not be found"
echo "Run 'sudo apt install libpng-dev' to fix."
ERROR=1
fi
if ! dpkg -l libopenjp2-7-dev &> /dev/null; then
echo "libopenjp2-7-dev could not be found"
echo "Run 'sudo apt install libopenjp2-7-dev' to fix."
ERROR=1
fi
if [ $ERROR = 1 ] ; then
echo
echo "Fix errors and re-run ./start.sh"
exit 1
fi
if pgrep -f "python3 src/main.py" &> /dev/null; then
echo "Detected active rascsi control board service"
echo "Terminating before launching a new one."
sudo pkill -f "python3 src/main.py"
fi
if ! i2cdetect -y 1 &> /dev/null ; then
echo "i2cdetect -y 1 did not find a screen."
exit 2
fi
# Compiler flags needed for gcc v10 and up
if [[ `gcc --version | awk '/gcc/' | awk -F ' ' '{print $3}' | awk -F '.' '{print $1}'` -ge 10 ]]; then
COMPILER_FLAGS="-fcommon"
fi
# Test for two known broken venv states
if test -e venv; then
GOOD_VENV=true
! test -e venv/bin/activate && GOOD_VENV=false
pip3 list 1> /dev/null
test $? -eq 1 && GOOD_VENV=false
if ! "$GOOD_VENV"; then
echo "Deleting bad python venv"
sudo rm -rf venv
fi
fi
# Create the venv if it doesn't exist
if ! test -e venv; then
echo "Creating python venv for RaSCSI control board service"
python3 -m venv venv --system-site-packages
echo "Activating venv"
source venv/bin/activate
echo "Installing requirements.txt"
pip3 install wheel
CFLAGS="$COMPILER_FLAGS" pip3 install -r requirements.txt
set +e
git rev-parse --is-inside-work-tree &> /dev/null
if [[ $? -eq 0 ]]; then
git rev-parse HEAD > current
fi
fi
source venv/bin/activate
# Detect if someone updates the git repo - we need to re-run pip3 install.
set +e
git rev-parse --is-inside-work-tree &> /dev/null
if [[ $? -eq 0 ]]; then
set -e
if ! test -e current; then
git rev-parse > current
elif [ "$(cat current)" != "$(git rev-parse HEAD)" ]; then
echo "New version detected, updating libraries from requirements.txt"
CFLAGS="$COMPILER_FLAGS" pip3 install -r requirements.txt
git rev-parse HEAD > current
fi
else
echo "Warning: Not running from a valid git repository. Will not be able to update the code."
fi
set -e
export PYTHONPATH=${PWD}/src:${PWD}/../common/src
echo "Starting RaSCSI control board service..."
if [[ ${PI_MODEL} =~ "Raspberry Pi 4" ]] || [[ ${PI_MODEL} =~ "Raspberry Pi 3" ]] ||
[[ ${PI_MODEL} =~ "Raspberry Pi Zero 2" ]]; then
echo "Detected: Raspberry Pi 4, Pi 3 or Pi Zero 2"
python3 src/main.py "$@" --transitions 1
else
echo "Detected: Raspberry Pi Zero, Zero W, Zero WH, ..."
echo "Transition animations will be disabled."
python3 src/main.py "$@" --transitions 0
fi

View File

@ -1,21 +0,0 @@
"""
Module with methods that interact with the Pi's Linux system
"""
def get_ip_and_host():
"""
Use a mock socket connection to identify the Pi's hostname and IP address
"""
from socket import socket, gethostname, AF_INET, SOCK_DGRAM
host = gethostname()
sock = socket(AF_INET, SOCK_DGRAM)
try:
# mock ip address; doesn't have to be reachable
sock.connect(('10.255.255.255', 1))
ip_addr = sock.getsockname()[0]
except Exception:
ip_addr = False
finally:
sock.close()
return ip_addr, host

View File

@ -39,9 +39,9 @@ from board import I2C
from adafruit_ssd1306 import SSD1306_I2C
from PIL import Image, ImageDraw, ImageFont
from interrupt_handler import GracefulInterruptHandler
from pi_cmds import get_ip_and_host
from rascsi.ractl_cmds import RaCtlCmds
from rascsi.socket_cmds import SocketCmds
from rascsi.sys_cmds import SysCmds
parser = argparse.ArgumentParser(description="RaSCSI OLED Monitor script")
parser.add_argument(
@ -99,6 +99,7 @@ TOKEN = args.password
sock_cmd = SocketCmds(host=args.rascsi_host, port=args.rascsi_port)
ractl_cmd = RaCtlCmds(sock_cmd=sock_cmd, token=TOKEN)
sys_cmd = SysCmds()
WIDTH = 128
BORDER = 5
@ -159,9 +160,9 @@ LINE_SPACING = 8
# Some other nice fonts to try: http://www.dafont.com/bitmap.php
FONT = ImageFont.truetype('resources/type_writer.ttf', FONT_SIZE)
IP_ADDR, HOSTNAME = get_ip_and_host()
IP_ADDR, HOSTNAME = sys_cmd.get_ip_and_host()
REMOVABLE_DEVICE_TYPES = ractl_cmd.get_removable_device_types()
SUPPORT_DEVICE_TYPES = ractl_cmd.get_support_device_types()
PERIPHERAL_DEVICE_TYPES = ractl_cmd.get_peripheral_device_types()
def formatted_output():
"""
@ -183,7 +184,7 @@ def formatted_output():
else:
output.append(f"{line['id']} {line['device_type'][2:4]} {line['status']}")
# Special handling of devices that don't use image files
elif line["device_type"] in (SUPPORT_DEVICE_TYPES):
elif line["device_type"] in PERIPHERAL_DEVICE_TYPES:
if line["vendor"] == "RaSCSI":
output.append(f"{line['id']} {line['device_type'][2:4]} {line['product']}")
else:

View File

@ -1,90 +0,0 @@
"""
Module for RaSCSI device management utility methods
"""
from flask_babel import _
def get_valid_scsi_ids(devices, reserved_ids):
"""
Takes a list of (dict)s devices, and list of (int)s reserved_ids.
Returns:
- (list) of (int)s valid_ids, which are the SCSI ids that are not reserved
- (int) recommended_id, which is the id that the Web UI should default to recommend
"""
occupied_ids = []
for device in devices:
occupied_ids.append(device["id"])
unoccupied_ids = [i for i in list(range(8)) if i not in reserved_ids + occupied_ids]
unoccupied_ids.sort()
valid_ids = [i for i in list(range(8)) if i not in reserved_ids]
valid_ids.sort(reverse=True)
if unoccupied_ids:
recommended_id = unoccupied_ids[-1]
else:
recommended_id = occupied_ids.pop(0)
return valid_ids, recommended_id
def sort_and_format_devices(devices):
"""
Takes a (list) of (dict)s devices and returns a (list) of (dict)s.
Sorts by SCSI ID acending (0 to 7).
For SCSI IDs where no device is attached, inject a (dict) with placeholder text.
"""
occupied_ids = []
for device in devices:
occupied_ids.append(device["id"])
formatted_devices = devices
# Add padding devices and sort the list
for i in range(8):
if i not in occupied_ids:
formatted_devices.append({"id": i, "device_type": "-", \
"status": "-", "file": "-", "product": "-"})
# Sort list of devices by id
formatted_devices.sort(key=lambda dic: str(dic["id"]))
return formatted_devices
def map_device_types_and_names(device_types):
"""
Takes a (dict) corresponding to the data structure returned by RaCtlCmds.get_device_types()
Returns a (dict) of device_type:device_name mappings of localized device names
"""
for key, value in device_types.items():
device_types[key]["name"] = get_device_name(key)
return device_types
def get_device_name(device_type):
"""
Takes a four letter device acronym (str) device_type.
Returns the human-readable name for the device type.
"""
if device_type == "SAHD":
return _("SASI Hard Disk")
elif device_type == "SCHD":
return _("SCSI Hard Disk")
elif device_type == "SCRM":
return _("Removable Disk")
elif device_type == "SCMO":
return _("Magneto-Optical")
elif device_type == "SCCD":
return _("CD / DVD")
elif device_type == "SCBR":
return _("X68000 Host Bridge")
elif device_type == "SCDP":
return _("DaynaPORT SCSI/Link")
elif device_type == "SCLP":
return _("Printer")
elif device_type == "SCHS":
return _("Host Services")
else:
return device_type

View File

@ -1,156 +0,0 @@
"""
Module for methods controlling and getting information about the Pi's Linux system
"""
import subprocess
import logging
from flask_babel import _
from settings import AUTH_GROUP
def running_env():
"""
Returns (str) git and (str) env
git contains the git hash of the checked out code
env is the various system information where this app is running
"""
try:
ra_git_version = (
subprocess.run(
["git", "rev-parse", "HEAD"],
capture_output=True,
check=True,
)
.stdout.decode("utf-8")
.strip()
)
except subprocess.CalledProcessError as error:
logging.warning("Executed shell command: %s", " ".join(error.cmd))
logging.warning("Got error: %s", error.stderr.decode("utf-8"))
ra_git_version = ""
try:
pi_version = (
subprocess.run(
["uname", "-a"],
capture_output=True,
check=True,
)
.stdout.decode("utf-8")
.strip()
)
except subprocess.CalledProcessError as error:
logging.warning("Executed shell command: %s", " ".join(error.cmd))
logging.warning("Got error: %s", error.stderr.decode("utf-8"))
pi_version = "Unknown"
return {"git": ra_git_version, "env": pi_version}
def running_proc(daemon):
"""
Takes (str) daemon
Returns (int) proc, which is the number of processes currently running
"""
try:
processes = (
subprocess.run(
["ps", "aux"],
capture_output=True,
check=True,
)
.stdout.decode("utf-8")
.strip()
)
except subprocess.CalledProcessError as error:
logging.warning("Executed shell command: %s", " ".join(error.cmd))
logging.warning("Got error: %s", error.stderr.decode("utf-8"))
processes = ""
from re import findall
matching_processes = findall(daemon, processes)
return len(matching_processes)
def is_bridge_setup():
"""
Returns (bool) True if the rascsi_bridge network interface exists
"""
try:
bridges = (
subprocess.run(
["brctl", "show"],
capture_output=True,
check=True,
)
.stdout.decode("utf-8")
.strip()
)
except subprocess.CalledProcessError as error:
logging.warning("Executed shell command: %s", " ".join(error.cmd))
logging.warning("Got error: %s", error.stderr.decode("utf-8"))
bridges = ""
if "rascsi_bridge" in bridges:
return True
return False
def disk_space():
"""
Returns a (dict) with (int) total (int) used (int) free
This is the disk space information of the volume where this app is running
"""
from shutil import disk_usage
total, used, free = disk_usage(__file__)
return {"total": total, "used": used, "free": free}
def get_ip_address():
"""
Use a mock socket connection to identify the Pi's IP address
"""
from socket import socket, AF_INET, SOCK_DGRAM
sock = socket(AF_INET, SOCK_DGRAM)
try:
# mock ip address; doesn't have to be reachable
sock.connect(('10.255.255.255', 1))
ip_addr = sock.getsockname()[0]
except Exception:
ip_addr = '127.0.0.1'
finally:
sock.close()
return ip_addr
def introspect_file(file_path, re_term):
"""
Takes a (str) file_path and (str) re_term in regex format
Will introspect file_path for the existance of re_term
and return True if found, False if not found
"""
from re import match
try:
ifile = open(file_path, "r", encoding="ISO-8859-1")
except:
return False
for line in ifile:
if match(re_term, line):
return True
return False
def auth_active():
"""
Inspects if the group defined in AUTH_GROUP exists on the system.
If it exists, tell the webapp to enable authentication.
Returns a (dict) with (bool) status and (str) msg
"""
from grp import getgrall
groups = [g.gr_name for g in getgrall()]
if AUTH_GROUP in groups:
return {
"status": True,
"msg": _("You must log in to use this function"),
}
return {"status": False, "msg": ""}

View File

@ -41,5 +41,8 @@ class ReturnCodeMapper:
parameters = payload["parameters"]
payload["msg"] = lazy_gettext(ReturnCodeMapper.MESSAGES[payload["return_code"]], **parameters)
payload["msg"] = lazy_gettext(
ReturnCodeMapper.MESSAGES[payload["return_code"]],
**parameters,
)
return payload

View File

@ -1,7 +1,7 @@
<!doctype html>
<html>
<head>
<title>{{ _("RaSCSI Control Page") }}</title>
<title>{{ _("RaSCSI Control Page") }} [{{ host }}]</title>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
<link rel="apple-touch-icon" sizes="57x57" href="/pwa/apple-icon-57x57.png">
@ -52,22 +52,27 @@
<div>{{ _("Log In to Use Web Interface") }}</div>
<input type="text" name="username" placeholder="{{ _("Username") }}">
<input type="password" name="password" placeholder="{{ _("Password") }}">
<input type="submit" value="Login">
<input type="submit" value="Login">
</form>
</span>
{% endif %}
{% else %}
<span style="display: inline-block; width: 100%; color: white; background-color: green; text-align: center; vertical-align: center; font-family: Arial, Helvetica, sans-serif;">{{ _("Web Interface Authentication Disabled") }} &#8211; {{ _("See <a href=\"%(url)s\" target=\"_blank\">Wiki</a> for more information", url="https://github.com/akuker/RASCSI/wiki/Web-Interface#enable-authentication") }}</span>
{% endif %}
<table width="100%">
<table width="100%" style="background-color: black;">
<tbody>
<tr style="background-color: black;">
<td style="background-color: black;">
<tr align="center">
<td>
<a href="http://github.com/akuker/RASCSI" target="_blank">
<h1>RaSCSI - 68kmla Edition</h1>
<h1>{{ _("RaSCSI Control Page") }}</h1>
</a>
</td>
</tr>
<tr>
<td style="color: white;">
hostname: {{ host }} ip: {{ ip_addr }}
</td>
</tr>
</tbody>
</table>
</div>

View File

@ -65,7 +65,7 @@
<input name="type" type="hidden" value="{{ device.device_type }}">
<input name="file_size" type="hidden" value="{{ device.size }}">
<select type="select" name="file_name">
{% for f in files %}
{% for f in files|sort(attribute='name') %}
{% if device.device_type == "SCCD" %}
{% if f["name"].lower().endswith(cdrom_file_suffix) %}
<option value="{{ f["name"] }}">{{ f["name"].replace(base_dir, '') }}</option>
@ -601,17 +601,23 @@
<td style="border: none; vertical-align:top;">
<form action="/logs/show" method="post">
<label for="lines">{{ _("Log Lines:") }}</label>
<input name="lines" type="number" placeholder="200" min="1" size="4">
<input name="lines" type="number" value="200" min="1" size="4">
<label for="scope">{{ _("Scope:") }}</label>
<select name="scope">
<option value="default">
default
<option value="">
{{ _("All logs") }}
</option>
<option value="rascsi">
rascsi.service
rascsi
</option>
<option value="rascsi-web">
rascsi-web.service
rascsi-web
</option>
<option value="rascsi-oled">
rascsi-oled
</option>
<option value="rascsi-ctrlboard">
rascsi-ctrlboard
</option>
</select>
<input type="submit" value="{{ _("Show Logs") }}">

View File

@ -6,6 +6,13 @@ import logging
import argparse
from pathlib import Path
from functools import wraps
from grp import getgrall
from ast import literal_eval
import bjoern
from werkzeug.utils import secure_filename
from simplepam import authenticate
from flask_babel import Babel, Locale, refresh, _
from flask import (
Flask,
@ -20,26 +27,30 @@ from flask import (
session,
abort,
)
from flask_babel import Babel, Locale, refresh, _
from pi_cmds import (
running_env,
running_proc,
is_bridge_setup,
disk_space,
get_ip_address,
introspect_file,
auth_active,
from rascsi.ractl_cmds import RaCtlCmds
from rascsi.file_cmds import FileCmds
from rascsi.sys_cmds import SysCmds
from rascsi.common_settings import (
CFG_DIR,
CONFIG_FILE_SUFFIX,
PROPERTIES_SUFFIX,
RESERVATIONS,
)
from device_utils import (
from return_code_mapper import ReturnCodeMapper
from socket_cmds_flask import SocketCmdsFlask
from web_utils import (
sort_and_format_devices,
get_valid_scsi_ids,
map_device_types_and_names,
get_device_name,
auth_active,
is_bridge_configured,
upload_with_dropzonejs,
)
from return_code_mapper import ReturnCodeMapper
from settings import (
AFP_DIR,
MAX_FILE_SIZE,
@ -50,17 +61,6 @@ from settings import (
LANGUAGES,
)
from rascsi.common_settings import (
CFG_DIR,
CONFIG_FILE_SUFFIX,
PROPERTIES_SUFFIX,
RESERVATIONS,
)
from rascsi.ractl_cmds import RaCtlCmds
from rascsi.file_cmds import FileCmds
from socket_cmds_flask import SocketCmdsFlask
APP = Flask(__name__)
BABEL = Babel(APP)
@ -93,12 +93,13 @@ def get_supported_locales():
return sorted_locales
# pylint: disable=too-many-locals
@APP.route("/")
def index():
"""
Sets up data structures for and renders the index page
"""
if not ractl.is_token_auth()["status"] and not APP.config["TOKEN"]:
if not ractl_cmd.is_token_auth()["status"] and not APP.config["TOKEN"]:
abort(
403,
_(
@ -107,11 +108,12 @@ def index():
),
)
server_info = ractl.get_server_info()
devices = ractl.list_devices()
device_types = map_device_types_and_names(ractl.get_device_types()["device_types"])
image_files = file_cmds.list_images()
config_files = file_cmds.list_config_files()
server_info = ractl_cmd.get_server_info()
devices = ractl_cmd.list_devices()
device_types = map_device_types_and_names(ractl_cmd.get_device_types()["device_types"])
image_files = file_cmd.list_images()
config_files = file_cmd.list_config_files()
ip_addr, host = sys_cmd.get_ip_and_host()
extended_image_files = []
for image in image_files["files"]:
@ -147,10 +149,11 @@ def index():
return render_template(
"index.html",
locales=get_supported_locales(),
bridge_configured=is_bridge_setup(),
netatalk_configured=running_proc("afpd"),
macproxy_configured=running_proc("macproxy"),
ip_addr=get_ip_address(),
bridge_configured=sys_cmd.is_bridge_setup(),
netatalk_configured=sys_cmd.running_proc("afpd"),
macproxy_configured=sys_cmd.running_proc("macproxy"),
ip_addr=ip_addr,
host=host,
devices=formatted_devices,
files=extended_image_files,
config_files=config_files,
@ -165,24 +168,24 @@ def index():
reserved_scsi_ids=reserved_scsi_ids,
RESERVATIONS=RESERVATIONS,
max_file_size=int(int(MAX_FILE_SIZE) / 1024 / 1024),
running_env=running_env(),
running_env=sys_cmd.running_env(),
version=server_info["version"],
log_levels=server_info["log_levels"],
current_log_level=server_info["current_log_level"],
netinfo=ractl.get_network_info(),
netinfo=ractl_cmd.get_network_info(),
device_types=device_types,
free_disk=int(disk_space()["free"] / 1024 / 1024),
free_disk=int(sys_cmd.disk_space()["free"] / 1024 / 1024),
valid_file_suffix=valid_file_suffix,
cdrom_file_suffix=tuple(server_info["sccd"]),
removable_file_suffix=tuple(server_info["scrm"]),
mo_file_suffix=tuple(server_info["scmo"]),
username=username,
auth_active=auth_active()["status"],
auth_active=auth_active(AUTH_GROUP)["status"],
ARCHIVE_FILE_SUFFIX=ARCHIVE_FILE_SUFFIX,
PROPERTIES_SUFFIX=PROPERTIES_SUFFIX,
REMOVABLE_DEVICE_TYPES=ractl.get_removable_device_types(),
DISK_DEVICE_TYPES=ractl.get_disk_device_types(),
PERIPHERAL_DEVICE_TYPES=ractl.get_peripheral_device_types(),
REMOVABLE_DEVICE_TYPES=ractl_cmd.get_removable_device_types(),
DISK_DEVICE_TYPES=ractl_cmd.get_disk_device_types(),
PERIPHERAL_DEVICE_TYPES=ractl_cmd.get_peripheral_device_types(),
)
@ -195,7 +198,7 @@ def drive_list():
# The file resides in the current dir of the web ui process
drive_properties = Path(DRIVE_PROPERTIES_FILE)
if drive_properties.is_file():
process = file_cmds.read_drive_properties(str(drive_properties))
process = file_cmd.read_drive_properties(str(drive_properties))
process = ReturnCodeMapper.add_msg(process)
if not process["status"]:
flash(process["msg"], "error")
@ -215,7 +218,6 @@ def drive_list():
cd_conf = []
rm_conf = []
from werkzeug.utils import secure_filename
for device in conf:
if device["device_type"] == "SCHD":
device["secure_name"] = secure_filename(device["name"])
@ -234,21 +236,21 @@ def drive_list():
else:
username = None
server_info = ractl.get_server_info()
server_info = ractl_cmd.get_server_info()
return render_template(
"drives.html",
files=file_cmds.list_images()["files"],
files=file_cmd.list_images()["files"],
base_dir=server_info["image_dir"],
hd_conf=hd_conf,
cd_conf=cd_conf,
rm_conf=rm_conf,
running_env=running_env(),
running_env=sys_cmd.running_env(),
version=server_info["version"],
free_disk=int(disk_space()["free"] / 1024 / 1024),
free_disk=int(sys_cmd.disk_space()["free"] / 1024 / 1024),
cdrom_file_suffix=tuple(server_info["sccd"]),
username=username,
auth_active=auth_active()["status"],
auth_active=auth_active(AUTH_GROUP)["status"],
)
@ -260,9 +262,6 @@ def login():
username = request.form["username"]
password = request.form["password"]
from simplepam import authenticate
from grp import getgrall
groups = [g.gr_name for g in getgrall() if username in g.gr_mem]
if AUTH_GROUP in groups:
if authenticate(str(username), str(password)):
@ -287,12 +286,12 @@ def logout():
return redirect(url_for("index"))
@APP.route("/pwa/<path:path>")
def send_pwa_files(path):
@APP.route("/pwa/<path:pwa_path>")
def send_pwa_files(pwa_path):
"""
Sets up mobile web resources
"""
return send_from_directory("pwa", path)
return send_from_directory("pwa", pwa_path)
def login_required(func):
@ -301,7 +300,7 @@ def login_required(func):
"""
@wraps(func)
def decorated_function(*args, **kwargs):
auth = auth_active()
auth = auth_active(AUTH_GROUP)
if auth["status"] and "username" not in session:
flash(auth["msg"], "error")
return redirect(url_for("index"))
@ -325,7 +324,7 @@ def drive_create():
full_file_name = file_name + "." + file_type
# Creating the image file
process = file_cmds.create_new_image(file_name, file_type, size)
process = file_cmd.create_new_image(file_name, file_type, size)
if process["status"]:
flash(_("Image file created: %(file_name)s", file_name=full_file_name))
else:
@ -340,7 +339,7 @@ def drive_create():
"revision": revision,
"block_size": block_size,
}
process = file_cmds.write_drive_properties(prop_file_name, properties)
process = file_cmd.write_drive_properties(prop_file_name, properties)
process = ReturnCodeMapper.add_msg(process)
if process["status"]:
flash(process["msg"])
@ -370,7 +369,7 @@ def drive_cdrom():
"revision": revision,
"block_size": block_size,
}
process = file_cmds.write_drive_properties(file_name, properties)
process = file_cmd.write_drive_properties(file_name, properties)
process = ReturnCodeMapper.add_msg(process)
if process["status"]:
flash(process["msg"])
@ -389,7 +388,7 @@ def config_save():
file_name = request.form.get("name") or "default"
file_name = f"{file_name}.{CONFIG_FILE_SUFFIX}"
process = file_cmds.write_config(file_name)
process = file_cmd.write_config(file_name)
process = ReturnCodeMapper.add_msg(process)
if process["status"]:
flash(process["msg"])
@ -408,7 +407,7 @@ def config_load():
file_name = request.form.get("name")
if "load" in request.form:
process = file_cmds.read_config(file_name)
process = file_cmd.read_config(file_name)
process = ReturnCodeMapper.add_msg(process)
if process["status"]:
flash(process["msg"])
@ -417,7 +416,7 @@ def config_load():
flash(process['msg'], "error")
return redirect(url_for("index"))
if "delete" in request.form:
process = file_cmds.delete_file(f"{CFG_DIR}/{file_name}")
process = file_cmd.delete_file(f"{CFG_DIR}/{file_name}")
process = ReturnCodeMapper.add_msg(process)
if process["status"]:
flash(process["msg"])
@ -436,29 +435,16 @@ def show_logs():
"""
Displays system logs
"""
lines = request.form.get("lines") or "200"
scope = request.form.get("scope") or "default"
lines = request.form.get("lines")
scope = request.form.get("scope")
from subprocess import run
if scope != "default":
process = run(
["journalctl", "-n", lines, "-u", scope],
capture_output=True,
check=True,
)
else:
process = run(
["journalctl", "-n", lines],
capture_output=True,
check=True,
)
if process.returncode == 0:
returncode, logs = sys_cmd.get_logs(lines, scope)
if returncode == 0:
headers = {"content-type": "text/plain"}
return process.stdout.decode("utf-8"), int(lines), headers
return logs, int(lines), headers
flash(_("An error occurred when fetching logs."))
flash(process.stderr.decode("utf-8"), "stderr")
flash(logs, "stderr")
return redirect(url_for("index"))
@ -470,7 +456,7 @@ def log_level():
"""
level = request.form.get("level") or "info"
process = ractl.set_log_level(level)
process = ractl_cmd.set_log_level(level)
if process["status"]:
flash(_("Log level set to %(value)s", value=level))
return redirect(url_for("index"))
@ -502,31 +488,18 @@ def attach_device():
error_msg = _("Please follow the instructions at %(url)s", url=error_url)
if "interface" in params.keys():
if params["interface"].startswith("wlan"):
if not introspect_file("/etc/sysctl.conf", r"^net\.ipv4\.ip_forward=1$"):
flash(_("Configure IPv4 forwarding before using a wireless network device."), "error")
flash(error_msg, "error")
return redirect(url_for("index"))
if not Path("/etc/iptables/rules.v4").is_file():
flash(_("Configure NAT before using a wireless network device."), "error")
flash(error_msg, "error")
return redirect(url_for("index"))
else:
if not introspect_file("/etc/dhcpcd.conf", r"^denyinterfaces " + params["interface"] + r"$"):
flash(_("Configure the network bridge before using a wired network device."), "error")
flash(error_msg, "error")
return redirect(url_for("index"))
if not Path("/etc/network/interfaces.d/rascsi_bridge").is_file():
flash(_("Configure the network bridge before using a wired network device."), "error")
flash(error_msg, "error")
return redirect(url_for("index"))
bridge_status = is_bridge_configured(params["interface"])
if bridge_status:
flash(bridge_status, "error")
flash(error_msg, "error")
return redirect(url_for("index"))
kwargs = {
"unit": int(unit),
"device_type": device_type,
"params": params,
}
process = ractl.attach_device(scsi_id, **kwargs)
process = ractl_cmd.attach_device(scsi_id, **kwargs)
process = ReturnCodeMapper.add_msg(process)
if process["status"]:
flash(_(
@ -557,14 +530,14 @@ def attach_image():
if device_type:
kwargs["device_type"] = device_type
device_types = ractl.get_device_types()
device_types = ractl_cmd.get_device_types()
expected_block_size = min(device_types["device_types"][device_type]["block_sizes"])
# Attempt to load the device properties file:
# same file name with PROPERTIES_SUFFIX appended
drive_properties = f"{CFG_DIR}/{file_name}.{PROPERTIES_SUFFIX}"
if Path(drive_properties).is_file():
process = file_cmds.read_drive_properties(drive_properties)
process = file_cmd.read_drive_properties(drive_properties)
process = ReturnCodeMapper.add_msg(process)
if not process["status"]:
flash(process["msg"], "error")
@ -576,7 +549,7 @@ def attach_image():
kwargs["block_size"] = conf["block_size"]
expected_block_size = conf["block_size"]
process = ractl.attach_device(scsi_id, **kwargs)
process = ractl_cmd.attach_device(scsi_id, **kwargs)
process = ReturnCodeMapper.add_msg(process)
if process["status"]:
flash(_((
@ -607,7 +580,7 @@ def detach_all_devices():
"""
Detaches all currently attached devices
"""
process = ractl.detach_all()
process = ractl_cmd.detach_all()
if process["status"]:
flash(_("Detached all SCSI devices"))
return redirect(url_for("index"))
@ -624,7 +597,7 @@ def detach():
"""
scsi_id = request.form.get("scsi_id")
unit = request.form.get("unit")
process = ractl.detach_by_id(scsi_id, unit)
process = ractl_cmd.detach_by_id(scsi_id, unit)
if process["status"]:
flash(_("Detached SCSI ID %(id_number)s LUN %(unit_number)s",
id_number=scsi_id, unit_number=unit))
@ -645,7 +618,7 @@ def eject():
scsi_id = request.form.get("scsi_id")
unit = request.form.get("unit")
process = ractl.eject_by_id(scsi_id, unit)
process = ractl_cmd.eject_by_id(scsi_id, unit)
if process["status"]:
flash(_("Ejected SCSI ID %(id_number)s LUN %(unit_number)s",
id_number=scsi_id, unit_number=unit))
@ -664,7 +637,7 @@ def device_info():
scsi_id = request.form.get("scsi_id")
unit = request.form.get("unit")
devices = ractl.list_devices(scsi_id, unit)
devices = ractl_cmd.list_devices(scsi_id, unit)
# First check if any device at all was returned
if not devices["status"]:
@ -700,9 +673,9 @@ def reserve_id():
"""
scsi_id = request.form.get("scsi_id")
memo = request.form.get("memo")
reserved_ids = ractl.get_reserved_ids()["ids"]
reserved_ids = ractl_cmd.get_reserved_ids()["ids"]
reserved_ids.extend(scsi_id)
process = ractl.reserve_scsi_ids(reserved_ids)
process = ractl_cmd.reserve_scsi_ids(reserved_ids)
if process["status"]:
RESERVATIONS[int(scsi_id)] = memo
flash(_("Reserved SCSI ID %(id_number)s", id_number=scsi_id))
@ -719,9 +692,9 @@ def release_id():
Releases the reservation of a SCSI ID as well as the memo for the reservation
"""
scsi_id = request.form.get("scsi_id")
reserved_ids = ractl.get_reserved_ids()["ids"]
reserved_ids = ractl_cmd.get_reserved_ids()["ids"]
reserved_ids.remove(scsi_id)
process = ractl.reserve_scsi_ids(reserved_ids)
process = ractl_cmd.reserve_scsi_ids(reserved_ids)
if process["status"]:
RESERVATIONS[int(scsi_id)] = ""
flash(_("Released the reservation for SCSI ID %(id_number)s", id_number=scsi_id))
@ -738,7 +711,7 @@ def restart():
"""
Restarts the Pi
"""
ractl.shutdown_pi("reboot")
ractl_cmd.shutdown_pi("reboot")
return redirect(url_for("index"))
@ -748,7 +721,7 @@ def shutdown():
"""
Shuts down the Pi
"""
ractl.shutdown_pi("system")
ractl_cmd.shutdown_pi("system")
return redirect(url_for("index"))
@ -762,7 +735,7 @@ def download_to_iso():
url = request.form.get("url")
iso_args = request.form.get("type").split()
process = file_cmds.download_file_to_iso(url, *iso_args)
process = file_cmd.download_file_to_iso(url, *iso_args)
process = ReturnCodeMapper.add_msg(process)
if process["status"]:
flash(process["msg"])
@ -772,7 +745,7 @@ def download_to_iso():
flash(process["msg"], "error")
return redirect(url_for("index"))
process_attach = ractl.attach_device(
process_attach = ractl_cmd.attach_device(
scsi_id,
device_type="SCCD",
params={"file": process["file_name"]},
@ -795,8 +768,8 @@ def download_img():
Downloads a remote file onto the images dir on the Pi
"""
url = request.form.get("url")
server_info = ractl.get_server_info()
process = file_cmds.download_to_dir(url, server_info["image_dir"], Path(url).name)
server_info = ractl_cmd.get_server_info()
process = file_cmd.download_to_dir(url, server_info["image_dir"], Path(url).name)
process = ReturnCodeMapper.add_msg(process)
if process["status"]:
flash(process["msg"])
@ -815,7 +788,7 @@ def download_afp():
"""
url = request.form.get("url")
file_name = Path(url).name
process = file_cmds.download_to_dir(url, AFP_DIR, file_name)
process = file_cmd.download_to_dir(url, AFP_DIR, file_name)
process = ReturnCodeMapper.add_msg(process)
if process["status"]:
flash(process["msg"])
@ -833,55 +806,12 @@ def upload_file():
Depending on the Dropzone.js JavaScript library
"""
# Due to the embedded javascript library, we cannot use the @login_required decorator
auth = auth_active()
auth = auth_active(AUTH_GROUP)
if auth["status"] and "username" not in session:
return make_response(auth["msg"], 403)
from werkzeug.utils import secure_filename
from os import path
log = logging.getLogger("pydrop")
file_object = request.files["file"]
file_name = secure_filename(file_object.filename)
server_info = ractl.get_server_info()
save_path = path.join(server_info["image_dir"], file_name)
current_chunk = int(request.form['dzchunkindex'])
# Makes sure not to overwrite an existing file,
# but continues writing to a file transfer in progress
if path.exists(save_path) and current_chunk == 0:
return make_response(_("The file already exists!"), 400)
try:
with open(save_path, "ab") as save:
save.seek(int(request.form["dzchunkbyteoffset"]))
save.write(file_object.stream.read())
except OSError:
log.exception("Could not write to file")
return make_response(_("Unable to write the file to disk!"), 500)
total_chunks = int(request.form["dztotalchunkcount"])
if current_chunk + 1 == total_chunks:
# Validate the resulting file size after writing the last chunk
if path.getsize(save_path) != int(request.form["dztotalfilesize"]):
log.error(
"Finished transferring %s, "
"but it has a size mismatch with the original file. "
"Got %s but we expected %s.",
file_object.filename,
path.getsize(save_path),
request.form['dztotalfilesize'],
)
return make_response(_("Transferred file corrupted!"), 500)
log.info("File %s has been uploaded successfully", file_object.filename)
log.debug("Chunk %s of %s for file %s completed.",
current_chunk + 1, total_chunks, file_object.filename)
return make_response(_("File upload successful!"), 200)
server_info = ractl_cmd.get_server_info()
return upload_with_dropzonejs(server_info["image_dir"])
@APP.route("/files/create", methods=["POST"])
@ -895,7 +825,7 @@ def create_file():
file_type = request.form.get("type")
full_file_name = file_name + "." + file_type
process = file_cmds.create_new_image(file_name, file_type, size)
process = file_cmd.create_new_image(file_name, file_type, size)
if process["status"]:
flash(_("Image file created: %(file_name)s", file_name=full_file_name))
return redirect(url_for("index"))
@ -922,7 +852,7 @@ def delete():
"""
file_name = request.form.get("file_name")
process = file_cmds.delete_image(file_name)
process = file_cmd.delete_image(file_name)
if process["status"]:
flash(_("Image file deleted: %(file_name)s", file_name=file_name))
else:
@ -932,7 +862,7 @@ def delete():
# Delete the drive properties file, if it exists
prop_file_path = f"{CFG_DIR}/{file_name}.{PROPERTIES_SUFFIX}"
if Path(prop_file_path).is_file():
process = file_cmds.delete_file(prop_file_path)
process = file_cmd.delete_file(prop_file_path)
process = ReturnCodeMapper.add_msg(process)
if process["status"]:
@ -954,7 +884,7 @@ def rename():
file_name = request.form.get("file_name")
new_file_name = request.form.get("new_file_name")
process = file_cmds.rename_image(file_name, new_file_name)
process = file_cmd.rename_image(file_name, new_file_name)
if process["status"]:
flash(_("Image file renamed to: %(file_name)s", file_name=new_file_name))
else:
@ -965,7 +895,7 @@ def rename():
prop_file_path = f"{CFG_DIR}/{file_name}.{PROPERTIES_SUFFIX}"
new_prop_file_path = f"{CFG_DIR}/{new_file_name}.{PROPERTIES_SUFFIX}"
if Path(prop_file_path).is_file():
process = file_cmds.rename_file(prop_file_path, new_prop_file_path)
process = file_cmd.rename_file(prop_file_path, new_prop_file_path)
process = ReturnCodeMapper.add_msg(process)
if process["status"]:
flash(process["msg"])
@ -987,11 +917,10 @@ def unzip():
zip_member = request.form.get("zip_member") or False
zip_members = request.form.get("zip_members") or False
from ast import literal_eval
if zip_members:
zip_members = literal_eval(zip_members)
process = file_cmds.unzip_file(zip_file, zip_member, zip_members)
process = file_cmd.unzip_file(zip_file, zip_member, zip_members)
if process["status"]:
if not process["msg"]:
flash(_("Aborted unzip: File(s) with the same name already exists."), "error")
@ -1015,8 +944,8 @@ def change_language():
"""
locale = request.form.get("locale")
session["language"] = locale
ractl.locale = session["language"]
file_cmds.locale = session["language"]
ractl_cmd.locale = session["language"]
file_cmd.locale = session["language"]
refresh()
language = Locale.parse(locale)
@ -1032,8 +961,8 @@ def detect_locale():
This requires the Flask app to have started first.
"""
session["language"] = get_locale()
ractl.locale = session["language"]
file_cmds.locale = session["language"]
ractl_cmd.locale = session["language"]
file_cmd.locale = session["language"]
if __name__ == "__main__":
@ -1074,12 +1003,12 @@ if __name__ == "__main__":
APP.config["TOKEN"] = arguments.password
sock_cmd = SocketCmdsFlask(host=arguments.rascsi_host, port=arguments.rascsi_port)
ractl = RaCtlCmds(sock_cmd=sock_cmd, token=APP.config["TOKEN"])
file_cmds = FileCmds(sock_cmd=sock_cmd, ractl=ractl, token=APP.config["TOKEN"])
ractl_cmd = RaCtlCmds(sock_cmd=sock_cmd, token=APP.config["TOKEN"])
file_cmd = FileCmds(sock_cmd=sock_cmd, ractl=ractl_cmd, token=APP.config["TOKEN"])
sys_cmd = SysCmds()
if Path(f"{CFG_DIR}/{DEFAULT_CONFIG}").is_file():
file_cmds.read_config(DEFAULT_CONFIG)
file_cmd.read_config(DEFAULT_CONFIG)
import bjoern
print("Serving rascsi-web...")
bjoern.run(APP, "0.0.0.0", arguments.port)

183
python/web/src/web_utils.py Normal file
View File

@ -0,0 +1,183 @@
"""
Module for RaSCSI Web Interface utility methods
"""
import logging
from grp import getgrall
from os import path
from pathlib import Path
from flask import request, make_response
from flask_babel import _
from werkzeug.utils import secure_filename
from rascsi.sys_cmds import SysCmds
def get_valid_scsi_ids(devices, reserved_ids):
"""
Takes a list of (dict)s devices, and list of (int)s reserved_ids.
Returns:
- (list) of (int)s valid_ids, which are the SCSI ids that are not reserved
- (int) recommended_id, which is the id that the Web UI should default to recommend
"""
occupied_ids = []
for device in devices:
occupied_ids.append(device["id"])
unoccupied_ids = [i for i in list(range(8)) if i not in reserved_ids + occupied_ids]
unoccupied_ids.sort()
valid_ids = [i for i in list(range(8)) if i not in reserved_ids]
valid_ids.sort(reverse=True)
if unoccupied_ids:
recommended_id = unoccupied_ids[-1]
else:
recommended_id = occupied_ids.pop(0)
return valid_ids, recommended_id
def sort_and_format_devices(devices):
"""
Takes a (list) of (dict)s devices and returns a (list) of (dict)s.
Sorts by SCSI ID acending (0 to 7).
For SCSI IDs where no device is attached, inject a (dict) with placeholder text.
"""
occupied_ids = []
for device in devices:
occupied_ids.append(device["id"])
formatted_devices = devices
# Add padding devices and sort the list
for i in range(8):
if i not in occupied_ids:
formatted_devices.append({"id": i, "device_type": "-", \
"status": "-", "file": "-", "product": "-"})
# Sort list of devices by id
formatted_devices.sort(key=lambda dic: str(dic["id"]))
return formatted_devices
def map_device_types_and_names(device_types):
"""
Takes a (dict) corresponding to the data structure returned by RaCtlCmds.get_device_types()
Returns a (dict) of device_type:device_name mappings of localized device names
"""
for device in device_types.keys():
device_types[device]["name"] = get_device_name(device)
return device_types
# pylint: disable=too-many-return-statements
def get_device_name(device_type):
"""
Takes a four letter device acronym (str) device_type.
Returns the human-readable name for the device type.
"""
if device_type == "SAHD":
return _("SASI Hard Disk")
if device_type == "SCHD":
return _("SCSI Hard Disk")
if device_type == "SCRM":
return _("Removable Disk")
if device_type == "SCMO":
return _("Magneto-Optical")
if device_type == "SCCD":
return _("CD / DVD")
if device_type == "SCBR":
return _("X68000 Host Bridge")
if device_type == "SCDP":
return _("DaynaPORT SCSI/Link")
if device_type == "SCLP":
return _("Printer")
if device_type == "SCHS":
return _("Host Services")
return device_type
def auth_active(group):
"""
Inspects if the group defined in (str) group exists on the system.
If it exists, tell the webapp to enable authentication.
Returns a (dict) with (bool) status and (str) msg
"""
groups = [g.gr_name for g in getgrall()]
if group in groups:
return {
"status": True,
"msg": _("You must log in to use this function"),
}
return {"status": False, "msg": ""}
def is_bridge_configured(interface):
"""
Takes (str) interface of a network device being attached.
Returns (bool) False if the network bridge is configured.
Returns (str) with an error message if the network bridge is not configured.
"""
sys_cmd = SysCmds()
if interface.startswith("wlan"):
if not sys_cmd.introspect_file("/etc/sysctl.conf", r"^net\.ipv4\.ip_forward=1$"):
return _("Configure IPv4 forwarding before using a wireless network device.")
if not Path("/etc/iptables/rules.v4").is_file():
return _("Configure NAT before using a wireless network device.")
else:
if not sys_cmd.introspect_file(
"/etc/dhcpcd.conf",
r"^denyinterfaces " + interface + r"$",
):
return _("Configure the network bridge before using a wired network device.")
if not Path("/etc/network/interfaces.d/rascsi_bridge").is_file():
return _("Configure the network bridge before using a wired network device.")
return False
def upload_with_dropzonejs(image_dir):
"""
Takes (str) image_dir which is the path to the image dir to store files.
Opens a stream to transfer a file via the embedded dropzonejs library.
"""
log = logging.getLogger("pydrop")
file_object = request.files["file"]
file_name = secure_filename(file_object.filename)
save_path = path.join(image_dir, file_name)
current_chunk = int(request.form['dzchunkindex'])
# Makes sure not to overwrite an existing file,
# but continues writing to a file transfer in progress
if path.exists(save_path) and current_chunk == 0:
return make_response(_("The file already exists!"), 400)
try:
with open(save_path, "ab") as save:
save.seek(int(request.form["dzchunkbyteoffset"]))
save.write(file_object.stream.read())
except OSError:
log.exception("Could not write to file")
return make_response(_("Unable to write the file to disk!"), 500)
total_chunks = int(request.form["dztotalchunkcount"])
if current_chunk + 1 == total_chunks:
# Validate the resulting file size after writing the last chunk
if path.getsize(save_path) != int(request.form["dztotalfilesize"]):
log.error(
"Finished transferring %s, "
"but it has a size mismatch with the original file. "
"Got %s but we expected %s.",
file_object.filename,
path.getsize(save_path),
request.form['dztotalfilesize'],
)
return make_response(_("Transferred file corrupted!"), 500)
log.info("File %s has been uploaded successfully", file_object.filename)
log.debug("Chunk %s of %s for file %s completed.",
current_chunk + 1, total_chunks, file_object.filename)
return make_response(_("File upload successful!"), 200)

View File

@ -184,7 +184,7 @@ ALL: all
docs: $(DOC_DIR)/rascsi_man_page.txt $(DOC_DIR)/rasctl_man_page.txt $(DOC_DIR)/scsimon_man_page.txt
$(BINDIR)/$(RASCSI): $(SRC_PROTOBUF) $(OBJ_RASCSI) | $(BINDIR)
$(CXX) $(CXXFLAGS) -o $@ $(OBJ_RASCSI) -lpthread -lz -lpcap -lprotobuf -lstdc++fs
$(CXX) $(CXXFLAGS) -o $@ $(OBJ_RASCSI) -lpthread -lpcap -lprotobuf -lstdc++fs
$(BINDIR)/$(RASCTL): $(SRC_PROTOBUF) $(OBJ_RASCTL) | $(BINDIR)
$(CXX) $(CXXFLAGS) -o $@ $(OBJ_RASCTL) -lpthread -lprotobuf -lstdc++fs

View File

@ -115,7 +115,7 @@ void SASIDEV::Connect(int id, BUS *bus)
// Set the logical unit
//
//---------------------------------------------------------------------------
void SASIDEV::SetUnit(int no, Device *dev)
void SASIDEV::SetUnit(int no, PrimaryDevice *dev)
{
ASSERT(no < UnitMax);
@ -626,7 +626,7 @@ void SASIDEV::DataOut()
void SASIDEV::Error(ERROR_CODES::sense_key sense_key, ERROR_CODES::asc asc, ERROR_CODES::status status)
{
// Get bus information
((GPIOBUS*)ctrl.bus)->Aquire();
ctrl.bus->Aquire();
// Reset check
if (ctrl.bus->GetRST()) {
@ -664,7 +664,7 @@ void SASIDEV::CmdTestUnitReady()
LOGTRACE("%s TEST UNIT READY Command ", __PRETTY_FUNCTION__);
// Command processing on drive
((Disk *)ctrl.device)->TestUnitReady(this);
ctrl.device->TestUnitReady(this);
}
//---------------------------------------------------------------------------
@ -690,7 +690,7 @@ void SASIDEV::CmdRequestSense()
LOGTRACE( "%s REQUEST SENSE Command ", __PRETTY_FUNCTION__);
// Command processing on drive
((Disk *)ctrl.device)->RequestSense(this);
ctrl.device->RequestSense(this);
}
//---------------------------------------------------------------------------
@ -847,8 +847,8 @@ void SASIDEV::CmdAssign()
{
LOGTRACE("%s ASSIGN Command ", __PRETTY_FUNCTION__);
// Command processing on drive
bool status = ((Disk *)ctrl.device)->CheckReady();
// Command processing on device
bool status = ctrl.device->CheckReady();
if (!status) {
// Failure (Error)
Error();
@ -871,8 +871,8 @@ void SASIDEV::CmdSpecify()
{
LOGTRACE("%s SPECIFY Command ", __PRETTY_FUNCTION__);
// Command processing on drive
bool status = ((Disk *)ctrl.device)->CheckReady();
// Command processing on device
bool status =ctrl.device->CheckReady();
if (!status) {
// Failure (Error)
Error();
@ -1143,10 +1143,12 @@ bool SASIDEV::XferOut(bool cont)
case SASIDEV::eCmdWrite16:
case SASIDEV::eCmdVerify10:
case SASIDEV::eCmdVerify16:
{
// If we're a host bridge, use the host bridge's SendMessage10 function
// TODO This class must not know about SCSIBR
if (device->IsBridge()) {
if (!((SCSIBR*)device)->SendMessage10(ctrl.cmd, ctrl.buffer)) {
SCSIBR *bridge = dynamic_cast<SCSIBR *>(device);
if (bridge) {
if (!bridge->SendMessage10(ctrl.cmd, ctrl.buffer)) {
// write failed
return false;
}
@ -1158,8 +1160,9 @@ bool SASIDEV::XferOut(bool cont)
// Special case Write function for DaynaPort
// TODO This class must not know about DaynaPort
if (device->IsDaynaPort()) {
if (!((SCSIDaynaPort*)device)->Write(ctrl.cmd, ctrl.buffer, ctrl.length)) {
SCSIDaynaPort *daynaport = dynamic_cast<SCSIDaynaPort *>(device);
if (daynaport) {
if (!daynaport->Write(ctrl.cmd, ctrl.buffer, ctrl.length)) {
// write failed
return false;
}
@ -1191,6 +1194,7 @@ bool SASIDEV::XferOut(bool cont)
// If normal, work setting
ctrl.offset = 0;
break;
}
// SPECIFY(SASI only)
case SASIDEV::eCmdInvalid:

View File

@ -21,6 +21,7 @@
#include "fileio.h"
class Device;
class PrimaryDevice;
//===========================================================================
//
@ -123,10 +124,10 @@ public:
DWORD length; // Transfer remaining length
// Logical unit
Device *unit[UnitMax];
PrimaryDevice *unit[UnitMax];
// The current device
Device *device;
PrimaryDevice *device;
// The LUN from the IDENTIFY message
int lun;
@ -144,7 +145,7 @@ public:
// Connect
void Connect(int id, BUS *sbus); // Controller connection
Device* GetUnit(int no); // Get logical unit
void SetUnit(int no, Device *dev); // Logical unit setting
void SetUnit(int no, PrimaryDevice *dev); // Logical unit setting
bool HasUnit(); // Has a valid logical unit
// Other

View File

@ -27,6 +27,7 @@
SCSIDEV::SCSIDEV() : SASIDEV()
{
scsi.is_byte_transfer = false;
scsi.bytes_to_transfer = 0;
shutdown_mode = NONE;
@ -45,6 +46,7 @@ SCSIDEV::~SCSIDEV()
void SCSIDEV::Reset()
{
scsi.is_byte_transfer = false;
scsi.bytes_to_transfer = 0;
// Work initialization
@ -63,7 +65,7 @@ BUS::phase_t SCSIDEV::Process(int initiator_id)
}
// Get bus information
((GPIOBUS*)ctrl.bus)->Aquire();
ctrl.bus->Aquire();
// Check to see if the reset signal was asserted
if (ctrl.bus->GetRST()) {
@ -164,18 +166,25 @@ void SCSIDEV::BusFree()
// When the bus is free RaSCSI or the Pi may be shut down
switch(shutdown_mode) {
case RASCSI:
case STOP_RASCSI:
LOGINFO("RaSCSI shutdown requested");
exit(0);
break;
case PI:
case STOP_PI:
LOGINFO("Raspberry Pi shutdown requested");
if (system("init 0") == -1) {
LOGERROR("Raspberry Pi shutdown failed: %s", strerror(errno));
}
break;
case RESTART_PI:
LOGINFO("Raspberry Pi restart requested");
if (system("init 6") == -1) {
LOGERROR("Raspberry Pi restart failed: %s", strerror(errno));
}
break;
default:
break;
}
@ -349,7 +358,7 @@ void SCSIDEV::MsgOut()
void SCSIDEV::Error(ERROR_CODES::sense_key sense_key, ERROR_CODES::asc asc, ERROR_CODES::status status)
{
// Get bus information
((GPIOBUS*)ctrl.bus)->Aquire();
ctrl.bus->Aquire();
// Reset check
if (ctrl.bus->GetRST()) {
@ -395,24 +404,11 @@ void SCSIDEV::Send()
ASSERT(!ctrl.bus->GetREQ());
ASSERT(ctrl.bus->GetIO());
//if Length! = 0, send
if (ctrl.length != 0) {
LOGTRACE("%s%s", __PRETTY_FUNCTION__, (" Sending handhake with offset " + to_string(ctrl.offset) + ", length "
+ to_string(ctrl.length)).c_str());
// TODO Get rid of Daynaport specific code in this class
// The Daynaport needs to have a delay after the size/flags field
// of the read response. In the MacOS driver, it looks like the
// driver is doing two "READ" system calls.
int len;
if (ctrl.unit[0] && ctrl.unit[0]->IsDaynaPort()) {
len = ((GPIOBUS*)ctrl.bus)->SendHandShake(
&ctrl.buffer[ctrl.offset], ctrl.length, SCSIDaynaPort::DAYNAPORT_READ_HEADER_SZ);
}
else
{
len = ctrl.bus->SendHandShake(&ctrl.buffer[ctrl.offset], ctrl.length, BUS::SEND_NO_DELAY);
}
int len = ctrl.bus->SendHandShake(&ctrl.buffer[ctrl.offset], ctrl.length, ctrl.unit[0]->GetSendDelay());
// If you cannot send all, move to status phase
if (len != (int)ctrl.length) {
@ -515,13 +511,13 @@ void SCSIDEV::Receive()
// Length != 0 if received
if (ctrl.length != 0) {
LOGTRACE("%s length is %d", __PRETTY_FUNCTION__, (int)ctrl.length);
LOGTRACE("%s Length is %d bytes", __PRETTY_FUNCTION__, (int)ctrl.length);
// Receive
len = ctrl.bus->ReceiveHandShake(&ctrl.buffer[ctrl.offset], ctrl.length);
// If not able to receive all, move to status phase
if (len != (int)ctrl.length) {
LOGERROR("%s Not able to receive %d data, only received %d. Going to error",__PRETTY_FUNCTION__, (int)ctrl.length, len);
LOGERROR("%s Not able to receive %d bytes of data, only received %d. Going to error",__PRETTY_FUNCTION__, (int)ctrl.length, len);
Error();
return;
}
@ -651,7 +647,7 @@ void SCSIDEV::Receive()
// Transfer period factor (limited to 50 x 4 = 200ns)
scsi.syncperiod = scsi.msb[i + 3];
if (scsi.syncperiod > 50) {
scsi.syncoffset = 50;
scsi.syncperiod = 50;
}
// REQ/ACK offset(limited to 16)
@ -729,13 +725,13 @@ void SCSIDEV::ReceiveBytes()
ASSERT(!ctrl.bus->GetIO());
if (ctrl.length) {
LOGTRACE("%s length is %d", __PRETTY_FUNCTION__, ctrl.length);
LOGTRACE("%s Length is %d bytes", __PRETTY_FUNCTION__, ctrl.length);
len = ctrl.bus->ReceiveHandShake(&ctrl.buffer[ctrl.offset], ctrl.length);
// If not able to receive all, move to status phase
if (len != ctrl.length) {
LOGERROR("%s Not able to receive %d data, only received %d. Going to error",
LOGERROR("%s Not able to receive %d bytes of data, only received %d. Going to error",
__PRETTY_FUNCTION__, ctrl.length, len);
Error();
return;
@ -897,6 +893,8 @@ bool SCSIDEV::XferOut(bool cont)
ASSERT(ctrl.phase == BUS::dataout);
scsi.is_byte_transfer = false;
PrimaryDevice *device = dynamic_cast<PrimaryDevice *>(ctrl.unit[GetEffectiveLun()]);
if (device && ctrl.cmd[0] == scsi_defs::eCmdWrite6) {
return device->WriteBytes(ctrl.buffer, scsi.bytes_to_transfer);

View File

@ -29,8 +29,9 @@ public:
enum rascsi_shutdown_mode {
NONE,
RASCSI,
PI
STOP_RASCSI,
STOP_PI,
RESTART_PI
};
// Internal data definition

View File

@ -6,7 +6,6 @@
// Powered by XM6 TypeG Technology.
// Copyright (C) 2016-2020 GIMONS
//
// Imported NetBSD support and some optimisation patch by Rin Okuyama.
// Imported sava's bugfix patch(in RASDRV DOS edition).
//
// [ Host File System for the X68000 ]
@ -30,21 +29,11 @@
//---------------------------------------------------------------------------
#define IC_BUF_SIZE 1024
static char convert_buf[IC_BUF_SIZE];
#ifndef __NetBSD__
// Using POSIX.1 compliant iconv(3)
#define CONVERT(src, dest, inbuf, outbuf, outsize) \
convert(src, dest, (char *)inbuf, outbuf, outsize)
static void convert(char const *src, char const *dest,
char *inbuf, char *outbuf, size_t outsize)
#else
// Using NetBSD version of iconv(3): The second argument is 'const char **'
#define CONVERT(src, dest, inbuf, outbuf, outsize) \
convert(src, dest, inbuf, outbuf, outsize)
static void convert(char const *src, char const *dest,
const char *inbuf, char *outbuf, size_t outsize)
#endif
{
#ifndef __APPLE__
*outbuf = '\0';
size_t in = strlen(inbuf);
size_t out = outsize - 1;
@ -61,7 +50,6 @@ static void convert(char const *src, char const *dest,
iconv_close(cd);
*outbuf = '\0';
#endif //ifndef __APPLE__
}
//---------------------------------------------------------------------------

View File

@ -7,8 +7,6 @@
// Copyright (C) 2016-2020 GIMONS
// Copyright (C) akuker
//
// Imported NetBSD support and some optimisation patches by Rin Okuyama.
//
// [ TAP Driver ]
//
//---------------------------------------------------------------------------
@ -20,8 +18,6 @@
#include <sys/ioctl.h>
#include <linux/sockios.h>
#endif
// TODO Try to get rid of zlib, there is only one operation using it
#include <zlib.h> // For crc32()
#include "os.h"
#include "ctapdriver.h"
#include "log.h"
@ -47,8 +43,6 @@ CTapDriver::CTapDriver()
// Initialization
//
//---------------------------------------------------------------------------
#ifdef __linux__
static bool br_setif(int br_socket_fd, const char* bridgename, const char* ifname, bool add) {
struct ifreq ifr;
ifr.ifr_ifindex = if_nametoindex(ifname);
@ -349,53 +343,6 @@ bool CTapDriver::Init(const map<string, string>& const_params)
return true;
}
#endif // __linux__
#ifdef __NetBSD__
bool CTapDriver::Init(const map<string, string>&)
{
struct ifreq ifr;
struct ifaddrs *ifa, *a;
// TAP Device Initialization
if ((m_hTAP = open("/dev/tap", O_RDWR)) < 0) {
LOGERROR("Can't open tap: %s", strerror(errno));
return false;
}
// Get device name
if (ioctl(m_hTAP, TAPGIFNAME, (void *)&ifr) < 0) {
LOGERROR("Can't ioctl TAPGIFNAME: %s", strerror(errno));
close(m_hTAP);
return false;
}
// Get MAC address
if (getifaddrs(&ifa) == -1) {
LOGERROR("Can't getifaddrs: %s", strerror(errno));
close(m_hTAP);
return false;
}
for (a = ifa; a != NULL; a = a->ifa_next)
if (strcmp(ifr.ifr_name, a->ifa_name) == 0 &&
a->ifa_addr->sa_family == AF_LINK)
break;
if (a == NULL) {
LOGERROR("Can't get MAC address: %s", strerror(errno));
close(m_hTAP);
return false;
}
// Save MAC address
memcpy(m_MacAddr, LLADDR((struct sockaddr_dl *)a->ifa_addr),
sizeof(m_MacAddr));
freeifaddrs(ifa);
LOGINFO("Tap device: %s\n", ifr.ifr_name);
return true;
}
#endif // __NetBSD__
void CTapDriver::OpenDump(const Filepath& path) {
if (m_pcap == NULL) {
@ -498,6 +445,19 @@ bool CTapDriver::PendingPackets()
}
}
// See https://stackoverflow.com/questions/21001659/crc32-algorithm-implementation-in-c-without-a-look-up-table-and-with-a-public-li
uint32_t crc32(BYTE *buf, int length) {
uint32_t crc = 0xffffffff;
for (int i = 0; i < length; i++) {
crc ^= buf[i];
for (int j = 0; j < 8; j++) {
uint32_t mask = -(crc & 1);
crc = (crc >> 1) ^ (0xEDB88320 & mask);
}
}
return ~crc;
}
//---------------------------------------------------------------------------
//
// Receive
@ -508,7 +468,7 @@ int CTapDriver::Rx(BYTE *buf)
ASSERT(m_hTAP != -1);
// Check if there is data that can be received
if(!PendingPackets()){
if (!PendingPackets()) {
return 0;
}
@ -524,11 +484,7 @@ int CTapDriver::Rx(BYTE *buf)
// We need to add the Frame Check Status (FCS) CRC back onto the end of the packet.
// The Linux network subsystem removes it, since most software apps shouldn't ever
// need it.
// Initialize the CRC
DWORD crc = crc32(0L, Z_NULL, 0);
// Calculate the CRC
crc = crc32(crc, buf, dwReceived);
int crc = crc32(buf, dwReceived);
buf[dwReceived + 0] = (BYTE)((crc >> 0) & 0xFF);
buf[dwReceived + 1] = (BYTE)((crc >> 8) & 0xFF);

View File

@ -7,8 +7,6 @@
// Copyright (C) 2016-2020 GIMONS
// Copyright (C) akuker
//
// Imported NetBSD support and some optimisation patch by Rin Okuyama.
//
// [ TAP Driver ]
//
//---------------------------------------------------------------------------

View File

@ -184,8 +184,4 @@ public:
bool IsSASIHD() const { return type == "SAHD"; }
bool IsSCSIHD() const { return type == "SCHD" || type == "SCRM"; }
bool IsCdRom() const { return type == "SCCD"; }
bool IsMo() const { return type == "SCMO"; }
bool IsBridge() const { return type == "SCBR"; }
bool IsDaynaPort() const { return type == "SCDP"; }
};

View File

@ -31,7 +31,6 @@ DeviceFactory::DeviceFactory()
sector_sizes[SCHD] = { 512, 1024, 2048, 4096 };
sector_sizes[SCRM] = { 512, 1024, 2048, 4096 };
sector_sizes[SCMO] = { 512, 1024, 2048, 4096 };
// Some old Sun CD-ROM drives support 512 bytes per sector
sector_sizes[SCCD] = { 512, 2048};
// 128 MB, 512 bytes per sector, 248826 sectors
@ -124,20 +123,17 @@ Device *DeviceFactory::CreateDevice(PbDeviceType type, const string& filename)
try {
switch (type) {
case SAHD:
device = new SASIHD();
device = new SASIHD(sector_sizes[SAHD]);
device->SetSupportedLuns(2);
device->SetProduct("SASI HD");
((Disk *)device)->SetSectorSizes(sector_sizes[SAHD]);
break;
case SCHD: {
string ext = GetExtension(filename);
if (ext == "hdn" || ext == "hdi" || ext == "nhd") {
device = new SCSIHD_NEC();
((Disk *)device)->SetSectorSizes({ 512 });
device = new SCSIHD_NEC({ 512 });
} else {
device = new SCSIHD(false);
((Disk *)device)->SetSectorSizes(sector_sizes[SCHD]);
device = new SCSIHD(sector_sizes[SCHD], false);
// Some Apple tools require a particular drive identification
if (ext == "hda") {
@ -151,34 +147,30 @@ Device *DeviceFactory::CreateDevice(PbDeviceType type, const string& filename)
}
case SCRM:
device = new SCSIHD(true);
device = new SCSIHD(sector_sizes[SCRM], true);
device->SetProtectable(true);
device->SetStoppable(true);
device->SetRemovable(true);
device->SetLockable(true);
device->SetProduct("SCSI HD (REM.)");
((Disk *)device)->SetSectorSizes(sector_sizes[SCRM]);
break;
case SCMO:
device = new SCSIMO();
device = new SCSIMO(sector_sizes[SCMO], geometries[SCMO]);
device->SetProtectable(true);
device->SetStoppable(true);
device->SetRemovable(true);
device->SetLockable(true);
device->SetProduct("SCSI MO");
((Disk *)device)->SetSectorSizes(sector_sizes[SCRM]);
((Disk *)device)->SetGeometries(geometries[SCMO]);
break;
case SCCD:
device = new SCSICD();
device = new SCSICD(sector_sizes[SCCD]);
device->SetReadOnly(true);
device->SetStoppable(true);
device->SetRemovable(true);
device->SetLockable(true);
device->SetProduct("SCSI CD-ROM");
((Disk *)device)->SetSectorSizes(sector_sizes[SCCD]);
break;
case SCBR:

View File

@ -10,7 +10,6 @@
// Copyright (C) 2010 Y.Sugahara
//
// Imported sava's Anex86/T98Next image and MO format support patch.
// Imported NetBSD support and some optimisation patch by Rin Okuyama.
// Comments translated to english by akuker.
//
//---------------------------------------------------------------------------
@ -40,8 +39,8 @@ Disk::Disk(const string& id) : ModePageDevice(id), ScsiBlockCommands()
dispatcher.AddCommand(eCmdRead6, "Read6", &Disk::Read6);
dispatcher.AddCommand(eCmdWrite6, "Write6", &Disk::Write6);
dispatcher.AddCommand(eCmdSeek6, "Seek6", &Disk::Seek6);
dispatcher.AddCommand(eCmdReserve6, "Reserve6", &Disk::Reserve6);
dispatcher.AddCommand(eCmdRelease6, "Release6", &Disk::Release6);
dispatcher.AddCommand(eCmdReserve6, "Reserve6", &Disk::Reserve);
dispatcher.AddCommand(eCmdRelease6, "Release6", &Disk::Release);
dispatcher.AddCommand(eCmdStartStop, "StartStopUnit", &Disk::StartStopUnit);
dispatcher.AddCommand(eCmdSendDiag, "SendDiagnostic", &Disk::SendDiagnostic);
dispatcher.AddCommand(eCmdRemoval, "PreventAllowMediumRemoval", &Disk::PreventAllowMediumRemoval);
@ -56,8 +55,8 @@ Disk::Disk(const string& id) : ModePageDevice(id), ScsiBlockCommands()
dispatcher.AddCommand(eCmdSynchronizeCache10, "SynchronizeCache10", &Disk::SynchronizeCache10);
dispatcher.AddCommand(eCmdSynchronizeCache16, "SynchronizeCache16", &Disk::SynchronizeCache16);
dispatcher.AddCommand(eCmdReadDefectData10, "ReadDefectData10", &Disk::ReadDefectData10);
dispatcher.AddCommand(eCmdReserve10, "Reserve10", &Disk::Reserve10);
dispatcher.AddCommand(eCmdRelease10, "Release10", &Disk::Release10);
dispatcher.AddCommand(eCmdReserve10, "Reserve10", &Disk::Reserve);
dispatcher.AddCommand(eCmdRelease10, "Release10", &Disk::Release);
dispatcher.AddCommand(eCmdRead16, "Read16", &Disk::Read16);
dispatcher.AddCommand(eCmdWrite16, "Write16", &Disk::Write16);
dispatcher.AddCommand(eCmdVerify16, "Verify16", &Disk::Verify16);
@ -103,7 +102,7 @@ bool Disk::Dispatch(SCSIDEV *controller)
//---------------------------------------------------------------------------
void Disk::Open(const Filepath& path)
{
ASSERT(disk.blocks > 0);
assert(disk.blocks > 0);
SetReady(true);
@ -185,8 +184,6 @@ void Disk::Read6(SASIDEV *controller)
{
uint64_t start;
if (GetStartAndCount(controller, start, ctrl->blocks, RW6)) {
LOGDEBUG("%s READ(6) command record=$%08X blocks=%d", __PRETTY_FUNCTION__, (uint32_t)start, ctrl->blocks);
Read(controller, start);
}
}
@ -195,8 +192,6 @@ void Disk::Read10(SASIDEV *controller)
{
uint64_t start;
if (GetStartAndCount(controller, start, ctrl->blocks, RW10)) {
LOGDEBUG("%s READ(10) command record=$%08X blocks=%d", __PRETTY_FUNCTION__, (uint32_t)start, ctrl->blocks);
Read(controller, start);
}
}
@ -205,8 +200,6 @@ void Disk::Read16(SASIDEV *controller)
{
uint64_t start;
if (GetStartAndCount(controller, start, ctrl->blocks, RW16)) {
LOGDEBUG("%s READ(16) command record=$%08X blocks=%d", __PRETTY_FUNCTION__, (uint32_t)start, ctrl->blocks);
Read(controller, start);
}
}
@ -269,8 +262,6 @@ void Disk::Write6(SASIDEV *controller)
{
uint64_t start;
if (GetStartAndCount(controller, start, ctrl->blocks, RW6)) {
LOGDEBUG("%s WRITE(6) command record=$%08X blocks=%d", __PRETTY_FUNCTION__, (uint32_t)start, ctrl->blocks);
Write(controller, start);
}
}
@ -279,8 +270,6 @@ void Disk::Write10(SASIDEV *controller)
{
uint64_t start;
if (GetStartAndCount(controller, start, ctrl->blocks, RW10)) {
LOGDEBUG("%s WRITE(10) command record=$%08X blocks=%d",__PRETTY_FUNCTION__, (uint32_t)start, ctrl->blocks);
Write(controller, start);
}
}
@ -289,8 +278,6 @@ void Disk::Write16(SASIDEV *controller)
{
uint64_t start;
if (GetStartAndCount(controller, start, ctrl->blocks, RW16)) {
LOGDEBUG("%s WRITE(16) command record=$%08X blocks=%d",__PRETTY_FUNCTION__, (uint32_t)start, ctrl->blocks);
Write(controller, start);
}
}
@ -331,8 +318,6 @@ void Disk::Verify10(SASIDEV *controller)
// Get record number and block number
uint64_t record;
if (GetStartAndCount(controller, record, ctrl->blocks, RW10)) {
LOGDEBUG("%s VERIFY(10) command record=$%08X blocks=%d",__PRETTY_FUNCTION__, (uint32_t)record, ctrl->blocks);
Verify(controller, record);
}
}
@ -342,8 +327,6 @@ void Disk::Verify16(SASIDEV *controller)
// Get record number and block number
uint64_t record;
if (GetStartAndCount(controller, record, ctrl->blocks, RW16)) {
LOGDEBUG("%s VERIFY(16) command record=$%08X blocks=%d",__PRETTY_FUNCTION__, (uint32_t)record, ctrl->blocks);
Verify(controller, record);
}
}
@ -398,7 +381,7 @@ void Disk::SynchronizeCache16(SASIDEV *controller)
void Disk::ReadDefectData10(SASIDEV *controller)
{
ctrl->length = ReadDefectData10(ctrl->cmd, ctrl->buffer);
ctrl->length = ReadDefectData10(ctrl->cmd, ctrl->buffer, ctrl->bufsize);
if (ctrl->length <= 4) {
controller->Error();
return;
@ -440,29 +423,9 @@ int Disk::ModeSense6(const DWORD *cdb, BYTE *buf)
int length = (int)cdb[4];
memset(buf, 0, length);
// Get changeable flag
bool change = (cdb[2] & 0xc0) == 0x40;
// Get page code (0x00 is valid from the beginning)
int page = cdb[2] & 0x3f;
bool valid = page == 0x00;
LOGTRACE("%s Requesting mode page $%02X", __PRETTY_FUNCTION__, page);
// Basic information
int size = 4;
// MEDIUM TYPE
if (IsMo()) {
// Optical reversible or erasable
buf[1] = 0x03;
}
// DEVICE SPECIFIC PARAMETER
if (IsProtected()) {
buf[2] = 0x80;
}
// Add block descriptor if DBD is 0
if ((cdb[1] & 0x08) == 0) {
// Mode parameter header, block descriptor length
@ -488,70 +451,14 @@ int Disk::ModeSense6(const DWORD *cdb, BYTE *buf)
size = 12;
}
// Page code 1 (read-write error recovery)
if (page == 0x01 || page == 0x3f) {
size += AddErrorPage(change, &buf[size]);
valid = true;
}
// Page code 3 (format device)
if (page == 0x03 || page == 0x3f) {
size += AddFormatPage(change, &buf[size]);
valid = true;
}
// Page code 4 (drive parameter)
if (page == 0x04 || page == 0x3f) {
size += AddDrivePage(change, &buf[size]);
valid = true;
}
// Page code 6 (optical)
if (IsMo()) {
if (page == 0x06 || page == 0x3f) {
size += AddOptionPage(change, &buf[size]);
valid = true;
}
}
// Page code 8 (caching)
if (page == 0x08 || page == 0x3f) {
size += AddCachePage(change, &buf[size]);
valid = true;
}
// Page code 13 (CD-ROM)
if (IsCdRom()) {
if (page == 0x0d || page == 0x3f) {
size += AddCDROMPage(change, &buf[size]);
valid = true;
}
}
// Page code 14 (CD-DA)
if (IsCdRom()) {
if (page == 0x0e || page == 0x3f) {
size += AddCDDAPage(change, &buf[size]);
valid = true;
}
}
// Page (vendor special)
int ret = AddVendorPage(page, change, &buf[size]);
if (ret > 0) {
size += ret;
valid = true;
}
if (!valid) {
LOGTRACE("%s Unsupported mode page $%02X", __PRETTY_FUNCTION__, page);
SetStatusCode(STATUS_INVALIDCDB);
int pages_size = super::AddModePages(cdb, &buf[size], length - size);
if (!pages_size) {
return 0;
}
size += pages_size;
// Do not return more than ALLOCATION LENGTH bytes
if (size > length) {
LOGTRACE("%s %d bytes available, %d bytes requested", __PRETTY_FUNCTION__, size, length);
size = length;
}
@ -561,40 +468,18 @@ int Disk::ModeSense6(const DWORD *cdb, BYTE *buf)
return size;
}
int Disk::ModeSense10(const DWORD *cdb, BYTE *buf)
int Disk::ModeSense10(const DWORD *cdb, BYTE *buf, int max_length)
{
// Get length, clear buffer
int length = cdb[7];
length <<= 8;
length |= cdb[8];
if (length > 0x800) {
length = 0x800;
int length = (cdb[7] << 8) | cdb[8];
if (length > max_length) {
length = max_length;
}
memset(buf, 0, length);
// Get changeable flag
bool change = (cdb[2] & 0xc0) == 0x40;
// Get page code (0x00 is valid from the beginning)
int page = cdb[2] & 0x3f;
bool valid = page == 0x00;
LOGTRACE("%s Requesting mode page $%02X", __PRETTY_FUNCTION__, page);
// Basic Information
int size = 8;
// MEDIUM TYPE
if (IsMo()) {
// Optical reversible or erasable
buf[2] = 0x03;
}
// DEVICE SPECIFIC PARAMETER
if (IsProtected()) {
buf[3] = 0x80;
}
// Add block descriptor if DBD is 0
if ((cdb[1] & 0x08) == 0) {
// Only if ready
@ -648,70 +533,14 @@ int Disk::ModeSense10(const DWORD *cdb, BYTE *buf)
}
}
// Page code 1 (read-write error recovery)
if (page == 0x01 || page == 0x3f) {
size += AddErrorPage(change, &buf[size]);
valid = true;
}
// Page code 3 (format device)
if (page == 0x03 || page == 0x3f) {
size += AddFormatPage(change, &buf[size]);
valid = true;
}
// Page code 4 (drive parameter)
if (page == 0x04 || page == 0x3f) {
size += AddDrivePage(change, &buf[size]);
valid = true;
}
// Page code 6 (optical)
if (IsMo()) {
if (page == 0x06 || page == 0x3f) {
size += AddOptionPage(change, &buf[size]);
valid = true;
}
}
// Page code 8 (caching)
if (page == 0x08 || page == 0x3f) {
size += AddCachePage(change, &buf[size]);
valid = true;
}
// Page code 13 (CD-ROM)
if (IsCdRom()) {
if (page == 0x0d || page == 0x3f) {
size += AddCDROMPage(change, &buf[size]);
valid = true;
}
}
// Page code 14 (CD-DA)
if (IsCdRom()) {
if (page == 0x0e || page == 0x3f) {
size += AddCDDAPage(change, &buf[size]);
valid = true;
}
}
// Page (vendor special)
int ret = AddVendorPage(page, change, &buf[size]);
if (ret > 0) {
size += ret;
valid = true;
}
if (!valid) {
LOGTRACE("%s Unsupported mode page $%02X", __PRETTY_FUNCTION__, page);
SetStatusCode(STATUS_INVALIDCDB);
int pages_size = super::AddModePages(cdb, &buf[size], length - size);
if (!pages_size) {
return 0;
}
size += pages_size;
// Do not return more than ALLOCATION LENGTH bytes
if (size > length) {
LOGTRACE("%s %d bytes available, %d bytes requested", __PRETTY_FUNCTION__, size, length);
size = length;
}
@ -722,28 +551,65 @@ int Disk::ModeSense10(const DWORD *cdb, BYTE *buf)
return size;
}
int Disk::AddErrorPage(bool change, BYTE *buf)
void Disk::SetDeviceParameters(BYTE *buf)
{
// Set the message length
buf[0] = 0x01;
buf[1] = 0x0a;
// Retry count is 0, limit time uses internal default value
return 12;
// DEVICE SPECIFIC PARAMETER
if (IsProtected()) {
buf[3] = 0x80;
}
}
int Disk::AddFormatPage(bool change, BYTE *buf)
void Disk::AddModePages(map<int, vector<BYTE>>& pages, int page, bool changeable) const
{
// Set the message length
buf[0] = 0x80 | 0x03;
buf[1] = 0x16;
// Page code 1 (read-write error recovery)
if (page == 0x01 || page == 0x3f) {
AddErrorPage(pages, changeable);
}
// Page code 3 (format device)
if (page == 0x03 || page == 0x3f) {
AddFormatPage(pages, changeable);
}
// Page code 4 (drive parameter)
if (page == 0x04 || page == 0x3f) {
AddDrivePage(pages, changeable);
}
// Page code 8 (caching)
if (page == 0x08 || page == 0x3f) {
AddCachePage(pages, changeable);
}
// Page (vendor special)
AddVendorPage(pages, page, changeable);
}
void Disk::AddErrorPage(map<int, vector<BYTE>>& pages, bool) const
{
vector<BYTE> buf(12);
// Retry count is 0, limit time uses internal default value
pages[1] = buf;
}
void Disk::AddFormatPage(map<int, vector<BYTE>>& pages, bool changeable) const
{
vector<BYTE> buf(24);
// Page can be saved
buf[0] = 0x80;
// Show the number of bytes in the physical sector as changeable
// (though it cannot be changed in practice)
if (change) {
if (changeable) {
buf[0xc] = 0xff;
buf[0xd] = 0xff;
return 24;
pages[3] = buf;
return;
}
if (IsReady()) {
@ -765,18 +631,18 @@ int Disk::AddFormatPage(bool change, BYTE *buf)
buf[20] = 0x20;
}
return 24;
pages[3] = buf;
}
int Disk::AddDrivePage(bool change, BYTE *buf)
void Disk::AddDrivePage(map<int, vector<BYTE>>& pages, bool changeable) const
{
// Set the message length
buf[0] = 0x04;
buf[1] = 0x16;
vector<BYTE> buf(24);
// No changeable area
if (change) {
return 24;
if (changeable) {
pages[4] = buf;
return;
}
if (IsReady()) {
@ -793,81 +659,30 @@ int Disk::AddDrivePage(bool change, BYTE *buf)
buf[0x5] = 0x8;
}
return 24;
pages[4] = buf;
}
int Disk::AddOptionPage(bool change, BYTE *buf)
void Disk::AddCachePage(map<int, vector<BYTE>>& pages, bool) const
{
// Set the message length
buf[0] = 0x06;
buf[1] = 0x02;
// Do not report update blocks
return 4;
}
int Disk::AddCachePage(bool change, BYTE *buf)
{
// Set the message length
buf[0] = 0x08;
buf[1] = 0x0a;
vector<BYTE> buf(12);
// Only read cache is valid, no prefetch
return 12;
pages[8] = buf;
}
int Disk::AddCDROMPage(bool change, BYTE *buf)
void Disk::AddVendorPage(map<int, vector<BYTE>>&, int, bool) const
{
// Set the message length
buf[0] = 0x0d;
buf[1] = 0x06;
// No changeable area
if (change) {
return 8;
}
// 2 seconds for inactive timer
buf[3] = 0x05;
// MSF multiples are 60 and 75 respectively
buf[5] = 60;
buf[7] = 75;
return 8;
// Nothing to add by default
}
int Disk::AddCDDAPage(bool change, BYTE *buf)
int Disk::ReadDefectData10(const DWORD *cdb, BYTE *buf, int max_length)
{
// Set the message length
buf[0] = 0x0e;
buf[1] = 0x0e;
// Audio waits for operation completion and allows
// PLAY across multiple tracks
return 16;
}
int Disk::AddVendorPage(int /*page*/, bool /*change*/, BYTE *buf)
{
ASSERT(buf);
return 0;
}
int Disk::ReadDefectData10(const DWORD *cdb, BYTE *buf)
{
ASSERT(cdb);
ASSERT(buf);
// Get length, clear buffer
DWORD length = cdb[7];
length <<= 8;
length |= cdb[8];
if (length > 0x800) {
length = 0x800;
int length = (cdb[7] << 8) | cdb[8];
if (length > max_length) {
length = max_length;
}
ASSERT((length >= 0) && (length < 0x800));
memset(buf, 0, length);
// P/G/FORMAT
@ -911,17 +726,10 @@ bool Disk::Format(const DWORD *cdb)
return true;
}
//---------------------------------------------------------------------------
//
// READ
//
//---------------------------------------------------------------------------
// TODO Read more than one block in a single call. Currently blocked by the SASI code (missing early range check)
// and the track-oriented cache.
int Disk::Read(const DWORD *cdb, BYTE *buf, uint64_t block)
{
ASSERT(buf);
LOGTRACE("%s", __PRETTY_FUNCTION__);
if (!CheckReady()) {
@ -941,14 +749,9 @@ int Disk::Read(const DWORD *cdb, BYTE *buf, uint64_t block)
}
// Success
return (1 << disk.size);
return 1 << disk.size;
}
//---------------------------------------------------------------------------
//
// WRITE check
//
//---------------------------------------------------------------------------
int Disk::WriteCheck(DWORD block)
{
// Status check
@ -970,20 +773,13 @@ int Disk::WriteCheck(DWORD block)
}
// Success
return (1 << disk.size);
return 1 << disk.size;
}
//---------------------------------------------------------------------------
//
// WRITE
//
//---------------------------------------------------------------------------
// TODO Write more than one block in a single call. Currently blocked by the SASI code (missing early range check)
// and the track-oriented cache.
bool Disk::Write(const DWORD *cdb, const BYTE *buf, DWORD block)
{
ASSERT(buf);
LOGTRACE("%s", __PRETTY_FUNCTION__);
// Error if not ready
@ -1013,6 +809,7 @@ bool Disk::Write(const DWORD *cdb, const BYTE *buf, DWORD block)
return true;
}
// TODO For SCSI the specification mandates that the block address is verified
void Disk::Seek(SASIDEV *controller)
{
if (!CheckReady()) {
@ -1091,23 +888,13 @@ bool Disk::SendDiag(const DWORD *cdb)
void Disk::ReadCapacity10(SASIDEV *controller)
{
if (!CheckReady() || disk.blocks <= 0) {
controller->Error(ERROR_CODES::sense_key::ILLEGAL_REQUEST, ERROR_CODES::asc::MEDIUM_NOT_PRESENT);
return;
}
BYTE *buf = ctrl->buffer;
memset(buf, 0, 8);
if (!CheckReady()) {
controller->Error(ERROR_CODES::sense_key::ILLEGAL_REQUEST, ERROR_CODES::asc::MEDIUM_NOT_PRESENT);
return;
}
if (disk.blocks <= 0) {
controller->Error(ERROR_CODES::sense_key::ILLEGAL_REQUEST, ERROR_CODES::asc::MEDIUM_NOT_PRESENT);
LOGWARN("%s Capacity not available, medium may not be present", __PRETTY_FUNCTION__);
return;
}
// Create end of logical block address (disk.blocks-1)
uint32_t blocks = disk.blocks - 1;
buf[0] = (BYTE)(blocks >> 24);
@ -1130,15 +917,13 @@ void Disk::ReadCapacity10(SASIDEV *controller)
void Disk::ReadCapacity16(SASIDEV *controller)
{
BYTE *buf = ctrl->buffer;
memset(buf, 0, 14);
if (!CheckReady() || disk.blocks <= 0) {
controller->Error(ERROR_CODES::sense_key::ILLEGAL_REQUEST, ERROR_CODES::asc::MEDIUM_NOT_PRESENT);
return;
}
BYTE *buf = ctrl->buffer;
// Create end of logical block address (disk.blocks-1)
uint64_t blocks = disk.blocks - 1;
buf[0] = (BYTE)(blocks >> 56);
@ -1157,6 +942,8 @@ void Disk::ReadCapacity16(SASIDEV *controller)
buf[10] = (BYTE)(length >> 8);
buf[11] = (BYTE)length;
buf[12] = 0;
// Logical blocks per physical block: not reported (1 or more)
buf[13] = 0;
@ -1186,7 +973,7 @@ void Disk::ReadCapacity16_ReadLong16(SASIDEV *controller)
//---------------------------------------------------------------------------
//
// RESERVE(6)
// RESERVE/RELEASE(6/10)
//
// The reserve/release commands are only used in multi-initiator
// environments. RaSCSI doesn't support this use case. However, some old
@ -1194,52 +981,12 @@ void Disk::ReadCapacity16_ReadLong16(SASIDEV *controller)
// just respond with an OK status.
//
//---------------------------------------------------------------------------
void Disk::Reserve6(SASIDEV *controller)
void Disk::Reserve(SASIDEV *controller)
{
controller->Status();
}
//---------------------------------------------------------------------------
//
// RESERVE(10)
//
// The reserve/release commands are only used in multi-initiator
// environments. RaSCSI doesn't support this use case. However, some old
// versions of Solaris will issue the reserve/release commands. We will
// just respond with an OK status.
//
//---------------------------------------------------------------------------
void Disk::Reserve10(SASIDEV *controller)
{
controller->Status();
}
//---------------------------------------------------------------------------
//
// RELEASE(6)
//
// The reserve/release commands are only used in multi-initiator
// environments. RaSCSI doesn't support this use case. However, some old
// versions of Solaris will issue the reserve/release commands. We will
// just respond with an OK status.
//
//---------------------------------------------------------------------------
void Disk::Release6(SASIDEV *controller)
{
controller->Status();
}
//---------------------------------------------------------------------------
//
// RELEASE(10)
//
// The reserve/release commands are only used in multi-initiator
// environments. RaSCSI doesn't support this use case. However, some old
// versions of Solaris will issue the reserve/release commands. We will
// just respond with an OK status.
//
//---------------------------------------------------------------------------
void Disk::Release10(SASIDEV *controller)
void Disk::Release(SASIDEV *controller)
{
controller->Status();
}
@ -1332,6 +1079,8 @@ bool Disk::GetStartAndCount(SASIDEV *controller, uint64_t& start, uint32_t& coun
}
}
LOGDEBUG("%s READ/WRITE/VERIFY command record=$%08X blocks=%d", __PRETTY_FUNCTION__, (uint32_t)start, count);
// Check capacity
uint64_t capacity = GetBlockCount();
if (start > capacity || start + count > capacity) {

View File

@ -96,10 +96,8 @@ private:
void Seek10(SASIDEV *);
void ReadCapacity10(SASIDEV *) override;
void ReadCapacity16(SASIDEV *) override;
void Reserve6(SASIDEV *);
void Reserve10(SASIDEV *);
void Release6(SASIDEV *);
void Release10(SASIDEV *);
void Reserve(SASIDEV *);
void Release(SASIDEV *);
public:
@ -117,7 +115,7 @@ public:
bool SendDiag(const DWORD *cdb); // SEND DIAGNOSTIC command
virtual int Read(const DWORD *cdb, BYTE *buf, uint64_t block);
int ReadDefectData10(const DWORD *cdb, BYTE *buf);
int ReadDefectData10(const DWORD *, BYTE *, int);
uint32_t GetSectorSizeInBytes() const;
void SetSectorSizeInBytes(uint32_t, bool);
@ -138,15 +136,14 @@ public:
protected:
int ModeSense6(const DWORD *cdb, BYTE *buf);
int ModeSense10(const DWORD *cdb, BYTE *buf);
virtual int AddErrorPage(bool change, BYTE *buf);
virtual int AddFormatPage(bool change, BYTE *buf);
virtual int AddDrivePage(bool change, BYTE *buf);
virtual int AddVendorPage(int page, bool change, BYTE *buf);
int AddOptionPage(bool change, BYTE *buf);
int AddCachePage(bool change, BYTE *buf);
int AddCDROMPage(bool change, BYTE *buf);
int AddCDDAPage(bool, BYTE *buf);
int ModeSense10(const DWORD *cdb, BYTE *buf, int);
virtual void SetDeviceParameters(BYTE *);
void AddModePages(map<int, vector<BYTE>>&, int, bool) const override;
virtual void AddErrorPage(map<int, vector<BYTE>>&, bool) const;
virtual void AddFormatPage(map<int, vector<BYTE>>&, bool) const;
virtual void AddDrivePage(map<int, vector<BYTE>>&, bool) const;
void AddCachePage(map<int, vector<BYTE>>&, bool) const;
virtual void AddVendorPage(map<int, vector<BYTE>>&, int, bool) const;
// Internal disk data
disk_t disk;

View File

@ -27,9 +27,10 @@
// uint8_t second; // 0-59
// } mode_page_datetime;
//
// 2. STOP UNIT shuts down RaSCSI or the Raspberry Pi
// 2. START/STOP UNIT shuts down RaSCSI or shuts down/reboots the Raspberry Pi
// a) !start && !load (STOP): Shut down RaSCSI
// b) !start && load (EJECT): Shut down the Raspberry Pi
// c) start && load (LOAD): Reboot the Raspberry Pi
//
#include "controllers/scsidev_ctrl.h"
@ -77,122 +78,115 @@ void HostServices::StartStopUnit(SCSIDEV *controller)
}
if (load) {
controller->ScheduleShutDown(SCSIDEV::rascsi_shutdown_mode::PI);
controller->ScheduleShutDown(SCSIDEV::rascsi_shutdown_mode::STOP_PI);
}
else {
controller->ScheduleShutDown(SCSIDEV::rascsi_shutdown_mode::RASCSI);
controller->ScheduleShutDown(SCSIDEV::rascsi_shutdown_mode::STOP_RASCSI);
}
controller->Status();
return;
}
else {
if (load) {
controller->ScheduleShutDown(SCSIDEV::rascsi_shutdown_mode::RESTART_PI);
controller->Status();
return;
}
}
controller->Error(ERROR_CODES::sense_key::ILLEGAL_REQUEST, ERROR_CODES::asc::INVALID_FIELD_IN_CDB);
}
int HostServices::ModeSense6(const DWORD *cdb, BYTE *buf)
{
// Get length, clear buffer
// Block descriptors cannot be returned
if (!(cdb[1] & 0x08)) {
return 0;
}
int length = (int)cdb[4];
memset(buf, 0, length);
// Get page code (0x00 is valid from the beginning)
int page = cdb[2] & 0x3f;
bool valid = page == 0x00;
LOGTRACE("%s Requesting mode page $%02X", __PRETTY_FUNCTION__, page);
// Basic information
int size = 4;
int ret = AddRealtimeClockPage(page, &buf[size]);
if (ret > 0) {
size += ret;
valid = true;
}
if (!valid) {
LOGTRACE("%s Unsupported mode page $%02X", __PRETTY_FUNCTION__, page);
SetStatusCode(STATUS_INVALIDCDB);
int pages_size = super::AddModePages(cdb, &buf[size], length - size);
if (!pages_size) {
return 0;
}
size += pages_size;
// Do not return more than ALLOCATION LENGTH bytes
if (size > length) {
LOGTRACE("%s %d bytes available, %d bytes requested", __PRETTY_FUNCTION__, size, length);
size = length;
}
// Final setting of mode data length
buf[0] = size;
return size;
}
int HostServices::ModeSense10(const DWORD *cdb, BYTE *buf)
int HostServices::ModeSense10(const DWORD *cdb, BYTE *buf, int max_length)
{
// Get length, clear buffer
int length = cdb[7];
length <<= 8;
length |= cdb[8];
if (length > 0x800) {
length = 0x800;
}
memset(buf, 0, length);
// Get page code (0x00 is valid from the beginning)
int page = cdb[2] & 0x3f;
bool valid = page == 0x00;
LOGTRACE("%s Requesting mode page $%02X", __PRETTY_FUNCTION__, page);
// Basic Information
int size = 8;
int ret = AddRealtimeClockPage(page, &buf[size]);
if (ret > 0) {
size += ret;
valid = true;
}
if (!valid) {
LOGTRACE("%s Unsupported mode page $%02X", __PRETTY_FUNCTION__, page);
SetStatusCode(STATUS_INVALIDCDB);
// Block descriptors cannot be returned
if (!(cdb[1] & 0x08)) {
return 0;
}
int length = (cdb[7] << 8) | cdb[8];
if (length > max_length) {
length = max_length;
}
memset(buf, 0, length);
// Basic information
int size = 8;
int pages_size = super::AddModePages(cdb, &buf[size], length - size);
if (!pages_size) {
return 0;
}
size += pages_size;
// Do not return more than ALLOCATION LENGTH bytes
if (size > length) {
LOGTRACE("%s %d bytes available, %d bytes requested", __PRETTY_FUNCTION__, size, length);
size = length;
}
// Final setting of mode data length
buf[0] = size >> 8;
buf[1] = size;
return size;
}
int HostServices::AddRealtimeClockPage(int page, BYTE *buf)
void HostServices::AddModePages(map<int, vector<BYTE>>& pages, int page, bool changeable) const
{
if (page == 0x20) {
if (page == 0x20 || page == 0x3f) {
AddRealtimeClockPage(pages, changeable);
}
}
void HostServices::AddRealtimeClockPage(map<int, vector<BYTE>>& pages, bool changeable) const
{
vector<BYTE> buf(10);
if (!changeable) {
// Data structure version 1.0
buf[0] = 0x01;
buf[1] = 0x00;
buf[2] = 0x01;
buf[3] = 0x00;
std::time_t t = std::time(NULL);
std::tm tm = *std::localtime(&t);
buf[2] = tm.tm_year;
buf[3] = tm.tm_mon;
buf[4] = tm.tm_mday;
buf[5] = tm.tm_hour;
buf[6] = tm.tm_min;
buf[4] = tm.tm_year;
buf[5] = tm.tm_mon;
buf[6] = tm.tm_mday;
buf[7] = tm.tm_hour;
buf[8] = tm.tm_min;
// Ignore leap second for simplicity
buf[7] = tm.tm_sec < 60 ? tm.tm_sec : 59;
return 8;
buf[9] = tm.tm_sec < 60 ? tm.tm_sec : 59;
}
return 0;
pages[32] = buf;
}

View File

@ -11,6 +11,7 @@
#pragma once
#include "mode_page_device.h"
#include <vector>
using namespace std;
@ -29,7 +30,7 @@ public:
void StartStopUnit(SCSIDEV *);
int ModeSense6(const DWORD *, BYTE *);
int ModeSense10(const DWORD *, BYTE *);
int ModeSense10(const DWORD *, BYTE *, int);
private:
@ -37,5 +38,6 @@ private:
Dispatcher<HostServices, SCSIDEV> dispatcher;
int AddRealtimeClockPage(int, BYTE *);
void AddModePages(map<int, vector<BYTE>>&, int, bool) const override;
void AddRealtimeClockPage(map<int, vector<BYTE>>&, bool) const;
};

View File

@ -30,6 +30,66 @@ bool ModePageDevice::Dispatch(SCSIDEV *controller)
return dispatcher.Dispatch(this, controller) ? true : super::Dispatch(controller);
}
int ModePageDevice::AddModePages(const DWORD *cdb, BYTE *buf, int max_length)
{
bool changeable = (cdb[2] & 0xc0) == 0x40;
// Get page code (0x3f means all pages)
int page = cdb[2] & 0x3f;
LOGTRACE("%s Requesting mode page $%02X", __PRETTY_FUNCTION__, page);
// Mode page data mapped to the respective page numbers, C++ maps are ordered by key
map<int, vector<BYTE>> pages;
AddModePages(pages, page, changeable);
// If no mode data were added at all something must be wrong
if (pages.empty()) {
LOGTRACE("%s Unsupported mode page $%02X", __PRETTY_FUNCTION__, page);
SetStatusCode(STATUS_INVALIDCDB);
return 0;
}
int size = 0;
vector<BYTE> page0;
for (auto const& page : pages) {
if (size + (int)page.second.size() > max_length) {
LOGWARN("Mode page data size exceeds reserved buffer size");
page0.clear();
break;
}
else {
// The specification mandates that page 0 must be returned after all others
if (page.first) {
// Page data
memcpy(&buf[size], page.second.data(), page.second.size());
// Page code, PS bit may already have been set
buf[size] |= page.first;
// Page payload size
buf[size + 1] = page.second.size() - 2;
size += page.second.size();
}
else {
page0 = page.second;
}
}
}
// Page 0 must be last
if (!page0.empty()) {
memcpy(&buf[size], page0.data(), page0.size());
// Page payload size
buf[size + 1] = page0.size() - 2;
size += page0.size();
}
return size;
}
void ModePageDevice::ModeSense6(SASIDEV *controller)
{
ctrl->length = ModeSense6(ctrl->cmd, ctrl->buffer);
@ -43,7 +103,7 @@ void ModePageDevice::ModeSense6(SASIDEV *controller)
void ModePageDevice::ModeSense10(SASIDEV *controller)
{
ctrl->length = ModeSense10(ctrl->cmd, ctrl->buffer);
ctrl->length = ModeSense10(ctrl->cmd, ctrl->buffer, ctrl->bufsize);
if (ctrl->length <= 0) {
controller->Error();
return;
@ -52,12 +112,9 @@ void ModePageDevice::ModeSense10(SASIDEV *controller)
controller->DataIn();
}
bool ModePageDevice::ModeSelect(const DWORD* /*cdb*/, const BYTE *buf, int length)
bool ModePageDevice::ModeSelect(const DWORD*, const BYTE *, int)
{
ASSERT(buf);
ASSERT(length >= 0);
// cannot be set
// Cannot be set
SetStatusCode(STATUS_INVALIDPRM);
return false;
@ -67,7 +124,7 @@ void ModePageDevice::ModeSelect6(SASIDEV *controller)
{
LOGTRACE("%s Unsupported mode page $%02X", __PRETTY_FUNCTION__, ctrl->buffer[0]);
ctrl->length = ModeSelectCheck6(ctrl->cmd);
ctrl->length = ModeSelectCheck6();
if (ctrl->length <= 0) {
controller->Error();
return;
@ -80,7 +137,7 @@ void ModePageDevice::ModeSelect10(SASIDEV *controller)
{
LOGTRACE("%s Unsupported mode page $%02X", __PRETTY_FUNCTION__, ctrl->buffer[0]);
ctrl->length = ModeSelectCheck10(ctrl->cmd);
ctrl->length = ModeSelectCheck10();
if (ctrl->length <= 0) {
controller->Error();
return;
@ -89,11 +146,11 @@ void ModePageDevice::ModeSelect10(SASIDEV *controller)
controller->DataOut();
}
int ModePageDevice::ModeSelectCheck(const DWORD *cdb, int length)
int ModePageDevice::ModeSelectCheck(int length)
{
// Error if save parameters are set for other types than of SCHD or SCRM
// TODO This assumption is not correct, and this code should be located elsewhere
if (!IsSCSIHD() && (cdb[1] & 0x01)) {
// TODO The assumption above is not correct, and this code should be located elsewhere
if (!IsSCSIHD() && (ctrl->cmd[1] & 0x01)) {
SetStatusCode(STATUS_INVALIDCDB);
return 0;
}
@ -101,21 +158,21 @@ int ModePageDevice::ModeSelectCheck(const DWORD *cdb, int length)
return length;
}
int ModePageDevice::ModeSelectCheck6(const DWORD *cdb)
int ModePageDevice::ModeSelectCheck6()
{
// Receive the data specified by the parameter length
return ModeSelectCheck(cdb, cdb[4]);
return ModeSelectCheck(ctrl->cmd[4]);
}
int ModePageDevice::ModeSelectCheck10(const DWORD *cdb)
int ModePageDevice::ModeSelectCheck10()
{
// Receive the data specified by the parameter length
int length = cdb[7];
int length = ctrl->cmd[7];
length <<= 8;
length |= cdb[8];
if (length > 0x800) {
length = 0x800;
length |= ctrl->cmd[8];
if (length > ctrl->bufsize) {
length = ctrl->bufsize;
}
return ModeSelectCheck(cdb, length);
return ModeSelectCheck(length);
}

View File

@ -11,6 +11,8 @@
#include "primary_device.h"
#include <string>
#include <vector>
#include <map>
using namespace std;
@ -24,11 +26,16 @@ public:
virtual bool Dispatch(SCSIDEV *) override;
virtual int ModeSense6(const DWORD *, BYTE *) = 0;
virtual int ModeSense10(const DWORD *, BYTE *) = 0;
virtual int ModeSense10(const DWORD *, BYTE *, int) = 0;
// TODO This method should not be called by SASIDEV
virtual bool ModeSelect(const DWORD *, const BYTE *, int);
protected:
int AddModePages(const DWORD *, BYTE *, int);
virtual void AddModePages(map<int, vector<BYTE>>&, int, bool) const = 0;
private:
typedef PrimaryDevice super;
@ -40,7 +47,7 @@ private:
void ModeSelect6(SASIDEV *);
void ModeSelect10(SASIDEV *);
int ModeSelectCheck(const DWORD *, int);
int ModeSelectCheck6(const DWORD *);
int ModeSelectCheck10(const DWORD *);
int ModeSelectCheck(int);
int ModeSelectCheck6();
int ModeSelectCheck10();
};

View File

@ -68,11 +68,6 @@ void PrimaryDevice::ReportLuns(SASIDEV *controller)
{
BYTE *buf = ctrl->buffer;
if (!CheckReady()) {
controller->Error();
return;
}
int allocation_length = (ctrl->cmd[6] << 24) + (ctrl->cmd[7] << 16) + (ctrl->cmd[8] << 8) + ctrl->cmd[9];
memset(buf, 0, allocation_length);
@ -111,7 +106,7 @@ void PrimaryDevice::RequestSense(SASIDEV *controller)
}
ctrl->length = ((PrimaryDevice *)ctrl->unit[lun])->RequestSense(ctrl->cmd, ctrl->buffer);
ASSERT(ctrl->length > 0);
assert(ctrl->length > 0);
LOGTRACE("%s Status $%02X, Sense Key $%02X, ASC $%02X",__PRETTY_FUNCTION__, ctrl->status, ctrl->buffer[2], ctrl->buffer[12]);
@ -124,7 +119,7 @@ bool PrimaryDevice::CheckReady()
if (IsReset()) {
SetStatusCode(STATUS_DEVRESET);
SetReset(false);
LOGTRACE("%s Disk in reset", __PRETTY_FUNCTION__);
LOGTRACE("%s Device in reset", __PRETTY_FUNCTION__);
return false;
}
@ -132,26 +127,32 @@ bool PrimaryDevice::CheckReady()
if (IsAttn()) {
SetStatusCode(STATUS_ATTENTION);
SetAttn(false);
LOGTRACE("%s Disk in needs attention", __PRETTY_FUNCTION__);
LOGTRACE("%s Device in needs attention", __PRETTY_FUNCTION__);
return false;
}
// Return status if not ready
if (!IsReady()) {
SetStatusCode(STATUS_NOTREADY);
LOGTRACE("%s Disk not ready", __PRETTY_FUNCTION__);
LOGTRACE("%s Device not ready", __PRETTY_FUNCTION__);
return false;
}
// Initialization with no error
LOGTRACE("%s Disk is ready", __PRETTY_FUNCTION__);
LOGTRACE("%s Device is ready", __PRETTY_FUNCTION__);
return true;
}
int PrimaryDevice::Inquiry(int type, int scsi_level, bool is_removable, const DWORD *cdb, BYTE *buf)
{
int allocation_length = cdb[4] + (((DWORD)cdb[3]) << 8);
// EVPD and page code check
if ((cdb[1] & 0x01) || cdb[2]) {
SetStatusCode(STATUS_INVALIDCDB);
return 0;
}
int allocation_length = cdb[4] + (cdb[3] << 8);
if (allocation_length > 4) {
if (allocation_length > 44) {
allocation_length = 44;
@ -167,6 +168,8 @@ int PrimaryDevice::Inquiry(int type, int scsi_level, bool is_removable, const DW
buf[0] = type;
buf[1] = is_removable ? 0x80 : 0x00;
buf[2] = scsi_level;
// Response data format is SCSI-2 for devices supporting SCSI-2 or newer, otherwise it is SCSI-1-CCS
buf[3] = scsi_level >= 2 ? 2 : 0;
buf[4] = 0x1F;
// Padded vendor, product, revision
@ -178,22 +181,16 @@ int PrimaryDevice::Inquiry(int type, int scsi_level, bool is_removable, const DW
int PrimaryDevice::RequestSense(const DWORD *cdb, BYTE *buf)
{
ASSERT(cdb);
ASSERT(buf);
// Return not ready only if there are no errors
if (GetStatusCode() == STATUS_NOERROR) {
if (!IsReady()) {
SetStatusCode(STATUS_NOTREADY);
}
if (GetStatusCode() == STATUS_NOERROR && !IsReady()) {
SetStatusCode(STATUS_NOTREADY);
}
// Size determination (according to allocation length)
int size = (int)cdb[4];
ASSERT((size >= 0) && (size < 0x100));
assert(size >= 0 && size < 0x100);
// For SCSI-1, transfer 4 bytes when the size is 0
// (Deleted this specification for SCSI-2)
if (size == 0) {
size = 4;
}

View File

@ -37,6 +37,7 @@ public:
virtual int Inquiry(const DWORD *, BYTE *) = 0;
virtual int RequestSense(const DWORD *, BYTE *);
virtual bool WriteBytes(BYTE *, uint32_t);
virtual int GetSendDelay() { return BUS::SEND_NO_DELAY; }
protected:

View File

@ -19,26 +19,11 @@
#include "exceptions.h"
#include "../config.h"
//===========================================================================
//
// SASI Hard Disk
//
//===========================================================================
//---------------------------------------------------------------------------
//
// Constructor
//
//---------------------------------------------------------------------------
SASIHD::SASIHD() : Disk("SAHD")
SASIHD::SASIHD(const set<uint32_t>& sector_sizes) : Disk("SAHD")
{
SetSectorSizes(sector_sizes);
}
//---------------------------------------------------------------------------
//
// Reset
//
//---------------------------------------------------------------------------
void SASIHD::Reset()
{
// Unlock, clear attention
@ -50,14 +35,9 @@ void SASIHD::Reset()
SetStatusCode(STATUS_NOERROR);
}
//---------------------------------------------------------------------------
//
// Open
//
//---------------------------------------------------------------------------
void SASIHD::Open(const Filepath& path)
{
ASSERT(!IsReady());
assert(!IsReady());
// Open as read-only
Fileio fio;
@ -101,30 +81,17 @@ void SASIHD::Open(const Filepath& path)
FileSupport::SetPath(path);
}
//---------------------------------------------------------------------------
//
// INQUIRY
//
//---------------------------------------------------------------------------
int SASIHD::Inquiry(const DWORD* /*cdb*/, BYTE* /*buf*/)
{
SetStatusCode(STATUS_INVALIDCMD);
return 0;
}
//---------------------------------------------------------------------------
//
// REQUEST SENSE
//
//---------------------------------------------------------------------------
int SASIHD::RequestSense(const DWORD *cdb, BYTE *buf)
{
ASSERT(cdb);
ASSERT(buf);
// Size decision
int size = (int)cdb[4];
ASSERT(size >= 0 && size < 0x100);
assert(size >= 0 && size < 0x100);
// Transfer 4 bytes when size 0 (Shugart Associates System Interface specification)
if (size == 0) {

View File

@ -27,8 +27,8 @@
class SASIHD : public Disk, public FileSupport
{
public:
SASIHD();
~SASIHD() {};
SASIHD(const set<uint32_t>&);
~SASIHD() {}
void Reset();
void Open(const Filepath& path) override;

View File

@ -583,3 +583,11 @@ void SCSIDaynaPort::EnableInterface(SASIDEV *controller)
controller->Status();
}
int SCSIDaynaPort::GetSendDelay()
{
// The Daynaport needs to have a delay after the size/flags field
// of the read response. In the MacOS driver, it looks like the
// driver is doing two "READ" system calls.
return DAYNAPORT_READ_HEADER_SZ;
}

View File

@ -67,6 +67,7 @@ public:
void SetInterfaceMode(SASIDEV *);
void SetMcastAddr(SASIDEV *);
void EnableInterface(SASIDEV *);
int GetSendDelay() override;
bool Dispatch(SCSIDEV *) override;

View File

@ -121,7 +121,7 @@ int SCSIBR::Inquiry(const DWORD *cdb, BYTE *buf)
buf[0] = 0x09;
buf[2] = 0x02;
buf[3] = 0x02;
buf[4] = 36 - 5 + 8; // required + 8 byte extension
buf[4] = 0x1F + 8; // required + 8 byte extension
// Padded vendor, product, revision
memcpy(&buf[8], GetPaddedName().c_str(), 28);
@ -152,13 +152,9 @@ int SCSIBR::Inquiry(const DWORD *cdb, BYTE *buf)
void SCSIBR::TestUnitReady(SASIDEV *controller)
{
// Always successful
controller->Status();}
controller->Status();
}
//---------------------------------------------------------------------------
//
// GET MESSAGE(10)
//
//---------------------------------------------------------------------------
int SCSIBR::GetMessage10(const DWORD *cdb, BYTE *buf)
{
// Type
@ -239,16 +235,8 @@ int SCSIBR::GetMessage10(const DWORD *cdb, BYTE *buf)
return 0;
}
//---------------------------------------------------------------------------
//
// SEND MESSAGE(10)
//
//---------------------------------------------------------------------------
bool SCSIBR::SendMessage10(const DWORD *cdb, BYTE *buf)
{
ASSERT(cdb);
ASSERT(buf);
// Type
int type = cdb[2];
@ -301,11 +289,6 @@ bool SCSIBR::SendMessage10(const DWORD *cdb, BYTE *buf)
return false;
}
//---------------------------------------------------------------------------
//
// GET MESSAGE(10)
//
//---------------------------------------------------------------------------
void SCSIBR::GetMessage10(SASIDEV *controller)
{
// Reallocate buffer (because it is not transfer for each block)
@ -365,41 +348,22 @@ void SCSIBR::SendMessage10(SASIDEV *controller)
controller->DataOut();
}
//---------------------------------------------------------------------------
//
// Get MAC Address
//
//---------------------------------------------------------------------------
int SCSIBR::GetMacAddr(BYTE *mac)
{
ASSERT(mac);
memcpy(mac, mac_addr, 6);
return 6;
}
//---------------------------------------------------------------------------
//
// Set MAC Address
//
//---------------------------------------------------------------------------
void SCSIBR::SetMacAddr(BYTE *mac)
{
ASSERT(mac);
memcpy(mac_addr, mac, 6);
}
//---------------------------------------------------------------------------
//
// Receive Packet
//
//---------------------------------------------------------------------------
void SCSIBR::ReceivePacket()
{
static const BYTE bcast_addr[6] = { 0xff, 0xff, 0xff, 0xff, 0xff, 0xff };
ASSERT(tap);
assert(tap);
// previous packet has not been received
if (packet_enable) {
@ -429,15 +393,9 @@ void SCSIBR::ReceivePacket()
}
}
//---------------------------------------------------------------------------
//
// Get Packet
//
//---------------------------------------------------------------------------
void SCSIBR::GetPacketBuf(BYTE *buf)
{
ASSERT(tap);
ASSERT(buf);
assert(tap);
// Size limit
int len = packet_len;
@ -452,15 +410,9 @@ void SCSIBR::GetPacketBuf(BYTE *buf)
packet_enable = false;
}
//---------------------------------------------------------------------------
//
// Send Packet
//
//---------------------------------------------------------------------------
void SCSIBR::SendPacket(BYTE *buf, int len)
{
ASSERT(tap);
ASSERT(buf);
assert(tap);
tap->Tx(buf, len);
}
@ -472,9 +424,6 @@ void SCSIBR::SendPacket(BYTE *buf, int len)
//---------------------------------------------------------------------------
void SCSIBR::FS_InitDevice(BYTE *buf)
{
ASSERT(fs);
ASSERT(buf);
fs->Reset();
fsresult = fs->InitDevice((Human68k::argument_t*)buf);
}
@ -486,13 +435,9 @@ void SCSIBR::FS_InitDevice(BYTE *buf)
//---------------------------------------------------------------------------
void SCSIBR::FS_CheckDir(BYTE *buf)
{
ASSERT(fs);
ASSERT(buf);
int i = 0;
DWORD *dp = (DWORD*)buf;
DWORD nUnit = ntohl(*dp);
i += sizeof(DWORD);
int i = sizeof(DWORD);
Human68k::namests_t *pNamests = (Human68k::namests_t*)&buf[i];
i += sizeof(Human68k::namests_t);
@ -507,13 +452,9 @@ void SCSIBR::FS_CheckDir(BYTE *buf)
//---------------------------------------------------------------------------
void SCSIBR::FS_MakeDir(BYTE *buf)
{
ASSERT(fs);
ASSERT(buf);
int i = 0;
DWORD *dp = (DWORD*)buf;
DWORD nUnit = ntohl(*dp);
i += sizeof(DWORD);
int i = sizeof(DWORD);
Human68k::namests_t *pNamests = (Human68k::namests_t*)&buf[i];
i += sizeof(Human68k::namests_t);
@ -528,13 +469,9 @@ void SCSIBR::FS_MakeDir(BYTE *buf)
//---------------------------------------------------------------------------
void SCSIBR::FS_RemoveDir(BYTE *buf)
{
ASSERT(fs);
ASSERT(buf);
int i = 0;
DWORD *dp = (DWORD*)buf;
DWORD nUnit = ntohl(*dp);
i += sizeof(DWORD);
int i = sizeof(DWORD);
Human68k::namests_t *pNamests = (Human68k::namests_t*)&buf[i];
i += sizeof(Human68k::namests_t);
@ -549,13 +486,9 @@ void SCSIBR::FS_RemoveDir(BYTE *buf)
//---------------------------------------------------------------------------
void SCSIBR::FS_Rename(BYTE *buf)
{
ASSERT(fs);
ASSERT(buf);
int i = 0;
DWORD *dp = (DWORD*)buf;
DWORD nUnit = ntohl(*dp);
i += sizeof(DWORD);
int i = sizeof(DWORD);
Human68k::namests_t *pNamests = (Human68k::namests_t*)&buf[i];
i += sizeof(Human68k::namests_t);
@ -573,13 +506,9 @@ void SCSIBR::FS_Rename(BYTE *buf)
//---------------------------------------------------------------------------
void SCSIBR::FS_Delete(BYTE *buf)
{
ASSERT(fs);
ASSERT(buf);
int i = 0;
DWORD *dp = (DWORD*)buf;
DWORD nUnit = ntohl(*dp);
i += sizeof(DWORD);
int i = sizeof(DWORD);
Human68k::namests_t *pNamests = (Human68k::namests_t*)&buf[i];
i += sizeof(Human68k::namests_t);
@ -594,13 +523,9 @@ void SCSIBR::FS_Delete(BYTE *buf)
//---------------------------------------------------------------------------
void SCSIBR::FS_Attribute(BYTE *buf)
{
ASSERT(fs);
ASSERT(buf);
int i = 0;
DWORD *dp = (DWORD*)buf;
DWORD nUnit = ntohl(*dp);
i += sizeof(DWORD);
int i = sizeof(DWORD);
Human68k::namests_t *pNamests = (Human68k::namests_t*)&buf[i];
i += sizeof(Human68k::namests_t);
@ -619,13 +544,9 @@ void SCSIBR::FS_Attribute(BYTE *buf)
//---------------------------------------------------------------------------
void SCSIBR::FS_Files(BYTE *buf)
{
ASSERT(fs);
ASSERT(buf);
int i = 0;
DWORD *dp = (DWORD*)buf;
DWORD nUnit = ntohl(*dp);
i += sizeof(DWORD);
int i = sizeof(DWORD);
dp = (DWORD*)&buf[i];
DWORD nKey = ntohl(*dp);
@ -665,13 +586,9 @@ void SCSIBR::FS_Files(BYTE *buf)
//---------------------------------------------------------------------------
void SCSIBR::FS_NFiles(BYTE *buf)
{
ASSERT(fs);
ASSERT(buf);
int i = 0;
DWORD *dp = (DWORD*)buf;
DWORD nUnit = ntohl(*dp);
i += sizeof(DWORD);
int i = sizeof(DWORD);
dp = (DWORD*)&buf[i];
DWORD nKey = ntohl(*dp);
@ -708,13 +625,9 @@ void SCSIBR::FS_NFiles(BYTE *buf)
//---------------------------------------------------------------------------
void SCSIBR::FS_Create(BYTE *buf)
{
ASSERT(fs);
ASSERT(buf);
int i = 0;
DWORD *dp = (DWORD*)buf;
DWORD nUnit = ntohl(*dp);
i += sizeof(DWORD);
int i = sizeof(DWORD);
dp = (DWORD*)&buf[i];
DWORD nKey = ntohl(*dp);
@ -762,13 +675,9 @@ void SCSIBR::FS_Create(BYTE *buf)
//---------------------------------------------------------------------------
void SCSIBR::FS_Open(BYTE *buf)
{
ASSERT(fs);
ASSERT(buf);
int i = 0;
DWORD *dp = (DWORD*)buf;
DWORD nUnit = ntohl(*dp);
i += sizeof(DWORD);
int i = sizeof(DWORD);
dp = (DWORD*)&buf[i];
DWORD nKey = ntohl(*dp);
@ -808,13 +717,9 @@ void SCSIBR::FS_Open(BYTE *buf)
//---------------------------------------------------------------------------
void SCSIBR::FS_Close(BYTE *buf)
{
ASSERT(fs);
ASSERT(buf);
int i = 0;
DWORD *dp = (DWORD*)buf;
DWORD nUnit = ntohl(*dp);
i += sizeof(DWORD);
int i = sizeof(DWORD);
dp = (DWORD*)&buf[i];
DWORD nKey = ntohl(*dp);
@ -851,13 +756,9 @@ void SCSIBR::FS_Close(BYTE *buf)
//---------------------------------------------------------------------------
void SCSIBR::FS_Read(BYTE *buf)
{
ASSERT(fs);
ASSERT(buf);
int i = 0;
DWORD *dp = (DWORD*)buf;
DWORD nKey = ntohl(*dp);
i += sizeof(DWORD);
int i = sizeof(DWORD);
Human68k::fcb_t *pFcb = (Human68k::fcb_t*)&buf[i];
i += sizeof(Human68k::fcb_t);
@ -896,13 +797,9 @@ void SCSIBR::FS_Read(BYTE *buf)
//---------------------------------------------------------------------------
void SCSIBR::FS_Write(BYTE *buf)
{
ASSERT(fs);
ASSERT(buf);
int i = 0;
DWORD *dp = (DWORD*)buf;
DWORD nKey = ntohl(*dp);
i += sizeof(DWORD);
int i = sizeof(DWORD);
Human68k::fcb_t *pFcb = (Human68k::fcb_t*)&buf[i];
i += sizeof(Human68k::fcb_t);
@ -939,13 +836,9 @@ void SCSIBR::FS_Write(BYTE *buf)
//---------------------------------------------------------------------------
void SCSIBR::FS_Seek(BYTE *buf)
{
ASSERT(fs);
ASSERT(buf);
int i = 0;
DWORD *dp = (DWORD*)buf;
DWORD nKey = ntohl(*dp);
i += sizeof(DWORD);
int i = sizeof(DWORD);
Human68k::fcb_t *pFcb = (Human68k::fcb_t*)&buf[i];
i += sizeof(Human68k::fcb_t);
@ -986,13 +879,9 @@ void SCSIBR::FS_Seek(BYTE *buf)
//---------------------------------------------------------------------------
void SCSIBR::FS_TimeStamp(BYTE *buf)
{
ASSERT(fs);
ASSERT(buf);
int i = 0;
DWORD *dp = (DWORD*)buf;
DWORD nUnit = ntohl(*dp);
i += sizeof(DWORD);
int i = sizeof(DWORD);
dp = (DWORD*)&buf[i];
DWORD nKey = ntohl(*dp);
@ -1033,9 +922,6 @@ void SCSIBR::FS_TimeStamp(BYTE *buf)
//---------------------------------------------------------------------------
void SCSIBR::FS_GetCapacity(BYTE *buf)
{
ASSERT(fs);
ASSERT(buf);
DWORD *dp = (DWORD*)buf;
DWORD nUnit = ntohl(*dp);
@ -1058,13 +944,9 @@ void SCSIBR::FS_GetCapacity(BYTE *buf)
//---------------------------------------------------------------------------
void SCSIBR::FS_CtrlDrive(BYTE *buf)
{
ASSERT(fs);
ASSERT(buf);
int i = 0;
DWORD *dp = (DWORD*)buf;
DWORD nUnit = ntohl(*dp);
i += sizeof(DWORD);
int i = sizeof(DWORD);
Human68k::ctrldrive_t *pCtrlDrive = (Human68k::ctrldrive_t*)&buf[i];
@ -1081,9 +963,6 @@ void SCSIBR::FS_CtrlDrive(BYTE *buf)
//---------------------------------------------------------------------------
void SCSIBR::FS_GetDPB(BYTE *buf)
{
ASSERT(fs);
ASSERT(buf);
DWORD *dp = (DWORD*)buf;
DWORD nUnit = ntohl(*dp);
@ -1108,13 +987,9 @@ void SCSIBR::FS_GetDPB(BYTE *buf)
//---------------------------------------------------------------------------
void SCSIBR::FS_DiskRead(BYTE *buf)
{
ASSERT(fs);
ASSERT(buf);
int i = 0;
DWORD *dp = (DWORD*)buf;
DWORD nUnit = ntohl(*dp);
i += sizeof(DWORD);
int i = sizeof(DWORD);
dp = (DWORD*)&buf[i];
DWORD nSector = ntohl(*dp);
@ -1135,9 +1010,6 @@ void SCSIBR::FS_DiskRead(BYTE *buf)
//---------------------------------------------------------------------------
void SCSIBR::FS_DiskWrite(BYTE *buf)
{
ASSERT(fs);
ASSERT(buf);
DWORD *dp = (DWORD*)buf;
DWORD nUnit = ntohl(*dp);
@ -1151,13 +1023,9 @@ void SCSIBR::FS_DiskWrite(BYTE *buf)
//---------------------------------------------------------------------------
void SCSIBR::FS_Ioctrl(BYTE *buf)
{
ASSERT(fs);
ASSERT(buf);
int i = 0;
DWORD *dp = (DWORD*)buf;
DWORD nUnit = ntohl(*dp);
i += sizeof(DWORD);
int i = sizeof(DWORD);
dp = (DWORD*)&buf[i];
DWORD nFunction = ntohl(*dp);
@ -1198,9 +1066,6 @@ void SCSIBR::FS_Ioctrl(BYTE *buf)
//---------------------------------------------------------------------------
void SCSIBR::FS_Flush(BYTE *buf)
{
ASSERT(fs);
ASSERT(buf);
DWORD *dp = (DWORD*)buf;
DWORD nUnit = ntohl(*dp);
@ -1214,9 +1079,6 @@ void SCSIBR::FS_Flush(BYTE *buf)
//---------------------------------------------------------------------------
void SCSIBR::FS_CheckMedia(BYTE *buf)
{
ASSERT(fs);
ASSERT(buf);
DWORD *dp = (DWORD*)buf;
DWORD nUnit = ntohl(*dp);
@ -1230,9 +1092,6 @@ void SCSIBR::FS_CheckMedia(BYTE *buf)
//---------------------------------------------------------------------------
void SCSIBR::FS_Lock(BYTE *buf)
{
ASSERT(fs);
ASSERT(buf);
DWORD *dp = (DWORD*)buf;
DWORD nUnit = ntohl(*dp);
@ -1246,8 +1105,6 @@ void SCSIBR::FS_Lock(BYTE *buf)
//---------------------------------------------------------------------------
int SCSIBR::ReadFsResult(BYTE *buf)
{
ASSERT(buf);
DWORD *dp = (DWORD*)buf;
*dp = htonl(fsresult);
return sizeof(DWORD);
@ -1260,8 +1117,6 @@ int SCSIBR::ReadFsResult(BYTE *buf)
//---------------------------------------------------------------------------
int SCSIBR::ReadFsOut(BYTE *buf)
{
ASSERT(buf);
memcpy(buf, fsout, fsoutlen);
return fsoutlen;
}
@ -1273,8 +1128,6 @@ int SCSIBR::ReadFsOut(BYTE *buf)
//---------------------------------------------------------------------------
int SCSIBR::ReadFsOpt(BYTE *buf)
{
ASSERT(buf);
memcpy(buf, fsopt, fsoptlen);
return fsoptlen;
}
@ -1286,8 +1139,6 @@ int SCSIBR::ReadFsOpt(BYTE *buf)
//---------------------------------------------------------------------------
void SCSIBR::WriteFs(int func, BYTE *buf)
{
ASSERT(buf);
fsresult = FS_FATAL_INVALIDCOMMAND;
fsoutlen = 0;
fsoptlen = 0;

View File

@ -17,7 +17,8 @@
// 2. The client sends the data to be printed with one or several PRINT commands. Due to
// https://github.com/akuker/RASCSI/issues/669 the maximum transfer size per PRINT command is
// limited to 4096 bytes.
// 3. The client triggers printing with SYNCHRONIZE BUFFER.
// 3. The client triggers printing with SYNCHRONIZE BUFFER. Each SYNCHRONIZE BUFFER results in
// the print command for this printer (see below) to be called for the data not yet printed.
// 4. The client releases the printer with RELEASE UNIT (optional step, mandatory for a
// multi-initiator environment).
//
@ -68,7 +69,7 @@ SCSIPrinter::SCSIPrinter() : PrimaryDevice("SCLP"), ScsiPrinterCommands()
SCSIPrinter::~SCSIPrinter()
{
DiscardReservation();
Cleanup();
}
bool SCSIPrinter::Init(const map<string, string>& params)
@ -168,7 +169,7 @@ void SCSIPrinter::Print(SCSIDEV *controller)
// TODO This device suffers from the statically allocated buffer size,
// see https://github.com/akuker/RASCSI/issues/669
if (length > (uint32_t)controller->DEFAULT_BUFFER_SIZE) {
if (length > (uint32_t)ctrl->bufsize) {
LOGERROR("Transfer buffer overflow");
controller->Error(ERROR_CODES::sense_key::ILLEGAL_REQUEST, ERROR_CODES::asc::INVALID_FIELD_IN_CDB);

View File

@ -177,8 +177,10 @@ bool CDTrack::IsAudio() const
//
//===========================================================================
SCSICD::SCSICD() : Disk("SCCD"), ScsiMmcCommands(), FileSupport()
SCSICD::SCSICD(const set<uint32_t>& sector_sizes) : Disk("SCCD"), ScsiMmcCommands(), FileSupport()
{
SetSectorSizes(sector_sizes);
// NOT in raw format
rawfile = false;
@ -263,7 +265,7 @@ void SCSICD::Open(const Filepath& path)
// Successful opening
ASSERT(GetBlockCount() > 0);
Disk::Open(path);
super::Open(path);
FileSupport::SetPath(path);
// Set RAW flag
@ -415,7 +417,7 @@ int SCSICD::Inquiry(const DWORD *cdb, BYTE *buf)
buf[1] = 0x80;
buf[2] = 0x02;
buf[3] = 0x02;
buf[4] = 36 - 5; // Required
buf[4] = 0x1F;
// Fill with blanks
memset(&buf[8], 0x20, buf[4] - 3);
@ -457,6 +459,48 @@ int SCSICD::Inquiry(const DWORD *cdb, BYTE *buf)
return size;
}
void SCSICD::AddModePages(map<int, vector<BYTE>>& pages, int page, bool changeable) const
{
super::AddModePages(pages, page, changeable);
// Page code 13
if (page == 0x0d || page == 0x3f) {
AddCDROMPage(pages, changeable);
}
// Page code 14
if (page == 0x0e || page == 0x3f) {
AddCDDAPage(pages, changeable);
}
}
void SCSICD::AddCDROMPage(map<int, vector<BYTE>>& pages, bool changeable) const
{
vector<BYTE> buf(8);
// No changeable area
if (!changeable) {
// 2 seconds for inactive timer
buf[3] = 0x05;
// MSF multiples are 60 and 75 respectively
buf[5] = 60;
buf[7] = 75;
}
pages[13] = buf;
}
void SCSICD::AddCDDAPage(map<int, vector<BYTE>>& pages, bool) const
{
vector<BYTE> buf(16);
// Audio waits for operation completion and allows
// PLAY across multiple tracks
pages[14] = buf;
}
int SCSICD::Read(const DWORD *cdb, BYTE *buf, uint64_t block)
{
ASSERT(buf);
@ -498,7 +542,7 @@ int SCSICD::Read(const DWORD *cdb, BYTE *buf, uint64_t block)
// Base class
ASSERT(dataindex >= 0);
return Disk::Read(cdb, buf, block);
return super::Read(cdb, buf, block);
}
int SCSICD::ReadToc(const DWORD *cdb, BYTE *buf)

View File

@ -76,7 +76,7 @@ public:
TrackMax = 96 // Maximum number of tracks
};
SCSICD();
SCSICD(const set<uint32_t>&);
~SCSICD();
bool Dispatch(SCSIDEV *) override;
@ -88,11 +88,18 @@ public:
int Read(const DWORD *cdb, BYTE *buf, uint64_t block) override; // READ command
int ReadToc(const DWORD *cdb, BYTE *buf); // READ TOC command
protected:
void AddModePages(map<int, vector<BYTE>>&, int, bool) const override;
private:
typedef Disk super;
Dispatcher<SCSICD, SASIDEV> dispatcher;
void AddCDROMPage(map<int, vector<BYTE>>&, bool) const;
void AddCDDAPage(map<int, vector<BYTE>>&, bool) const;
// Open
void OpenCue(const Filepath& path); // Open(CUE)
void OpenIso(const Filepath& path); // Open(ISO)

View File

@ -26,8 +26,9 @@
//
//===========================================================================
SCSIHD::SCSIHD(bool removable) : Disk(removable ? "SCRM" : "SCHD")
SCSIHD::SCSIHD(const set<uint32_t>& sector_sizes, bool removable) : Disk(removable ? "SCRM" : "SCHD")
{
SetSectorSizes(sector_sizes);
}
void SCSIHD::FinalizeSetup(const Filepath &path, off_t size)
@ -75,7 +76,7 @@ void SCSIHD::Reset()
void SCSIHD::Open(const Filepath& path)
{
ASSERT(!IsReady());
assert(!IsReady());
// Open as read-only
Fileio fio;
@ -105,12 +106,6 @@ int SCSIHD::Inquiry(const DWORD *cdb, BYTE *buf)
return 0;
}
// Ready check (Error if no image file)
if (!IsReady()) {
SetStatusCode(STATUS_NOTREADY);
return 0;
}
// Basic data
// buf[0] ... Direct Access Device
// buf[1] ... Bit 7 set means removable
@ -121,7 +116,7 @@ int SCSIHD::Inquiry(const DWORD *cdb, BYTE *buf)
buf[1] = IsRemovable() ? 0x80 : 0x00;
buf[2] = 0x02;
buf[3] = 0x02;
buf[4] = 28 + 3; // Value close to real HDD
buf[4] = 0x1F;
// Padded vendor, product, revision
memcpy(&buf[8], GetPaddedName().c_str(), 28);
@ -139,10 +134,9 @@ int SCSIHD::Inquiry(const DWORD *cdb, BYTE *buf)
bool SCSIHD::ModeSelect(const DWORD *cdb, const BYTE *buf, int length)
{
int size;
assert(length >= 0);
ASSERT(buf);
ASSERT(length >= 0);
int size;
// PF
if (cdb[1] & 0x10) {
@ -216,23 +210,19 @@ bool SCSIHD::ModeSelect(const DWORD *cdb, const BYTE *buf, int length)
// Add Vendor special page to make drive Apple compatible
//
//---------------------------------------------------------------------------
int SCSIHD::AddVendorPage(int page, bool change, BYTE *buf)
void SCSIHD::AddVendorPage(map<int, vector<BYTE>>& pages, int page, bool changeable) const
{
ASSERT(buf);
// Page code 48 or 63
// Page code 48
if (page != 0x30 && page != 0x3f) {
return 0;
return;
}
// Set the message length
buf[0] = 0x30;
buf[1] = 0x1c;
vector<BYTE> buf(30);
// No changeable area
if (!change) {
memcpy(&buf[0xa], "APPLE COMPUTER, INC.", 20);
if (!changeable) {
memcpy(&buf.data()[0xa], "APPLE COMPUTER, INC.", 20);
}
return 30;
pages[48] = buf;
}

View File

@ -22,8 +22,8 @@
class SCSIHD : public Disk, public FileSupport
{
public:
SCSIHD(bool);
virtual ~SCSIHD() {};
SCSIHD(const set<uint32_t>&, bool);
virtual ~SCSIHD() {}
void FinalizeSetup(const Filepath&, off_t);
@ -34,6 +34,5 @@ public:
virtual int Inquiry(const DWORD *cdb, BYTE *buf) override;
bool ModeSelect(const DWORD *cdb, const BYTE *buf, int length) override;
// Add vendor special page
int AddVendorPage(int page, bool change, BYTE *buf) override;
void AddVendorPage(map<int, vector<BYTE>>&, int, bool) const override;
};

View File

@ -18,7 +18,7 @@
#include "fileio.h"
#include "exceptions.h"
SCSIHD_NEC::SCSIHD_NEC() : SCSIHD(false)
SCSIHD_NEC::SCSIHD_NEC(const set<uint32_t>& sector_sizes) : SCSIHD(sector_sizes, false)
{
// Work initialization
cylinders = 0;
@ -146,31 +146,30 @@ int SCSIHD_NEC::Inquiry(const DWORD *cdb, BYTE *buf)
return size;
}
int SCSIHD_NEC::AddErrorPage(bool change, BYTE *buf)
void SCSIHD_NEC::AddErrorPage(map<int, vector<BYTE>>& pages, bool) const
{
ASSERT(buf);
// Set the message length
buf[0] = 0x01;
buf[1] = 0x06;
vector<BYTE> buf(8);
// The retry count is 0, and the limit time uses the default value inside the device.
return 8;
pages[1] = buf;
}
int SCSIHD_NEC::AddFormatPage(bool change, BYTE *buf)
void SCSIHD_NEC::AddFormatPage(map<int, vector<BYTE>>& pages, bool changeable) const
{
ASSERT(buf);
vector<BYTE> buf(24);
// Set the message length
buf[0] = 0x80 | 0x03;
buf[1] = 0x16;
// Page can be saved
buf[0] = 0x80;
// Make the number of bytes in the physical sector appear mutable (although it cannot actually be)
if (change) {
if (changeable) {
buf[0xc] = 0xff;
buf[0xd] = 0xff;
return 24;
pages[3] = buf;
return;
}
if (IsReady()) {
@ -193,19 +192,15 @@ int SCSIHD_NEC::AddFormatPage(bool change, BYTE *buf)
buf[20] = 0x20;
}
return 24;
pages[3] = buf;
}
int SCSIHD_NEC::AddDrivePage(bool change, BYTE *buf)
void SCSIHD_NEC::AddDrivePage(map<int, vector<BYTE>>& pages, bool changeable) const
{
ASSERT(buf);
// Set the message length
buf[0] = 0x04;
buf[1] = 0x12;
vector<BYTE> buf(20);
// No changeable area
if (!change && IsReady()) {
if (!changeable && IsReady()) {
// Set the number of cylinders
buf[0x2] = (BYTE)(cylinders >> 16);
buf[0x3] = (BYTE)(cylinders >> 8);
@ -215,5 +210,5 @@ int SCSIHD_NEC::AddDrivePage(bool change, BYTE *buf)
buf[0x5] = (BYTE)heads;
}
return 20;
pages[4] = buf;
}

View File

@ -25,17 +25,17 @@
class SCSIHD_NEC : public SCSIHD
{
public:
SCSIHD_NEC();
~SCSIHD_NEC() {};
SCSIHD_NEC(const set<uint32_t>&);
~SCSIHD_NEC() {}
void Open(const Filepath& path) override;
// Commands
int Inquiry(const DWORD *cdb, BYTE *buf) override;
int AddErrorPage(bool change, BYTE *buf) override;
int AddFormatPage(bool change, BYTE *buf) override;
int AddDrivePage(bool change, BYTE *buf) override;
void AddErrorPage(map<int, vector<BYTE>>&, bool) const override;
void AddFormatPage(map<int, vector<BYTE>>&, bool) const override;
void AddDrivePage(map<int, vector<BYTE>>&, bool) const override;
private:
// Geometry data

View File

@ -19,29 +19,15 @@
#include "fileio.h"
#include "exceptions.h"
//===========================================================================
//
// SCSI magneto-optical disk
//
//===========================================================================
//---------------------------------------------------------------------------
//
// Constructor
//
//---------------------------------------------------------------------------
SCSIMO::SCSIMO() : Disk("SCMO")
SCSIMO::SCSIMO(const set<uint32_t>& sector_sizes, const map<uint64_t, Geometry>& geometries) : Disk("SCMO")
{
SetSectorSizes(sector_sizes);
SetGeometries(geometries);
}
//---------------------------------------------------------------------------
//
// Open
//
//---------------------------------------------------------------------------
void SCSIMO::Open(const Filepath& path)
{
ASSERT(!IsReady());
assert(!IsReady());
// Open as read-only
Fileio fio;
@ -77,16 +63,8 @@ void SCSIMO::Open(const Filepath& path)
}
}
//---------------------------------------------------------------------------
//
// INQUIRY
//
//---------------------------------------------------------------------------
int SCSIMO::Inquiry(const DWORD *cdb, BYTE *buf)
{
ASSERT(cdb);
ASSERT(buf);
// EVPD check
if (cdb[1] & 0x01) {
SetStatusCode(STATUS_INVALIDCDB);
@ -104,7 +82,7 @@ int SCSIMO::Inquiry(const DWORD *cdb, BYTE *buf)
buf[1] = 0x80;
buf[2] = 0x02;
buf[3] = 0x02;
buf[4] = 36 - 5; // required
buf[4] = 0x1F;
// Padded vendor, product, revision
memcpy(&buf[8], GetPaddedName().c_str(), 28);
@ -120,16 +98,36 @@ int SCSIMO::Inquiry(const DWORD *cdb, BYTE *buf)
return size;
}
//---------------------------------------------------------------------------
//
// MODE SELECT
//
//---------------------------------------------------------------------------
void SCSIMO::SetDeviceParameters(BYTE *buf)
{
Disk::SetDeviceParameters(buf);
// MEDIUM TYPE: Optical reversible or erasable
buf[2] = 0x03;
}
void SCSIMO::AddModePages(map<int, vector<BYTE>>& pages, int page, bool changeable) const
{
Disk::AddModePages(pages, page, changeable);
// Page code 6
if (page == 0x06 || page == 0x3f) {
AddOptionPage(pages, changeable);
}
}
void SCSIMO::AddOptionPage(map<int, vector<BYTE>>& pages, bool) const
{
vector<BYTE> buf(4);
pages[6] = buf;
// Do not report update blocks
}
bool SCSIMO::ModeSelect(const DWORD *cdb, const BYTE *buf, int length)
{
int size;
ASSERT(buf);
ASSERT(length >= 0);
// PF
@ -191,22 +189,20 @@ bool SCSIMO::ModeSelect(const DWORD *cdb, const BYTE *buf, int length)
// Vendor Unique Format Page 20h (MO)
//
//---------------------------------------------------------------------------
int SCSIMO::AddVendor(int page, BOOL change, BYTE *buf)
void SCSIMO::AddVendorPage(map<int, vector<BYTE>>& pages, int page, bool changeable) const
{
ASSERT(buf);
// Page code 20h
if ((page != 0x20) && (page != 0x3f)) {
return 0;
if (page != 0x20 && page != 0x3f) {
return;
}
// Set the message length
buf[0] = 0x20;
buf[1] = 0x0a;
vector<BYTE> buf(12);
// No changeable area
if (change) {
return 12;
if (changeable) {
pages[32] = buf;
return;
}
/*
@ -287,5 +283,7 @@ int SCSIMO::AddVendor(int page, BOOL change, BYTE *buf)
buf[11] = (BYTE)bands;
}
return 12;
pages[32] = buf;
return;
}

View File

@ -22,8 +22,8 @@
class SCSIMO : public Disk, public FileSupport
{
public:
SCSIMO();
~SCSIMO() {};
SCSIMO(const set<uint32_t>&, const map<uint64_t, Geometry>&);
~SCSIMO() {}
void Open(const Filepath& path) override;
@ -31,6 +31,14 @@ public:
int Inquiry(const DWORD *cdb, BYTE *buf) override;
bool ModeSelect(const DWORD *cdb, const BYTE *buf, int length) override;
protected:
// Internal processing
int AddVendor(int page, BOOL change, BYTE *buf); // Add vendor special page
void SetDeviceParameters(BYTE *) override;
void AddModePages(map<int, vector<BYTE>>&, int, bool) const override;
void AddVendorPage(map<int, vector<BYTE>>&, int, bool) const override;
private:
void AddOptionPage(map<int, vector<BYTE>>&, bool) const;
};

View File

@ -10,7 +10,6 @@
// Copyright (C) 2010 Y.Sugahara
//
// Imported sava's Anex86/T98Next image and MO format support patch.
// Imported NetBSD support and some optimisation patch by Rin Okuyama.
// Comments translated to english by akuker.
//
// [ DiskTrack and DiskCache ]

View File

@ -6,8 +6,6 @@
// Powered by XM6 TypeG Technology.
// Copyright (C) 2016-2020 GIMONS
//
// Imported NetBSD support and some optimisation patch by Rin Okuyama.
//
// [ GPIO-SCSI bus ]
//
//---------------------------------------------------------------------------

View File

@ -488,7 +488,7 @@ public:
// Bus signal acquisition
//
//---------------------------------------------------------------------------
inline DWORD Aquire()
inline DWORD Aquire() override
{
#if defined(__x86_64__) || defined(__X86__)
// Only used for development/debugging purposes. Isn't really applicable
@ -509,24 +509,24 @@ public:
void SetENB(BOOL ast);
// Set ENB signal
bool GetBSY();
bool GetBSY() override;
// Get BSY signal
void SetBSY(bool ast);
void SetBSY(bool ast) override;
// Set BSY signal
BOOL GetSEL();
BOOL GetSEL() override;
// Get SEL signal
void SetSEL(BOOL ast);
void SetSEL(BOOL ast) override;
// Set SEL signal
BOOL GetATN();
BOOL GetATN() override;
// Get ATN signal
void SetATN(BOOL ast);
void SetATN(BOOL ast) override;
// Set ATN signal
BOOL GetACK();
BOOL GetACK() override;
// Get ACK signal
void SetACK(BOOL ast);
void SetACK(BOOL ast) override;
// Set ACK signal
BOOL GetACT();
@ -534,42 +534,42 @@ public:
void SetACT(BOOL ast);
// Set ACT signal
BOOL GetRST();
BOOL GetRST() override;
// Get RST signal
void SetRST(BOOL ast);
void SetRST(BOOL ast) override;
// Set RST signal
BOOL GetMSG();
BOOL GetMSG() override;
// Get MSG signal
void SetMSG(BOOL ast);
void SetMSG(BOOL ast) override;
// Set MSG signal
BOOL GetCD();
BOOL GetCD() override;
// Get CD signal
void SetCD(BOOL ast);
void SetCD(BOOL ast) override;
// Set CD signal
BOOL GetIO();
BOOL GetIO() override;
// Get IO signal
void SetIO(BOOL ast);
void SetIO(BOOL ast) override;
// Set IO signal
BOOL GetREQ();
BOOL GetREQ() override;
// Get REQ signal
void SetREQ(BOOL ast);
void SetREQ(BOOL ast) override;
// Set REQ signal
BYTE GetDAT();
BYTE GetDAT() override;
// Get DAT signal
void SetDAT(BYTE dat);
void SetDAT(BYTE dat) override;
// Set DAT signal
BOOL GetDP();
BOOL GetDP() override;
// Get Data parity signal
int CommandHandShake(BYTE *buf);
int CommandHandShake(BYTE *buf) override;
// Command receive handshake
int ReceiveHandShake(BYTE *buf, int count);
int ReceiveHandShake(BYTE *buf, int count) override;
// Data receive handshake
int SendHandShake(BYTE *buf, int count, int delay_after_bytes);
int SendHandShake(BYTE *buf, int count, int delay_after_bytes) override;
// Data transmission handshake
static BUS::phase_t GetPhaseRaw(DWORD raw_data);

View File

@ -7,8 +7,6 @@
// Copyright (C) 2016-2020 GIMONS
// Copyright (C) 2020 akuker
//
// Imported NetBSD support and some optimisation patch by Rin Okuyama.
//
// [ OS related definitions ]
//
//---------------------------------------------------------------------------
@ -58,23 +56,11 @@
#include <pwd.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#if defined(__linux__)
#include <sys/epoll.h>
#endif
#include <netinet/in.h>
#if defined(__linux__)
#include <linux/gpio.h>
#include <linux/if.h>
#include <linux/if_tun.h>
#elif defined(__NetBSD__)
#include <sys/param.h>
#include <sys/sysctl.h>
#include <net/if.h>
#include <net/if_dl.h>
#include <net/if_tap.h>
#include <ifaddrs.h>
#endif
//---------------------------------------------------------------------------
//

View File

@ -773,8 +773,9 @@ bool Insert(const CommandContext& context, const PbDeviceDefinition& pb_device,
LOGINFO("Insert %sfile '%s' requested into %s ID %d, unit %d", pb_device.protected_() ? "protected " : "",
filename.c_str(), device->GetType().c_str(), pb_device.id(), pb_device.unit());
Disk *disk = dynamic_cast<Disk *>(device);
if (pb_device.block_size()) {
Disk *disk = dynamic_cast<Disk *>(device);
if (disk && disk->IsSectorSizeConfigurable()) {
if (!disk->SetConfiguredSectorSize(pb_device.block_size())) {
return ReturnLocalizedError(context, ERROR_BLOCK_SIZE, to_string(pb_device.block_size()));
@ -823,7 +824,6 @@ bool Insert(const CommandContext& context, const PbDeviceDefinition& pb_device,
device->SetProtected(pb_device.protected_());
}
Disk *disk = dynamic_cast<Disk *>(device);
if (disk) {
disk->MediumChanged();
}

View File

@ -148,6 +148,7 @@ public:
virtual void SetDAT(BYTE dat) = 0;
virtual BOOL GetDP() = 0; // Get parity signal
virtual DWORD Aquire() = 0;
virtual int CommandHandShake(BYTE *buf) = 0;
virtual int ReceiveHandShake(BYTE *buf, int count) = 0;
virtual int SendHandShake(BYTE *buf, int count, int delay_after_bytes) = 0;