Auto-format Python sources with black, fix all issues reported by flake8 (#1010)

* Update config for black and flake8
* Auto-format Python sources with black
* Fix issues reported by flake8
* Exclude protobuf files from black
* Address formatting feedback
This commit is contained in:
nucleogenic 2022-11-30 05:19:17 +00:00 committed by GitHub
parent 5afc6b911f
commit 315ef9f248
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
44 changed files with 1073 additions and 725 deletions

5
python/.flake8 Normal file
View File

@ -0,0 +1,5 @@
[flake8]
max-line-length = 100
exclude =
venv
rascsi_interface_pb2.py

View File

@ -13,17 +13,11 @@ CONFIG_FILE_SUFFIX = "json"
PROPERTIES_SUFFIX = "properties" PROPERTIES_SUFFIX = "properties"
# Supported archive file suffixes # Supported archive file suffixes
ARCHIVE_FILE_SUFFIXES = [ ARCHIVE_FILE_SUFFIXES = ["zip", "sit", "tar", "gz", "7z"]
"zip",
"sit",
"tar",
"gz",
"7z"
]
# The RESERVATIONS list is used to keep track of the reserved ID memos. # The RESERVATIONS list is used to keep track of the reserved ID memos.
# Initialize with a list of 8 empty strings. # Initialize with a list of 8 empty strings.
RESERVATIONS = ["" for _ in range(0, 8)] RESERVATIONS = ["" for _ in range(0, 8)]
# Standard error message for shell commands # Standard error message for shell commands
SHELL_ERROR = "Shell command: \"%s\" led to error: %s" SHELL_ERROR = 'Shell command: "%s" led to error: %s'

View File

@ -8,8 +8,8 @@ class FailedSocketConnectionException(Exception):
class EmptySocketChunkException(Exception): class EmptySocketChunkException(Exception):
"""Raise when a socket payload contains an empty chunk which implies a possible problem. """ """Raise when a socket payload contains an empty chunk which implies a possible problem."""
class InvalidProtobufResponse(Exception): class InvalidProtobufResponse(Exception):
"""Raise when a rascsi socket payload contains unpexpected data. """ """Raise when a rascsi socket payload contains unpexpected data."""

View File

@ -35,6 +35,7 @@ FILE_READ_ERROR = "Unhandled exception when reading file: %s"
FILE_WRITE_ERROR = "Unhandled exception when writing to file: %s" FILE_WRITE_ERROR = "Unhandled exception when writing to file: %s"
URL_SAFE = "/:?&" URL_SAFE = "/:?&"
class FileCmds: class FileCmds:
""" """
class for methods reading from and writing to the file system class for methods reading from and writing to the file system
@ -57,15 +58,10 @@ class FileCmds:
files_list = [] files_list = []
for file_path, _dirs, files in walk(dir_path): for file_path, _dirs, files in walk(dir_path):
# Only list selected file types # Only list selected file types
# TODO: Refactor for readability?
files = [f for f in files if f.lower().endswith(file_types)] files = [f for f in files if f.lower().endswith(file_types)]
files_list.extend( files_list.extend(
[ [(file, path.getsize(path.join(file_path, file))) for file in files],
(
file,
path.getsize(path.join(file_path, file))
)
for file in files
]
) )
return files_list return files_list
@ -108,7 +104,7 @@ class FileCmds:
if file.name in prop_files: if file.name in prop_files:
process = self.read_drive_properties( process = self.read_drive_properties(
Path(CFG_DIR) / f"{file.name}.{PROPERTIES_SUFFIX}" Path(CFG_DIR) / f"{file.name}.{PROPERTIES_SUFFIX}"
) )
prop = process["conf"] prop = process["conf"]
else: else:
prop = False prop = False
@ -118,12 +114,14 @@ class FileCmds:
try: try:
archive_info = self._get_archive_info( archive_info = self._get_archive_info(
f"{server_info['image_dir']}/{file.name}", f"{server_info['image_dir']}/{file.name}",
_cache_extra_key=file.size _cache_extra_key=file.size,
) )
properties_files = [x["path"] properties_files = [
for x in archive_info["members"] x["path"]
if x["path"].endswith(PROPERTIES_SUFFIX)] for x in archive_info["members"]
if x["path"].endswith(PROPERTIES_SUFFIX)
]
for member in archive_info["members"]: for member in archive_info["members"]:
if member["is_dir"] or member["is_resource_fork"]: if member["is_dir"] or member["is_resource_fork"]:
@ -132,7 +130,9 @@ class FileCmds:
if PurePath(member["path"]).suffix.lower()[1:] == PROPERTIES_SUFFIX: if PurePath(member["path"]).suffix.lower()[1:] == PROPERTIES_SUFFIX:
member["is_properties_file"] = True member["is_properties_file"] = True
elif f"{member['path']}.{PROPERTIES_SUFFIX}" in properties_files: elif f"{member['path']}.{PROPERTIES_SUFFIX}" in properties_files:
member["related_properties_file"] = f"{member['path']}.{PROPERTIES_SUFFIX}" member[
"related_properties_file"
] = f"{member['path']}.{PROPERTIES_SUFFIX}"
archive_contents.append(member) archive_contents.append(member)
except (unarchiver.LsarCommandError, unarchiver.LsarOutputError): except (unarchiver.LsarCommandError, unarchiver.LsarOutputError):
@ -140,14 +140,16 @@ class FileCmds:
size_mb = "{:,.1f}".format(file.size / 1024 / 1024) size_mb = "{:,.1f}".format(file.size / 1024 / 1024)
dtype = proto.PbDeviceType.Name(file.type) dtype = proto.PbDeviceType.Name(file.type)
files.append({ files.append(
"name": file.name, {
"size": file.size, "name": file.name,
"size_mb": size_mb, "size": file.size,
"detected_type": dtype, "size_mb": size_mb,
"prop": prop, "detected_type": dtype,
"archive_contents": archive_contents, "prop": prop,
}) "archive_contents": archive_contents,
}
)
return {"status": result.status, "msg": result.msg, "files": files} return {"status": result.status, "msg": result.msg, "files": files}
@ -233,22 +235,20 @@ class FileCmds:
Takes (Path) file_path for the file to delete Takes (Path) file_path for the file to delete
Returns (dict) with (bool) status and (str) msg Returns (dict) with (bool) status and (str) msg
""" """
parameters = { parameters = {"file_path": file_path}
"file_path": file_path
}
if file_path.exists(): if file_path.exists():
file_path.unlink() file_path.unlink()
return { return {
"status": True, "status": True,
"return_code": ReturnCodes.DELETEFILE_SUCCESS, "return_code": ReturnCodes.DELETEFILE_SUCCESS,
"parameters": parameters,
}
return {
"status": False,
"return_code": ReturnCodes.DELETEFILE_FILE_NOT_FOUND,
"parameters": parameters, "parameters": parameters,
} }
return {
"status": False,
"return_code": ReturnCodes.DELETEFILE_FILE_NOT_FOUND,
"parameters": parameters,
}
# noinspection PyMethodMayBeStatic # noinspection PyMethodMayBeStatic
def rename_file(self, file_path, target_path): def rename_file(self, file_path, target_path):
@ -258,21 +258,19 @@ class FileCmds:
- (Path) target_path for the name to rename - (Path) target_path for the name to rename
Returns (dict) with (bool) status and (str) msg Returns (dict) with (bool) status and (str) msg
""" """
parameters = { parameters = {"target_path": target_path}
"target_path": target_path
}
if target_path.parent.exists: if target_path.parent.exists:
file_path.rename(target_path) file_path.rename(target_path)
return { return {
"status": True, "status": True,
"return_code": ReturnCodes.RENAMEFILE_SUCCESS, "return_code": ReturnCodes.RENAMEFILE_SUCCESS,
"parameters": parameters, "parameters": parameters,
} }
return { return {
"status": False, "status": False,
"return_code": ReturnCodes.RENAMEFILE_UNABLE_TO_MOVE, "return_code": ReturnCodes.RENAMEFILE_UNABLE_TO_MOVE,
"parameters": parameters, "parameters": parameters,
} }
# noinspection PyMethodMayBeStatic # noinspection PyMethodMayBeStatic
def copy_file(self, file_path, target_path): def copy_file(self, file_path, target_path):
@ -282,21 +280,19 @@ class FileCmds:
- (Path) target_path for the name to copy to - (Path) target_path for the name to copy to
Returns (dict) with (bool) status and (str) msg Returns (dict) with (bool) status and (str) msg
""" """
parameters = { parameters = {"target_path": target_path}
"target_path": target_path
}
if target_path.parent.exists: if target_path.parent.exists:
copyfile(str(file_path), str(target_path)) copyfile(str(file_path), str(target_path))
return { return {
"status": True, "status": True,
"return_code": ReturnCodes.WRITEFILE_SUCCESS, "return_code": ReturnCodes.WRITEFILE_SUCCESS,
"parameters": parameters, "parameters": parameters,
} }
return { return {
"status": False, "status": False,
"return_code": ReturnCodes.WRITEFILE_UNABLE_TO_WRITE, "return_code": ReturnCodes.WRITEFILE_UNABLE_TO_WRITE,
"parameters": parameters, "parameters": parameters,
} }
def extract_image(self, file_path, members=None, move_properties_files_to_config=True): def extract_image(self, file_path, members=None, move_properties_files_to_config=True):
""" """
@ -312,60 +308,66 @@ class FileCmds:
return { return {
"status": False, "status": False,
"return_code": ReturnCodes.EXTRACTIMAGE_NO_FILES_SPECIFIED, "return_code": ReturnCodes.EXTRACTIMAGE_NO_FILES_SPECIFIED,
} }
try: try:
extract_result = unarchiver.extract_archive( extract_result = unarchiver.extract_archive(
f"{server_info['image_dir']}/{file_path}", f"{server_info['image_dir']}/{file_path}",
members=members, members=members,
output_dir=server_info["image_dir"], output_dir=server_info["image_dir"],
) )
properties_files_moved = [] properties_files_moved = []
if move_properties_files_to_config: if move_properties_files_to_config:
for file in extract_result["extracted"]: for file in extract_result["extracted"]:
if file.get("name").endswith(f".{PROPERTIES_SUFFIX}"): if file.get("name").endswith(f".{PROPERTIES_SUFFIX}"):
prop_path = Path(CFG_DIR) / file["name"] prop_path = Path(CFG_DIR) / file["name"]
if (self.rename_file( if self.rename_file(
Path(file["absolute_path"]), Path(file["absolute_path"]),
prop_path, prop_path,
)): ):
properties_files_moved.append({ properties_files_moved.append(
"status": True, {
"name": file["path"], "status": True,
"path": str(prop_path), "name": file["path"],
}) "path": str(prop_path),
}
)
else: else:
properties_files_moved.append({ properties_files_moved.append(
"status": False, {
"name": file["path"], "status": False,
"path": str(prop_path), "name": file["path"],
}) "path": str(prop_path),
}
)
return { return {
"status": True, "status": True,
"return_code": ReturnCodes.EXTRACTIMAGE_SUCCESS, "return_code": ReturnCodes.EXTRACTIMAGE_SUCCESS,
"parameters": { "parameters": {
"count": len(extract_result["extracted"]), "count": len(extract_result["extracted"]),
}, },
"extracted": extract_result["extracted"], "extracted": extract_result["extracted"],
"skipped": extract_result["skipped"], "skipped": extract_result["skipped"],
"properties_files_moved": properties_files_moved, "properties_files_moved": properties_files_moved,
} }
except unarchiver.UnarNoFilesExtractedError: except unarchiver.UnarNoFilesExtractedError:
return { return {
"status": False, "status": False,
"return_code": ReturnCodes.EXTRACTIMAGE_NO_FILES_EXTRACTED, "return_code": ReturnCodes.EXTRACTIMAGE_NO_FILES_EXTRACTED,
} }
except (unarchiver.UnarCommandError, unarchiver.UnarUnexpectedOutputError) as error: except (
unarchiver.UnarCommandError,
unarchiver.UnarUnexpectedOutputError,
) as error:
return { return {
"status": False, "status": False,
"return_code": ReturnCodes.EXTRACTIMAGE_COMMAND_ERROR, "return_code": ReturnCodes.EXTRACTIMAGE_COMMAND_ERROR,
"parameters": { "parameters": {
"error": error, "error": error,
} },
} }
# noinspection PyMethodMayBeStatic # noinspection PyMethodMayBeStatic
def partition_disk(self, file_name, volume_name, disk_format): def partition_disk(self, file_name, volume_name, disk_format):
@ -399,42 +401,42 @@ class FileCmds:
if disk_format == "HFS": if disk_format == "HFS":
partitioning_tool = "hfdisk" partitioning_tool = "hfdisk"
commands = [ commands = [
"i", "i",
"", "",
"C", "C",
"", "",
"32", "32",
"Driver_Partition", "Driver_Partition",
"Apple_Driver", "Apple_Driver",
"C", "C",
"", "",
"", "",
volume_name, volume_name,
"Apple_HFS", "Apple_HFS",
"w", "w",
"y", "y",
"p", "p",
] ]
# Create a DOS label, primary partition, W95 FAT type # Create a DOS label, primary partition, W95 FAT type
elif disk_format == "FAT": elif disk_format == "FAT":
partitioning_tool = "fdisk" partitioning_tool = "fdisk"
commands = [ commands = [
"o", "o",
"n", "n",
"p", "p",
"", "",
"", "",
"", "",
"t", "t",
"b", "b",
"w", "w",
] ]
try: try:
process = Popen( process = Popen(
[partitioning_tool, str(full_file_path)], [partitioning_tool, str(full_file_path)],
stdin=PIPE, stdin=PIPE,
stdout=PIPE, stdout=PIPE,
) )
for command in commands: for command in commands:
process.stdin.write(bytes(command + "\n", "utf-8")) process.stdin.write(bytes(command + "\n", "utf-8"))
process.stdin.flush() process.stdin.flush()
@ -464,7 +466,6 @@ class FileCmds:
return {"status": True, "msg": ""} return {"status": True, "msg": ""}
# noinspection PyMethodMayBeStatic # noinspection PyMethodMayBeStatic
def format_hfs(self, file_name, volume_name, driver_path): def format_hfs(self, file_name, volume_name, driver_path):
""" """
@ -514,7 +515,6 @@ class FileCmds:
return {"status": True, "msg": ""} return {"status": True, "msg": ""}
# noinspection PyMethodMayBeStatic # noinspection PyMethodMayBeStatic
def format_fat(self, file_name, volume_name, fat_size): def format_fat(self, file_name, volume_name, fat_size):
""" """
@ -538,21 +538,21 @@ class FileCmds:
else: else:
logging.info(process.stdout.decode("utf-8")) logging.info(process.stdout.decode("utf-8"))
self.delete_file(Path(file_name)) self.delete_file(Path(file_name))
return {"status": False, "msg": error.stderr.decode("utf-8")} return {"status": False, "msg": process.stderr.decode("utf-8")}
except (FileNotFoundError, CalledProcessError) as error: except (FileNotFoundError, CalledProcessError) as error:
logging.warning(SHELL_ERROR, " ".join(error.cmd), error.stderr.decode("utf-8")) logging.warning(SHELL_ERROR, " ".join(error.cmd), error.stderr.decode("utf-8"))
self.delete_file(Path(file_name)) self.delete_file(Path(file_name))
return {"status": False, "msg": error.stderr.decode("utf-8")} return {"status": False, "msg": error.stderr.decode("utf-8")}
args = [ args = [
"mkfs.fat", "mkfs.fat",
"-v", "-v",
"-F", "-F",
fat_size, fat_size,
"-n", "-n",
volume_name, volume_name,
"/dev/mapper/" + loopback_device, "/dev/mapper/" + loopback_device,
] ]
try: try:
process = run( process = run(
args, args,
@ -582,7 +582,6 @@ class FileCmds:
return {"status": True, "msg": ""} return {"status": True, "msg": ""}
def download_file_to_iso(self, url, *iso_args): def download_file_to_iso(self, url, *iso_args):
""" """
Takes (str) url and one or more (str) *iso_args Takes (str) url and one or more (str) *iso_args
@ -592,7 +591,7 @@ class FileCmds:
server_info = self.ractl.get_server_info() server_info = self.ractl.get_server_info()
file_name = PurePath(url).name file_name = PurePath(url).name
iso_filename = Path(server_info['image_dir']) / f"{file_name}.iso" iso_filename = Path(server_info["image_dir"]) / f"{file_name}.iso"
with TemporaryDirectory() as tmp_dir: with TemporaryDirectory() as tmp_dir:
req_proc = self.download_to_dir(quote(url, safe=URL_SAFE), tmp_dir, file_name) req_proc = self.download_to_dir(quote(url, safe=URL_SAFE), tmp_dir, file_name)
@ -603,23 +602,30 @@ class FileCmds:
tmp_full_path = Path(tmp_dir) / file_name tmp_full_path = Path(tmp_dir) / file_name
if is_zipfile(tmp_full_path): if is_zipfile(tmp_full_path):
if "XtraStuf.mac" in str(ZipFile(str(tmp_full_path)).namelist()): if "XtraStuf.mac" in str(ZipFile(str(tmp_full_path)).namelist()):
logging.info("MacZip file format detected. Will not unzip to retain resource fork.") logging.info(
"MacZip file format detected. Will not unzip to retain resource fork."
)
else: else:
logging.info( logging.info(
"%s is a zipfile! Will attempt to unzip and store the resulting files.", "%s is a zipfile! Will attempt to unzip and store the resulting files.",
tmp_full_path, tmp_full_path,
)
unzip_proc = asyncio.run(
self.run_async(
"unzip",
[
"-d",
str(tmp_dir),
"-n",
str(tmp_full_path),
],
) )
unzip_proc = asyncio.run(self.run_async("unzip", [ )
"-d",
str(tmp_dir),
"-n",
str(tmp_full_path),
]))
if not unzip_proc["returncode"]: if not unzip_proc["returncode"]:
logging.info( logging.info(
"%s was successfully unzipped. Deleting the zipfile.", "%s was successfully unzipped. Deleting the zipfile.",
tmp_full_path, tmp_full_path,
) )
tmp_full_path.unlink(True) tmp_full_path.unlink(True)
try: try:
@ -638,9 +644,7 @@ class FileCmds:
logging.warning(SHELL_ERROR, " ".join(error.cmd), error.stderr.decode("utf-8")) logging.warning(SHELL_ERROR, " ".join(error.cmd), error.stderr.decode("utf-8"))
return {"status": False, "msg": error.stderr.decode("utf-8")} return {"status": False, "msg": error.stderr.decode("utf-8")}
parameters = { parameters = {"value": " ".join(iso_args)}
"value": " ".join(iso_args)
}
return { return {
"status": True, "status": True,
"return_code": ReturnCodes.DOWNLOADFILETOISO_SUCCESS, "return_code": ReturnCodes.DOWNLOADFILETOISO_SUCCESS,
@ -658,10 +662,10 @@ class FileCmds:
try: try:
with requests.get( with requests.get(
quote(url, safe=URL_SAFE), quote(url, safe=URL_SAFE),
stream=True, stream=True,
headers={"User-Agent": "Mozilla/5.0"}, headers={"User-Agent": "Mozilla/5.0"},
) as req: ) as req:
req.raise_for_status() req.raise_for_status()
try: try:
with open(f"{save_dir}/{file_name}", "wb") as download: with open(f"{save_dir}/{file_name}", "wb") as download:
@ -677,10 +681,7 @@ class FileCmds:
logging.info("Response content-type: %s", req.headers["content-type"]) logging.info("Response content-type: %s", req.headers["content-type"])
logging.info("Response status code: %s", req.status_code) logging.info("Response status code: %s", req.status_code)
parameters = { parameters = {"file_name": file_name, "save_dir": save_dir}
"file_name": file_name,
"save_dir": save_dir
}
return { return {
"status": True, "status": True,
"return_code": ReturnCodes.DOWNLOADTODIR_SUCCESS, "return_code": ReturnCodes.DOWNLOADTODIR_SUCCESS,
@ -715,28 +716,29 @@ class FileCmds:
reserved_ids_and_memos = [] reserved_ids_and_memos = []
reserved_ids = self.ractl.get_reserved_ids()["ids"] reserved_ids = self.ractl.get_reserved_ids()["ids"]
for scsi_id in reserved_ids: for scsi_id in reserved_ids:
reserved_ids_and_memos.append({"id": scsi_id, reserved_ids_and_memos.append(
"memo": RESERVATIONS[int(scsi_id)]}) {"id": scsi_id, "memo": RESERVATIONS[int(scsi_id)]}
dump(
{"version": version,
"devices": devices,
"reserved_ids": reserved_ids_and_memos},
json_file,
indent=4
) )
parameters = { dump(
"target_path": file_path {
} "version": version,
"devices": devices,
"reserved_ids": reserved_ids_and_memos,
},
json_file,
indent=4,
)
parameters = {"target_path": file_path}
return { return {
"status": True, "status": True,
"return_code": ReturnCodes.WRITEFILE_SUCCESS, "return_code": ReturnCodes.WRITEFILE_SUCCESS,
"parameters": parameters, "parameters": parameters,
} }
except (IOError, ValueError, EOFError, TypeError) as error: except (IOError, ValueError, EOFError, TypeError) as error:
logging.error(str(error)) logging.error(str(error))
self.delete_file(Path(file_path)) self.delete_file(Path(file_path))
return {"status": False, "msg": str(error)} return {"status": False, "msg": str(error)}
except: except Exception:
logging.error(FILE_WRITE_ERROR, file_name) logging.error(FILE_WRITE_ERROR, file_name)
self.delete_file(Path(file_path)) self.delete_file(Path(file_path))
raise raise
@ -770,7 +772,7 @@ class FileCmds:
"revision": row["revision"], "revision": row["revision"],
"block_size": row["block_size"], "block_size": row["block_size"],
"params": dict(row["params"]), "params": dict(row["params"]),
} }
if row["image"]: if row["image"]:
kwargs["params"]["file"] = row["image"] kwargs["params"]["file"] = row["image"]
self.ractl.attach_device(row["id"], **kwargs) self.ractl.attach_device(row["id"], **kwargs)
@ -789,27 +791,27 @@ class FileCmds:
"revision": row["revision"], "revision": row["revision"],
"block_size": row["block_size"], "block_size": row["block_size"],
"params": dict(row["params"]), "params": dict(row["params"]),
} }
if row["image"]: if row["image"]:
kwargs["params"]["file"] = row["image"] kwargs["params"]["file"] = row["image"]
self.ractl.attach_device(row["id"], **kwargs) self.ractl.attach_device(row["id"], **kwargs)
logging.warning("%s is in an obsolete config file format", file_name) logging.warning("%s is in an obsolete config file format", file_name)
else: else:
return {"status": False, return {
"return_code": ReturnCodes.READCONFIG_INVALID_CONFIG_FILE_FORMAT} "status": False,
"return_code": ReturnCodes.READCONFIG_INVALID_CONFIG_FILE_FORMAT,
}
parameters = { parameters = {"file_name": file_name}
"file_name": file_name
}
return { return {
"status": True, "status": True,
"return_code": ReturnCodes.READCONFIG_SUCCESS, "return_code": ReturnCodes.READCONFIG_SUCCESS,
"parameters": parameters "parameters": parameters,
} }
except (IOError, ValueError, EOFError, TypeError) as error: except (IOError, ValueError, EOFError, TypeError) as error:
logging.error(str(error)) logging.error(str(error))
return {"status": False, "msg": str(error)} return {"status": False, "msg": str(error)}
except: except Exception:
logging.error(FILE_READ_ERROR, str(file_path)) logging.error(FILE_READ_ERROR, str(file_path))
raise raise
@ -823,19 +825,17 @@ class FileCmds:
try: try:
with open(file_path, "w") as json_file: with open(file_path, "w") as json_file:
dump(conf, json_file, indent=4) dump(conf, json_file, indent=4)
parameters = { parameters = {"target_path": str(file_path)}
"target_path": str(file_path)
}
return { return {
"status": True, "status": True,
"return_code": ReturnCodes.WRITEFILE_SUCCESS, "return_code": ReturnCodes.WRITEFILE_SUCCESS,
"parameters": parameters, "parameters": parameters,
} }
except (IOError, ValueError, EOFError, TypeError) as error: except (IOError, ValueError, EOFError, TypeError) as error:
logging.error(str(error)) logging.error(str(error))
self.delete_file(file_path) self.delete_file(file_path)
return {"status": False, "msg": str(error)} return {"status": False, "msg": str(error)}
except: except Exception:
logging.error(FILE_WRITE_ERROR, str(file_path)) logging.error(FILE_WRITE_ERROR, str(file_path))
self.delete_file(file_path) self.delete_file(file_path)
raise raise
@ -850,19 +850,17 @@ class FileCmds:
try: try:
with open(file_path) as json_file: with open(file_path) as json_file:
conf = load(json_file) conf = load(json_file)
parameters = { parameters = {"file_path": str(file_path)}
"file_path": str(file_path)
}
return { return {
"status": True, "status": True,
"return_codes": ReturnCodes.READDRIVEPROPS_SUCCESS, "return_codes": ReturnCodes.READDRIVEPROPS_SUCCESS,
"parameters": parameters, "parameters": parameters,
"conf": conf, "conf": conf,
} }
except (IOError, ValueError, EOFError, TypeError) as error: except (IOError, ValueError, EOFError, TypeError) as error:
logging.error(str(error)) logging.error(str(error))
return {"status": False, "msg": str(error)} return {"status": False, "msg": str(error)}
except: except Exception:
logging.error(FILE_READ_ERROR, str(file_path)) logging.error(FILE_READ_ERROR, str(file_path))
raise raise
@ -877,11 +875,17 @@ class FileCmds:
program, program,
*args, *args,
stdout=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE) stderr=asyncio.subprocess.PIPE,
)
stdout, stderr = await proc.communicate() stdout, stderr = await proc.communicate()
logging.info("Executed command \"%s %s\" with status code %d", program, " ".join(args), proc.returncode) logging.info(
'Executed command "%s %s" with status code %d',
program,
" ".join(args),
proc.returncode,
)
if stdout: if stdout:
stdout = stdout.decode() stdout = stdout.decode()
logging.info("stdout: %s", stdout) logging.info("stdout: %s", stdout)

View File

@ -11,6 +11,7 @@ class RaCtlCmds:
""" """
Class for commands sent to the RaSCSI backend service. Class for commands sent to the RaSCSI backend service.
""" """
def __init__(self, sock_cmd: SocketCmds, token=None, locale="en"): def __init__(self, sock_cmd: SocketCmds, token=None, locale="en"):
self.sock_cmd = sock_cmd self.sock_cmd = sock_cmd
self.token = token self.token = token
@ -37,9 +38,13 @@ class RaCtlCmds:
data = self.sock_cmd.send_pb_command(command.SerializeToString()) data = self.sock_cmd.send_pb_command(command.SerializeToString())
result = proto.PbResult() result = proto.PbResult()
result.ParseFromString(data) result.ParseFromString(data)
version = (str(result.server_info.version_info.major_version) + "." + version = (
str(result.server_info.version_info.minor_version) + "." + str(result.server_info.version_info.major_version)
str(result.server_info.version_info.patch_version)) + "."
+ str(result.server_info.version_info.minor_version)
+ "."
+ str(result.server_info.version_info.patch_version)
)
log_levels = list(result.server_info.log_level_info.log_levels) log_levels = list(result.server_info.log_level_info.log_levels)
current_log_level = result.server_info.log_level_info.current_log_level current_log_level = result.server_info.log_level_info.current_log_level
reserved_ids = list(result.server_info.reserved_ids_info.ids) reserved_ids = list(result.server_info.reserved_ids_info.ids)
@ -74,7 +79,7 @@ class RaCtlCmds:
"scrm": scrm, "scrm": scrm,
"scmo": scmo, "scmo": scmo,
"sccd": sccd, "sccd": sccd,
} }
def get_reserved_ids(self): def get_reserved_ids(self):
""" """
@ -137,11 +142,11 @@ class RaCtlCmds:
for key, value in device.properties.default_params.items(): for key, value in device.properties.default_params.items():
params[key] = value params[key] = value
device_types[proto.PbDeviceType.Name(device.type)] = { device_types[proto.PbDeviceType.Name(device.type)] = {
"removable": device.properties.removable, "removable": device.properties.removable,
"supports_file": device.properties.supports_file, "supports_file": device.properties.supports_file,
"params": params, "params": params,
"block_sizes": list(device.properties.block_sizes), "block_sizes": list(device.properties.block_sizes),
} }
return {"status": result.status, "device_types": device_types} return {"status": result.status, "device_types": device_types}
def get_removable_device_types(self): def get_removable_device_types(self):
@ -176,8 +181,8 @@ class RaCtlCmds:
device_types = self.get_device_types() device_types = self.get_device_types()
image_device_types = self.get_disk_device_types() image_device_types = self.get_disk_device_types()
peripheral_device_types = [ peripheral_device_types = [
x for x in device_types["device_types"] if x not in image_device_types x for x in device_types["device_types"] if x not in image_device_types
] ]
return peripheral_device_types return peripheral_device_types
def get_image_files_info(self): def get_image_files_info(self):
@ -205,7 +210,7 @@ class RaCtlCmds:
"images_dir": images_dir, "images_dir": images_dir,
"image_files": image_files, "image_files": image_files,
"scan_depth": scan_depth, "scan_depth": scan_depth,
} }
def attach_device(self, scsi_id, **kwargs): def attach_device(self, scsi_id, **kwargs):
""" """
@ -245,13 +250,13 @@ class RaCtlCmds:
if current_type != device_type: if current_type != device_type:
parameters = { parameters = {
"device_type": device_type, "device_type": device_type,
"current_device_type": current_type "current_device_type": current_type,
} }
return { return {
"status": False, "status": False,
"return_code": ReturnCodes.ATTACHIMAGE_COULD_NOT_ATTACH, "return_code": ReturnCodes.ATTACHIMAGE_COULD_NOT_ATTACH,
"parameters": parameters, "parameters": parameters,
} }
command.operation = proto.PbOperation.INSERT command.operation = proto.PbOperation.INSERT
# Handling attaching a new device # Handling attaching a new device
@ -394,20 +399,22 @@ class RaCtlCmds:
dblock = result.devices_info.devices[i].block_size dblock = result.devices_info.devices[i].block_size
dsize = int(result.devices_info.devices[i].block_count) * int(dblock) dsize = int(result.devices_info.devices[i].block_count) * int(dblock)
device_list.append({ device_list.append(
"id": did, {
"unit": dunit, "id": did,
"device_type": dtype, "unit": dunit,
"status": ", ".join(dstat_msg), "device_type": dtype,
"image": dpath, "status": ", ".join(dstat_msg),
"file": dfile, "image": dpath,
"params": dparam, "file": dfile,
"vendor": dven, "params": dparam,
"product": dprod, "vendor": dven,
"revision": drev, "product": dprod,
"block_size": dblock, "revision": drev,
"size": dsize, "block_size": dblock,
}) "size": dsize,
}
)
i += 1 i += 1
return {"status": result.status, "msg": result.msg, "device_list": device_list} return {"status": result.status, "msg": result.msg, "device_list": device_list}

