diff --git a/easyinstall.sh b/easyinstall.sh index 6bfa87ca..288fa308 100755 --- a/easyinstall.sh +++ b/easyinstall.sh @@ -52,6 +52,7 @@ VIRTUAL_DRIVER_PATH="$HOME/images" CFG_PATH="$HOME/.config/rascsi" WEB_INSTALL_PATH="$BASE/python/web" OLED_INSTALL_PATH="$BASE/python/oled" +CTRLBOARD_INSTALL_PATH="$BASE/python/ctrlboard" PYTHON_COMMON_PATH="$BASE/python/common" SYSTEMD_PATH="/etc/systemd/system" HFS_FORMAT=/usr/bin/hformat @@ -314,6 +315,17 @@ function stopRaScsiScreen() { fi } +# Stops the rascsi-ctrlboard service if it is running +function stopRaScsiCtrlBoard() { + if [[ -f "$SYSTEMD_PATH/rascsi-ctrlboard.service" ]]; then + SERVICE_RASCSI_CTRLBOARD_RUNNING=0 + sudo systemctl is-active --quiet rascsi-ctrlboard.service >/dev/null 2>&1 || SERVICE_RASCSI_CTRLBOARD_RUNNING=$? + if [[ SERVICE_RASCSI_CTRLBOARD_RUNNING -eq 0 ]]; then + sudo systemctl stop rascsi-ctrlboard.service + fi + fi +} + # disables and removes the old monitor_rascsi service function disableOldRaScsiMonitorService() { if [ -f "$SYSTEMD_PATH/monitor_rascsi.service" ]; then @@ -332,6 +344,40 @@ function disableOldRaScsiMonitorService() { fi } +# disables the rascsi-oled service +function disableRaScsiOledService() { + if [ -f "$SYSTEMD_PATH/rascsi-oled.service" ]; then + SERVICE_RASCSI_OLED_RUNNING=0 + sudo systemctl is-active --quiet rascsi-oled.service >/dev/null 2>&1 || SERVICE_RASCSI_OLED_RUNNING=$? + if [[ $SERVICE_RASCSI_OLED_RUNNING -eq 0 ]]; then + sudo systemctl stop rascsi-oled.service + fi + + SERVICE_RASCSI_OLED_ENABLED=0 + sudo systemctl is-enabled --quiet rascsi-oled.service >/dev/null 2>&1 || SERVICE_RASCSI_OLED_ENABLED=$? + if [[ $SERVICE_RASCSI_OLED_ENABLED -eq 0 ]]; then + sudo systemctl disable rascsi-oled.service + fi + fi +} + +# disables the rascsi-ctrlboard service +function disableRaScsiCtrlBoardService() { + if [ -f "$SYSTEMD_PATH/rascsi-ctrlboard.service" ]; then + SERVICE_RASCSI_CTRLBOARD_RUNNING=0 + sudo systemctl is-active --quiet rascsi-ctrlboard.service >/dev/null 2>&1 || SERVICE_RASCSI_CTRLBOARD_RUNNING=$? + if [[ $SERVICE_RASCSI_CTRLBOARD_RUNNING -eq 0 ]]; then + sudo systemctl stop rascsi-ctrlboard.service + fi + + SERVICE_RASCSI_CTRLBOARD_ENABLED=0 + sudo systemctl is-enabled --quiet rascsi-ctrlboard.service >/dev/null 2>&1 || SERVICE_RASCSI_CTRLBOARD_ENABLED=$? + if [[ $SERVICE_RASCSI_CTRLBOARD_ENABLED -eq 0 ]]; then + sudo systemctl disable rascsi-ctrlboard.service + fi + fi +} + # Stops the macproxy service if it is running function stopMacproxy() { if [ -f "$SYSTEMD_PATH/macproxy.service" ]; then @@ -339,7 +385,7 @@ function stopMacproxy() { fi } -# Starts the rascsi-oled service if installed +# Checks whether the rascsi-oled service is installed function isRaScsiScreenInstalled() { SERVICE_RASCSI_OLED_ENABLED=0 if [[ -f "$SYSTEMD_PATH/rascsi-oled.service" ]]; then @@ -353,7 +399,19 @@ function isRaScsiScreenInstalled() { echo $SERVICE_RASCSI_OLED_ENABLED } -# Starts the rascsi-oled service if installed +# Checks whether the rascsi-ctrlboard service is installed +function isRaScsiCtrlBoardInstalled() { + SERVICE_RASCSI_CTRLBOARD_ENABLED=0 + if [[ -f "$SYSTEMD_PATH/rascsi-ctrlboard.service" ]]; then + sudo systemctl is-enabled --quiet rascsi-ctrlboard.service >/dev/null 2>&1 || SERVICE_RASCSI_CTRLBOARD_ENABLED=$? + else + SERVICE_RASCSI_CTRLBOARD_ENABLED=1 + fi + + echo $SERVICE_RASCSI_CTRLBOARD_ENABLED +} + +# Checks whether the rascsi-oled service is running function isRaScsiScreenRunning() { SERVICE_RASCSI_OLED_RUNNING=0 if [[ -f "$SYSTEMD_PATH/rascsi-oled.service" ]]; then @@ -367,6 +425,19 @@ function isRaScsiScreenRunning() { echo $SERVICE_RASCSI_OLED_RUNNING } +# Checks whether the rascsi-oled service is running +function isRaScsiCtrlBoardRunning() { + SERVICE_RASCSI_CTRLBOARD_RUNNING=0 + if [[ -f "$SYSTEMD_PATH/rascsi-ctrlboard.service" ]]; then + sudo systemctl is-active --quiet rascsi-ctrlboard.service >/dev/null 2>&1 || SERVICE_RASCSI_CTRLBOARD_RUNNING=$? + else + SERVICE_RASCSI_CTRLBOARD_RUNNING=1 + fi + + echo $SERVICE_RASCSI_CTRLBOARD_RUNNING +} + + # Starts the rascsi-oled service if installed function startRaScsiScreen() { if [[ $(isRaScsiScreenInstalled) -eq 0 ]] && [[ $(isRaScsiScreenRunning) -ne 1 ]]; then @@ -375,6 +446,14 @@ function startRaScsiScreen() { fi } +# Starts the rascsi-ctrlboard service if installed +function startRaScsiCtrlBoard() { + if [[ $(isRaScsiCtrlBoardInstalled) -eq 0 ]] && [[ $(isRaScsiCtrlBoardRunning) -ne 1 ]]; then + sudo systemctl start rascsi-ctrlboard.service + showRaScsiCtrlBoardStatus + fi +} + # Starts the macproxy service if installed function startMacproxy() { if [ -f "$SYSTEMD_PATH/macproxy.service" ]; then @@ -398,6 +477,11 @@ function showRaScsiScreenStatus() { systemctl status rascsi-oled | tee } +# Shows status for the rascsi-ctrlboard service +function showRaScsiCtrlBoardStatus() { + systemctl status rascsi-ctrlboard | tee +} + # Shows status for the macproxy service function showMacproxyStatus() { systemctl status macproxy | tee @@ -828,6 +912,7 @@ function installRaScsiScreen() { fi stopRaScsiScreen + disableRaScsiCtrlBoardService updateRaScsiGit sudo apt-get update && sudo apt-get install libjpeg-dev libpng-dev libopenjp2-7-dev i2c-tools raspi-config -y = 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 + "!") diff --git a/python/ctrlboard/src/ctrlboard_event_handler/ctrlboard_print_event_handler.py b/python/ctrlboard/src/ctrlboard_event_handler/ctrlboard_print_event_handler.py new file mode 100644 index 00000000..5992d383 --- /dev/null +++ b/python/ctrlboard/src/ctrlboard_event_handler/ctrlboard_print_event_handler.py @@ -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) diff --git a/python/ctrlboard/src/ctrlboard_event_handler/rascsi_profile_cycler.py b/python/ctrlboard/src/ctrlboard_event_handler/rascsi_profile_cycler.py new file mode 100644 index 00000000..581e99d5 --- /dev/null +++ b/python/ctrlboard/src/ctrlboard_event_handler/rascsi_profile_cycler.py @@ -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 diff --git a/python/ctrlboard/src/ctrlboard_event_handler/rascsi_shutdown_cycler.py b/python/ctrlboard/src/ctrlboard_event_handler/rascsi_shutdown_cycler.py new file mode 100644 index 00000000..3cd90b27 --- /dev/null +++ b/python/ctrlboard/src/ctrlboard_event_handler/rascsi_shutdown_cycler.py @@ -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" diff --git a/python/ctrlboard/src/ctrlboard_hw/__init__.py b/python/ctrlboard/src/ctrlboard_hw/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/python/ctrlboard/src/ctrlboard_hw/ctrlboard_hw.py b/python/ctrlboard/src/ctrlboard_hw/ctrlboard_hw.py new file mode 100644 index 00000000..6600d7f0 --- /dev/null +++ b/python/ctrlboard/src/ctrlboard_hw/ctrlboard_hw.py @@ -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() diff --git a/python/ctrlboard/src/ctrlboard_hw/ctrlboard_hw_constants.py b/python/ctrlboard/src/ctrlboard_hw/ctrlboard_hw_constants.py new file mode 100644 index 00000000..d1c4606e --- /dev/null +++ b/python/ctrlboard/src/ctrlboard_hw/ctrlboard_hw_constants.py @@ -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" diff --git a/python/ctrlboard/src/ctrlboard_hw/encoder.py b/python/ctrlboard/src/ctrlboard_hw/encoder.py new file mode 100644 index 00000000..0af34d7a --- /dev/null +++ b/python/ctrlboard/src/ctrlboard_hw/encoder.py @@ -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 diff --git a/python/ctrlboard/src/ctrlboard_hw/hardware_button.py b/python/ctrlboard/src/ctrlboard_hw/hardware_button.py new file mode 100644 index 00000000..43268f68 --- /dev/null +++ b/python/ctrlboard/src/ctrlboard_hw/hardware_button.py @@ -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) diff --git a/python/ctrlboard/src/ctrlboard_hw/pca9554multiplexer.py b/python/ctrlboard/src/ctrlboard_hw/pca9554multiplexer.py new file mode 100644 index 00000000..bf25fad6 --- /dev/null +++ b/python/ctrlboard/src/ctrlboard_hw/pca9554multiplexer.py @@ -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 diff --git a/python/ctrlboard/src/ctrlboard_menu_builder.py b/python/ctrlboard/src/ctrlboard_menu_builder.py new file mode 100644 index 00000000..160eb2bd --- /dev/null +++ b/python/ctrlboard/src/ctrlboard_menu_builder.py @@ -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 diff --git a/python/ctrlboard/src/main.py b/python/ctrlboard/src/main.py new file mode 100644 index 00000000..403a7923 --- /dev/null +++ b/python/ctrlboard/src/main.py @@ -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() diff --git a/python/ctrlboard/src/menu/__init__.py b/python/ctrlboard/src/menu/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/python/ctrlboard/src/menu/blank_screensaver.py b/python/ctrlboard/src/menu/blank_screensaver.py new file mode 100644 index 00000000..3f253f18 --- /dev/null +++ b/python/ctrlboard/src/menu/blank_screensaver.py @@ -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 diff --git a/python/ctrlboard/src/menu/cycler.py b/python/ctrlboard/src/menu/cycler.py new file mode 100644 index 00000000..f1744227 --- /dev/null +++ b/python/ctrlboard/src/menu/cycler.py @@ -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() diff --git a/python/ctrlboard/src/menu/menu.py b/python/ctrlboard/src/menu/menu.py new file mode 100644 index 00000000..17bbdafb --- /dev/null +++ b/python/ctrlboard/src/menu/menu.py @@ -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)) diff --git a/python/ctrlboard/src/menu/menu_builder.py b/python/ctrlboard/src/menu/menu_builder.py new file mode 100644 index 00000000..6bf680e4 --- /dev/null +++ b/python/ctrlboard/src/menu/menu_builder.py @@ -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""" diff --git a/python/ctrlboard/src/menu/menu_controller.py b/python/ctrlboard/src/menu/menu_controller.py new file mode 100644 index 00000000..6fe03c15 --- /dev/null +++ b/python/ctrlboard/src/menu/menu_controller.py @@ -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() diff --git a/python/ctrlboard/src/menu/menu_renderer.py b/python/ctrlboard/src/menu/menu_renderer.py new file mode 100644 index 00000000..8f7c17bf --- /dev/null +++ b/python/ctrlboard/src/menu/menu_renderer.py @@ -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) diff --git a/python/ctrlboard/src/menu/menu_renderer_adafruit_ssd1306.py b/python/ctrlboard/src/menu/menu_renderer_adafruit_ssd1306.py new file mode 100644 index 00000000..d5d4c2c8 --- /dev/null +++ b/python/ctrlboard/src/menu/menu_renderer_adafruit_ssd1306.py @@ -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() diff --git a/python/ctrlboard/src/menu/menu_renderer_config.py b/python/ctrlboard/src/menu/menu_renderer_config.py new file mode 100644 index 00000000..0d12e72c --- /dev/null +++ b/python/ctrlboard/src/menu/menu_renderer_config.py @@ -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] diff --git a/python/ctrlboard/src/menu/menu_renderer_luma_oled.py b/python/ctrlboard/src/menu/menu_renderer_luma_oled.py new file mode 100644 index 00000000..e6c30b53 --- /dev/null +++ b/python/ctrlboard/src/menu/menu_renderer_luma_oled.py @@ -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() diff --git a/python/ctrlboard/src/menu/screensaver.py b/python/ctrlboard/src/menu/screensaver.py new file mode 100644 index 00000000..f291f753 --- /dev/null +++ b/python/ctrlboard/src/menu/screensaver.py @@ -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 diff --git a/python/ctrlboard/src/menu/timer.py b/python/ctrlboard/src/menu/timer.py new file mode 100644 index 00000000..3d41cc6b --- /dev/null +++ b/python/ctrlboard/src/menu/timer.py @@ -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 diff --git a/python/ctrlboard/src/menu/transition.py b/python/ctrlboard/src/menu/transition.py new file mode 100644 index 00000000..a61df0a5 --- /dev/null +++ b/python/ctrlboard/src/menu/transition.py @@ -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) diff --git a/python/ctrlboard/src/observable.py b/python/ctrlboard/src/observable.py new file mode 100644 index 00000000..946dd1c5 --- /dev/null +++ b/python/ctrlboard/src/observable.py @@ -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) diff --git a/python/ctrlboard/src/observer.py b/python/ctrlboard/src/observer.py new file mode 100644 index 00000000..0fda4830 --- /dev/null +++ b/python/ctrlboard/src/observer.py @@ -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.""" diff --git a/python/ctrlboard/src/rascsi_menu_controller.py b/python/ctrlboard/src/rascsi_menu_controller.py new file mode 100644 index 00000000..9b0a538d --- /dev/null +++ b/python/ctrlboard/src/rascsi_menu_controller.py @@ -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) diff --git a/python/ctrlboard/start.sh b/python/ctrlboard/start.sh new file mode 100755 index 00000000..bf268346 --- /dev/null +++ b/python/ctrlboard/start.sh @@ -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 diff --git a/python/oled/src/pi_cmds.py b/python/oled/src/pi_cmds.py deleted file mode 100644 index a33913db..00000000 --- a/python/oled/src/pi_cmds.py +++ /dev/null @@ -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 diff --git a/python/oled/src/rascsi_oled_monitor.py b/python/oled/src/rascsi_oled_monitor.py index 343bd396..3201e768 100755 --- a/python/oled/src/rascsi_oled_monitor.py +++ b/python/oled/src/rascsi_oled_monitor.py @@ -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: diff --git a/python/web/src/device_utils.py b/python/web/src/device_utils.py deleted file mode 100644 index 0d1d6cf8..00000000 --- a/python/web/src/device_utils.py +++ /dev/null @@ -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 diff --git a/python/web/src/pi_cmds.py b/python/web/src/pi_cmds.py deleted file mode 100644 index 55fe4d86..00000000 --- a/python/web/src/pi_cmds.py +++ /dev/null @@ -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": ""} diff --git a/python/web/src/return_code_mapper.py b/python/web/src/return_code_mapper.py index a25a3f11..d42fad93 100644 --- a/python/web/src/return_code_mapper.py +++ b/python/web/src/return_code_mapper.py @@ -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 diff --git a/python/web/src/templates/base.html b/python/web/src/templates/base.html index da316687..9871e8d8 100644 --- a/python/web/src/templates/base.html +++ b/python/web/src/templates/base.html @@ -1,7 +1,7 @@ - {{ _("RaSCSI Control Page") }} + {{ _("RaSCSI Control Page") }} [{{ host }}] @@ -52,22 +52,27 @@
{{ _("Log In to Use Web Interface") }}
- + {% endif %} {% else %} {{ _("Web Interface Authentication Disabled") }} – {{ _("See Wiki for more information", url="https://github.com/akuker/RASCSI/wiki/Web-Interface#enable-authentication") }} {% endif %} - +
- - + + + +
+
-