View File

@ -6,6 +6,7 @@ Module for return codes that are refrenced in the return payloads of the rascsi
# pylint: disable=too-few-public-methods # pylint: disable=too-few-public-methods
class ReturnCodes: class ReturnCodes:
"""Class for the return codes used within the rascsi module.""" """Class for the return codes used within the rascsi module."""
DELETEFILE_SUCCESS = 0 DELETEFILE_SUCCESS = 0
DELETEFILE_FILE_NOT_FOUND = 1 DELETEFILE_FILE_NOT_FOUND = 1
RENAMEFILE_SUCCESS = 10 RENAMEFILE_SUCCESS = 10

View File

@ -7,15 +7,18 @@ import socket
from time import sleep from time import sleep
from struct import pack, unpack from struct import pack, unpack
from rascsi.exceptions import (EmptySocketChunkException, from rascsi.exceptions import (
InvalidProtobufResponse, EmptySocketChunkException,
FailedSocketConnectionException) InvalidProtobufResponse,
FailedSocketConnectionException,
)
class SocketCmds: class SocketCmds:
""" """
Class for sending and receiving data over a socket connection with the RaSCSI backend Class for sending and receiving data over a socket connection with the RaSCSI backend
""" """
def __init__(self, host="localhost", port=6868): def __init__(self, host="localhost", port=6868):
self.host = host self.host = host
self.port = port self.port = port
@ -38,8 +41,11 @@ class SocketCmds:
return response return response
except socket.error as error: except socket.error as error:
counter += 1 counter += 1
logging.warning("The RaSCSI service is not responding - attempt %s/%s", logging.warning(
str(counter), str(tries)) "The RaSCSI service is not responding - attempt %s/%s",
str(counter),
str(tries),
)
error_msg = str(error) error_msg = str(error)
sleep(0.2) sleep(0.2)
except EmptySocketChunkException as ex: except EmptySocketChunkException as ex:
@ -75,18 +81,22 @@ class SocketCmds:
bytes_recvd = 0 bytes_recvd = 0
while bytes_recvd < response_length: while bytes_recvd < response_length:
chunk = sock.recv(min(response_length - bytes_recvd, 2048)) chunk = sock.recv(min(response_length - bytes_recvd, 2048))
if chunk == b'': if chunk == b"":
error_message = ("Read an empty chunk from the socket. Socket connection has " error_message = (
"dropped unexpectedly. RaSCSI may have crashed.") "Read an empty chunk from the socket. Socket connection has "
"dropped unexpectedly. RaSCSI may have crashed."
)
logging.error(error_message) logging.error(error_message)
raise EmptySocketChunkException(error_message) raise EmptySocketChunkException(error_message)
chunks.append(chunk) chunks.append(chunk)
bytes_recvd = bytes_recvd + len(chunk) bytes_recvd = bytes_recvd + len(chunk)
response_message = b''.join(chunks) response_message = b"".join(chunks)
return response_message return response_message
error_message = ("The response from RaSCSI did not contain a protobuf header. " error_message = (
"RaSCSI may have crashed.") "The response from RaSCSI did not contain a protobuf header. "
"RaSCSI may have crashed."
)
logging.error(error_message) logging.error(error_message)
raise InvalidProtobufResponse(error_message) raise InvalidProtobufResponse(error_message)

View File

@ -12,6 +12,7 @@ from platform import uname
from rascsi.common_settings import SHELL_ERROR from rascsi.common_settings import SHELL_ERROR
class SysCmds: class SysCmds:
""" """
Class for commands sent to the Pi's Linux system. Class for commands sent to the Pi's Linux system.
@ -30,7 +31,7 @@ class SysCmds:
["git", "rev-parse", "HEAD"], ["git", "rev-parse", "HEAD"],
capture_output=True, capture_output=True,
check=True, check=True,
) )
.stdout.decode("utf-8") .stdout.decode("utf-8")
.strip() .strip()
) )
@ -68,7 +69,7 @@ class SysCmds:
return { return {
"git": ra_git_version, "git": ra_git_version,
"env": f"{hardware}, {env.system} {env.release} {env.machine}", "env": f"{hardware}, {env.system} {env.release} {env.machine}",
} }
@staticmethod @staticmethod
def running_proc(daemon): def running_proc(daemon):
@ -82,7 +83,7 @@ class SysCmds:
["ps", "aux"], ["ps", "aux"],
capture_output=True, capture_output=True,
check=True, check=True,
) )
.stdout.decode("utf-8") .stdout.decode("utf-8")
.strip() .strip()
) )
@ -104,7 +105,7 @@ class SysCmds:
["brctl", "show"], ["brctl", "show"],
capture_output=True, capture_output=True,
check=True, check=True,
) )
.stdout.decode("utf-8") .stdout.decode("utf-8")
.strip() .strip()
) )
@ -155,7 +156,7 @@ class SysCmds:
sock = socket(AF_INET, SOCK_DGRAM) sock = socket(AF_INET, SOCK_DGRAM)
try: try:
# mock ip address; doesn't have to be reachable # mock ip address; doesn't have to be reachable
sock.connect(('10.255.255.255', 1)) sock.connect(("10.255.255.255", 1))
ip_addr = sock.getsockname()[0] ip_addr = sock.getsockname()[0]
except Exception: except Exception:
ip_addr = False ip_addr = False
@ -170,10 +171,10 @@ class SysCmds:
""" """
try: try:
process = run( process = run(
["hostnamectl", "status", "--pretty"], ["hostnamectl", "status", "--pretty"],
capture_output=True, capture_output=True,
check=True, check=True,
) )
pretty_hostname = process.stdout.decode("utf-8").rstrip() pretty_hostname = process.stdout.decode("utf-8").rstrip()
if pretty_hostname: if pretty_hostname:
return pretty_hostname return pretty_hostname
@ -188,11 +189,11 @@ class SysCmds:
Set the pretty hostname for the system Set the pretty hostname for the system
""" """
try: try:
process = run( run(
["sudo", "hostnamectl", "set-hostname", "--pretty", name], ["sudo", "hostnamectl", "set-hostname", "--pretty", name],
capture_output=False, capture_output=False,
check=True, check=True,
) )
except CalledProcessError as error: except CalledProcessError as error:
logging.error(str(error)) logging.error(str(error))
return False return False
@ -213,9 +214,9 @@ class SysCmds:
if scope: if scope:
scope_param = ["-u", scope] scope_param = ["-u", scope]
process = run( process = run(
["journalctl"] + line_param + scope_param, ["journalctl"] + line_param + scope_param,
capture_output=True, capture_output=True,
) )
if process.returncode == 0: if process.returncode == 0:
return process.returncode, process.stdout.decode("utf-8") return process.returncode, process.stdout.decode("utf-8")
@ -228,9 +229,9 @@ class SysCmds:
Returns either the disktype output, or the stderr output. Returns either the disktype output, or the stderr output.
""" """
process = run( process = run(
["disktype", file_path], ["disktype", file_path],
capture_output=True, capture_output=True,
) )
if process.returncode == 0: if process.returncode == 0:
return process.returncode, process.stdout.decode("utf-8") return process.returncode, process.stdout.decode("utf-8")
@ -243,9 +244,9 @@ class SysCmds:
Returns either the man2html output, or the stderr output. Returns either the man2html output, or the stderr output.
""" """
process = run( process = run(
["man2html", file_path, "-M", "/"], ["man2html", file_path, "-M", "/"],
capture_output=True, capture_output=True,
) )
if process.returncode == 0: if process.returncode == 0:
return process.returncode, process.stdout.decode("utf-8") return process.returncode, process.stdout.decode("utf-8")
@ -257,9 +258,9 @@ class SysCmds:
Sends a reboot command to the system Sends a reboot command to the system
""" """
process = run( process = run(
["sudo", "reboot"], ["sudo", "reboot"],
capture_output=True, capture_output=True,
) )
if process.returncode == 0: if process.returncode == 0:
return process.returncode, process.stdout.decode("utf-8") return process.returncode, process.stdout.decode("utf-8")
@ -271,9 +272,9 @@ class SysCmds:
Sends a shutdown command to the system Sends a shutdown command to the system
""" """
process = run( process = run(
["sudo", "shutdown", "-h", "now"], ["sudo", "shutdown", "-h", "now"],
capture_output=True, capture_output=True,
) )
if process.returncode == 0: if process.returncode == 0:
return process.returncode, process.stdout.decode("utf-8") return process.returncode, process.stdout.decode("utf-8")

View File

@ -4,31 +4,27 @@ Utility module for running system commands with basic logging
import asyncio import asyncio
import logging import logging
import os
def run(program, args=None): def run(program, args=None):
""" Run a command and return its output """ """Run a command and return its output"""
return asyncio.run(run_async(program, args)) return asyncio.run(run_async(program, args))
async def run_async(program, args=None): async def run_async(program, args=None):
""" Run a command in the background """ """Run a command in the background"""
proc = await asyncio.create_subprocess_exec( proc = await asyncio.create_subprocess_exec(
program, program, *args, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
*args, )
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE
)
stdout, stderr = await proc.communicate() stdout, stderr = await proc.communicate()
logging.info( logging.info(
"Executed command \"%s %s\" with status code %d", 'Executed command "%s %s" with status code %d',
program, program,
" ".join(args), " ".join(args),
proc.returncode proc.returncode,
) )
if stdout: if stdout:
stdout = stdout.decode() stdout = stdout.decode()
@ -42,4 +38,4 @@ async def run_async(program, args=None):
"returncode": proc.returncode, "returncode": proc.returncode,
"stdout": stdout, "stdout": stdout,
"stderr": stderr, "stderr": stderr,
} }

View File

@ -25,7 +25,8 @@ def extract_archive(file_path, **kwargs):
Takes (str) file_path, and kwargs: Takes (str) file_path, and kwargs:
- (list) members - list of (str) files to be extracted (all files are extracted if None) - (list) members - list of (str) files to be extracted (all files are extracted if None)
- (str) output_dir - directory to place the extracted files - (str) output_dir - directory to place the extracted files
- (str) fork_output_type - output type for resource forks; "visible" for *.rsrc files, "hidden" for ._* files - (str) fork_output_type - output type for resource forks;
"visible" for *.rsrc files, "hidden" for ._* files
Returns (dict) of extracted and skipped members Returns (dict) of extracted and skipped members
""" """
members = kwargs.get("members") members = kwargs.get("members")
@ -39,7 +40,9 @@ def extract_archive(file_path, **kwargs):
if kwargs.get("fork_output_type"): if kwargs.get("fork_output_type"):
if kwargs["fork_output_type"] not in FORK_OUTPUT_TYPES: if kwargs["fork_output_type"] not in FORK_OUTPUT_TYPES:
raise ValueError(f"Argument fork_output_type must be one of: {','.join(FORK_OUTPUT_TYPES)} ") raise ValueError(
f"Argument fork_output_type must be one of: {','.join(FORK_OUTPUT_TYPES)} "
)
fork_output_type = kwargs["fork_output_type"] fork_output_type = kwargs["fork_output_type"]
fork_output_type_args = ["-forks", fork_output_type or FORK_OUTPUT_TYPE_VISIBLE] fork_output_type_args = ["-forks", fork_output_type or FORK_OUTPUT_TYPE_VISIBLE]
else: else:
@ -53,9 +56,9 @@ def extract_archive(file_path, **kwargs):
"-force-skip", "-force-skip",
"-no-directory", "-no-directory",
*fork_output_type_args, *fork_output_type_args,
'--', "--",
file_path, file_path,
] ]
if members: if members:
for member in members: for member in members:
@ -68,8 +71,10 @@ def extract_archive(file_path, **kwargs):
unar_result_success = r'^Successfully extracted to "(?P<destination>.+)".$' unar_result_success = r'^Successfully extracted to "(?P<destination>.+)".$'
unar_result_no_files = "No files extracted." unar_result_no_files = "No files extracted."
unar_file_extracted = \ unar_file_extracted = (
r"^ {2}(?P<path>.+). \(((?P<size>\d+) B)?(?P<types>(dir)?(, )?(rsrc)?)\)\.\.\. (?P<status>[A-Z]+)\.$" r"^ {2}(?P<path>.+). \(((?P<size>\d+) B)?(?P<types>(dir)?(, )?"
r"(rsrc)?)\)\.\.\. (?P<status>[A-Z]+)\.$"
)
lines = process["stdout"].rstrip("\n").split("\n") lines = process["stdout"].rstrip("\n").split("\n")
@ -90,7 +95,7 @@ def extract_archive(file_path, **kwargs):
"is_dir": False, "is_dir": False,
"is_resource_fork": False, "is_resource_fork": False,
"absolute_path": str(pathlib.PurePath(tmp_dir).joinpath(matches["path"])), "absolute_path": str(pathlib.PurePath(tmp_dir).joinpath(matches["path"])),
} }
member_types = matches.get("types", "") member_types = matches.get("types", "")
if member_types.startswith(", "): if member_types.startswith(", "):
@ -112,10 +117,14 @@ def extract_archive(file_path, **kwargs):
member["name"] = f"._{member['name']}" member["name"] = f"._{member['name']}"
else: else:
member["name"] += ".rsrc" member["name"] += ".rsrc"
member["path"] = str(pathlib.PurePath(member["path"]).parent.joinpath(member["name"])) member["path"] = str(
member["absolute_path"] = str(pathlib.PurePath(tmp_dir).joinpath(member["path"])) pathlib.PurePath(member["path"]).parent.joinpath(member["name"])
)
member["absolute_path"] = str(
pathlib.PurePath(tmp_dir).joinpath(member["path"])
)
logging.debug("Extracted: %s -> %s", member['path'], member['absolute_path']) logging.debug("Extracted: %s -> %s", member["path"], member["absolute_path"])
extracted_members.append(member) extracted_members.append(member)
else: else:
raise UnarUnexpectedOutputError(f"Unexpected output: {line}") raise UnarUnexpectedOutputError(f"Unexpected output: {line}")
@ -128,7 +137,10 @@ def extract_archive(file_path, **kwargs):
member["absolute_path"] = str(target_path) member["absolute_path"] = str(target_path)
if target_path.exists(): if target_path.exists():
logging.info("Skipping temp file/dir as the target already exists: %s", target_path) logging.info(
"Skipping temp file/dir as the target already exists: %s",
target_path,
)
skipped.append(member) skipped.append(member)
continue continue
@ -147,7 +159,7 @@ def extract_archive(file_path, **kwargs):
return { return {
"extracted": moved, "extracted": moved,
"skipped": skipped, "skipped": skipped,
} }
raise UnarUnexpectedOutputError(lines[-1]) raise UnarUnexpectedOutputError(lines[-1])
@ -171,37 +183,41 @@ def inspect_archive(file_path):
except JSONDecodeError as error: except JSONDecodeError as error:
raise LsarOutputError(f"Unable to read JSON output from lsar: {error.msg}") from error raise LsarOutputError(f"Unable to read JSON output from lsar: {error.msg}") from error
members = [{ members = [
"name": pathlib.PurePath(member.get("XADFileName")).name, {
"path": member.get("XADFileName"), "name": pathlib.PurePath(member.get("XADFileName")).name,
"size": member.get("XADFileSize"), "path": member.get("XADFileName"),
"is_dir": member.get("XADIsDirectory"), "size": member.get("XADFileSize"),
"is_resource_fork": member.get("XADIsResourceFork"), "is_dir": member.get("XADIsDirectory"),
"raw": member, "is_resource_fork": member.get("XADIsResourceFork"),
} for member in archive_info.get("lsarContents", [])] "raw": member,
}
for member in archive_info.get("lsarContents", [])
]
return { return {
"format": archive_info.get("lsarFormatName"), "format": archive_info.get("lsarFormatName"),
"members": members, "members": members,
} }
class UnarCommandError(Exception): class UnarCommandError(Exception):
""" Command execution was unsuccessful """ """Command execution was unsuccessful"""
pass pass
class UnarNoFilesExtractedError(Exception): class UnarNoFilesExtractedError(Exception):
""" Command completed, but no files extracted """ """Command completed, but no files extracted"""
class UnarUnexpectedOutputError(Exception): class UnarUnexpectedOutputError(Exception):
""" Command output not recognized """ """Command output not recognized"""
class LsarCommandError(Exception): class LsarCommandError(Exception):
""" Command execution was unsuccessful """ """Command execution was unsuccessful"""
class LsarOutputError(Exception): class LsarOutputError(Exception):
""" Command output could not be parsed""" """Command output could not be parsed"""

View File

@ -7,6 +7,7 @@ from ctrlboard_hw.ctrlboard_hw_constants import CtrlBoardHardwareConstants
# pylint: disable=too-few-public-methods # pylint: disable=too-few-public-methods
class CtrlboardConfig: class CtrlboardConfig:
"""Class for central RaSCSI control board configuration parameters""" """Class for central RaSCSI control board configuration parameters"""
ROTATION = 0 ROTATION = 0
WIDTH = 128 WIDTH = 128
HEIGHT = 64 HEIGHT = 64

View File