RaSCSI - 68kmla Edition

+

{{ _("RaSCSI Control Page") }}

+ hostname: {{ host }} ip: {{ ip_addr }} +
diff --git a/python/web/src/templates/index.html b/python/web/src/templates/index.html index 43d0a328..269e1d58 100644 --- a/python/web/src/templates/index.html +++ b/python/web/src/templates/index.html @@ -65,7 +65,7 @@ + diff --git a/python/web/src/web.py b/python/web/src/web.py index 59a89862..15a4582f 100644 --- a/python/web/src/web.py +++ b/python/web/src/web.py @@ -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/") -def send_pwa_files(path): +@APP.route("/pwa/") +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) diff --git a/python/web/src/web_utils.py b/python/web/src/web_utils.py new file mode 100644 index 00000000..0d6403d9 --- /dev/null +++ b/python/web/src/web_utils.py @@ -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) diff --git a/src/raspberrypi/Makefile b/src/raspberrypi/Makefile index 4ede6e67..83baef0a 100644 --- a/src/raspberrypi/Makefile +++ b/src/raspberrypi/Makefile @@ -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 diff --git a/src/raspberrypi/controllers/sasidev_ctrl.cpp b/src/raspberrypi/controllers/sasidev_ctrl.cpp index 5fa72f51..b02e6a6f 100644 --- a/src/raspberrypi/controllers/sasidev_ctrl.cpp +++ b/src/raspberrypi/controllers/sasidev_ctrl.cpp @@ -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(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(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: diff --git a/src/raspberrypi/controllers/sasidev_ctrl.h b/src/raspberrypi/controllers/sasidev_ctrl.h index 136dc14f..c4253c1e 100644 --- a/src/raspberrypi/controllers/sasidev_ctrl.h +++ b/src/raspberrypi/controllers/sasidev_ctrl.h @@ -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 diff --git a/src/raspberrypi/controllers/scsidev_ctrl.cpp b/src/raspberrypi/controllers/scsidev_ctrl.cpp index b8699d55..c7efe4c9 100644 --- a/src/raspberrypi/controllers/scsidev_ctrl.cpp +++ b/src/raspberrypi/controllers/scsidev_ctrl.cpp @@ -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(ctrl.unit[GetEffectiveLun()]); if (device && ctrl.cmd[0] == scsi_defs::eCmdWrite6) { return device->WriteBytes(ctrl.buffer, scsi.bytes_to_transfer); diff --git a/src/raspberrypi/controllers/scsidev_ctrl.h b/src/raspberrypi/controllers/scsidev_ctrl.h index 9a3621cd..68fc3772 100644 --- a/src/raspberrypi/controllers/scsidev_ctrl.h +++ b/src/raspberrypi/controllers/scsidev_ctrl.h @@ -29,8 +29,9 @@ public: enum rascsi_shutdown_mode { NONE, - RASCSI, - PI + STOP_RASCSI, + STOP_PI, + RESTART_PI }; // Internal data definition diff --git a/src/raspberrypi/devices/cfilesystem.cpp b/src/raspberrypi/devices/cfilesystem.cpp index 96adb034..ee4eddd3 100644 --- a/src/raspberrypi/devices/cfilesystem.cpp +++ b/src/raspberrypi/devices/cfilesystem.cpp @@ -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__ } //--------------------------------------------------------------------------- diff --git a/src/raspberrypi/devices/ctapdriver.cpp b/src/raspberrypi/devices/ctapdriver.cpp index 0b9de36e..3a328339 100644 --- a/src/raspberrypi/devices/ctapdriver.cpp +++ b/src/raspberrypi/devices/ctapdriver.cpp @@ -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 #include #endif -// TODO Try to get rid of zlib, there is only one operation using it -#include // 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& const_params) return true; } -#endif // __linux__ - -#ifdef __NetBSD__ -bool CTapDriver::Init(const map&) -{ - 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); diff --git a/src/raspberrypi/devices/ctapdriver.h b/src/raspberrypi/devices/ctapdriver.h index 0d55dc1f..70ff51e3 100644 --- a/src/raspberrypi/devices/ctapdriver.h +++ b/src/raspberrypi/devices/ctapdriver.h @@ -7,8 +7,6 @@ // Copyright (C) 2016-2020 GIMONS // Copyright (C) akuker // -// Imported NetBSD support and some optimisation patch by Rin Okuyama. -// // [ TAP Driver ] // //--------------------------------------------------------------------------- diff --git a/src/raspberrypi/devices/device.h b/src/raspberrypi/devices/device.h index aeb94d9d..b3b321f8 100644 --- a/src/raspberrypi/devices/device.h +++ b/src/raspberrypi/devices/device.h @@ -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"; } }; diff --git a/src/raspberrypi/devices/device_factory.cpp b/src/raspberrypi/devices/device_factory.cpp index 86da4c23..989cf7f1 100644 --- a/src/raspberrypi/devices/device_factory.cpp +++ b/src/raspberrypi/devices/device_factory.cpp @@ -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: diff --git a/src/raspberrypi/devices/disk.cpp b/src/raspberrypi/devices/disk.cpp index 80d2d027..18e741cf 100644 --- a/src/raspberrypi/devices/disk.cpp +++ b/src/raspberrypi/devices/disk.cpp @@ -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>& 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>& pages, bool) const +{ + vector buf(12); + + // Retry count is 0, limit time uses internal default value + + pages[1] = buf; +} + +void Disk::AddFormatPage(map>& pages, bool changeable) const +{ + vector 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>& pages, bool changeable) const { - // Set the message length - buf[0] = 0x04; - buf[1] = 0x16; + vector 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>& 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 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, 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) { diff --git a/src/raspberrypi/devices/disk.h b/src/raspberrypi/devices/disk.h index 1a3c9687..be69b789 100644 --- a/src/raspberrypi/devices/disk.h +++ b/src/raspberrypi/devices/disk.h @@ -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, bool) const override; + virtual void AddErrorPage(map>&, bool) const; + virtual void AddFormatPage(map>&, bool) const; + virtual void AddDrivePage(map>&, bool) const; + void AddCachePage(map>&, bool) const; + virtual void AddVendorPage(map>&, int, bool) const; // Internal disk data disk_t disk; diff --git a/src/raspberrypi/devices/host_services.cpp b/src/raspberrypi/devices/host_services.cpp index 6a141399..0c25bdf0 100644 --- a/src/raspberrypi/devices/host_services.cpp +++ b/src/raspberrypi/devices/host_services.cpp @@ -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>& pages, int page, bool changeable) const { - if (page == 0x20) { + if (page == 0x20 || page == 0x3f) { + AddRealtimeClockPage(pages, changeable); + } +} + +void HostServices::AddRealtimeClockPage(map>& pages, bool changeable) const +{ + vector 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; } diff --git a/src/raspberrypi/devices/host_services.h b/src/raspberrypi/devices/host_services.h index 23276189..d71ee9b2 100644 --- a/src/raspberrypi/devices/host_services.h +++ b/src/raspberrypi/devices/host_services.h @@ -11,6 +11,7 @@ #pragma once #include "mode_page_device.h" +#include 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 dispatcher; - int AddRealtimeClockPage(int, BYTE *); + void AddModePages(map>&, int, bool) const override; + void AddRealtimeClockPage(map>&, bool) const; }; diff --git a/src/raspberrypi/devices/mode_page_device.cpp b/src/raspberrypi/devices/mode_page_device.cpp index 7ebce681..d625d580 100644 --- a/src/raspberrypi/devices/mode_page_device.cpp +++ b/src/raspberrypi/devices/mode_page_device.cpp @@ -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> 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 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); } diff --git a/src/raspberrypi/devices/mode_page_device.h b/src/raspberrypi/devices/mode_page_device.h index 3b50c236..19ec9db0 100644 --- a/src/raspberrypi/devices/mode_page_device.h +++ b/src/raspberrypi/devices/mode_page_device.h @@ -11,6 +11,8 @@ #include "primary_device.h" #include +#include +#include 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, 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(); }; diff --git a/src/raspberrypi/devices/primary_device.cpp b/src/raspberrypi/devices/primary_device.cpp index 9e3b3122..cad1212f 100644 --- a/src/raspberrypi/devices/primary_device.cpp +++ b/src/raspberrypi/devices/primary_device.cpp @@ -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; } diff --git a/src/raspberrypi/devices/primary_device.h b/src/raspberrypi/devices/primary_device.h index c986bc19..c8923fee 100644 --- a/src/raspberrypi/devices/primary_device.h +++ b/src/raspberrypi/devices/primary_device.h @@ -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: diff --git a/src/raspberrypi/devices/sasihd.cpp b/src/raspberrypi/devices/sasihd.cpp index be8293d2..463eda6b 100644 --- a/src/raspberrypi/devices/sasihd.cpp +++ b/src/raspberrypi/devices/sasihd.cpp @@ -19,26 +19,11 @@ #include "exceptions.h" #include "../config.h" -//=========================================================================== -// -// SASI Hard Disk -// -//=========================================================================== - -//--------------------------------------------------------------------------- -// -// Constructor -// -//--------------------------------------------------------------------------- -SASIHD::SASIHD() : Disk("SAHD") +SASIHD::SASIHD(const set& 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) { diff --git a/src/raspberrypi/devices/sasihd.h b/src/raspberrypi/devices/sasihd.h index a553fcac..c802364b 100644 --- a/src/raspberrypi/devices/sasihd.h +++ b/src/raspberrypi/devices/sasihd.h @@ -27,8 +27,8 @@ class SASIHD : public Disk, public FileSupport { public: - SASIHD(); - ~SASIHD() {}; + SASIHD(const set&); + ~SASIHD() {} void Reset(); void Open(const Filepath& path) override; diff --git a/src/raspberrypi/devices/scsi_daynaport.cpp b/src/raspberrypi/devices/scsi_daynaport.cpp index f70b6e7f..4094fbcc 100644 --- a/src/raspberrypi/devices/scsi_daynaport.cpp +++ b/src/raspberrypi/devices/scsi_daynaport.cpp @@ -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; +} diff --git a/src/raspberrypi/devices/scsi_daynaport.h b/src/raspberrypi/devices/scsi_daynaport.h index 1dbbef60..cda2cb0c 100644 --- a/src/raspberrypi/devices/scsi_daynaport.h +++ b/src/raspberrypi/devices/scsi_daynaport.h @@ -67,6 +67,7 @@ public: void SetInterfaceMode(SASIDEV *); void SetMcastAddr(SASIDEV *); void EnableInterface(SASIDEV *); + int GetSendDelay() override; bool Dispatch(SCSIDEV *) override; diff --git a/src/raspberrypi/devices/scsi_host_bridge.cpp b/src/raspberrypi/devices/scsi_host_bridge.cpp index d9be4579..d0dbb8c7 100644 --- a/src/raspberrypi/devices/scsi_host_bridge.cpp +++ b/src/raspberrypi/devices/scsi_host_bridge.cpp @@ -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; diff --git a/src/raspberrypi/devices/scsi_printer.cpp b/src/raspberrypi/devices/scsi_printer.cpp index 066eb4ac..e340ac6d 100644 --- a/src/raspberrypi/devices/scsi_printer.cpp +++ b/src/raspberrypi/devices/scsi_printer.cpp @@ -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& 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); diff --git a/src/raspberrypi/devices/scsicd.cpp b/src/raspberrypi/devices/scsicd.cpp index 255160e6..b6e34872 100644 --- a/src/raspberrypi/devices/scsicd.cpp +++ b/src/raspberrypi/devices/scsicd.cpp @@ -177,8 +177,10 @@ bool CDTrack::IsAudio() const // //=========================================================================== -SCSICD::SCSICD() : Disk("SCCD"), ScsiMmcCommands(), FileSupport() +SCSICD::SCSICD(const set& 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>& 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>& pages, bool changeable) const +{ + vector 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>& pages, bool) const +{ + vector 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) diff --git a/src/raspberrypi/devices/scsicd.h b/src/raspberrypi/devices/scsicd.h index 42150b5c..b22f398d 100644 --- a/src/raspberrypi/devices/scsicd.h +++ b/src/raspberrypi/devices/scsicd.h @@ -76,7 +76,7 @@ public: TrackMax = 96 // Maximum number of tracks }; - SCSICD(); + SCSICD(const set&); ~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, bool) const override; + private: typedef Disk super; Dispatcher dispatcher; + void AddCDROMPage(map>&, bool) const; + void AddCDDAPage(map>&, bool) const; + // Open void OpenCue(const Filepath& path); // Open(CUE) void OpenIso(const Filepath& path); // Open(ISO) diff --git a/src/raspberrypi/devices/scsihd.cpp b/src/raspberrypi/devices/scsihd.cpp index 4451c0dc..6a0cc194 100644 --- a/src/raspberrypi/devices/scsihd.cpp +++ b/src/raspberrypi/devices/scsihd.cpp @@ -26,8 +26,9 @@ // //=========================================================================== -SCSIHD::SCSIHD(bool removable) : Disk(removable ? "SCRM" : "SCHD") +SCSIHD::SCSIHD(const set& 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>& 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 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; } diff --git a/src/raspberrypi/devices/scsihd.h b/src/raspberrypi/devices/scsihd.h index 7bafb2f9..128578f5 100644 --- a/src/raspberrypi/devices/scsihd.h +++ b/src/raspberrypi/devices/scsihd.h @@ -22,8 +22,8 @@ class SCSIHD : public Disk, public FileSupport { public: - SCSIHD(bool); - virtual ~SCSIHD() {}; + SCSIHD(const set&, 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, bool) const override; }; diff --git a/src/raspberrypi/devices/scsihd_nec.cpp b/src/raspberrypi/devices/scsihd_nec.cpp index 0302b7dd..02b93eae 100644 --- a/src/raspberrypi/devices/scsihd_nec.cpp +++ b/src/raspberrypi/devices/scsihd_nec.cpp @@ -18,7 +18,7 @@ #include "fileio.h" #include "exceptions.h" -SCSIHD_NEC::SCSIHD_NEC() : SCSIHD(false) +SCSIHD_NEC::SCSIHD_NEC(const set& 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>& pages, bool) const { - ASSERT(buf); - - // Set the message length - buf[0] = 0x01; - buf[1] = 0x06; + vector 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>& pages, bool changeable) const { - ASSERT(buf); + vector 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>& pages, bool changeable) const { - ASSERT(buf); - - // Set the message length - buf[0] = 0x04; - buf[1] = 0x12; + vector 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; } diff --git a/src/raspberrypi/devices/scsihd_nec.h b/src/raspberrypi/devices/scsihd_nec.h index 82e2baa7..75923123 100644 --- a/src/raspberrypi/devices/scsihd_nec.h +++ b/src/raspberrypi/devices/scsihd_nec.h @@ -25,17 +25,17 @@ class SCSIHD_NEC : public SCSIHD { public: - SCSIHD_NEC(); - ~SCSIHD_NEC() {}; + SCSIHD_NEC(const set&); + ~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>&, bool) const override; + void AddFormatPage(map>&, bool) const override; + void AddDrivePage(map>&, bool) const override; private: // Geometry data diff --git a/src/raspberrypi/devices/scsimo.cpp b/src/raspberrypi/devices/scsimo.cpp index 75d1bc2c..0bed7926 100644 --- a/src/raspberrypi/devices/scsimo.cpp +++ b/src/raspberrypi/devices/scsimo.cpp @@ -19,29 +19,15 @@ #include "fileio.h" #include "exceptions.h" -//=========================================================================== -// -// SCSI magneto-optical disk -// -//=========================================================================== - -//--------------------------------------------------------------------------- -// -// Constructor -// -//--------------------------------------------------------------------------- -SCSIMO::SCSIMO() : Disk("SCMO") +SCSIMO::SCSIMO(const set& sector_sizes, const map& 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>& 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>& pages, bool) const +{ + vector 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>& 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 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; } diff --git a/src/raspberrypi/devices/scsimo.h b/src/raspberrypi/devices/scsimo.h index 3ed4738c..b55d42ea 100644 --- a/src/raspberrypi/devices/scsimo.h +++ b/src/raspberrypi/devices/scsimo.h @@ -22,8 +22,8 @@ class SCSIMO : public Disk, public FileSupport { public: - SCSIMO(); - ~SCSIMO() {}; + SCSIMO(const set&, const map&); + ~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, bool) const override; + void AddVendorPage(map>&, int, bool) const override; + +private: + + void AddOptionPage(map>&, bool) const; }; diff --git a/src/raspberrypi/file_access/disk_track_cache.cpp b/src/raspberrypi/file_access/disk_track_cache.cpp index fc35875d..5e31636f 100644 --- a/src/raspberrypi/file_access/disk_track_cache.cpp +++ b/src/raspberrypi/file_access/disk_track_cache.cpp @@ -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 ] diff --git a/src/raspberrypi/gpiobus.cpp b/src/raspberrypi/gpiobus.cpp index e1797bb0..6d0b155d 100644 --- a/src/raspberrypi/gpiobus.cpp +++ b/src/raspberrypi/gpiobus.cpp @@ -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 ] // //--------------------------------------------------------------------------- diff --git a/src/raspberrypi/gpiobus.h b/src/raspberrypi/gpiobus.h index 78b01b54..622cc493 100644 --- a/src/raspberrypi/gpiobus.h +++ b/src/raspberrypi/gpiobus.h @@ -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); diff --git a/src/raspberrypi/os.h b/src/raspberrypi/os.h index 29de7867..3be011fb 100644 --- a/src/raspberrypi/os.h +++ b/src/raspberrypi/os.h @@ -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 #include #include -#if defined(__linux__) #include -#endif #include - -#if defined(__linux__) #include #include #include -#elif defined(__NetBSD__) -#include -#include -#include -#include -#include -#include -#endif //--------------------------------------------------------------------------- // diff --git a/src/raspberrypi/rascsi.cpp b/src/raspberrypi/rascsi.cpp index 65698978..4cd7cf3c 100644 --- a/src/raspberrypi/rascsi.cpp +++ b/src/raspberrypi/rascsi.cpp @@ -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(device); + if (pb_device.block_size()) { - Disk *disk = dynamic_cast(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(device); if (disk) { disk->MediumChanged(); } diff --git a/src/raspberrypi/scsi.h b/src/raspberrypi/scsi.h index b648390c..0c9ec109 100644 --- a/src/raspberrypi/scsi.h +++ b/src/raspberrypi/scsi.h @@ -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;