@ -19,8 +19,12 @@ from rascsi_menu_controller import RascsiMenuController
class CtrlBoardMenuUpdateEventHandler(Observer): class CtrlBoardMenuUpdateEventHandler(Observer):
"""Class interfacing the menu controller the RaSCSI Control Board hardware.""" """Class interfacing the menu controller the RaSCSI Control Board hardware."""
def __init__(self, menu_controller: RascsiMenuController, sock_cmd: SocketCmds, def __init__(
ractl_cmd: RaCtlCmds): self,
menu_controller: RascsiMenuController,
sock_cmd: SocketCmds,
ractl_cmd: RaCtlCmds,
):
self.message = None self.message = None
self._menu_controller = menu_controller self._menu_controller = menu_controller
self._menu_renderer_config = self._menu_controller.get_menu_renderer().get_config() self._menu_renderer_config = self._menu_controller.get_menu_renderer().get_config()
@ -73,16 +77,18 @@ class CtrlBoardMenuUpdateEventHandler(Observer):
def handle_button1(self): def handle_button1(self):
"""Method for handling the first cycle button (cycle profiles)""" """Method for handling the first cycle button (cycle profiles)"""
if self.rascsi_profile_cycler is None: if self.rascsi_profile_cycler is None:
self.rascsi_profile_cycler = RascsiProfileCycler(self._menu_controller, self.sock_cmd, self.rascsi_profile_cycler = RascsiProfileCycler(
self.ractl_cmd, return_entry=True) self._menu_controller, self.sock_cmd, self.ractl_cmd, return_entry=True
)
else: else:
self.rascsi_profile_cycler.cycle() self.rascsi_profile_cycler.cycle()
def handle_button2(self): def handle_button2(self):
"""Method for handling the second cycle button (cycle shutdown)""" """Method for handling the second cycle button (cycle shutdown)"""
if self.rascsi_shutdown_cycler is None: if self.rascsi_shutdown_cycler is None:
self.rascsi_shutdown_cycler = RascsiShutdownCycler(self._menu_controller, self.sock_cmd, self.rascsi_shutdown_cycler = RascsiShutdownCycler(
self.ractl_cmd) self._menu_controller, self.sock_cmd, self.ractl_cmd
)
else: else:
self.rascsi_shutdown_cycler.cycle() self.rascsi_shutdown_cycler.cycle()
@ -100,8 +106,10 @@ class CtrlBoardMenuUpdateEventHandler(Observer):
handler_function(info_object) handler_function(info_object)
except AttributeError: except AttributeError:
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
log.error("Handler function [%s] not found or returned an error. Skipping.", log.error(
str(handler_function_name)) "Handler function [%s] not found or returned an error. Skipping.",
str(handler_function_name),
)
# noinspection PyUnusedLocal # noinspection PyUnusedLocal
# pylint: disable=unused-argument # pylint: disable=unused-argument
@ -109,9 +117,11 @@ class CtrlBoardMenuUpdateEventHandler(Observer):
"""Method handles the rotary button press with the scsi list to open the action menu.""" """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() context_object = self._menu_controller.get_active_menu().get_current_info_object()
self.context_stack.append(context_object) self.context_stack.append(context_object)
self._menu_controller.segue(CtrlBoardMenuBuilder.ACTION_MENU, context_object=context_object, self._menu_controller.segue(
transition_attributes=self._menu_renderer_config. CtrlBoardMenuBuilder.ACTION_MENU,
transition_attributes_left) context_object=context_object,
transition_attributes=self._menu_renderer_config.transition_attributes_left,
)
# noinspection PyUnusedLocal # noinspection PyUnusedLocal
# pylint: disable=unused-argument # pylint: disable=unused-argument
@ -119,9 +129,10 @@ class CtrlBoardMenuUpdateEventHandler(Observer):
"""Method handles the rotary button press to return from the """Method handles the rotary button press to return from the
action menu to the scsi list.""" action menu to the scsi list."""
self.context_stack.pop() self.context_stack.pop()
self._menu_controller.segue(CtrlBoardMenuBuilder.SCSI_ID_MENU, self._menu_controller.segue(
transition_attributes=self._menu_renderer_config. CtrlBoardMenuBuilder.SCSI_ID_MENU,
transition_attributes_right) transition_attributes=self._menu_renderer_config.transition_attributes_right,
)
# noinspection PyUnusedLocal # noinspection PyUnusedLocal
# pylint: disable=unused-argument # pylint: disable=unused-argument
@ -129,9 +140,11 @@ class CtrlBoardMenuUpdateEventHandler(Observer):
"""Method handles the rotary button press on attach in the action menu.""" """Method handles the rotary button press on attach in the action menu."""
context_object = self._menu_controller.get_active_menu().context_object context_object = self._menu_controller.get_active_menu().context_object
self.context_stack.append(context_object) self.context_stack.append(context_object)
self._menu_controller.segue(CtrlBoardMenuBuilder.IMAGES_MENU, context_object=context_object, self._menu_controller.segue(
transition_attributes=self._menu_renderer_config. CtrlBoardMenuBuilder.IMAGES_MENU,
transition_attributes_left) context_object=context_object,
transition_attributes=self._menu_renderer_config.transition_attributes_left,
)
# noinspection PyUnusedLocal # noinspection PyUnusedLocal
def handle_action_menu_slot_detacheject(self, info_object): def handle_action_menu_slot_detacheject(self, info_object):
@ -139,39 +152,43 @@ class CtrlBoardMenuUpdateEventHandler(Observer):
context_object = self._menu_controller.get_active_menu().context_object context_object = self._menu_controller.get_active_menu().context_object
self.detach_eject_scsi_id() self.detach_eject_scsi_id()
self.context_stack = [] self.context_stack = []
self._menu_controller.segue(CtrlBoardMenuBuilder.SCSI_ID_MENU, self._menu_controller.segue(
context_object=context_object, CtrlBoardMenuBuilder.SCSI_ID_MENU,
transition_attributes=self._menu_renderer_config. context_object=context_object,
transition_attributes_right) transition_attributes=self._menu_renderer_config.transition_attributes_right,
)
# noinspection PyUnusedLocal # noinspection PyUnusedLocal
def handle_action_menu_slot_info(self, info_object): def handle_action_menu_slot_info(self, info_object):
"""Method handles the rotary button press on 'Info' in the action menu.""" """Method handles the rotary button press on 'Info' in the action menu."""
context_object = self._menu_controller.get_active_menu().context_object context_object = self._menu_controller.get_active_menu().context_object
self.context_stack.append(context_object) self.context_stack.append(context_object)
self._menu_controller.segue(CtrlBoardMenuBuilder.DEVICEINFO_MENU, self._menu_controller.segue(
transition_attributes=self._menu_renderer_config. CtrlBoardMenuBuilder.DEVICEINFO_MENU,
transition_attributes_left, transition_attributes=self._menu_renderer_config.transition_attributes_left,
context_object=context_object) context_object=context_object,
)
# noinspection PyUnusedLocal # noinspection PyUnusedLocal
def handle_device_info_menu_return(self, info_object): def handle_device_info_menu_return(self, info_object):
"""Method handles the rotary button press on 'Return' in the info menu.""" """Method handles the rotary button press on 'Return' in the info menu."""
self.context_stack.pop() self.context_stack.pop()
context_object = self._menu_controller.get_active_menu().context_object context_object = self._menu_controller.get_active_menu().context_object
self._menu_controller.segue(CtrlBoardMenuBuilder.ACTION_MENU, self._menu_controller.segue(
transition_attributes=self._menu_renderer_config. CtrlBoardMenuBuilder.ACTION_MENU,
transition_attributes_right, transition_attributes=self._menu_renderer_config.transition_attributes_right,
context_object=context_object) context_object=context_object,
)
# noinspection PyUnusedLocal # noinspection PyUnusedLocal
def handle_action_menu_loadprofile(self, info_object): def handle_action_menu_loadprofile(self, info_object):
"""Method handles the rotary button press on 'Load Profile' in the action menu.""" """Method handles the rotary button press on 'Load Profile' in the action menu."""
context_object = self._menu_controller.get_active_menu().context_object context_object = self._menu_controller.get_active_menu().context_object
self.context_stack.append(context_object) self.context_stack.append(context_object)
self._menu_controller.segue(CtrlBoardMenuBuilder.PROFILES_MENU, self._menu_controller.segue(
transition_attributes=self._menu_renderer_config. CtrlBoardMenuBuilder.PROFILES_MENU,
transition_attributes_left) transition_attributes=self._menu_renderer_config.transition_attributes_left,
)
# noinspection PyUnusedLocal # noinspection PyUnusedLocal
def handle_profiles_menu_loadprofile(self, info_object): def handle_profiles_menu_loadprofile(self, info_object):
@ -186,28 +203,31 @@ class CtrlBoardMenuUpdateEventHandler(Observer):
self._menu_controller.show_message("Loading failed!") self._menu_controller.show_message("Loading failed!")
self.context_stack = [] self.context_stack = []
self._menu_controller.segue(CtrlBoardMenuBuilder.SCSI_ID_MENU, self._menu_controller.segue(
transition_attributes=self._menu_renderer_config. CtrlBoardMenuBuilder.SCSI_ID_MENU,
transition_attributes_right) transition_attributes=self._menu_renderer_config.transition_attributes_right,
)
# noinspection PyUnusedLocal # noinspection PyUnusedLocal
def handle_action_menu_shutdown(self, info_object): def handle_action_menu_shutdown(self, info_object):
"""Method handles the rotary button press on 'Shutdown' in the action menu.""" """Method handles the rotary button press on 'Shutdown' in the action menu."""
self.ractl_cmd.shutdown("system") self.ractl_cmd.shutdown("system")
self._menu_controller.show_message("Shutting down!", 150) self._menu_controller.show_message("Shutting down!", 150)
self._menu_controller.segue(CtrlBoardMenuBuilder.SCSI_ID_MENU, self._menu_controller.segue(
transition_attributes=self._menu_renderer_config. CtrlBoardMenuBuilder.SCSI_ID_MENU,
transition_attributes_right) transition_attributes=self._menu_renderer_config.transition_attributes_right,
)
# noinspection PyUnusedLocal # noinspection PyUnusedLocal
def handle_images_menu_return(self, info_object): def handle_images_menu_return(self, info_object):
"""Method handles the rotary button press on 'Return' in the image selection menu """Method handles the rotary button press on 'Return' in the image selection menu
(through attach/insert).""" (through attach/insert)."""
context_object = self.context_stack.pop() context_object = self.context_stack.pop()
self._menu_controller.segue(CtrlBoardMenuBuilder.ACTION_MENU, self._menu_controller.segue(
context_object=context_object, CtrlBoardMenuBuilder.ACTION_MENU,
transition_attributes=self._menu_renderer_config. context_object=context_object,
transition_attributes_right) transition_attributes=self._menu_renderer_config.transition_attributes_right,
)
def handle_images_menu_image_attachinsert(self, info_object): def handle_images_menu_image_attachinsert(self, info_object):
"""Method handles the rotary button press on an image in the image selection menu """Method handles the rotary button press on an image in the image selection menu
@ -215,10 +235,11 @@ class CtrlBoardMenuUpdateEventHandler(Observer):
context_object = self._menu_controller.get_active_menu().context_object context_object = self._menu_controller.get_active_menu().context_object
self.attach_insert_scsi_id(info_object) self.attach_insert_scsi_id(info_object)
self.context_stack = [] self.context_stack = []
self._menu_controller.segue(CtrlBoardMenuBuilder.SCSI_ID_MENU, self._menu_controller.segue(
context_object=context_object, CtrlBoardMenuBuilder.SCSI_ID_MENU,
transition_attributes=self._menu_renderer_config. context_object=context_object,
transition_attributes_right) transition_attributes=self._menu_renderer_config.transition_attributes_right,
)
def attach_insert_scsi_id(self, info_object): def attach_insert_scsi_id(self, info_object):
"""Helper method to attach/insert an image on a scsi id given through the menu context""" """Helper method to attach/insert an image on a scsi id given through the menu context"""
@ -227,9 +248,9 @@ class CtrlBoardMenuUpdateEventHandler(Observer):
context_object = self._menu_controller.get_active_menu().context_object context_object = self._menu_controller.get_active_menu().context_object
scsi_id = context_object["scsi_id"] scsi_id = context_object["scsi_id"]
params = {"file": image_name} params = {"file": image_name}
result = self.ractl_cmd.attach_device(scsi_id=scsi_id, result = self.ractl_cmd.attach_device(
device_type=device_type, scsi_id=scsi_id, device_type=device_type, params=params
params=params) )
if result["status"] is False: if result["status"] is False:
self._menu_controller.show_message("Attach failed!") self._menu_controller.show_message("Attach failed!")
@ -268,7 +289,10 @@ class CtrlBoardMenuUpdateEventHandler(Observer):
self._menu_controller.show_message("Detach failed!") self._menu_controller.show_message("Detach failed!")
else: else:
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
log.info("Device type '%s' currently unsupported for detach/eject!", str(device_type)) log.info(
"Device type '%s' currently unsupported for detach/eject!",
str(device_type),
)
def show_id_action_message(self, scsi_id, action: str): def show_id_action_message(self, scsi_id, action: str):
"""Helper method for displaying an action message in the case of an exception.""" """Helper method for displaying an action message in the case of an exception."""

View File

@ -8,6 +8,7 @@ from ctrlboard_hw.encoder import Encoder
class CtrlBoardPrintEventHandler(observer.Observer): class CtrlBoardPrintEventHandler(observer.Observer):
"""Class implements a basic event handler that prints button presses from the RaSCSI """Class implements a basic event handler that prints button presses from the RaSCSI
Control Board hardware.""" Control Board hardware."""
def update(self, updated_object): def update(self, updated_object):
if isinstance(updated_object, HardwareButton): if isinstance(updated_object, HardwareButton):
print(updated_object.name + " has been pressed!") print(updated_object.name + " has been pressed!")

View File

@ -6,8 +6,13 @@ class RascsiShutdownCycler(Cycler):
"""Class implementing the shutdown cycler for the RaSCSI Control Board UI""" """Class implementing the shutdown cycler for the RaSCSI Control Board UI"""
def __init__(self, menu_controller, sock_cmd, ractl_cmd): def __init__(self, menu_controller, sock_cmd, ractl_cmd):
super().__init__(menu_controller, sock_cmd, ractl_cmd, return_entry=True, super().__init__(
empty_messages=False) menu_controller,
sock_cmd,
ractl_cmd,
return_entry=True,
empty_messages=False,
)
self.executed_once = False self.executed_once = False
def populate_cycle_entries(self): def populate_cycle_entries(self):

View File

@ -16,6 +16,7 @@ from observable import Observable
# pylint: disable=too-many-instance-attributes # pylint: disable=too-many-instance-attributes
class CtrlBoardHardware(Observable): class CtrlBoardHardware(Observable):
"""Class implements the RaSCSI Control Board hardware and provides an interface to it.""" """Class implements the RaSCSI Control Board hardware and provides an interface to it."""
def __init__(self, display_i2c_address, pca9554_i2c_address, debounce_ms=200): def __init__(self, display_i2c_address, pca9554_i2c_address, debounce_ms=200):
self.display_i2c_address = display_i2c_address self.display_i2c_address = display_i2c_address
self.pca9554_i2c_address = pca9554_i2c_address self.pca9554_i2c_address = pca9554_i2c_address
@ -33,63 +34,78 @@ class CtrlBoardHardware(Observable):
self.pca_driver = pca9554multiplexer.PCA9554Multiplexer(self.pca9554_i2c_address) self.pca_driver = pca9554multiplexer.PCA9554Multiplexer(self.pca9554_i2c_address)
# setup pca9554 # setup pca9554
self.pca_driver.write_configuration_register_port(CtrlBoardHardwareConstants. self.pca_driver.write_configuration_register_port(
PCA9554_PIN_ENC_A, CtrlBoardHardwareConstants.PCA9554_PIN_ENC_A,
PCA9554Multiplexer.PIN_ENABLED_AS_INPUT) PCA9554Multiplexer.PIN_ENABLED_AS_INPUT,
self.pca_driver.write_configuration_register_port(CtrlBoardHardwareConstants. )
PCA9554_PIN_ENC_B, self.pca_driver.write_configuration_register_port(
PCA9554Multiplexer.PIN_ENABLED_AS_INPUT) CtrlBoardHardwareConstants.PCA9554_PIN_ENC_B,
self.pca_driver.write_configuration_register_port(CtrlBoardHardwareConstants. PCA9554Multiplexer.PIN_ENABLED_AS_INPUT,
PCA9554_PIN_BUTTON_1, )
PCA9554Multiplexer.PIN_ENABLED_AS_INPUT) self.pca_driver.write_configuration_register_port(
self.pca_driver.write_configuration_register_port(CtrlBoardHardwareConstants. CtrlBoardHardwareConstants.PCA9554_PIN_BUTTON_1,
PCA9554_PIN_BUTTON_2, PCA9554Multiplexer.PIN_ENABLED_AS_INPUT,
PCA9554Multiplexer.PIN_ENABLED_AS_INPUT) )
self.pca_driver.write_configuration_register_port(CtrlBoardHardwareConstants. self.pca_driver.write_configuration_register_port(
PCA9554_PIN_BUTTON_ROTARY, CtrlBoardHardwareConstants.PCA9554_PIN_BUTTON_2,
PCA9554Multiplexer.PIN_ENABLED_AS_INPUT) PCA9554Multiplexer.PIN_ENABLED_AS_INPUT,
self.pca_driver.write_configuration_register_port(CtrlBoardHardwareConstants. )
PCA9554_PIN_LED_1, self.pca_driver.write_configuration_register_port(
PCA9554Multiplexer.PIN_ENABLED_AS_OUTPUT) CtrlBoardHardwareConstants.PCA9554_PIN_BUTTON_ROTARY,
self.pca_driver.write_configuration_register_port(CtrlBoardHardwareConstants. PCA9554Multiplexer.PIN_ENABLED_AS_INPUT,
PCA9554_PIN_LED_2, )
PCA9554Multiplexer.PIN_ENABLED_AS_OUTPUT) 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 = 0 self.input_register_buffer = 0
# pylint: disable=no-member # pylint: disable=no-member
GPIO.setmode(GPIO.BCM) GPIO.setmode(GPIO.BCM)
GPIO.setup(CtrlBoardHardwareConstants.PI_PIN_INTERRUPT, GPIO.IN) GPIO.setup(CtrlBoardHardwareConstants.PI_PIN_INTERRUPT, GPIO.IN)
GPIO.add_event_detect(CtrlBoardHardwareConstants.PI_PIN_INTERRUPT, GPIO.FALLING, GPIO.add_event_detect(
callback=self.button_pressed_callback) CtrlBoardHardwareConstants.PI_PIN_INTERRUPT,
GPIO.FALLING,
callback=self.button_pressed_callback,
)
# configure button of the rotary encoder # configure button of the rotary encoder
self.rotary_button = HardwareButton(self.pca_driver, self.rotary_button = HardwareButton(
CtrlBoardHardwareConstants.PCA9554_PIN_BUTTON_ROTARY) self.pca_driver, CtrlBoardHardwareConstants.PCA9554_PIN_BUTTON_ROTARY
)
self.rotary_button.state = True self.rotary_button.state = True
self.rotary_button.name = CtrlBoardHardwareConstants.ROTARY_BUTTON self.rotary_button.name = CtrlBoardHardwareConstants.ROTARY_BUTTON
# configure button 1 # configure button 1
self.button1 = HardwareButton(self.pca_driver, self.button1 = HardwareButton(
CtrlBoardHardwareConstants.PCA9554_PIN_BUTTON_1) self.pca_driver, CtrlBoardHardwareConstants.PCA9554_PIN_BUTTON_1
)
self.button1.state = True self.button1.state = True
self.button1.name = CtrlBoardHardwareConstants.BUTTON_1 self.button1.name = CtrlBoardHardwareConstants.BUTTON_1
# configure button 2 # configure button 2
self.button2 = HardwareButton(self.pca_driver, self.button2 = HardwareButton(
CtrlBoardHardwareConstants.PCA9554_PIN_BUTTON_2) self.pca_driver, CtrlBoardHardwareConstants.PCA9554_PIN_BUTTON_2
)
self.button2.state = True self.button2.state = True
self.button2.name = CtrlBoardHardwareConstants.BUTTON_2 self.button2.name = CtrlBoardHardwareConstants.BUTTON_2
# configure rotary encoder pin a # configure rotary encoder pin a
self.rotary_a = HardwareButton(self.pca_driver, self.rotary_a = HardwareButton(
CtrlBoardHardwareConstants.PCA9554_PIN_ENC_A) self.pca_driver, CtrlBoardHardwareConstants.PCA9554_PIN_ENC_A
)
self.rotary_a.state = True self.rotary_a.state = True
self.rotary_a.directionalTransition = False self.rotary_a.directionalTransition = False
self.rotary_a.name = CtrlBoardHardwareConstants.ROTARY_A self.rotary_a.name = CtrlBoardHardwareConstants.ROTARY_A
# configure rotary encoder pin b # configure rotary encoder pin b
self.rotary_b = HardwareButton(self.pca_driver, self.rotary_b = HardwareButton(
CtrlBoardHardwareConstants.PCA9554_PIN_ENC_B) self.pca_driver, CtrlBoardHardwareConstants.PCA9554_PIN_ENC_B
)
self.rotary_b.state = True self.rotary_b.state = True
self.rotary_b.directionalTransition = False self.rotary_b.directionalTransition = False
self.rotary_b.name = CtrlBoardHardwareConstants.ROTARY_B self.rotary_b.name = CtrlBoardHardwareConstants.ROTARY_B
@ -117,7 +133,7 @@ class CtrlBoardHardware(Observable):
# ignore button press if debounce time is not reached # ignore button press if debounce time is not reached
if button.last_press is not None: if button.last_press is not None:
elapsed = (time.time_ns() - button.last_press)/1000000 elapsed = (time.time_ns() - button.last_press) / 1000000
if elapsed < self.debounce_ms: if elapsed < self.debounce_ms:
return return
@ -149,8 +165,8 @@ class CtrlBoardHardware(Observable):
@staticmethod @staticmethod
def button_value_shifted_list(input_register_buffer, bit): def button_value_shifted_list(input_register_buffer, bit):
"""Helper method for dealing with multiple buffered input registers""" """Helper method for dealing with multiple buffered input registers"""
input_register_buffer_length = int(len(format(input_register_buffer, 'b'))/8) input_register_buffer_length = int(len(format(input_register_buffer, "b")) / 8)
shiftval = (input_register_buffer_length-1)*8 shiftval = (input_register_buffer_length - 1) * 8
tmp = input_register_buffer >> shiftval tmp = input_register_buffer >> shiftval
bitmask = 1 << bit bitmask = 1 << bit
tmp &= bitmask tmp &= bitmask
@ -162,12 +178,12 @@ class CtrlBoardHardware(Observable):
input_register_buffer = self.input_register_buffer input_register_buffer = self.input_register_buffer
self.input_register_buffer = 0 self.input_register_buffer = 0
input_register_buffer_length = int(len(format(input_register_buffer, 'b'))/8) input_register_buffer_length = int(len(format(input_register_buffer, "b")) / 8)
if input_register_buffer_length < 1: if input_register_buffer_length < 1:
return return
for i in range(0, input_register_buffer_length): for i in range(0, input_register_buffer_length):
shiftval = (input_register_buffer_length-1-i)*8 shiftval = (input_register_buffer_length - 1 - i) * 8
input_register = (input_register_buffer >> shiftval) & 0b11111111 input_register = (input_register_buffer >> shiftval) & 0b11111111
rot_a = self.button_value(input_register, 0) rot_a = self.button_value(input_register, 0)
@ -211,7 +227,7 @@ class CtrlBoardHardware(Observable):
if 2 < _address < 120: if 2 < _address < 120:
try: try:
_bus.read_byte(_address) _bus.read_byte(_address)
address = '%02x' % _address address = "%02x" % _address
detected_i2c_addresses.append(int(address, base=16)) detected_i2c_addresses.append(int(address, base=16))
except IOError: # simply skip unsuccessful i2c probes except IOError: # simply skip unsuccessful i2c probes
pass pass
@ -223,8 +239,12 @@ class CtrlBoardHardware(Observable):
the expected i2c addresses are detected.""" the expected i2c addresses are detected."""
# pylint: disable=c-extension-no-member # pylint: disable=c-extension-no-member
i2c_addresses = self.detect_i2c_devices(smbus.SMBus(1)) i2c_addresses = self.detect_i2c_devices(smbus.SMBus(1))
return bool((int(self.display_i2c_address) in i2c_addresses and return bool(
(int(self.pca9554_i2c_address) in i2c_addresses))) (
int(self.display_i2c_address) in i2c_addresses
and (int(self.pca9554_i2c_address) in i2c_addresses)
)
)
def detect_display(self): def detect_display(self):
"""Detects whether an i2c display is connected to the RaSCSI hat.""" """Detects whether an i2c display is connected to the RaSCSI hat."""

View File

@ -4,8 +4,9 @@
# pylint: disable=too-few-public-methods # pylint: disable=too-few-public-methods
class CtrlBoardHardwareConstants: class CtrlBoardHardwareConstants:
"""Class containing the RaSCSI Control Board hardware constants""" """Class containing the RaSCSI Control Board hardware constants"""
DISPLAY_I2C_ADDRESS = 0x3c
PCA9554_I2C_ADDRESS = 0x3f DISPLAY_I2C_ADDRESS = 0x3C
PCA9554_I2C_ADDRESS = 0x3F
PCA9554_PIN_ENC_A = 0 PCA9554_PIN_ENC_A = 0
PCA9554_PIN_ENC_B = 1 PCA9554_PIN_ENC_B = 1
PCA9554_PIN_BUTTON_1 = 2 PCA9554_PIN_BUTTON_1 = 2
@ -14,7 +15,7 @@ class CtrlBoardHardwareConstants:
PCA9554_PIN_LED_1 = 6 PCA9554_PIN_LED_1 = 6
PCA9554_PIN_LED_2 = 7 PCA9554_PIN_LED_2 = 7
PI_PIN_INTERRUPT = 9 # BCM PI_PIN_INTERRUPT = 9 # BCM
BUTTON_1 = "Bt1" BUTTON_1 = "Bt1"
BUTTON_2 = "Bt2" BUTTON_2 = "Bt2"

View File

@ -6,6 +6,7 @@ from ctrlboard_hw.hardware_button import HardwareButton
class Encoder: class Encoder:
"""Class implementing a detection mechanism to detect the rotary encoder directions """Class implementing a detection mechanism to detect the rotary encoder directions
through the i2c multiplexer + interrupt""" through the i2c multiplexer + interrupt"""
def __init__(self, enc_a: HardwareButton, enc_b: HardwareButton): def __init__(self, enc_a: HardwareButton, enc_b: HardwareButton):
self.enc_a = enc_a self.enc_a = enc_a
self.enc_b = enc_b self.enc_b = enc_b
@ -27,16 +28,27 @@ class Encoder:
state |= 0b10000000 state |= 0b10000000
# clockwise pattern detection # clockwise pattern detection
if (state == 0b11010010 or state == 0b11001000 or state == 0b11011000 or if (
state == 0b11010001 or state == 0b11011011 or state == 0b11100000 or state == 0b11010010
state == 0b11001011): or state == 0b11001000
or state == 0b11011000
or state == 0b11010001
or state == 0b11011011
or state == 0b11100000
or state == 0b11001011
):
self.pos += 1 self.pos += 1
self.direction = 1 self.direction = 1
self.state = 0b00000000 self.state = 0b00000000
return return
# counter-clockwise pattern detection # counter-clockwise pattern detection
elif (state == 0b11000100 or state == 0b11100100 or state == 0b11100001 or elif (
state == 0b11000111 or state == 0b11100111): state == 0b11000100
or state == 0b11100100
or state == 0b11100001
or state == 0b11000111
or state == 0b11100111
):
self.pos -= 1 self.pos -= 1
self.direction = -1 self.direction = -1
self.state = 0b00000000 self.state = 0b00000000

View File

@ -19,8 +19,10 @@ class PCA9554Multiplexer:
try: try:
self.i2c_bus = smbus.SMBus(1) self.i2c_bus = smbus.SMBus(1)
if self.read_input_register() is None: if self.read_input_register() is None:
logging.error("PCA9554 initialization test on specified i2c address %s failed", logging.error(
self.i2c_address) "PCA9554 initialization test on specified i2c address %s failed",
self.i2c_address,
)
self.i2c_bus = None self.i2c_bus = None
except IOError: except IOError:
logging.error("Could not open the i2c bus.") logging.error("Could not open the i2c bus.")
@ -35,8 +37,9 @@ class PCA9554Multiplexer:
if bit_value: if bit_value:
updated_configuration_register = configuration_register | (1 << port_bit) updated_configuration_register = configuration_register | (1 << port_bit)
else: else:
updated_configuration_register = configuration_register & (0xFF - updated_configuration_register = configuration_register & (
(1 << port_bit)) 0xFF - (1 << port_bit)
)
self.i2c_bus.write_byte_data(self.i2c_address, 3, updated_configuration_register) self.i2c_bus.write_byte_data(self.i2c_address, 3, updated_configuration_register)
return True return True

View File

@ -9,6 +9,7 @@ from rascsi.ractl_cmds import RaCtlCmds
class CtrlBoardMenuBuilder(MenuBuilder): class CtrlBoardMenuBuilder(MenuBuilder):
"""Class fgor building the control board UI specific menus""" """Class fgor building the control board UI specific menus"""
SCSI_ID_MENU = "scsi_id_menu" SCSI_ID_MENU = "scsi_id_menu"
ACTION_MENU = "action_menu" ACTION_MENU = "action_menu"
IMAGES_MENU = "images_menu" IMAGES_MENU = "images_menu"
@ -27,8 +28,12 @@ class CtrlBoardMenuBuilder(MenuBuilder):
def __init__(self, ractl_cmd: RaCtlCmds): def __init__(self, ractl_cmd: RaCtlCmds):
super().__init__() super().__init__()
self._rascsi_client = ractl_cmd self._rascsi_client = ractl_cmd
self.file_cmd = FileCmds(sock_cmd=ractl_cmd.sock_cmd, ractl=ractl_cmd, self.file_cmd = FileCmds(
token=ractl_cmd.token, locale=ractl_cmd.locale) 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: def build(self, name: str, context_object=None) -> Menu:
if name == CtrlBoardMenuBuilder.SCSI_ID_MENU: if name == CtrlBoardMenuBuilder.SCSI_ID_MENU:
@ -93,9 +98,14 @@ class CtrlBoardMenuBuilder(MenuBuilder):
if device_type != "": if device_type != "":
menu_str += " [" + device_type + "]" menu_str += " [" + device_type + "]"
menu.add_entry(menu_str, {"context": self.SCSI_ID_MENU, menu.add_entry(
"action": self.ACTION_OPENACTIONMENU, menu_str,
"scsi_id": scsi_id}) {
"context": self.SCSI_ID_MENU,
"action": self.ACTION_OPENACTIONMENU,
"scsi_id": scsi_id,
},
)
return menu return menu
@ -103,18 +113,30 @@ class CtrlBoardMenuBuilder(MenuBuilder):
def create_action_menu(self, context_object=None): def create_action_menu(self, context_object=None):
"""Method creates the action submenu with action that can be performed on a scsi slot""" """Method creates the action submenu with action that can be performed on a scsi slot"""
menu = Menu(CtrlBoardMenuBuilder.ACTION_MENU) menu = Menu(CtrlBoardMenuBuilder.ACTION_MENU)
menu.add_entry("Return", {"context": self.ACTION_MENU, menu.add_entry(
"action": self.ACTION_RETURN}) "Return",
menu.add_entry("Attach/Insert", {"context": self.ACTION_MENU, {"context": self.ACTION_MENU, "action": self.ACTION_RETURN},
"action": self.ACTION_SLOT_ATTACHINSERT}) )
menu.add_entry("Detach/Eject", {"context": self.ACTION_MENU, menu.add_entry(
"action": self.ACTION_SLOT_DETACHEJECT}) "Attach/Insert",
menu.add_entry("Info", {"context": self.ACTION_MENU, {"context": self.ACTION_MENU, "action": self.ACTION_SLOT_ATTACHINSERT},
"action": self.ACTION_SLOT_INFO}) )
menu.add_entry("Load Profile", {"context": self.ACTION_MENU, menu.add_entry(
"action": self.ACTION_LOADPROFILE}) "Detach/Eject",
menu.add_entry("Shutdown", {"context": self.ACTION_MENU, {"context": self.ACTION_MENU, "action": self.ACTION_SLOT_DETACHEJECT},
"action": self.ACTION_SHUTDOWN}) )
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 return menu
def create_images_menu(self, context_object=None): def create_images_menu(self, context_object=None):
@ -123,12 +145,15 @@ class CtrlBoardMenuBuilder(MenuBuilder):
images_info = self.file_cmd.list_images() images_info = self.file_cmd.list_images()
menu.add_entry("Return", {"context": self.IMAGES_MENU, "action": self.ACTION_RETURN}) menu.add_entry("Return", {"context": self.IMAGES_MENU, "action": self.ACTION_RETURN})
images = images_info["files"] images = images_info["files"]
sorted_images = sorted(images, key=lambda d: d['name']) sorted_images = sorted(images, key=lambda d: d["name"])
for image in sorted_images: for image in sorted_images:
image_str = image["name"] + " [" + image["detected_type"] + "]" image_str = image["name"] + " [" + image["detected_type"] + "]"
image_context = {"context": self.IMAGES_MENU, "name": str(image["name"]), image_context = {
"device_type": str(image["detected_type"]), "context": self.IMAGES_MENU,
"action": self.ACTION_IMAGE_ATTACHINSERT} "name": str(image["name"]),
"device_type": str(image["detected_type"]),
"action": self.ACTION_IMAGE_ATTACHINSERT,
}
menu.add_entry(image_str, image_context) menu.add_entry(image_str, image_context)
return menu return menu
@ -138,9 +163,14 @@ class CtrlBoardMenuBuilder(MenuBuilder):
menu.add_entry("Return", {"context": self.IMAGES_MENU, "action": self.ACTION_RETURN}) menu.add_entry("Return", {"context": self.IMAGES_MENU, "action": self.ACTION_RETURN})
config_files = self.file_cmd.list_config_files() config_files = self.file_cmd.list_config_files()
for config_file in config_files: for config_file in config_files:
menu.add_entry(str(config_file), menu.add_entry(
{"context": self.PROFILES_MENU, "name": str(config_file), str(config_file),
"action": self.ACTION_LOADPROFILE}) {
"context": self.PROFILES_MENU,
"name": str(config_file),
"action": self.ACTION_LOADPROFILE,
},
)
return menu return menu

View File

@ -6,14 +6,17 @@ import logging
from config import CtrlboardConfig from config import CtrlboardConfig
from ctrlboard_hw.ctrlboard_hw import CtrlBoardHardware from ctrlboard_hw.ctrlboard_hw import CtrlBoardHardware
from ctrlboard_hw.ctrlboard_hw_constants import CtrlBoardHardwareConstants from ctrlboard_hw.ctrlboard_hw_constants import CtrlBoardHardwareConstants
from ctrlboard_event_handler.ctrlboard_menu_update_event_handler \ from ctrlboard_event_handler.ctrlboard_menu_update_event_handler import (
import CtrlBoardMenuUpdateEventHandler CtrlBoardMenuUpdateEventHandler,
)
from ctrlboard_menu_builder import CtrlBoardMenuBuilder from ctrlboard_menu_builder import CtrlBoardMenuBuilder
from menu.menu_renderer_config import MenuRendererConfig from menu.menu_renderer_config import MenuRendererConfig
from menu.menu_renderer_luma_oled import MenuRendererLumaOled from menu.menu_renderer_luma_oled import MenuRendererLumaOled
from rascsi.exceptions import (EmptySocketChunkException, from rascsi.exceptions import (
InvalidProtobufResponse, EmptySocketChunkException,
FailedSocketConnectionException) InvalidProtobufResponse,
FailedSocketConnectionException,
)
from rascsi.ractl_cmds import RaCtlCmds from rascsi.ractl_cmds import RaCtlCmds
from rascsi.socket_cmds import SocketCmds from rascsi.socket_cmds import SocketCmds
@ -23,7 +26,7 @@ from rascsi_menu_controller import RascsiMenuController
def parse_config(): def parse_config():
"""Parses the command line parameters and configured the RaSCSI Control Board UI accordingly""" """Parses the command line parameters and configured the RaSCSI Control Board UI accordingly"""
config = CtrlboardConfig() config = CtrlboardConfig()
cmdline_args_parser = argparse.ArgumentParser(description='RaSCSI ctrlboard service') cmdline_args_parser = argparse.ArgumentParser(description="RaSCSI ctrlboard service")
cmdline_args_parser.add_argument( cmdline_args_parser.add_argument(
"--rotation", "--rotation",
type=int, type=int,
@ -68,7 +71,7 @@ def parse_config():
default=logging.WARN, default=logging.WARN,
action="store", action="store",
help="Loglevel. Valid values: 0 (notset), 10 (debug), 30 (warning), " help="Loglevel. Valid values: 0 (notset), 10 (debug), 30 (warning), "
"40 (error), 50 (critical). Default: Warning", "40 (error), 50 (critical). Default: Warning",
) )
cmdline_args_parser.add_argument( cmdline_args_parser.add_argument(
"--transitions", "--transitions",
@ -115,14 +118,14 @@ def main():
config = parse_config() config = parse_config()
log_format = "%(asctime)s:%(name)s:%(levelname)s - %(message)s" log_format = "%(asctime)s:%(name)s:%(levelname)s - %(message)s"
logging.basicConfig(stream=sys.stdout, logging.basicConfig(stream=sys.stdout, format=log_format, level=config.LOG_LEVEL)
format=log_format,
level=config.LOG_LEVEL)
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
log.debug("RaSCSI ctrlboard service started.") log.debug("RaSCSI ctrlboard service started.")
ctrlboard_hw = CtrlBoardHardware(display_i2c_address=config.DISPLAY_I2C_ADDRESS, ctrlboard_hw = CtrlBoardHardware(
pca9554_i2c_address=config.PCA9554_I2C_ADDRESS) display_i2c_address=config.DISPLAY_I2C_ADDRESS,
pca9554_i2c_address=config.PCA9554_I2C_ADDRESS,
)
# for now, we require the complete rascsi ctrlboard hardware. # for now, we require the complete rascsi ctrlboard hardware.
# Oled only will be supported as well at some later point in time. # Oled only will be supported as well at some later point in time.
@ -143,8 +146,10 @@ def main():
exit(1) exit(1)
if check_rascsi_connection(ractl_cmd) is False: if check_rascsi_connection(ractl_cmd) is False:
log.error("Communication with RaSCSI failed. Please check if password token must be set " log.error(
"and whether is set correctly.") "Communication with RaSCSI failed. Please check if password token must be set "
"and whether is set correctly."
)
exit(1) exit(1)
menu_renderer_config = MenuRendererConfig() menu_renderer_config = MenuRendererConfig()
@ -156,18 +161,21 @@ def main():
menu_renderer_config.rotation = config.ROTATION menu_renderer_config.rotation = config.ROTATION
menu_builder = CtrlBoardMenuBuilder(ractl_cmd) menu_builder = CtrlBoardMenuBuilder(ractl_cmd)
menu_controller = RascsiMenuController(config.MENU_REFRESH_INTERVAL, menu_builder=menu_builder, menu_controller = RascsiMenuController(
menu_renderer=MenuRendererLumaOled(menu_renderer_config), config.MENU_REFRESH_INTERVAL,
menu_renderer_config=menu_renderer_config) 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.SCSI_ID_MENU)
menu_controller.add(CtrlBoardMenuBuilder.ACTION_MENU) menu_controller.add(CtrlBoardMenuBuilder.ACTION_MENU)
menu_controller.show_splash_screen(f"resources/splash_start_64.bmp") menu_controller.show_splash_screen("resources/splash_start_64.bmp")
menu_update_event_handler = CtrlBoardMenuUpdateEventHandler(menu_controller, menu_update_event_handler = CtrlBoardMenuUpdateEventHandler(
sock_cmd=sock_cmd, menu_controller, sock_cmd=sock_cmd, ractl_cmd=ractl_cmd
ractl_cmd=ractl_cmd) )
ctrlboard_hw.attach(menu_update_event_handler) ctrlboard_hw.attach(menu_update_event_handler)
menu_controller.set_active_menu(CtrlBoardMenuBuilder.SCSI_ID_MENU) menu_controller.set_active_menu(CtrlBoardMenuBuilder.SCSI_ID_MENU)
@ -184,5 +192,5 @@ def main():
print(ex) print(ex)
if __name__ == '__main__': if __name__ == "__main__":
main() main()

View File

@ -5,6 +5,7 @@ from menu.screensaver import ScreenSaver
class BlankScreenSaver(ScreenSaver): class BlankScreenSaver(ScreenSaver):
"""Class implementing a blank screen safer that simply blanks the screen after a """Class implementing a blank screen safer that simply blanks the screen after a
configured activation delay""" configured activation delay"""
def __init__(self, activation_delay, menu_renderer): def __init__(self, activation_delay, menu_renderer):
super().__init__(activation_delay, menu_renderer) super().__init__(activation_delay, menu_renderer)
self._initial_draw_call = None self._initial_draw_call = None

View File

@ -8,9 +8,17 @@ class Cycler:
"""Class implementing button cycling functionality. Message is shown at the center of """Class implementing button cycling functionality. Message is shown at the center of
the screen where repeated button presses cycle through the available selection the screen where repeated button presses cycle through the available selection
possibilities. Inactivity (cycle_timeout) actives cycle entry last shown on the screen.""" 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 ->", def __init__(
return_entry=True, empty_messages=True): 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._cycle_profile_timer_flag = Timer(activation_delay=cycle_timeout)
self._menu_controller = menu_controller self._menu_controller = menu_controller
self.sock_cmd = sock_cmd self.sock_cmd = sock_cmd
@ -39,7 +47,7 @@ class Cycler:
"""Perform the return action, i.e., when no selection is chosen""" """Perform the return action, i.e., when no selection is chosen"""
def update(self): def update(self):
""" Returns True if object has completed its task and can be deleted """ """Returns True if object has completed its task and can be deleted"""
if self._cycle_profile_timer_flag is None: if self._cycle_profile_timer_flag is None:
return None return None

View File

@ -4,6 +4,7 @@ from typing import List
class Menu: class Menu:
"""Class implement the Menu class""" """Class implement the Menu class"""
def __init__(self, name: str): def __init__(self, name: str):
self.entries: List = [] self.entries: List = []
self.item_selection = 0 self.item_selection = 0
@ -17,11 +18,11 @@ class Menu:
def get_current_text(self): def get_current_text(self):
"""Returns the text content of the currently selected text in the menu.""" """Returns the text content of the currently selected text in the menu."""
return self.entries[self.item_selection]['text'] return self.entries[self.item_selection]["text"]
def get_current_info_object(self): def get_current_info_object(self):
"""Returns the data object to the currently selected menu item""" """Returns the data object to the currently selected menu item"""
return self.entries[self.item_selection]['data_object'] return self.entries[self.item_selection]["data_object"]
def __repr__(self): def __repr__(self):
print("entries: " + str(self.entries)) print("entries: " + str(self.entries))

View File

@ -6,6 +6,7 @@ from menu.menu import Menu
# pylint: disable=too-few-public-methods # pylint: disable=too-few-public-methods
class MenuBuilder(ABC): class MenuBuilder(ABC):
"""Base class for menu builders""" """Base class for menu builders"""
def __init__(self): def __init__(self):
pass pass

View File

@ -18,6 +18,7 @@ from menu.screensaver import ScreenSaver
class MenuRenderer(ABC): class MenuRenderer(ABC):
"""The abstract menu renderer class provides the base for concrete menu """The abstract menu renderer class provides the base for concrete menu
renderer classes that implement functionality based on conrete hardware or available APIs.""" renderer classes that implement functionality based on conrete hardware or available APIs."""
def __init__(self, config: MenuRendererConfig): def __init__(self, config: MenuRendererConfig):
self.message = "" self.message = ""
self.mini_message = "" self.mini_message = ""
@ -25,7 +26,7 @@ class MenuRenderer(ABC):
self._config = config self._config = config
self.disp = self.display_init() self.disp = self.display_init()
self.image = Image.new('1', (self.disp.width, self.disp.height)) self.image = Image.new("1", (self.disp.width, self.disp.height))
self.draw = ImageDraw.Draw(self.image) self.draw = ImageDraw.Draw(self.image)
self.font = ImageFont.truetype(config.font_path, size=config.font_size) self.font = ImageFont.truetype(config.font_path, size=config.font_size)
# just a sample text to work with the font height # just a sample text to work with the font height
@ -83,14 +84,21 @@ class MenuRenderer(ABC):
def draw_row(self, row_number: int, text: str, selected: bool): def draw_row(self, row_number: int, text: str, selected: bool):
"""Draws a single row of the menu.""" """Draws a single row of the menu."""
x_pos = 0 x_pos = 0
y_pos = row_number*self.font_height y_pos = row_number * self.font_height
if selected: if selected:
selection_extension = 0 selection_extension = 0
if row_number < self.rows_per_screen(): if row_number < self.rows_per_screen():
selection_extension = self._config.row_selection_pixel_extension selection_extension = self._config.row_selection_pixel_extension
self.draw.rectangle((x_pos, y_pos, self.disp.width, self.draw.rectangle(
y_pos+self._config.font_size+selection_extension), (
outline=0, fill=255) 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 # in stage 1, we initialize scrolling for the currently selected line
if self._perform_scrolling_stage == 1: if self._perform_scrolling_stage == 1:
@ -99,9 +107,9 @@ class MenuRenderer(ABC):
# in stage 2, we know the details and can thus perform the scrolling to the left # in stage 2, we know the details and can thus perform the scrolling to the left
if self._perform_scrolling_stage == 2: if self._perform_scrolling_stage == 2:
if self._current_line_horizontal_overlap+self._x_scrolling > 0: if self._current_line_horizontal_overlap + self._x_scrolling > 0:
self._x_scrolling -= 1 self._x_scrolling -= 1
if self._current_line_horizontal_overlap+self._x_scrolling == 0: if self._current_line_horizontal_overlap + self._x_scrolling == 0:
self._stage_timestamp = int(time.time()) self._stage_timestamp = int(time.time())
self._perform_scrolling_stage = 3 self._perform_scrolling_stage = 3
@ -115,11 +123,12 @@ class MenuRenderer(ABC):
# in stage 4, we scroll back to the right # in stage 4, we scroll back to the right
if self._perform_scrolling_stage == 4: if self._perform_scrolling_stage == 4:
if self._current_line_horizontal_overlap+self._x_scrolling >= 0: if self._current_line_horizontal_overlap + self._x_scrolling >= 0:
self._x_scrolling += 1 self._x_scrolling += 1
if (self._current_line_horizontal_overlap + if (
self._x_scrolling) == self._current_line_horizontal_overlap: self._current_line_horizontal_overlap + self._x_scrolling
) == self._current_line_horizontal_overlap:
self._stage_timestamp = int(time.time()) self._stage_timestamp = int(time.time())
self._perform_scrolling_stage = 5 self._perform_scrolling_stage = 5
@ -131,8 +140,14 @@ class MenuRenderer(ABC):
self._stage_timestamp = None self._stage_timestamp = None
self._perform_scrolling_stage = 2 self._perform_scrolling_stage = 2
self.draw.text((x_pos+self._x_scrolling, y_pos), text, font=self.font, self.draw.text(
spacing=0, stroke_fill=0, fill=0) (x_pos + self._x_scrolling, y_pos),
text,
font=self.font,
spacing=0,
stroke_fill=0,
fill=0,
)
else: else:
self.draw.text((x_pos, y_pos), text, font=self.font, spacing=0, stroke_fill=0, fill=255) self.draw.text((x_pos, y_pos), text, font=self.font, spacing=0, stroke_fill=0, fill=255)
@ -143,8 +158,15 @@ class MenuRenderer(ABC):
centered_height = (self.disp.height - font_height) / 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.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, self.draw.text(
stroke_fill=0, fill=0, textsize=20) (centered_width, centered_height),
text,
align="center",
font=self.font,
stroke_fill=0,
fill=0,
textsize=20,
)
def draw_mini_message(self, text: str): def draw_mini_message(self, text: str):
"""Draws a fullscreen message, i.e., a message covering only the center portion of """Draws a fullscreen message, i.e., a message covering only the center portion of
@ -153,16 +175,33 @@ class MenuRenderer(ABC):
centered_width = (self.disp.width - font_width) / 2 centered_width = (self.disp.width - font_width) / 2
centered_height = (self.disp.height - self.font_height) / 2 centered_height = (self.disp.height - self.font_height) / 2
self.draw.rectangle((0, centered_height-4, self.disp.width, self.draw.rectangle(
centered_height+self.font_height+4), outline=0, fill=255) (
self.draw.text((centered_width, centered_height), text, align="center", font=self.font, 0,
stroke_fill=0, fill=0, textsize=20) 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): def draw_menu(self):
"""Method draws the menu set to the class instance.""" """Method draws the menu set to the class instance."""
if self._menu.item_selection >= self.frame_start_row + self.rows_per_screen(): if self._menu.item_selection >= self.frame_start_row + self.rows_per_screen():
if self._config.scroll_behavior == "page": if self._config.scroll_behavior == "page":
self.frame_start_row = self.frame_start_row + (round(self.rows_per_screen()/2)) + 1 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(): 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() self.frame_start_row = len(self._menu.entries) - self.rows_per_screen()
else: # extend as default behavior else: # extend as default behavior
@ -170,13 +209,15 @@ class MenuRenderer(ABC):
if self._menu.item_selection < self.frame_start_row: if self._menu.item_selection < self.frame_start_row:
if self._config.scroll_behavior == "page": if self._config.scroll_behavior == "page":
self.frame_start_row = self.frame_start_row - (round(self.rows_per_screen()/2)) - 1 self.frame_start_row = (
self.frame_start_row - (round(self.rows_per_screen() / 2)) - 1
)
if self.frame_start_row < 0: if self.frame_start_row < 0:
self.frame_start_row = 0 self.frame_start_row = 0
else: # extend as default behavior else: # extend as default behavior
self.frame_start_row = self._menu.item_selection self.frame_start_row = self._menu.item_selection
self.draw_menu_frame(self.frame_start_row, self.frame_start_row+self.rows_per_screen()) 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): 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 row frame_start_row to frame_end_row of the class instance menu, i.e., it

View File

@ -11,8 +11,9 @@ class MenuRendererAdafruitSSD1306(MenuRenderer):
def display_init(self): def display_init(self):
i2c = busio.I2C(SCL, SDA) i2c = busio.I2C(SCL, SDA)
self.disp = adafruit_ssd1306.SSD1306_I2C(self._config.width, self._config.height, i2c, self.disp = adafruit_ssd1306.SSD1306_I2C(
addr=self._config.i2c_address) self._config.width, self._config.height, i2c, addr=self._config.i2c_address
)
self.disp.rotation = self._config.get_mapped_rotation() self.disp.rotation = self._config.get_mapped_rotation()
self.disp.fill(0) self.disp.fill(0)
self.disp.show() self.disp.show()

View File

@ -5,20 +5,16 @@
class MenuRendererConfig: class MenuRendererConfig:
"""Class for configuring menu renderer instances. Provides configuration options """Class for configuring menu renderer instances. Provides configuration options
such as width, height, i2c address, font, transitions, etc.""" such as width, height, i2c address, font, transitions, etc."""
_rotation_mapper = {
0: 0, _rotation_mapper = {0: 0, 90: 1, 180: 2, 270: 3}
90: 1,
180: 2,
270: 3
}
def __init__(self): def __init__(self):
self.width = 128 self.width = 128
self.height = 64 self.height = 64
self.i2c_address = 0x3c self.i2c_address = 0x3C
self.i2c_port = 1 self.i2c_port = 1
self.display_type = "ssd1306" # luma-oled supported devices, "sh1106", "ssd1306", ... self.display_type = "ssd1306" # luma-oled supported devices, "sh1106", "ssd1306", ...
self.font_path = 'resources/DejaVuSansMono-Bold.ttf' self.font_path = "resources/DejaVuSansMono-Bold.ttf"
self.font_size = 12 self.font_size = 12
self.row_selection_pixel_extension = 2 self.row_selection_pixel_extension = 2
self.scroll_behavior = "page" # "extend" or "page" self.scroll_behavior = "page" # "extend" or "page"

View File

@ -5,14 +5,19 @@ from menu.menu_renderer import MenuRenderer
class MenuRendererLumaOled(MenuRenderer): class MenuRendererLumaOled(MenuRenderer):
"""Class implementing the luma oled menu renderer""" """Class implementing the luma oled menu renderer"""
def display_init(self): def display_init(self):
serial = i2c(port=self._config.i2c_port, address=self._config.i2c_address) serial = i2c(port=self._config.i2c_port, address=self._config.i2c_address)
import luma.oled.device import luma.oled.device
device = getattr(luma.oled.device, self._config.display_type) device = getattr(luma.oled.device, self._config.display_type)
self.disp = device(serial_interface=serial, width=self._config.width, self.disp = device(
height=self._config.height, serial_interface=serial,
rotate=self._config.get_mapped_rotation()) width=self._config.width,
height=self._config.height,
rotate=self._config.get_mapped_rotation(),
)
self.disp.clear() self.disp.clear()
self.disp.show() self.disp.show()

View File

@ -5,6 +5,7 @@ import time
class Timer: class Timer:
"""Class implementing a timer class. Takes an activation delay and """Class implementing a timer class. Takes an activation delay and
sets a flag if the activation delay exprires.""" sets a flag if the activation delay exprires."""
def __init__(self, activation_delay): def __init__(self, activation_delay):
self.start_timestamp = int(time.time()) self.start_timestamp = int(time.time())
self.activation_delay = activation_delay self.activation_delay = activation_delay

View File

@ -17,6 +17,7 @@ class Transition:
class PushTransition(Transition): class PushTransition(Transition):
"""Class implementing a push left/right transition.""" """Class implementing a push left/right transition."""
PUSH_LEFT_TRANSITION = "push_left" PUSH_LEFT_TRANSITION = "push_left"
PUSH_RIGHT_TRANSITION = "push_right" PUSH_RIGHT_TRANSITION = "push_right"
@ -32,7 +33,7 @@ class PushTransition(Transition):
if transition_attributes is not None and transition_attributes != {}: if transition_attributes is not None and transition_attributes != {}:
direction = transition_attributes["direction"] direction = transition_attributes["direction"]
transition_image = Image.new('1', (self.disp.width, self.disp.height)) transition_image = Image.new("1", (self.disp.width, self.disp.height))
if direction == PushTransition.PUSH_LEFT_TRANSITION: if direction == PushTransition.PUSH_LEFT_TRANSITION:
self.perform_push_left(end_image, start_image, transition_image) self.perform_push_left(end_image, start_image, transition_image)
@ -57,8 +58,8 @@ class PushTransition(Transition):
"""Implements a push right transition. Is called by perform depending on the transition """Implements a push right transition. Is called by perform depending on the transition
attribute 'direction'.""" attribute 'direction'."""
for x_pos in range(0, 128, self.transition_attributes["transition_speed"]): for x_pos in range(0, 128, self.transition_attributes["transition_speed"]):
left_region = start_image.crop((0, 0, 128-x_pos, 64)) left_region = start_image.crop((0, 0, 128 - x_pos, 64))
right_region = end_image.crop((128-x_pos, 0, 128, 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(left_region, (x_pos, 0, 128, 64))
transition_image.paste(right_region, (0, 0, x_pos, 64)) transition_image.paste(right_region, (0, 0, x_pos, 64))
self.disp.display(transition_image) self.disp.display(transition_image)

View File

@ -5,6 +5,7 @@ from observer import Observer
class Observable: class Observable:
"""Class implementing the Observable pattern""" """Class implementing the Observable pattern"""
_observers: List[Observer] = [] _observers: List[Observer] = []
def attach(self, observer: Observer): def attach(self, observer: Observer):

View File

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

View File

@ -9,8 +9,13 @@ from menu.timer import Timer
class RascsiMenuController(MenuController): class RascsiMenuController(MenuController):
"""Class implementing a RaSCSI Control Board UI specific menu controller""" """Class implementing a RaSCSI Control Board UI specific menu controller"""
def __init__(self, refresh_interval, menu_builder: MenuBuilder, def __init__(
menu_renderer=None, menu_renderer_config=None): self,
refresh_interval,
menu_builder: MenuBuilder,
menu_renderer=None,
menu_renderer_config=None,
):
super().__init__(menu_builder, menu_renderer, menu_renderer_config) super().__init__(menu_builder, menu_renderer, menu_renderer_config)
self._refresh_interval = refresh_interval self._refresh_interval = refresh_interval
self._menu_renderer: MenuRenderer = menu_renderer self._menu_renderer: MenuRenderer = menu_renderer

View File

@ -66,91 +66,175 @@ rascsi_none = -1
# Matrix showing all of the SCSI signals, along what signal they're looped back to. # Matrix showing all of the SCSI signals, along what signal they're looped back to.
# dir_ctrl indicates which direction control pin is associated with that output # dir_ctrl indicates which direction control pin is associated with that output
gpio_map = [ gpio_map = [
{ 'gpio_num': scsi_d0_gpio, 'attached_to': scsi_ack_gpio, 'dir_ctrl': rascsi_dtd_gpio}, {
{ 'gpio_num': scsi_d1_gpio, 'attached_to': scsi_sel_gpio, 'dir_ctrl': rascsi_dtd_gpio}, "gpio_num": scsi_d0_gpio,
{ 'gpio_num': scsi_d2_gpio, 'attached_to': scsi_atn_gpio, 'dir_ctrl': rascsi_dtd_gpio}, "attached_to": scsi_ack_gpio,
{ 'gpio_num': scsi_d3_gpio, 'attached_to': scsi_rst_gpio, 'dir_ctrl': rascsi_dtd_gpio}, "dir_ctrl": rascsi_dtd_gpio,
{ 'gpio_num': scsi_d4_gpio, 'attached_to': scsi_cd_gpio, 'dir_ctrl': rascsi_dtd_gpio}, },
{ 'gpio_num': scsi_d5_gpio, 'attached_to': scsi_io_gpio, 'dir_ctrl': rascsi_dtd_gpio}, {
{ 'gpio_num': scsi_d6_gpio, 'attached_to': scsi_msg_gpio, 'dir_ctrl': rascsi_dtd_gpio}, "gpio_num": scsi_d1_gpio,
{ 'gpio_num': scsi_d7_gpio, 'attached_to': scsi_req_gpio, 'dir_ctrl': rascsi_dtd_gpio}, "attached_to": scsi_sel_gpio,
{ 'gpio_num': scsi_dp_gpio, 'attached_to': scsi_bsy_gpio, 'dir_ctrl': rascsi_dtd_gpio}, "dir_ctrl": rascsi_dtd_gpio,
{ 'gpio_num': scsi_atn_gpio, 'attached_to': scsi_d2_gpio, 'dir_ctrl': rascsi_ind_gpio}, },
{ 'gpio_num': scsi_rst_gpio, 'attached_to': scsi_d3_gpio, 'dir_ctrl': rascsi_ind_gpio}, {
{ 'gpio_num': scsi_ack_gpio, 'attached_to': scsi_d0_gpio, 'dir_ctrl': rascsi_ind_gpio}, "gpio_num": scsi_d2_gpio,
{ 'gpio_num': scsi_req_gpio, 'attached_to': scsi_d7_gpio, 'dir_ctrl': rascsi_tad_gpio}, "attached_to": scsi_atn_gpio,
{ 'gpio_num': scsi_msg_gpio, 'attached_to': scsi_d6_gpio, 'dir_ctrl': rascsi_tad_gpio}, "dir_ctrl": rascsi_dtd_gpio,
{ 'gpio_num': scsi_cd_gpio, 'attached_to': scsi_d4_gpio, 'dir_ctrl': rascsi_tad_gpio}, },
{ 'gpio_num': scsi_io_gpio, 'attached_to': scsi_d5_gpio, 'dir_ctrl': rascsi_tad_gpio}, {
{ 'gpio_num': scsi_bsy_gpio, 'attached_to': scsi_dp_gpio, 'dir_ctrl': rascsi_tad_gpio}, "gpio_num": scsi_d3_gpio,
{ 'gpio_num': scsi_sel_gpio, 'attached_to': scsi_d1_gpio, 'dir_ctrl': rascsi_ind_gpio}, "attached_to": scsi_rst_gpio,
"dir_ctrl": rascsi_dtd_gpio,
},
{
"gpio_num": scsi_d4_gpio,
"attached_to": scsi_cd_gpio,
"dir_ctrl": rascsi_dtd_gpio,
},
{
"gpio_num": scsi_d5_gpio,
"attached_to": scsi_io_gpio,
"dir_ctrl": rascsi_dtd_gpio,
},
{
"gpio_num": scsi_d6_gpio,
"attached_to": scsi_msg_gpio,
"dir_ctrl": rascsi_dtd_gpio,
},
{
"gpio_num": scsi_d7_gpio,
"attached_to": scsi_req_gpio,
"dir_ctrl": rascsi_dtd_gpio,
},
{
"gpio_num": scsi_dp_gpio,
"attached_to": scsi_bsy_gpio,
"dir_ctrl": rascsi_dtd_gpio,
},
{
"gpio_num": scsi_atn_gpio,
"attached_to": scsi_d2_gpio,
"dir_ctrl": rascsi_ind_gpio,
},
{
"gpio_num": scsi_rst_gpio,
"attached_to": scsi_d3_gpio,
"dir_ctrl": rascsi_ind_gpio,
},
{
"gpio_num": scsi_ack_gpio,
"attached_to": scsi_d0_gpio,
"dir_ctrl": rascsi_ind_gpio,
},
{
"gpio_num": scsi_req_gpio,
"attached_to": scsi_d7_gpio,
"dir_ctrl": rascsi_tad_gpio,
},
{
"gpio_num": scsi_msg_gpio,
"attached_to": scsi_d6_gpio,
"dir_ctrl": rascsi_tad_gpio,
},
{
"gpio_num": scsi_cd_gpio,
"attached_to": scsi_d4_gpio,
"dir_ctrl": rascsi_tad_gpio,
},
{
"gpio_num": scsi_io_gpio,
"attached_to": scsi_d5_gpio,
"dir_ctrl": rascsi_tad_gpio,
},
{
"gpio_num": scsi_bsy_gpio,
"attached_to": scsi_dp_gpio,
"dir_ctrl": rascsi_tad_gpio,
},
{
"gpio_num": scsi_sel_gpio,
"attached_to": scsi_d1_gpio,
"dir_ctrl": rascsi_ind_gpio,
},
] ]
# List of all of the SCSI signals that is also a dictionary to their human readable name # List of all of the SCSI signals that is also a dictionary to their human readable name
scsi_signals = { scsi_signals = {
scsi_d0_gpio: 'D0', scsi_d0_gpio: "D0",
scsi_d1_gpio: 'D1', scsi_d1_gpio: "D1",
scsi_d2_gpio: 'D2', scsi_d2_gpio: "D2",
scsi_d3_gpio: 'D3', scsi_d3_gpio: "D3",
scsi_d4_gpio: 'D4', scsi_d4_gpio: "D4",
scsi_d5_gpio: 'D5', scsi_d5_gpio: "D5",
scsi_d6_gpio: 'D6', scsi_d6_gpio: "D6",
scsi_d7_gpio: 'D7', scsi_d7_gpio: "D7",
scsi_dp_gpio: 'DP', scsi_dp_gpio: "DP",
scsi_atn_gpio: 'ATN', scsi_atn_gpio: "ATN",
scsi_rst_gpio: 'RST', scsi_rst_gpio: "RST",
scsi_ack_gpio: 'ACK', scsi_ack_gpio: "ACK",
scsi_req_gpio: 'REQ', scsi_req_gpio: "REQ",
scsi_msg_gpio: 'MSG', scsi_msg_gpio: "MSG",
scsi_cd_gpio: 'CD', scsi_cd_gpio: "CD",
scsi_io_gpio: 'IO', scsi_io_gpio: "IO",
scsi_bsy_gpio: 'BSY', scsi_bsy_gpio: "BSY",
scsi_sel_gpio: 'SEL' scsi_sel_gpio: "SEL",
} }
# Debug function that just dumps the status of all of the scsi signals to the console # Debug function that just dumps the status of all of the scsi signals to the console
def print_all(): def print_all():
for cur_gpio in gpio_map: for cur_gpio in gpio_map:
print(cur_gpio['name']+"="+str(gpio.input(cur_gpio['gpio_num'])) + " ", end='', flush=True) print(
cur_gpio["name"] + "=" + str(gpio.input(cur_gpio["gpio_num"])) + " ",
end="",
flush=True,
)
print("") print("")
# Set transceivers IC1 and IC2 to OUTPUT # Set transceivers IC1 and IC2 to OUTPUT
def set_dtd_out(): def set_dtd_out():
gpio.output(rascsi_dtd_gpio,gpio.LOW) gpio.output(rascsi_dtd_gpio, gpio.LOW)
# Set transceivers IC1 and IC2 to INPUT # Set transceivers IC1 and IC2 to INPUT
def set_dtd_in(): def set_dtd_in():
gpio.output(rascsi_dtd_gpio,gpio.HIGH) gpio.output(rascsi_dtd_gpio, gpio.HIGH)
# Set transceiver IC4 to OUTPUT # Set transceiver IC4 to OUTPUT
def set_ind_out(): def set_ind_out():
gpio.output(rascsi_ind_gpio,gpio.HIGH) gpio.output(rascsi_ind_gpio, gpio.HIGH)
# Set transceiver IC4 to INPUT # Set transceiver IC4 to INPUT
def set_ind_in(): def set_ind_in():
gpio.output(rascsi_ind_gpio,gpio.LOW) gpio.output(rascsi_ind_gpio, gpio.LOW)
# Set transceiver IC3 to OUTPUT # Set transceiver IC3 to OUTPUT
def set_tad_out(): def set_tad_out():
gpio.output(rascsi_tad_gpio,gpio.HIGH) gpio.output(rascsi_tad_gpio, gpio.HIGH)
# Set transceiver IC3 to INPUT # Set transceiver IC3 to INPUT
def set_tad_in(): def set_tad_in():
gpio.output(rascsi_tad_gpio,gpio.LOW) gpio.output(rascsi_tad_gpio, gpio.LOW)
# Set the specified transciever to an OUTPUT. All of the other transceivers # Set the specified transciever to an OUTPUT. All of the other transceivers
# will be set to inputs. If a non-existent direction gpio is specified, this # will be set to inputs. If a non-existent direction gpio is specified, this
# will set all of the transceivers to inputs. # will set all of the transceivers to inputs.
def set_output_channel(out_gpio): def set_output_channel(out_gpio):
if(out_gpio == rascsi_tad_gpio): if out_gpio == rascsi_tad_gpio:
set_tad_out() set_tad_out()
else: else:
set_tad_in() set_tad_in()
if(out_gpio == rascsi_dtd_gpio): if out_gpio == rascsi_dtd_gpio:
set_dtd_out() set_dtd_out()
else: else:
set_dtd_in() set_dtd_in()
if(out_gpio == rascsi_ind_gpio): if out_gpio == rascsi_ind_gpio:
set_ind_out() set_ind_out()
else: else:
set_ind_in() set_ind_in()
@ -161,11 +245,11 @@ def set_output_channel(out_gpio):
def test_gpio_pin(gpio_rec): def test_gpio_pin(gpio_rec):
global err_count global err_count
set_output_channel(gpio_rec['dir_ctrl']) set_output_channel(gpio_rec["dir_ctrl"])
############################################ ############################################
# set the test gpio low # set the test gpio low
gpio.output(gpio_rec['gpio_num'], gpio.LOW) gpio.output(gpio_rec["gpio_num"], gpio.LOW)
time.sleep(pin_settle_delay) time.sleep(pin_settle_delay)
@ -173,18 +257,34 @@ def test_gpio_pin(gpio_rec):
for cur_gpio in scsi_signals: for cur_gpio in scsi_signals:
# all of the gpios should be high except for the test gpio and the connected gpio # all of the gpios should be high except for the test gpio and the connected gpio
cur_val = gpio.input(cur_gpio) cur_val = gpio.input(cur_gpio)
if( cur_gpio == gpio_rec['gpio_num']): if cur_gpio == gpio_rec["gpio_num"]:
if(cur_val != gpio.LOW): if cur_val != gpio.LOW:
print("Error: Test commanded GPIO " + scsi_signals[gpio_rec['gpio_num']] + " to be low, but it did not respond") print(
err_count = err_count+1 "Error: Test commanded GPIO "
elif (cur_gpio == gpio_rec['attached_to']): + scsi_signals[gpio_rec["gpio_num"]]
if(cur_val != gpio.LOW): + " to be low, but it did not respond"
print("Error: GPIO " + scsi_signals[gpio_rec['gpio_num']] + " should drive " + scsi_signals[gpio_rec['attached_to']] + " low, but did not") )
err_count = err_count+1 err_count = err_count + 1
elif cur_gpio == gpio_rec["attached_to"]:
if cur_val != gpio.LOW:
print(
"Error: GPIO "
+ scsi_signals[gpio_rec["gpio_num"]]
+ " should drive "
+ scsi_signals[gpio_rec["attached_to"]]
+ " low, but did not"
)
err_count = err_count + 1
else: else:
if(cur_val != gpio.HIGH): if cur_val != gpio.HIGH:
print("Error: GPIO " + scsi_signals[gpio_rec['gpio_num']] + " incorrectly pulled " + scsi_signals[cur_gpio] + " LOW, when it shouldn't have") print(
err_count = err_count+1 "Error: GPIO "
+ scsi_signals[gpio_rec["gpio_num"]]
+ " incorrectly pulled "
+ scsi_signals[cur_gpio]
+ " LOW, when it shouldn't have"
)
err_count = err_count + 1
############################################ ############################################
# set the transceivers to input # set the transceivers to input
@ -196,22 +296,31 @@ def test_gpio_pin(gpio_rec):
for cur_gpio in scsi_signals: for cur_gpio in scsi_signals:
# all of the gpios should be high except for the test gpio # all of the gpios should be high except for the test gpio
cur_val = gpio.input(cur_gpio) cur_val = gpio.input(cur_gpio)
if( cur_gpio == gpio_rec['gpio_num']): if cur_gpio == gpio_rec["gpio_num"]:
if(cur_val != gpio.LOW): if cur_val != gpio.LOW:
print("Error: Test commanded GPIO " + scsi_signals[gpio_rec['gpio_num']] + " to be low, but it did not respond") print(
err_count = err_count+1 "Error: Test commanded GPIO "
+ scsi_signals[gpio_rec["gpio_num"]]
+ " to be low, but it did not respond"
)
err_count = err_count + 1
else: else:
if(cur_val != gpio.HIGH): if cur_val != gpio.HIGH:
print("Error: GPIO " + scsi_signals[gpio_rec['gpio_num']] + " incorrectly pulled " + scsi_signals[cur_gpio] + " LOW, when it shouldn't have") print(
err_count = err_count+1 "Error: GPIO "
+ scsi_signals[gpio_rec["gpio_num"]]
+ " incorrectly pulled "
+ scsi_signals[cur_gpio]
+ " LOW, when it shouldn't have"
)
err_count = err_count + 1
# Set the transceiver back to output # Set the transceiver back to output
set_output_channel(gpio_rec['dir_ctrl']) set_output_channel(gpio_rec["dir_ctrl"])
############################################# #############################################
# set the test gpio high # set the test gpio high
gpio.output(gpio_rec['gpio_num'], gpio.HIGH) gpio.output(gpio_rec["gpio_num"], gpio.HIGH)
time.sleep(pin_settle_delay) time.sleep(pin_settle_delay)
@ -219,14 +328,24 @@ def test_gpio_pin(gpio_rec):
for cur_gpio in scsi_signals: for cur_gpio in scsi_signals:
# all of the gpios should be high # all of the gpios should be high
cur_val = gpio.input(cur_gpio) cur_val = gpio.input(cur_gpio)
if( cur_gpio == gpio_rec['gpio_num']): if cur_gpio == gpio_rec["gpio_num"]:
if(cur_val != gpio.HIGH): if cur_val != gpio.HIGH:
print("Error: Test commanded GPIO " + scsi_signals[gpio_rec['gpio_num']] + " to be high, but it did not respond") print(
err_count = err_count+1 "Error: Test commanded GPIO "
+ scsi_signals[gpio_rec["gpio_num"]]
+ " to be high, but it did not respond"
)
err_count = err_count + 1
else: else:
if(cur_val != gpio.HIGH): if cur_val != gpio.HIGH:
print("Error: GPIO " + scsi_signals[gpio_rec['gpio_num']] + " incorrectly pulled " + scsi_signals[cur_gpio] + " LOW, when it shouldn't have") print(
err_count = err_count+1 "Error: GPIO "
+ scsi_signals[gpio_rec["gpio_num"]]
+ " incorrectly pulled "
+ scsi_signals[cur_gpio]
+ " LOW, when it shouldn't have"
)
err_count = err_count + 1
# Initialize the GPIO library, set all of the gpios associated with SCSI signals to outputs and set # Initialize the GPIO library, set all of the gpios associated with SCSI signals to outputs and set
@ -235,7 +354,7 @@ def setup():
gpio.setmode(gpio.BOARD) gpio.setmode(gpio.BOARD)
gpio.setwarnings(False) gpio.setwarnings(False)
for cur_gpio in gpio_map: for cur_gpio in gpio_map:
gpio.setup(cur_gpio['gpio_num'], gpio.OUT, initial=gpio.HIGH) gpio.setup(cur_gpio["gpio_num"], gpio.OUT, initial=gpio.HIGH)
# Setup direction control # Setup direction control
gpio.setup(rascsi_ind_gpio, gpio.OUT) gpio.setup(rascsi_ind_gpio, gpio.OUT)
@ -244,7 +363,7 @@ def setup():
# Main functions for running the actual test. # Main functions for running the actual test.
if __name__ == '__main__': if __name__ == "__main__":
# setup the GPIOs # setup the GPIOs
setup() setup()
# Test each SCSI signal in the gpio_map # Test each SCSI signal in the gpio_map
@ -252,7 +371,7 @@ if __name__ == '__main__':
test_gpio_pin(cur_gpio) test_gpio_pin(cur_gpio)
# Print the test results # Print the test results
if(err_count == 0): if err_count == 0:
print("-------- Test PASSED --------") print("-------- Test PASSED --------")
else: else:
print("!!!!!!!! Test FAILED !!!!!!!!") print("!!!!!!!! Test FAILED !!!!!!!!")

View File

@ -8,6 +8,7 @@ class GracefulInterruptHandler:
""" """
Class for handling Linux signal interrupts Class for handling Linux signal interrupts
""" """
def __init__(self, signals=(signal.SIGINT, signal.SIGTERM)): def __init__(self, signals=(signal.SIGINT, signal.SIGTERM)):
self.signals = signals self.signals = signals
self.original_handlers = {} self.original_handlers = {}

View File

@ -52,7 +52,7 @@ parser.add_argument(
default=180, default=180,
action="store", action="store",
help="The rotation of the screen buffer in degrees", help="The rotation of the screen buffer in degrees",
) )
parser.add_argument( parser.add_argument(
"--height", "--height",
type=int, type=int,
@ -60,7 +60,7 @@ parser.add_argument(
default=32, default=32,
action="store", action="store",
help="The pixel height of the screen buffer", help="The pixel height of the screen buffer",
) )
parser.add_argument( parser.add_argument(
"--refresh_interval", "--refresh_interval",
type=int, type=int,
@ -68,14 +68,14 @@ parser.add_argument(
default=1000, default=1000,
action="store", action="store",
help="Interval in ms between each screen refresh", help="Interval in ms between each screen refresh",
) )
parser.add_argument( parser.add_argument(
"--password", "--password",
type=str, type=str,
default="", default="",
action="store", action="store",
help="Token password string for authenticating with the backend", help="Token password string for authenticating with the backend",
) )
parser.add_argument( parser.add_argument(
"--host", "--host",
type=str, type=str,
@ -156,7 +156,7 @@ LINE_SPACING = 8
# When using other fonts, you may need to adjust PADDING, FONT_SIZE, # When using other fonts, you may need to adjust PADDING, FONT_SIZE,
# LINE_SPACING, and LINES. # LINE_SPACING, and LINES.
# Some other nice fonts to try: http://www.dafont.com/bitmap.php # Some other nice fonts to try: http://www.dafont.com/bitmap.php
FONT = ImageFont.truetype('resources/type_writer.ttf', FONT_SIZE) FONT = ImageFont.truetype("resources/type_writer.ttf", FONT_SIZE)
REMOVABLE_DEVICE_TYPES = ractl_cmd.get_removable_device_types() REMOVABLE_DEVICE_TYPES = ractl_cmd.get_removable_device_types()
PERIPHERAL_DEVICE_TYPES = ractl_cmd.get_peripheral_device_types() PERIPHERAL_DEVICE_TYPES = ractl_cmd.get_peripheral_device_types()
@ -176,6 +176,7 @@ DRAW = ImageDraw.Draw(IMAGE)
# Draw a black filled box to clear the image. # Draw a black filled box to clear the image.
DRAW.rectangle((0, 0, WIDTH, HEIGHT), outline=0, fill=0) DRAW.rectangle((0, 0, WIDTH, HEIGHT), outline=0, fill=0)
def formatted_output(): def formatted_output():
""" """
Formats the strings to be displayed on the Screen Formats the strings to be displayed on the Screen
@ -216,6 +217,7 @@ def formatted_output():
output += ["No network connection"] output += ["No network connection"]
return output return output
def shutdown(): def shutdown():
""" """
Display the shutdown splash, then blank the screen after a sleep Display the shutdown splash, then blank the screen after a sleep
@ -224,7 +226,7 @@ def shutdown():
OLED.image(IMAGE_STOP) OLED.image(IMAGE_STOP)
OLED.show() OLED.show()
OLED.fill(0) OLED.fill(0)
sleep(700/1000) sleep(700 / 1000)
OLED.show() OLED.show()
sys.exit("Shutting down the OLED display...") sys.exit("Shutting down the OLED display...")
@ -264,7 +266,7 @@ with GracefulInterruptHandler() as handler:
# Display image. # Display image.
OLED.image(IMAGE) OLED.image(IMAGE)
OLED.show() OLED.show()
sleep(args.refresh_interval/1000) sleep(args.refresh_interval / 1000)
snapshot = formatted_output() snapshot = formatted_output()

4
python/pyproject.toml Normal file
View File

@ -0,0 +1,4 @@
[tool.black]
line-length = 100
target-version = ['py37', 'py38', 'py39']
extend-exclude = ".*_pb2.py"

View File

@ -1,2 +0,0 @@
[flake8]
max-line-length = 100

View File

@ -1,8 +1,4 @@
[tool.pytest.ini_options] [tool.pytest.ini_options]
addopts = "--junitxml=report.xml" addopts = "--junitxml=report.xml"
log_cli = true log_cli = true
log_cli_level = "warn" log_cli_level = "warn"
[tool.black]
line-length = 100
target-version = ['py37', 'py38', 'py39']

View File

@ -1,4 +1,4 @@
"""Module for mapping between rascsi return codes and translated strings""" """Module for mapping between return codes and translated strings"""
from rascsi.return_codes import ReturnCodes from rascsi.return_codes import ReturnCodes
from flask_babel import _, lazy_gettext from flask_babel import _, lazy_gettext
@ -6,8 +6,9 @@ from flask_babel import _, lazy_gettext
# pylint: disable=too-few-public-methods # pylint: disable=too-few-public-methods
class ReturnCodeMapper: class ReturnCodeMapper:
"""Class for mapping between rascsi return codes and translated strings""" """Class for mapping between return codes and translated strings"""
# fmt: off
MESSAGES = { MESSAGES = {
ReturnCodes.DELETEFILE_SUCCESS: ReturnCodes.DELETEFILE_SUCCESS:
_("File deleted: %(file_path)s"), _("File deleted: %(file_path)s"),
@ -46,11 +47,12 @@ class ReturnCodeMapper:
ReturnCodes.EXTRACTIMAGE_COMMAND_ERROR: ReturnCodes.EXTRACTIMAGE_COMMAND_ERROR:
_("Unable to extract archive: %(error)s"), _("Unable to extract archive: %(error)s"),
} }
# fmt: on
@staticmethod @staticmethod
def add_msg(payload): def add_msg(payload):
"""adds a msg key to a given payload with a rascsi module return code """adds a msg key to a given payload with a module return code
with a translated return code message string. """ with a translated return code message string."""
if "return_code" not in payload: if "return_code" not in payload:
return payload return payload
@ -60,7 +62,7 @@ class ReturnCodeMapper:
payload["msg"] = lazy_gettext( payload["msg"] = lazy_gettext(
ReturnCodeMapper.MESSAGES[payload["return_code"]], ReturnCodeMapper.MESSAGES[payload["return_code"]],
**parameters, **parameters,
) )
else: else:
payload["msg"] = lazy_gettext(ReturnCodeMapper.MESSAGES[payload["return_code"]]) payload["msg"] = lazy_gettext(ReturnCodeMapper.MESSAGES[payload["return_code"]])

View File

@ -30,4 +30,4 @@ TEMPLATE_THEMES = ["classic", "modern"]
TEMPLATE_THEME_DEFAULT = "modern" TEMPLATE_THEME_DEFAULT = "modern"
# Fallback theme for older browsers # Fallback theme for older browsers
TEMPLATE_THEME_LEGACY = "classic" TEMPLATE_THEME_LEGACY = "classic"

View File

@ -5,9 +5,11 @@ Module for sending and receiving data over a socket connection with the RaSCSI b
from flask import abort from flask import abort
from flask_babel import _ from flask_babel import _
from rascsi.exceptions import (EmptySocketChunkException, from rascsi.exceptions import (
InvalidProtobufResponse, EmptySocketChunkException,
FailedSocketConnectionException) InvalidProtobufResponse,
FailedSocketConnectionException,
)
from rascsi.socket_cmds import SocketCmds from rascsi.socket_cmds import SocketCmds
@ -15,6 +17,7 @@ class SocketCmdsFlask(SocketCmds):
""" """
Class for sending and receiving data over a socket connection with the RaSCSI backend Class for sending and receiving data over a socket connection with the RaSCSI backend
""" """
# pylint: disable=useless-super-delegation # pylint: disable=useless-super-delegation
def __init__(self, host="localhost", port=6868): def __init__(self, host="localhost", port=6868):
super().__init__(host, port) super().__init__(host, port)
@ -28,11 +31,16 @@ class SocketCmdsFlask(SocketCmds):
return super().send_pb_command(payload) return super().send_pb_command(payload)
except FailedSocketConnectionException as err: except FailedSocketConnectionException as err:
# After failing all attempts, throw a 404 error # After failing all attempts, throw a 404 error
abort(404, _( abort(
"The RaSCSI Web Interface failed to connect to RaSCSI at %(host)s:%(port)s " 404,
"with error: %(error_msg)s. The RaSCSI process is not running or may have crashed.", _(
host=self.host, port=self.port, error_msg=str(err), "The RaSCSI Web Interface failed to connect to RaSCSI at "
) "%(host)s:%(port)s with error: %(error_msg)s. The RaSCSI "
"process is not running or may have crashed.",
host=self.host,
port=self.port,
error_msg=str(err),
),
) )
return None return None
@ -42,19 +50,21 @@ class SocketCmdsFlask(SocketCmds):
return super().send_over_socket(sock, payload) return super().send_over_socket(sock, payload)
except EmptySocketChunkException: except EmptySocketChunkException:
abort( abort(
503, _( 503,
_(
"The RaSCSI Web Interface lost connection to RaSCSI. " "The RaSCSI Web Interface lost connection to RaSCSI. "
"Please go back and try again. " "Please go back and try again. "
"If the issue persists, please report a bug." "If the issue persists, please report a bug."
) ),
) )
return None return None
except InvalidProtobufResponse: except InvalidProtobufResponse:
abort( abort(
500, _( 500,
_(
"The RaSCSI Web Interface did not get a valid response from RaSCSI. " "The RaSCSI Web Interface did not get a valid response from RaSCSI. "
"Please go back and try again. " "Please go back and try again. "
"If the issue persists, please report a bug." "If the issue persists, please report a bug."
) ),
) )
return None return None

View File

@ -11,7 +11,6 @@ from grp import getgrall
import bjoern import bjoern
from rascsi.return_codes import ReturnCodes from rascsi.return_codes import ReturnCodes
from werkzeug.utils import secure_filename
from simplepam import authenticate from simplepam import authenticate
from flask_babel import Babel, Locale, refresh, _ from flask_babel import Babel, Locale, refresh, _
@ -75,6 +74,7 @@ from settings import (
APP = Flask(__name__) APP = Flask(__name__)
BABEL = Babel(APP) BABEL = Babel(APP)
def get_env_info(): def get_env_info():
""" """
Get information about the app/host environment Get information about the app/host environment
@ -113,7 +113,7 @@ def response(
redirect_url=None, redirect_url=None,
error=False, error=False,
status_code=200, status_code=200,
**kwargs **kwargs,
): ):
""" """
Generates a HTML or JSON HTTP response Generates a HTML or JSON HTTP response
@ -128,11 +128,16 @@ def response(
messages = [(str(message), status)] messages = [(str(message), status)]
if request.headers.get("accept") == "application/json": if request.headers.get("accept") == "application/json":
return jsonify({ return (
"status": status, jsonify(
"messages": [{"message": m, "category": c} for m, c in messages], {
"data": kwargs "status": status,
}), status_code "messages": [{"message": m, "category": c} for m, c in messages],
"data": kwargs,
}
),
status_code,
)
if messages: if messages:
for message, category in messages: for message, category in messages:
@ -182,7 +187,7 @@ def get_supported_locales():
locales = [ locales = [
{"language": x.language, "display_name": x.display_name} {"language": x.language, "display_name": x.display_name}
for x in [*BABEL.list_translations(), Locale("en")] for x in [*BABEL.list_translations(), Locale("en")]
] ]
return sorted(locales, key=lambda x: x["language"]) return sorted(locales, key=lambda x: x["language"])
@ -199,8 +204,8 @@ def index():
_( _(
"RaSCSI is password protected. " "RaSCSI is password protected. "
"Start the Web Interface with the --password parameter." "Start the Web Interface with the --password parameter."
), ),
) )
server_info = ractl_cmd.get_server_info() server_info = ractl_cmd.get_server_info()
devices = ractl_cmd.list_devices() devices = ractl_cmd.list_devices()
@ -233,18 +238,15 @@ def index():
# This might break if something like 'hdt' etc. gets added in the future. # This might break if something like 'hdt' etc. gets added in the future.
sorted( sorted(
[suffix for suffix in server_info["schd"] if suffix not in {"hdi", "nhd"}], [suffix for suffix in server_info["schd"] if suffix not in {"hdi", "nhd"}],
reverse=True reverse=True,
) +
server_info["scrm"] +
server_info["scmo"]
) )
+ server_info["scrm"]
+ server_info["scmo"]
)
valid_image_suffixes = ( valid_image_suffixes = (
server_info["schd"] + server_info["schd"] + server_info["scrm"] + server_info["scmo"] + server_info["sccd"]
server_info["scrm"] + )
server_info["scmo"] +
server_info["sccd"]
)
return response( return response(
template="index.html", template="index.html",
@ -296,7 +298,7 @@ def drive_list():
template="drives.html", template="drives.html",
files=file_cmd.list_images()["files"], files=file_cmd.list_images()["files"],
drive_properties=format_drive_properties(APP.config["RASCSI_DRIVE_PROPERTIES"]), drive_properties=format_drive_properties(APP.config["RASCSI_DRIVE_PROPERTIES"]),
) )
@APP.route("/login", methods=["POST"]) @APP.route("/login", methods=["POST"])
@ -312,10 +314,14 @@ def login():
session["username"] = request.form["username"] session["username"] = request.form["username"]
return response(env=get_env_info()) return response(env=get_env_info())
return response(error=True, status_code=401, message=_( return response(
"You must log in with valid credentials for a user in the '%(group)s' group", error=True,
group=AUTH_GROUP, status_code=401,
)) message=_(
"You must log in with valid credentials for a user in the '%(group)s' group",
group=AUTH_GROUP,
),
)
@APP.route("/logout") @APP.route("/logout")
@ -339,12 +345,14 @@ def login_required(func):
""" """
Wrapper method for enabling authentication for an endpoint Wrapper method for enabling authentication for an endpoint
""" """
@wraps(func) @wraps(func)
def decorated_function(*args, **kwargs): def decorated_function(*args, **kwargs):
auth = auth_active(AUTH_GROUP) auth = auth_active(AUTH_GROUP)
if auth["status"] and "username" not in session: if auth["status"] and "username" not in session:
return response(error=True, message=auth["msg"]) return response(error=True, message=auth["msg"])
return func(*args, **kwargs) return func(*args, **kwargs)
return decorated_function return decorated_function
@ -357,10 +365,7 @@ def drive_create():
drive_name = request.form.get("drive_name") drive_name = request.form.get("drive_name")
file_name = Path(request.form.get("file_name")).name file_name = Path(request.form.get("file_name")).name
properties = get_properties_by_drive_name( properties = get_properties_by_drive_name(APP.config["RASCSI_DRIVE_PROPERTIES"], drive_name)
APP.config["RASCSI_DRIVE_PROPERTIES"],
drive_name
)
if not properties: if not properties:
return response( return response(
@ -373,7 +378,7 @@ def drive_create():
file_name, file_name,
properties["file_type"], properties["file_type"],
properties["size"], properties["size"],
) )
if not process["status"]: if not process["status"]:
return response(error=True, message=process["msg"]) return response(error=True, message=process["msg"])
@ -385,8 +390,11 @@ def drive_create():
if not process["status"]: if not process["status"]:
return response(error=True, message=process["msg"]) return response(error=True, message=process["msg"])
return response(message= return response(
_("Image file with properties created: %(file_name)s", file_name=full_file_name) message=_(
"Image file with properties created: %(file_name)s",
file_name=full_file_name,
)
) )
@ -401,10 +409,7 @@ def drive_cdrom():
# Creating the drive properties file # Creating the drive properties file
file_name = f"{file_name}.{PROPERTIES_SUFFIX}" file_name = f"{file_name}.{PROPERTIES_SUFFIX}"
properties = get_properties_by_drive_name( properties = get_properties_by_drive_name(APP.config["RASCSI_DRIVE_PROPERTIES"], drive_name)
APP.config["RASCSI_DRIVE_PROPERTIES"],
drive_name
)
if not properties: if not properties:
return response( return response(
@ -474,19 +479,17 @@ def show_diskinfo():
if not safe_path["status"]: if not safe_path["status"]:
return response(error=True, message=safe_path["msg"]) return response(error=True, message=safe_path["msg"])
server_info = ractl_cmd.get_server_info() server_info = ractl_cmd.get_server_info()
returncode, diskinfo = sys_cmd.get_diskinfo( returncode, diskinfo = sys_cmd.get_diskinfo(Path(server_info["image_dir"]) / file_name)
Path(server_info["image_dir"]) / file_name
)
if returncode == 0: if returncode == 0:
return response( return response(
template="diskinfo.html", template="diskinfo.html",
file_name=str(file_name), file_name=str(file_name),
diskinfo=diskinfo, diskinfo=diskinfo,
) )
return response( return response(
error=True, error=True,
message=_("An error occurred when getting disk info: %(error)s", error=diskinfo) message=_("An error occurred when getting disk info: %(error)s", error=diskinfo),
) )
@ -497,23 +500,20 @@ def show_manpage():
""" """
app_allowlist = ["rascsi", "rasctl", "rasdump", "scsimon"] app_allowlist = ["rascsi", "rasctl", "rasdump", "scsimon"]
app = request.args.get("app", type = str) app = request.args.get("app", type=str)
if app not in app_allowlist: if app not in app_allowlist:
return response( return response(error=True, message=_("%(app)s is not a recognized RaSCSI app", app=app))
error=True,
message=_("%(app)s is not a recognized RaSCSI app", app=app)
)
file_path = f"{WEB_DIR}/../../../doc/{app}.1" file_path = f"{WEB_DIR}/../../../doc/{app}.1"
html_to_strip = [ html_to_strip = [
"Content-type", "Content-type",
"!DOCTYPE", "!DOCTYPE",
"<HTML>", "<HTML>",
"<HEAD>", "<HEAD>",
"<BODY>", "<BODY>",
"<H1>", "<H1>",
] ]
returncode, manpage = sys_cmd.get_manpage(file_path) returncode, manpage = sys_cmd.get_manpage(file_path)
if returncode == 0: if returncode == 0:
@ -524,7 +524,7 @@ def show_manpage():
line = line.replace("/?1+", "manpage?app=") line = line.replace("/?1+", "manpage?app=")
# Strip out useless hyperlink # Strip out useless hyperlink
elif "man2html" in line: elif "man2html" in line:
line = line.replace("<A HREF=\"/\">man2html</A>", "man2html") line = line.replace('<A HREF="/">man2html</A>', "man2html")
if not any(ele in line for ele in html_to_strip): if not any(ele in line for ele in html_to_strip):
formatted_manpage += line formatted_manpage += line
@ -532,11 +532,11 @@ def show_manpage():
template="manpage.html", template="manpage.html",
app=app, app=app,
manpage=formatted_manpage, manpage=formatted_manpage,
) )
return response( return response(
error=True, error=True,
message=_("An error occurred when accessing man page: %(error)s", error=manpage) message=_("An error occurred when accessing man page: %(error)s", error=manpage),
) )
@ -555,11 +555,11 @@ def show_logs():
scope=scope, scope=scope,
lines=lines, lines=lines,
logs=logs, logs=logs,
) )
return response( return response(
error=True, error=True,
message=_("An error occurred when fetching logs: %(error)s", error=logs) message=_("An error occurred when fetching logs: %(error)s", error=logs),
) )
@ -615,10 +615,10 @@ def attach_device():
return response(error=True, message=bridge_status["msg"]) return response(error=True, message=bridge_status["msg"])
kwargs = { kwargs = {
"unit": int(unit), "unit": int(unit),
"device_type": device_type, "device_type": device_type,
"params": params, "params": params,
} }
if drive_props: if drive_props:
kwargs["vendor"] = drive_props["vendor"] kwargs["vendor"] = drive_props["vendor"]
kwargs["product"] = drive_props["product"] kwargs["product"] = drive_props["product"]
@ -628,12 +628,14 @@ def attach_device():
process = ractl_cmd.attach_device(scsi_id, **kwargs) process = ractl_cmd.attach_device(scsi_id, **kwargs)
process = ReturnCodeMapper.add_msg(process) process = ReturnCodeMapper.add_msg(process)
if process["status"]: if process["status"]:
return response(message=_( return response(
"Attached %(device_type)s to SCSI ID %(id_number)s LUN %(unit_number)s", message=_(
device_type=get_device_name(device_type), "Attached %(device_type)s to SCSI ID %(id_number)s LUN %(unit_number)s",
id_number=scsi_id, device_type=get_device_name(device_type),
unit_number=unit, id_number=scsi_id,
)) unit_number=unit,
)
)
return response(error=True, message=process["msg"]) return response(error=True, message=process["msg"])
@ -687,7 +689,7 @@ def attach_image():
"The image may be corrupted, so proceed with caution.", "The image may be corrupted, so proceed with caution.",
file_size, file_size,
expected_block_size, expected_block_size,
) )
return response( return response(
message=_( message=_(
"Attached %(file_name)s as %(device_type)s to " "Attached %(file_name)s as %(device_type)s to "
@ -696,8 +698,8 @@ def attach_image():
device_type=get_device_name(device_type), device_type=get_device_name(device_type),
id_number=scsi_id, id_number=scsi_id,
unit_number=unit, unit_number=unit,
)
) )
)
return response(error=True, message=process["msg"]) return response(error=True, message=process["msg"])
@ -725,8 +727,13 @@ def detach():
unit = request.form.get("unit") unit = request.form.get("unit")
process = ractl_cmd.detach_by_id(scsi_id, unit) process = ractl_cmd.detach_by_id(scsi_id, unit)
if process["status"]: if process["status"]:
return response(message=_("Detached SCSI ID %(id_number)s LUN %(unit_number)s", return response(
id_number=scsi_id, unit_number=unit)) message=_(
"Detached SCSI ID %(id_number)s LUN %(unit_number)s",
id_number=scsi_id,
unit_number=unit,
)
)
return response(error=True, message=process["msg"]) return response(error=True, message=process["msg"])
@ -742,8 +749,13 @@ def eject():
process = ractl_cmd.eject_by_id(scsi_id, unit) process = ractl_cmd.eject_by_id(scsi_id, unit)
if process["status"]: if process["status"]:
return response(message=_("Ejected SCSI ID %(id_number)s LUN %(unit_number)s", return response(
id_number=scsi_id, unit_number=unit)) message=_(
"Ejected SCSI ID %(id_number)s LUN %(unit_number)s",
id_number=scsi_id,
unit_number=unit,
)
)
return response(error=True, message=process["msg"]) return response(error=True, message=process["msg"])
@ -758,7 +770,7 @@ def device_info():
return response( return response(
template="deviceinfo.html", template="deviceinfo.html",
devices=process["device_list"], devices=process["device_list"],
) )
return response(error=True, message=_("No devices attached")) return response(error=True, message=_("No devices attached"))
@ -793,7 +805,9 @@ def release_id():
process = ractl_cmd.reserve_scsi_ids(reserved_ids) process = ractl_cmd.reserve_scsi_ids(reserved_ids)
if process["status"]: if process["status"]:
RESERVATIONS[int(scsi_id)] = "" RESERVATIONS[int(scsi_id)] = ""
return response(message=_("Released the reservation for SCSI ID %(id_number)s", id_number=scsi_id)) return response(
message=_("Released the reservation for SCSI ID %(id_number)s", id_number=scsi_id)
)
return response(error=True, message=process["msg"]) return response(error=True, message=process["msg"])
@ -868,7 +882,7 @@ def download_to_iso():
else: else:
return response( return response(
error=True, error=True,
message=_("%(iso_type)s is not a valid CD-ROM format.", iso_type=iso_type) message=_("%(iso_type)s is not a valid CD-ROM format.", iso_type=iso_type),
) )
process = file_cmd.download_file_to_iso(url, *iso_args) process = file_cmd.download_file_to_iso(url, *iso_args)
@ -883,10 +897,10 @@ def download_to_iso():
) )
process_attach = ractl_cmd.attach_device( process_attach = ractl_cmd.attach_device(
scsi_id, scsi_id,
device_type="SCCD", device_type="SCCD",
params={"file": process["file_name"]}, params={"file": process["file_name"]},
) )
process_attach = ReturnCodeMapper.add_msg(process_attach) process_attach = ReturnCodeMapper.add_msg(process_attach)
if process_attach["status"]: if process_attach["status"]:
return response( return response(
@ -965,7 +979,7 @@ def create_file():
Creates an empty image file in the images dir Creates an empty image file in the images dir
""" """
file_name = Path(request.form.get("file_name")) file_name = Path(request.form.get("file_name"))
size = (int(request.form.get("size")) * 1024 * 1024) size = int(request.form.get("size")) * 1024 * 1024
file_type = request.form.get("type") file_type = request.form.get("type")
drive_name = request.form.get("drive_name") drive_name = request.form.get("drive_name")
drive_format = request.form.get("drive_format") drive_format = request.form.get("drive_format")
@ -984,11 +998,11 @@ def create_file():
if drive_format: if drive_format:
volume_name = f"HD {size / 1024 / 1024:0.0f}M" volume_name = f"HD {size / 1024 / 1024:0.0f}M"
known_formats = [ known_formats = [
"Lido 7.56", "Lido 7.56",
"SpeedTools 3.6", "SpeedTools 3.6",
"FAT16", "FAT16",
"FAT32", "FAT32",
] ]
message_postfix = f" ({drive_format})" message_postfix = f" ({drive_format})"
if drive_format not in known_formats: if drive_format not in known_formats:
@ -997,7 +1011,7 @@ def create_file():
message=_( message=_(
"%(drive_format)s is not a valid hard disk format.", "%(drive_format)s is not a valid hard disk format.",
drive_format=drive_format, drive_format=drive_format,
) ),
) )
elif drive_format.startswith("FAT"): elif drive_format.startswith("FAT"):
if drive_format == "FAT16": if drive_format == "FAT16":
@ -1010,7 +1024,7 @@ def create_file():
message=_( message=_(
"%(drive_format)s is not a valid hard disk format.", "%(drive_format)s is not a valid hard disk format.",
drive_format=drive_format, drive_format=drive_format,
) ),
) )
process = file_cmd.partition_disk(full_file_name, volume_name, "FAT") process = file_cmd.partition_disk(full_file_name, volume_name, "FAT")
@ -1018,11 +1032,11 @@ def create_file():
return response(error=True, message=process["msg"]) return response(error=True, message=process["msg"])
process = file_cmd.format_fat( process = file_cmd.format_fat(
full_file_name, full_file_name,
# FAT volume labels are max 11 chars # FAT volume labels are max 11 chars
volume_name[:11], volume_name[:11],
fat_size, fat_size,
) )
if not process["status"]: if not process["status"]:
return response(error=True, message=process["msg"]) return response(error=True, message=process["msg"])
@ -1033,19 +1047,16 @@ def create_file():
return response(error=True, message=process["msg"]) return response(error=True, message=process["msg"])
process = file_cmd.format_hfs( process = file_cmd.format_hfs(
full_file_name, full_file_name,
volume_name, volume_name,
driver_base_path / Path(drive_format.replace(" ", "-") + ".img"), driver_base_path / Path(drive_format.replace(" ", "-") + ".img"),
) )
if not process["status"]: if not process["status"]:
return response(error=True, message=process["msg"]) return response(error=True, message=process["msg"])
# Creating the drive properties file, if one is chosen # Creating the drive properties file, if one is chosen
if drive_name: if drive_name:
properties = get_properties_by_drive_name( properties = get_properties_by_drive_name(APP.config["RASCSI_DRIVE_PROPERTIES"], drive_name)
APP.config["RASCSI_DRIVE_PROPERTIES"],
drive_name
)
if properties: if properties:
prop_file_name = f"{full_file_name}.{PROPERTIES_SUFFIX}" prop_file_name = f"{full_file_name}.{PROPERTIES_SUFFIX}"
process = file_cmd.write_drive_properties(prop_file_name, properties) process = file_cmd.write_drive_properties(prop_file_name, properties)
@ -1221,26 +1232,23 @@ def extract_image():
safe_path = is_safe_path(archive_file) safe_path = is_safe_path(archive_file)
if not safe_path["status"]: if not safe_path["status"]:
return response(error=True, message=safe_path["msg"]) return response(error=True, message=safe_path["msg"])
extract_result = file_cmd.extract_image( extract_result = file_cmd.extract_image(str(archive_file), archive_members)
str(archive_file),
archive_members
)
if extract_result["return_code"] == ReturnCodes.EXTRACTIMAGE_SUCCESS: if extract_result["return_code"] == ReturnCodes.EXTRACTIMAGE_SUCCESS:
for properties_file in extract_result["properties_files_moved"]: for properties_file in extract_result["properties_files_moved"]:
if properties_file["status"]: if properties_file["status"]:
logging.info( logging.info(
"Properties file %s moved to %s", "Properties file %s moved to %s",
properties_file["name"], properties_file["name"],
CFG_DIR, CFG_DIR,
) )
else: else:
logging.warning( logging.warning(
"Failed to move properties file %s to %s", "Failed to move properties file %s to %s",
properties_file["name"], properties_file["name"],
CFG_DIR, CFG_DIR,
) )
return response(message=ReturnCodeMapper.add_msg(extract_result).get("msg")) return response(message=ReturnCodeMapper.add_msg(extract_result).get("msg"))
@ -1300,28 +1308,28 @@ if __name__ == "__main__":
default=8080, default=8080,
action="store", action="store",
help="Port number the web server will run on", help="Port number the web server will run on",
) )
parser.add_argument( parser.add_argument(
"--password", "--password",
type=str, type=str,
default="", default="",
action="store", action="store",
help="Token password string for authenticating with RaSCSI", help="Token password string for authenticating with RaSCSI",
) )
parser.add_argument( parser.add_argument(
"--rascsi-host", "--rascsi-host",
type=str, type=str,
default="localhost", default="localhost",
action="store", action="store",
help="RaSCSI host. Default: localhost", help="RaSCSI host. Default: localhost",
) )
parser.add_argument( parser.add_argument(
"--rascsi-port", "--rascsi-port",
type=int, type=int,
default=6868, default=6868,
action="store", action="store",
help="RaSCSI port. Default: 6868", help="RaSCSI port. Default: 6868",
) )
parser.add_argument( parser.add_argument(
"--log-level", "--log-level",
type=str, type=str,
@ -1329,12 +1337,12 @@ if __name__ == "__main__":
action="store", action="store",
help="Log level for Web UI. Default: warning", help="Log level for Web UI. Default: warning",
choices=["debug", "info", "warning", "error", "critical"], choices=["debug", "info", "warning", "error", "critical"],
) )
parser.add_argument( parser.add_argument(
"--dev-mode", "--dev-mode",
action="store_true", action="store_true",
help="Run in development mode" help="Run in development mode",
) )
arguments = parser.parse_args() arguments = parser.parse_args()
APP.config["RASCSI_TOKEN"] = arguments.password APP.config["RASCSI_TOKEN"] = arguments.password
@ -1357,14 +1365,17 @@ if __name__ == "__main__":
APP.config["RASCSI_DRIVE_PROPERTIES"] = [] APP.config["RASCSI_DRIVE_PROPERTIES"] = []
logging.warning("Could not read drive properties from %s", DRIVE_PROPERTIES_FILE) logging.warning("Could not read drive properties from %s", DRIVE_PROPERTIES_FILE)
logging.basicConfig(stream=sys.stdout, logging.basicConfig(
format="%(asctime)s %(levelname)s %(filename)s:%(lineno)s %(message)s", stream=sys.stdout,
level=arguments.log_level.upper()) format="%(asctime)s %(levelname)s %(filename)s:%(lineno)s %(message)s",
level=arguments.log_level.upper(),
)
if arguments.dev_mode: if arguments.dev_mode:
print("Running rascsi-web in development mode ...") print("Running rascsi-web in development mode ...")
APP.debug = True APP.debug = True
from werkzeug.debug import DebuggedApplication from werkzeug.debug import DebuggedApplication
try: try:
bjoern.run(DebuggedApplication(APP, evalex=False), "0.0.0.0", arguments.port) bjoern.run(DebuggedApplication(APP, evalex=False), "0.0.0.0", arguments.port)
except KeyboardInterrupt: except KeyboardInterrupt:

View File

@ -14,6 +14,7 @@ from werkzeug.utils import secure_filename
from rascsi.sys_cmds import SysCmds from rascsi.sys_cmds import SysCmds
def get_valid_scsi_ids(devices, reserved_ids): def get_valid_scsi_ids(devices, reserved_ids):
""" """
Takes a list of (dict)s devices, and list of (int)s reserved_ids. Takes a list of (dict)s devices, and list of (int)s reserved_ids.
@ -43,7 +44,7 @@ def get_valid_scsi_ids(devices, reserved_ids):
"valid_ids": valid_ids, "valid_ids": valid_ids,
"occupied_ids": occupied_ids, "occupied_ids": occupied_ids,
"recommended_id": recommended_id, "recommended_id": recommended_id,
} }
def sort_and_format_devices(devices): def sort_and_format_devices(devices):
@ -180,7 +181,8 @@ def format_drive_properties(drive_properties):
"cd_conf": cd_conf, "cd_conf": cd_conf,
"rm_conf": rm_conf, "rm_conf": rm_conf,
"mo_conf": mo_conf, "mo_conf": mo_conf,
} }
def get_properties_by_drive_name(drives, drive_name): def get_properties_by_drive_name(drives, drive_name):
""" """
@ -198,11 +200,12 @@ def get_properties_by_drive_name(drives, drive_name):
"revision": drive["revision"], "revision": drive["revision"],
"block_size": drive["block_size"], "block_size": drive["block_size"],
"size": drive["size"], "size": drive["size"],
} }
logging.error("Properties for drive '%s' does not exist in database", drive_name) logging.error("Properties for drive '%s' does not exist in database", drive_name)
return False return False
def auth_active(group): def auth_active(group):
""" """
Inspects if the group defined in (str) group exists on the system. Inspects if the group defined in (str) group exists on the system.
@ -212,9 +215,9 @@ def auth_active(group):
groups = [g.gr_name for g in getgrall()] groups = [g.gr_name for g in getgrall()]
if group in groups: if group in groups:
return { return {
"status": True, "status": True,
"msg": _("You must log in to use this function"), "msg": _("You must log in to use this function"),
} }
return {"status": False, "msg": ""} return {"status": False, "msg": ""}
@ -272,7 +275,7 @@ def upload_with_dropzonejs(image_dir):
file_name = secure_filename(file_object.filename) file_name = secure_filename(file_object.filename)
save_path = path.join(image_dir, file_name) save_path = path.join(image_dir, file_name)
current_chunk = int(request.form['dzchunkindex']) current_chunk = int(request.form["dzchunkindex"])
# Makes sure not to overwrite an existing file, # Makes sure not to overwrite an existing file,
# but continues writing to a file transfer in progress # but continues writing to a file transfer in progress
@ -307,21 +310,21 @@ def browser_supports_modern_themes():
return False return False
user_agent = user_agent_parser.Parse(user_agent_string) user_agent = user_agent_parser.Parse(user_agent_string)
if not user_agent['user_agent']['family']: if not user_agent["user_agent"]["family"]:
return False return False
# (family, minimum version) # (family, minimum version)
supported_browsers = [ supported_browsers = [
('Safari', 14), ("Safari", 14),
('Chrome', 100), ("Chrome", 100),
('Firefox', 100), ("Firefox", 100),
('Edge', 100), ("Edge", 100),
('Mobile Safari', 14), ("Mobile Safari", 14),
('Chrome Mobile', 100), ("Chrome Mobile", 100),
] ]
current_ua_family = user_agent['user_agent']['family'] current_ua_family = user_agent["user_agent"]["family"]
current_ua_version = float(user_agent['user_agent']['major']) current_ua_version = float(user_agent["user_agent"]["major"])
logging.info(f"Identified browser as family={current_ua_family}, version={current_ua_version}") logging.info(f"Identified browser as family={current_ua_family}, version={current_ua_version}")
for supported_browser, supported_version in supported_browsers: for supported_browser, supported_version in supported_browsers: