Merge branch 'sim-mgm-update' into serialization-prototyping
All checks were successful
Rust/sat-rs/pipeline/head This commit looks good

This commit is contained in:
Robin Müller 2024-05-25 13:09:25 +02:00
commit a6d9bee5df
39 changed files with 2980 additions and 690 deletions

View File

@ -27,6 +27,9 @@ serde_json = "1"
path = "../satrs" path = "../satrs"
features = ["test_util"] features = ["test_util"]
[dependencies.satrs-minisim]
path = "../satrs-minisim"
[dependencies.satrs-mib] [dependencies.satrs-mib]
version = "0.1.1" version = "0.1.1"
path = "../satrs-mib" path = "../satrs-mib"

View File

@ -48,16 +48,17 @@ It is recommended to use a virtual environment to do this. To set up one in the
you can use `python3 -m venv venv` on Unix systems or `py -m venv venv` on Windows systems. you can use `python3 -m venv venv` on Unix systems or `py -m venv venv` on Windows systems.
After doing this, you can check the [venv tutorial](https://docs.python.org/3/tutorial/venv.html) After doing this, you can check the [venv tutorial](https://docs.python.org/3/tutorial/venv.html)
on how to activate the environment and then use the following command to install the required on how to activate the environment and then use the following command to install the required
dependency: dependency interactively:
```sh ```sh
pip install -r requirements.txt pip install -e .
``` ```
Alternatively, if you would like to use the GUI functionality provided by `tmtccmd`, you can also Alternatively, if you would like to use the GUI functionality provided by `tmtccmd`, you can also
install it manually with install it manually with
```sh ```sh
pip install -e .
pip install tmtccmd[gui] pip install tmtccmd[gui]
``` ```

View File

@ -1,3 +1,4 @@
/tmtc_conf.json
__pycache__ __pycache__
/venv /venv
@ -7,3 +8,136 @@ __pycache__
/seqcnt.txt /seqcnt.txt
/.tmtc-history.txt /.tmtc-history.txt
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# PyCharm
.idea

View File

@ -3,27 +3,17 @@
import logging import logging
import sys import sys
import time import time
from typing import Any, Optional
from prompt_toolkit.history import History
from prompt_toolkit.history import FileHistory
from spacepackets.ccsds import PacketId, PacketType
import tmtccmd import tmtccmd
from spacepackets.ecss import PusTelemetry, PusVerificator from spacepackets.ecss import PusVerificator
from spacepackets.ecss.pus_17_test import Service17Tm
from spacepackets.ecss.pus_1_verification import UnpackParams, Service1Tm
from spacepackets.ccsds.time import CdsShortTimestamp
from tmtccmd import TcHandlerBase, ProcedureParamsWrapper from tmtccmd import ProcedureParamsWrapper
from tmtccmd.core.base import BackendRequest from tmtccmd.core.base import BackendRequest
from tmtccmd.pus import VerificationWrapper from tmtccmd.pus import VerificationWrapper
from tmtccmd.tmtc import CcsdsTmHandler, GenericApidHandlerBase from tmtccmd.tmtc import CcsdsTmHandler
from tmtccmd.com import ComInterface
from tmtccmd.config import ( from tmtccmd.config import (
CmdTreeNode,
default_json_path, default_json_path,
SetupParams, SetupParams,
HookBase,
params_to_procedure_conversion, params_to_procedure_conversion,
) )
from tmtccmd.config import PreArgsParsingWrapper, SetupWrapper from tmtccmd.config import PreArgsParsingWrapper, SetupWrapper
@ -33,193 +23,20 @@ from tmtccmd.logging.pus import (
RawTmtcTimedLogWrapper, RawTmtcTimedLogWrapper,
TimedLogWhen, TimedLogWhen,
) )
from tmtccmd.tmtc import ( from spacepackets.seqcount import PusFileSeqCountProvider
TcQueueEntryType,
ProcedureWrapper,
TcProcedureType,
FeedWrapper,
SendCbParams,
DefaultPusQueueHelper,
QueueWrapper,
)
from spacepackets.seqcount import FileSeqCountProvider, PusFileSeqCountProvider
from tmtccmd.util.obj_id import ObjectIdDictT
import pus_tc from pytmtc.config import SatrsConfigHook
from common import Apid, EventU32 from pytmtc.pus_tc import TcHandler
from pytmtc.pus_tm import PusHandler
_LOGGER = logging.getLogger() _LOGGER = logging.getLogger()
class SatRsConfigHook(HookBase):
def __init__(self, json_cfg_path: str):
super().__init__(json_cfg_path=json_cfg_path)
def get_communication_interface(self, com_if_key: str) -> Optional[ComInterface]:
from tmtccmd.config.com import (
create_com_interface_default,
create_com_interface_cfg_default,
)
assert self.cfg_path is not None
packet_id_list = []
for apid in Apid:
packet_id_list.append(PacketId(PacketType.TM, True, apid))
cfg = create_com_interface_cfg_default(
com_if_key=com_if_key,
json_cfg_path=self.cfg_path,
space_packet_ids=packet_id_list,
)
assert cfg is not None
return create_com_interface_default(cfg)
def get_command_definitions(self) -> CmdTreeNode:
"""This function should return the root node of the command definition tree."""
return pus_tc.create_cmd_definition_tree()
def get_cmd_history(self) -> Optional[History]:
"""Optionlly return a history class for the past command paths which will be used
when prompting a command path from the user in CLI mode."""
return FileHistory(".tmtc-history.txt")
def get_object_ids(self) -> ObjectIdDictT:
from tmtccmd.config.objects import get_core_object_ids
return get_core_object_ids()
class PusHandler(GenericApidHandlerBase):
def __init__(
self,
file_logger: logging.Logger,
verif_wrapper: VerificationWrapper,
raw_logger: RawTmtcTimedLogWrapper,
):
super().__init__(None)
self.file_logger = file_logger
self.raw_logger = raw_logger
self.verif_wrapper = verif_wrapper
def handle_tm(self, apid: int, packet: bytes, _user_args: Any):
try:
pus_tm = PusTelemetry.unpack(
packet, timestamp_len=CdsShortTimestamp.TIMESTAMP_SIZE
)
except ValueError as e:
_LOGGER.warning("Could not generate PUS TM object from raw data")
_LOGGER.warning(f"Raw Packet: [{packet.hex(sep=',')}], REPR: {packet!r}")
raise e
service = pus_tm.service
if service == 1:
tm_packet = Service1Tm.unpack(
data=packet, params=UnpackParams(CdsShortTimestamp.TIMESTAMP_SIZE, 1, 2)
)
res = self.verif_wrapper.add_tm(tm_packet)
if res is None:
_LOGGER.info(
f"Received Verification TM[{tm_packet.service}, {tm_packet.subservice}] "
f"with Request ID {tm_packet.tc_req_id.as_u32():#08x}"
)
_LOGGER.warning(
f"No matching telecommand found for {tm_packet.tc_req_id}"
)
else:
self.verif_wrapper.log_to_console(tm_packet, res)
self.verif_wrapper.log_to_file(tm_packet, res)
elif service == 3:
_LOGGER.info("No handling for HK packets implemented")
_LOGGER.info(f"Raw packet: 0x[{packet.hex(sep=',')}]")
pus_tm = PusTelemetry.unpack(
packet, timestamp_len=CdsShortTimestamp.TIMESTAMP_SIZE
)
if pus_tm.subservice == 25:
if len(pus_tm.source_data) < 8:
raise ValueError("No addressable ID in HK packet")
json_str = pus_tm.source_data[8:]
_LOGGER.info(json_str)
elif service == 5:
tm_packet = PusTelemetry.unpack(
packet, timestamp_len=CdsShortTimestamp.TIMESTAMP_SIZE
)
src_data = tm_packet.source_data
event_u32 = EventU32.unpack(src_data)
_LOGGER.info(
f"Received event packet. Source APID: {Apid(tm_packet.apid)!r}, Event: {event_u32}"
)
if event_u32.group_id == 0 and event_u32.unique_id == 0:
_LOGGER.info("Received test event")
elif service == 17:
tm_packet = Service17Tm.unpack(
packet, timestamp_len=CdsShortTimestamp.TIMESTAMP_SIZE
)
if tm_packet.subservice == 2:
self.file_logger.info("Received Ping Reply TM[17,2]")
_LOGGER.info("Received Ping Reply TM[17,2]")
else:
self.file_logger.info(
f"Received Test Packet with unknown subservice {tm_packet.subservice}"
)
_LOGGER.info(
f"Received Test Packet with unknown subservice {tm_packet.subservice}"
)
else:
_LOGGER.info(
f"The service {service} is not implemented in Telemetry Factory"
)
tm_packet = PusTelemetry.unpack(
packet, timestamp_len=CdsShortTimestamp.TIMESTAMP_SIZE
)
self.raw_logger.log_tm(pus_tm)
class TcHandler(TcHandlerBase):
def __init__(
self,
seq_count_provider: FileSeqCountProvider,
verif_wrapper: VerificationWrapper,
):
super(TcHandler, self).__init__()
self.seq_count_provider = seq_count_provider
self.verif_wrapper = verif_wrapper
self.queue_helper = DefaultPusQueueHelper(
queue_wrapper=QueueWrapper.empty(),
tc_sched_timestamp_len=CdsShortTimestamp.TIMESTAMP_SIZE,
seq_cnt_provider=seq_count_provider,
pus_verificator=self.verif_wrapper.pus_verificator,
default_pus_apid=None,
)
def send_cb(self, send_params: SendCbParams):
entry_helper = send_params.entry
if entry_helper.is_tc:
if entry_helper.entry_type == TcQueueEntryType.PUS_TC:
pus_tc_wrapper = entry_helper.to_pus_tc_entry()
raw_tc = pus_tc_wrapper.pus_tc.pack()
_LOGGER.info(f"Sending {pus_tc_wrapper.pus_tc}")
send_params.com_if.send(raw_tc)
elif entry_helper.entry_type == TcQueueEntryType.LOG:
log_entry = entry_helper.to_log_entry()
_LOGGER.info(log_entry.log_str)
def queue_finished_cb(self, info: ProcedureWrapper):
if info.proc_type == TcProcedureType.TREE_COMMANDING:
def_proc = info.to_tree_commanding_procedure()
_LOGGER.info(f"Queue handling finished for command {def_proc.cmd_path}")
def feed_cb(self, info: ProcedureWrapper, wrapper: FeedWrapper):
q = self.queue_helper
q.queue_wrapper = wrapper.queue_wrapper
if info.proc_type == TcProcedureType.TREE_COMMANDING:
def_proc = info.to_tree_commanding_procedure()
assert def_proc.cmd_path is not None
pus_tc.pack_pus_telecommands(q, def_proc.cmd_path)
def main(): def main():
add_colorlog_console_logger(_LOGGER) add_colorlog_console_logger(_LOGGER)
tmtccmd.init_printout(False) tmtccmd.init_printout(False)
hook_obj = SatRsConfigHook(json_cfg_path=default_json_path()) hook_obj = SatrsConfigHook(json_cfg_path=default_json_path())
parser_wrapper = PreArgsParsingWrapper() parser_wrapper = PreArgsParsingWrapper()
parser_wrapper.create_default_parent_parser() parser_wrapper.create_default_parent_parser()
parser_wrapper.create_default_parser() parser_wrapper.create_default_parser()

View File

@ -0,0 +1,27 @@
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"
[project]
name = "pytmtc"
description = "Python TMTC client for OPS-SAT"
readme = "README.md"
version = "0.1.0"
requires-python = ">=3.8"
authors = [
{name = "Robin Mueller", email = "robin.mueller.m@gmail.com"},
]
dependencies = [
"tmtccmd~=8.0",
"pydantic~=2.7"
]
[tool.setuptools.packages]
find = {}
[tool.ruff]
extend-exclude = ["archive"]
[tool.ruff.lint]
ignore = ["E501"]
[tool.ruff.lint.extend-per-file-ignores]
"__init__.py" = ["F401"]

View File

@ -0,0 +1,47 @@
from typing import Optional
from prompt_toolkit.history import FileHistory, History
from spacepackets.ccsds import PacketId, PacketType
from tmtccmd import HookBase
from tmtccmd.com import ComInterface
from tmtccmd.config import CmdTreeNode
from tmtccmd.util.obj_id import ObjectIdDictT
from pytmtc.common import Apid
from pytmtc.pus_tc import create_cmd_definition_tree
class SatrsConfigHook(HookBase):
def __init__(self, json_cfg_path: str):
super().__init__(json_cfg_path=json_cfg_path)
def get_communication_interface(self, com_if_key: str) -> Optional[ComInterface]:
from tmtccmd.config.com import (
create_com_interface_default,
create_com_interface_cfg_default,
)
assert self.cfg_path is not None
packet_id_list = []
for apid in Apid:
packet_id_list.append(PacketId(PacketType.TM, True, apid))
cfg = create_com_interface_cfg_default(
com_if_key=com_if_key,
json_cfg_path=self.cfg_path,
space_packet_ids=packet_id_list,
)
assert cfg is not None
return create_com_interface_default(cfg)
def get_command_definitions(self) -> CmdTreeNode:
"""This function should return the root node of the command definition tree."""
return create_cmd_definition_tree()
def get_cmd_history(self) -> Optional[History]:
"""Optionlly return a history class for the past command paths which will be used
when prompting a command path from the user in CLI mode."""
return FileHistory(".tmtc-history.txt")
def get_object_ids(self) -> ObjectIdDictT:
from tmtccmd.config.objects import get_core_object_ids
return get_core_object_ids()

View File

@ -0,0 +1,42 @@
import logging
import struct
from spacepackets.ecss.pus_3_hk import Subservice
from spacepackets.ecss import PusTm
from pytmtc.common import AcsId, Apid
from pytmtc.mgms import handle_mgm_hk_report
_LOGGER = logging.getLogger(__name__)
def handle_hk_packet(pus_tm: PusTm):
if len(pus_tm.source_data) < 4:
raise ValueError("no unique ID in HK packet")
unique_id = struct.unpack("!I", pus_tm.source_data[:4])[0]
if (
pus_tm.subservice == Subservice.TM_HK_REPORT
or pus_tm.subservice == Subservice.TM_DIAGNOSTICS_REPORT
):
if len(pus_tm.source_data) < 8:
raise ValueError("no set ID in HK packet")
set_id = struct.unpack("!I", pus_tm.source_data[4:8])[0]
handle_hk_report(pus_tm, unique_id, set_id)
_LOGGER.warning(
f"handling for HK packet with subservice {pus_tm.subservice} not implemented yet"
)
def handle_hk_report(pus_tm: PusTm, unique_id: int, set_id: int):
hk_data = pus_tm.source_data[8:]
if pus_tm.apid == Apid.ACS:
if unique_id == AcsId.MGM_0:
handle_mgm_hk_report(pus_tm, set_id, hk_data)
else:
_LOGGER.warning(
f"handling for HK report with unique ID {unique_id} not implemented yet"
)
else:
_LOGGER.warning(
f"handling for HK report with apid {pus_tm.apid} not implemented yet"
)

View File

@ -0,0 +1,16 @@
import struct
from spacepackets.ecss import PusService, PusTc
from spacepackets.ecss.pus_3_hk import Subservice
def create_request_one_shot_hk_cmd(apid: int, unique_id: int, set_id: int) -> PusTc:
app_data = bytearray()
app_data.extend(struct.pack("!I", unique_id))
app_data.extend(struct.pack("!I", set_id))
return PusTc(
service=PusService.S3_HOUSEKEEPING,
subservice=Subservice.TC_GENERATE_ONE_PARAMETER_REPORT,
apid=apid,
app_data=app_data,
)

View File

@ -0,0 +1,45 @@
import logging
import struct
import enum
from typing import List
from spacepackets.ecss import PusTm
from tmtccmd.tmtc import DefaultPusQueueHelper
from pytmtc.common import AcsId, Apid
from pytmtc.hk_common import create_request_one_shot_hk_cmd
from pytmtc.mode import handle_set_mode_cmd
_LOGGER = logging.getLogger(__name__)
class SetId(enum.IntEnum):
SENSOR_SET = 0
def create_mgm_cmds(q: DefaultPusQueueHelper, cmd_path: List[str]):
assert len(cmd_path) >= 3
if cmd_path[2] == "hk":
if cmd_path[3] == "one_shot_hk":
q.add_log_cmd("Sending HK one shot request")
q.add_pus_tc(
create_request_one_shot_hk_cmd(Apid.ACS, AcsId.MGM_0, SetId.SENSOR_SET)
)
if cmd_path[2] == "mode":
if cmd_path[3] == "set_mode":
handle_set_mode_cmd(q, "MGM 0", cmd_path[4], Apid.ACS, AcsId.MGM_0)
def handle_mgm_hk_report(pus_tm: PusTm, set_id: int, hk_data: bytes):
if set_id == SetId.SENSOR_SET:
if len(hk_data) != 13:
raise ValueError(f"invalid HK data length, expected 13, got {len(hk_data)}")
data_valid = hk_data[0]
mgm_x = struct.unpack("!f", hk_data[1:5])[0]
mgm_y = struct.unpack("!f", hk_data[5:9])[0]
mgm_z = struct.unpack("!f", hk_data[9:13])[0]
_LOGGER.info(
f"received MGM HK set in uT: Valid {data_valid} X {mgm_x} Y {mgm_y} Z {mgm_z}"
)
pass

View File

@ -0,0 +1,31 @@
import struct
from spacepackets.ecss import PusTc
from tmtccmd.pus.s200_fsfw_mode import Mode, Subservice
from tmtccmd.tmtc import DefaultPusQueueHelper
def create_set_mode_cmd(apid: int, unique_id: int, mode: int, submode: int) -> PusTc:
app_data = bytearray()
app_data.extend(struct.pack("!I", unique_id))
app_data.extend(struct.pack("!I", mode))
app_data.extend(struct.pack("!H", submode))
return PusTc(
service=200,
subservice=Subservice.TC_MODE_COMMAND,
apid=apid,
app_data=app_data,
)
def handle_set_mode_cmd(
q: DefaultPusQueueHelper, target_str: str, mode_str: str, apid: int, unique_id: int
):
if mode_str == "off":
q.add_log_cmd(f"Sending Mode OFF to {target_str}")
q.add_pus_tc(create_set_mode_cmd(apid, unique_id, Mode.OFF, 0))
elif mode_str == "on":
q.add_log_cmd(f"Sending Mode ON to {target_str}")
q.add_pus_tc(create_set_mode_cmd(apid, unique_id, Mode.ON, 0))
elif mode_str == "normal":
q.add_log_cmd(f"Sending Mode NORMAL to {target_str}")
q.add_pus_tc(create_set_mode_cmd(apid, unique_id, Mode.NORMAL, 0))

View File

@ -1,34 +1,70 @@
import datetime import datetime
import struct
import logging import logging
from spacepackets.ccsds import CdsShortTimestamp from spacepackets.ccsds import CdsShortTimestamp
from spacepackets.ecss import PusTelecommand from spacepackets.ecss import PusTelecommand
from spacepackets.seqcount import FileSeqCountProvider
from tmtccmd import ProcedureWrapper, TcHandlerBase
from tmtccmd.config import CmdTreeNode from tmtccmd.config import CmdTreeNode
from tmtccmd.pus.tc.s200_fsfw_mode import Mode from tmtccmd.pus import VerificationWrapper
from tmtccmd.tmtc import DefaultPusQueueHelper from tmtccmd.tmtc import (
DefaultPusQueueHelper,
FeedWrapper,
QueueWrapper,
SendCbParams,
TcProcedureType,
TcQueueEntryType,
)
from tmtccmd.pus.s11_tc_sched import create_time_tagged_cmd from tmtccmd.pus.s11_tc_sched import create_time_tagged_cmd
from tmtccmd.pus.s200_fsfw_mode import Subservice as ModeSubservice
from common import AcsId, Apid from pytmtc.common import Apid
from pytmtc.mgms import create_mgm_cmds
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
def create_set_mode_cmd( class TcHandler(TcHandlerBase):
apid: int, unique_id: int, mode: int, submode: int def __init__(
) -> PusTelecommand: self,
app_data = bytearray() seq_count_provider: FileSeqCountProvider,
app_data.extend(struct.pack("!I", unique_id)) verif_wrapper: VerificationWrapper,
app_data.extend(struct.pack("!I", mode)) ):
app_data.extend(struct.pack("!H", submode)) super(TcHandler, self).__init__()
return PusTelecommand( self.seq_count_provider = seq_count_provider
service=200, self.verif_wrapper = verif_wrapper
subservice=ModeSubservice.TC_MODE_COMMAND, self.queue_helper = DefaultPusQueueHelper(
apid=apid, queue_wrapper=QueueWrapper.empty(),
app_data=app_data, tc_sched_timestamp_len=CdsShortTimestamp.TIMESTAMP_SIZE,
seq_cnt_provider=seq_count_provider,
pus_verificator=self.verif_wrapper.pus_verificator,
default_pus_apid=None,
) )
def send_cb(self, send_params: SendCbParams):
entry_helper = send_params.entry
if entry_helper.is_tc:
if entry_helper.entry_type == TcQueueEntryType.PUS_TC:
pus_tc_wrapper = entry_helper.to_pus_tc_entry()
raw_tc = pus_tc_wrapper.pus_tc.pack()
_LOGGER.info(f"Sending {pus_tc_wrapper.pus_tc}")
send_params.com_if.send(raw_tc)
elif entry_helper.entry_type == TcQueueEntryType.LOG:
log_entry = entry_helper.to_log_entry()
_LOGGER.info(log_entry.log_str)
def queue_finished_cb(self, info: ProcedureWrapper):
if info.proc_type == TcProcedureType.TREE_COMMANDING:
def_proc = info.to_tree_commanding_procedure()
_LOGGER.info(f"Queue handling finished for command {def_proc.cmd_path}")
def feed_cb(self, info: ProcedureWrapper, wrapper: FeedWrapper):
q = self.queue_helper
q.queue_wrapper = wrapper.queue_wrapper
if info.proc_type == TcProcedureType.TREE_COMMANDING:
def_proc = info.to_tree_commanding_procedure()
assert def_proc.cmd_path is not None
pack_pus_telecommands(q, def_proc.cmd_path)
def create_cmd_definition_tree() -> CmdTreeNode: def create_cmd_definition_tree() -> CmdTreeNode:
@ -112,32 +148,4 @@ def pack_pus_telecommands(q: DefaultPusQueueHelper, cmd_path: str):
if cmd_path_list[0] == "acs": if cmd_path_list[0] == "acs":
assert len(cmd_path_list) >= 2 assert len(cmd_path_list) >= 2
if cmd_path_list[1] == "mgms": if cmd_path_list[1] == "mgms":
assert len(cmd_path_list) >= 3 create_mgm_cmds(q, cmd_path_list)
if cmd_path_list[2] == "hk":
if cmd_path_list[3] == "one_shot_hk":
q.add_log_cmd("Sending HK one shot request")
# TODO: Fix
# q.add_pus_tc(
# create_request_one_hk_command(
# make_addressable_id(Apid.ACS, AcsId.MGM_SET)
# )
# )
if cmd_path_list[2] == "mode":
if cmd_path_list[3] == "set_mode":
handle_set_mode_cmd(
q, "MGM 0", cmd_path_list[4], Apid.ACS, AcsId.MGM_0
)
def handle_set_mode_cmd(
q: DefaultPusQueueHelper, target_str: str, mode_str: str, apid: int, unique_id: int
):
if mode_str == "off":
q.add_log_cmd(f"Sending Mode OFF to {target_str}")
q.add_pus_tc(create_set_mode_cmd(apid, unique_id, Mode.OFF, 0))
elif mode_str == "on":
q.add_log_cmd(f"Sending Mode ON to {target_str}")
q.add_pus_tc(create_set_mode_cmd(apid, unique_id, Mode.ON, 0))
elif mode_str == "normal":
q.add_log_cmd(f"Sending Mode NORMAL to {target_str}")
q.add_pus_tc(create_set_mode_cmd(apid, unique_id, Mode.NORMAL, 0))

View File

@ -0,0 +1,93 @@
import logging
from typing import Any
from spacepackets.ccsds.time import CdsShortTimestamp
from spacepackets.ecss import PusTm
from spacepackets.ecss.pus_17_test import Service17Tm
from spacepackets.ecss.pus_1_verification import Service1Tm, UnpackParams
from tmtccmd.logging.pus import RawTmtcTimedLogWrapper
from tmtccmd.pus import VerificationWrapper
from tmtccmd.tmtc import GenericApidHandlerBase
from pytmtc.common import Apid, EventU32
from pytmtc.hk import handle_hk_packet
_LOGGER = logging.getLogger(__name__)
class PusHandler(GenericApidHandlerBase):
def __init__(
self,
file_logger: logging.Logger,
verif_wrapper: VerificationWrapper,
raw_logger: RawTmtcTimedLogWrapper,
):
super().__init__(None)
self.file_logger = file_logger
self.raw_logger = raw_logger
self.verif_wrapper = verif_wrapper
def handle_tm(self, apid: int, packet: bytes, _user_args: Any):
try:
pus_tm = PusTm.unpack(
packet, timestamp_len=CdsShortTimestamp.TIMESTAMP_SIZE
)
except ValueError as e:
_LOGGER.warning("Could not generate PUS TM object from raw data")
_LOGGER.warning(f"Raw Packet: [{packet.hex(sep=',')}], REPR: {packet!r}")
raise e
service = pus_tm.service
if service == 1:
tm_packet = Service1Tm.unpack(
data=packet, params=UnpackParams(CdsShortTimestamp.TIMESTAMP_SIZE, 1, 2)
)
res = self.verif_wrapper.add_tm(tm_packet)
if res is None:
_LOGGER.info(
f"Received Verification TM[{tm_packet.service}, {tm_packet.subservice}] "
f"with Request ID {tm_packet.tc_req_id.as_u32():#08x}"
)
_LOGGER.warning(
f"No matching telecommand found for {tm_packet.tc_req_id}"
)
else:
self.verif_wrapper.log_to_console(tm_packet, res)
self.verif_wrapper.log_to_file(tm_packet, res)
elif service == 3:
pus_tm = PusTm.unpack(
packet, timestamp_len=CdsShortTimestamp.TIMESTAMP_SIZE
)
handle_hk_packet(pus_tm)
elif service == 5:
tm_packet = PusTm.unpack(
packet, timestamp_len=CdsShortTimestamp.TIMESTAMP_SIZE
)
src_data = tm_packet.source_data
event_u32 = EventU32.unpack(src_data)
_LOGGER.info(
f"Received event packet. Source APID: {Apid(tm_packet.apid)!r}, Event: {event_u32}"
)
if event_u32.group_id == 0 and event_u32.unique_id == 0:
_LOGGER.info("Received test event")
elif service == 17:
tm_packet = Service17Tm.unpack(
packet, timestamp_len=CdsShortTimestamp.TIMESTAMP_SIZE
)
if tm_packet.subservice == 2:
self.file_logger.info("Received Ping Reply TM[17,2]")
_LOGGER.info("Received Ping Reply TM[17,2]")
else:
self.file_logger.info(
f"Received Test Packet with unknown subservice {tm_packet.subservice}"
)
_LOGGER.info(
f"Received Test Packet with unknown subservice {tm_packet.subservice}"
)
else:
_LOGGER.info(
f"The service {service} is not implemented in Telemetry Factory"
)
tm_packet = PusTm.unpack(
packet, timestamp_len=CdsShortTimestamp.TIMESTAMP_SIZE
)
self.raw_logger.log_tm(pus_tm)

View File

@ -1,2 +1 @@
tmtccmd == 8.0.0rc2 .
# -e git+https://github.com/robamu-org/tmtccmd@97e5e51101a08b21472b3ddecc2063359f7e307a#egg=tmtccmd

View File

@ -1,38 +0,0 @@
from tmtccmd.config import OpCodeEntry, TmtcDefinitionWrapper, CoreServiceList
from tmtccmd.config.globals import get_default_tmtc_defs
from common import HkOpCodes
def tc_definitions() -> TmtcDefinitionWrapper:
defs = get_default_tmtc_defs()
srv_5 = OpCodeEntry()
srv_5.add("0", "Event Test")
defs.add_service(
name=CoreServiceList.SERVICE_5.value,
info="PUS Service 5 Event",
op_code_entry=srv_5,
)
srv_17 = OpCodeEntry()
srv_17.add("ping", "Ping Test")
srv_17.add("trigger_event", "Trigger Event")
defs.add_service(
name=CoreServiceList.SERVICE_17_ALT,
info="PUS Service 17 Test",
op_code_entry=srv_17,
)
srv_3 = OpCodeEntry()
srv_3.add(HkOpCodes.GENERATE_ONE_SHOT, "Generate AOCS one shot HK")
defs.add_service(
name=CoreServiceList.SERVICE_3,
info="PUS Service 3 Housekeeping",
op_code_entry=srv_3,
)
srv_11 = OpCodeEntry()
srv_11.add("0", "Scheduled TC Test")
defs.add_service(
name=CoreServiceList.SERVICE_11,
info="PUS Service 11 TC Scheduling",
op_code_entry=srv_11,
)
return defs

View File

View File

@ -0,0 +1,48 @@
from unittest import TestCase
from spacepackets.ccsds import CdsShortTimestamp
from tmtccmd.tmtc import DefaultPusQueueHelper, QueueEntryHelper
from tmtccmd.tmtc.queue import QueueWrapper
from pytmtc.config import SatrsConfigHook
from pytmtc.pus_tc import pack_pus_telecommands
class TestTcModules(TestCase):
def setUp(self):
self.hook = SatrsConfigHook(json_cfg_path="tmtc_conf.json")
self.queue_helper = DefaultPusQueueHelper(
queue_wrapper=QueueWrapper.empty(),
tc_sched_timestamp_len=CdsShortTimestamp.TIMESTAMP_SIZE,
seq_cnt_provider=None,
pus_verificator=None,
default_pus_apid=None,
)
def test_cmd_tree_creation_works_without_errors(self):
cmd_defs = self.hook.get_command_definitions()
self.assertIsNotNone(cmd_defs)
def test_ping_cmd_generation(self):
pack_pus_telecommands(self.queue_helper, "/test/ping")
queue_entry = self.queue_helper.queue_wrapper.queue.popleft()
entry_helper = QueueEntryHelper(queue_entry)
log_queue = entry_helper.to_log_entry()
self.assertEqual(log_queue.log_str, "Sending PUS ping telecommand")
queue_entry = self.queue_helper.queue_wrapper.queue.popleft()
entry_helper.entry = queue_entry
pus_tc_entry = entry_helper.to_pus_tc_entry()
self.assertEqual(pus_tc_entry.pus_tc.service, 17)
self.assertEqual(pus_tc_entry.pus_tc.subservice, 1)
def test_event_trigger_generation(self):
pack_pus_telecommands(self.queue_helper, "/test/trigger_event")
queue_entry = self.queue_helper.queue_wrapper.queue.popleft()
entry_helper = QueueEntryHelper(queue_entry)
log_queue = entry_helper.to_log_entry()
self.assertEqual(log_queue.log_str, "Triggering test event")
queue_entry = self.queue_helper.queue_wrapper.queue.popleft()
entry_helper.entry = queue_entry
pus_tc_entry = entry_helper.to_pus_tc_entry()
self.assertEqual(pus_tc_entry.pus_tc.service, 17)
self.assertEqual(pus_tc_entry.pus_tc.subservice, 128)

View File

@ -1,12 +1,18 @@
use derive_new::new; use derive_new::new;
use satrs::hk::{HkRequest, HkRequestVariant}; use satrs::hk::{HkRequest, HkRequestVariant};
use satrs::power::{PowerSwitchInfo, PowerSwitcherCommandSender};
use satrs::queue::{GenericSendError, GenericTargetedMessagingError}; use satrs::queue::{GenericSendError, GenericTargetedMessagingError};
use satrs::spacepackets::ecss::hk; use satrs_example::{DeviceMode, TimestampHelper};
use satrs::spacepackets::ecss::tm::{PusTmCreator, PusTmSecondaryHeader}; use satrs_minisim::acs::lis3mdl::{
use satrs::spacepackets::SpHeader; MgmLis3MdlReply, MgmLis3RawValues, FIELD_LSB_PER_GAUSS_4_SENS, GAUSS_TO_MICROTESLA_FACTOR,
use satrs_example::{DeviceMode, TimeStampHelper}; };
use satrs_minisim::acs::MgmRequestLis3Mdl;
use satrs_minisim::eps::PcduSwitch;
use satrs_minisim::{SerializableSimMsgPayload, SimReply, SimRequest};
use std::fmt::Debug;
use std::sync::mpsc::{self}; use std::sync::mpsc::{self};
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use std::time::Duration;
use satrs::mode::{ use satrs::mode::{
ModeAndSubmode, ModeError, ModeProvider, ModeReply, ModeRequest, ModeRequestHandler, ModeAndSubmode, ModeError, ModeProvider, ModeReply, ModeRequest, ModeRequestHandler,
@ -15,38 +21,106 @@ use satrs::pus::{EcssTmSender, PusTmVariant};
use satrs::request::{GenericMessage, MessageMetadata, UniqueApidTargetId}; use satrs::request::{GenericMessage, MessageMetadata, UniqueApidTargetId};
use satrs_example::config::components::PUS_MODE_SERVICE; use satrs_example::config::components::PUS_MODE_SERVICE;
use crate::hk::PusHkHelper;
use crate::pus::hk::{HkReply, HkReplyVariant}; use crate::pus::hk::{HkReply, HkReplyVariant};
use crate::requests::CompositeRequest; use crate::requests::CompositeRequest;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
const GAUSS_TO_MICROTESLA_FACTOR: f32 = 100.0; pub const NR_OF_DATA_AND_CFG_REGISTERS: usize = 14;
// This is the selected resoltion for the STM LIS3MDL device for the 4 Gauss sensitivity setting.
const FIELD_LSB_PER_GAUSS_4_SENS: f32 = 1.0 / 6842.0; // Register adresses to access various bytes from the raw reply.
pub const X_LOWBYTE_IDX: usize = 9;
pub const Y_LOWBYTE_IDX: usize = 11;
pub const Z_LOWBYTE_IDX: usize = 13;
#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
#[repr(u32)]
pub enum SetId {
SensorData = 0,
}
#[derive(Default, Debug, PartialEq, Eq)]
pub enum TransitionState {
#[default]
Idle,
PowerSwitching,
Done,
}
pub trait SpiInterface { pub trait SpiInterface {
type Error; type Error: Debug;
fn transfer(&mut self, tx: &[u8], rx: &mut [u8]) -> Result<(), Self::Error>; fn transfer(&mut self, tx: &[u8], rx: &mut [u8]) -> Result<(), Self::Error>;
} }
#[derive(Default)] #[derive(Default)]
pub struct SpiDummyInterface { pub struct SpiDummyInterface {
pub dummy_val_0: i16, pub dummy_values: MgmLis3RawValues,
pub dummy_val_1: i16,
pub dummy_val_2: i16,
} }
impl SpiInterface for SpiDummyInterface { impl SpiInterface for SpiDummyInterface {
type Error = (); type Error = ();
fn transfer(&mut self, _tx: &[u8], rx: &mut [u8]) -> Result<(), Self::Error> { fn transfer(&mut self, _tx: &[u8], rx: &mut [u8]) -> Result<(), Self::Error> {
rx[0..2].copy_from_slice(&self.dummy_val_0.to_be_bytes()); rx[X_LOWBYTE_IDX..X_LOWBYTE_IDX + 2].copy_from_slice(&self.dummy_values.x.to_le_bytes());
rx[2..4].copy_from_slice(&self.dummy_val_1.to_be_bytes()); rx[Y_LOWBYTE_IDX..Y_LOWBYTE_IDX + 2].copy_from_slice(&self.dummy_values.y.to_be_bytes());
rx[4..6].copy_from_slice(&self.dummy_val_2.to_be_bytes()); rx[Z_LOWBYTE_IDX..Z_LOWBYTE_IDX + 2].copy_from_slice(&self.dummy_values.z.to_be_bytes());
Ok(()) Ok(())
} }
} }
pub struct SpiSimInterface {
pub sim_request_tx: mpsc::Sender<SimRequest>,
pub sim_reply_rx: mpsc::Receiver<SimReply>,
}
impl SpiInterface for SpiSimInterface {
type Error = ();
// Right now, we only support requesting sensor data and not configuration of the sensor.
fn transfer(&mut self, _tx: &[u8], rx: &mut [u8]) -> Result<(), Self::Error> {
let mgm_sensor_request = MgmRequestLis3Mdl::RequestSensorData;
if let Err(e) = self
.sim_request_tx
.send(SimRequest::new_with_epoch_time(mgm_sensor_request))
{
log::error!("failed to send MGM LIS3 request: {}", e);
}
match self.sim_reply_rx.recv_timeout(Duration::from_millis(50)) {
Ok(sim_reply) => {
let sim_reply_lis3 = MgmLis3MdlReply::from_sim_message(&sim_reply)
.expect("failed to parse LIS3 reply");
rx[X_LOWBYTE_IDX..X_LOWBYTE_IDX + 2]
.copy_from_slice(&sim_reply_lis3.raw.x.to_le_bytes());
rx[Y_LOWBYTE_IDX..Y_LOWBYTE_IDX + 2]
.copy_from_slice(&sim_reply_lis3.raw.y.to_le_bytes());
rx[Z_LOWBYTE_IDX..Z_LOWBYTE_IDX + 2]
.copy_from_slice(&sim_reply_lis3.raw.z.to_le_bytes());
}
Err(e) => {
log::warn!("MGM LIS3 SIM reply timeout: {}", e);
}
}
Ok(())
}
}
pub enum SpiSimInterfaceWrapper {
Dummy(SpiDummyInterface),
Sim(SpiSimInterface),
}
impl SpiInterface for SpiSimInterfaceWrapper {
type Error = ();
fn transfer(&mut self, tx: &[u8], rx: &mut [u8]) -> Result<(), Self::Error> {
match self {
SpiSimInterfaceWrapper::Dummy(dummy) => dummy.transfer(tx, rx),
SpiSimInterfaceWrapper::Sim(sim_if) => sim_if.transfer(tx, rx),
}
}
}
#[derive(Default, Debug, Copy, Clone, Serialize, Deserialize)] #[derive(Default, Debug, Copy, Clone, Serialize, Deserialize)]
pub struct MgmData { pub struct MgmData {
pub valid: bool, pub valid: bool,
@ -57,61 +131,85 @@ pub struct MgmData {
pub struct MpscModeLeafInterface { pub struct MpscModeLeafInterface {
pub request_rx: mpsc::Receiver<GenericMessage<ModeRequest>>, pub request_rx: mpsc::Receiver<GenericMessage<ModeRequest>>,
pub reply_tx_to_pus: mpsc::Sender<GenericMessage<ModeReply>>, pub reply_to_pus_tx: mpsc::Sender<GenericMessage<ModeReply>>,
pub reply_tx_to_parent: mpsc::Sender<GenericMessage<ModeReply>>, pub reply_to_parent_tx: mpsc::SyncSender<GenericMessage<ModeReply>>,
}
#[derive(Default)]
pub struct BufWrapper {
tx_buf: [u8; 32],
rx_buf: [u8; 32],
tm_buf: [u8; 32],
}
pub struct ModeHelpers {
current: ModeAndSubmode,
target: Option<ModeAndSubmode>,
requestor_info: Option<MessageMetadata>,
transition_state: TransitionState,
}
impl Default for ModeHelpers {
fn default() -> Self {
Self {
current: ModeAndSubmode::new(DeviceMode::Off as u32, 0),
target: Default::default(),
requestor_info: Default::default(),
transition_state: Default::default(),
}
}
} }
/// Example MGM device handler strongly based on the LIS3MDL MEMS device. /// Example MGM device handler strongly based on the LIS3MDL MEMS device.
#[derive(new)] #[derive(new)]
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub struct MgmHandlerLis3Mdl<ComInterface: SpiInterface, TmSender: EcssTmSender> { pub struct MgmHandlerLis3Mdl<
ComInterface: SpiInterface,
TmSender: EcssTmSender,
SwitchHelper: PowerSwitchInfo<PcduSwitch> + PowerSwitcherCommandSender<PcduSwitch>,
> {
id: UniqueApidTargetId, id: UniqueApidTargetId,
dev_str: &'static str, dev_str: &'static str,
mode_interface: MpscModeLeafInterface, mode_interface: MpscModeLeafInterface,
composite_request_receiver: mpsc::Receiver<GenericMessage<CompositeRequest>>, composite_request_rx: mpsc::Receiver<GenericMessage<CompositeRequest>>,
hk_reply_sender: mpsc::Sender<GenericMessage<HkReply>>, hk_reply_tx: mpsc::Sender<GenericMessage<HkReply>>,
switch_helper: SwitchHelper,
tm_sender: TmSender, tm_sender: TmSender,
com_interface: ComInterface, pub com_interface: ComInterface,
shared_mgm_set: Arc<Mutex<MgmData>>, shared_mgm_set: Arc<Mutex<MgmData>>,
#[new(value = "ModeAndSubmode::new(satrs_example::DeviceMode::Off as u32, 0)")] #[new(value = "PusHkHelper::new(id)")]
mode_and_submode: ModeAndSubmode, hk_helper: PusHkHelper,
#[new(default)] #[new(default)]
tx_buf: [u8; 12], mode_helpers: ModeHelpers,
#[new(default)] #[new(default)]
rx_buf: [u8; 12], bufs: BufWrapper,
#[new(default)] #[new(default)]
tm_buf: [u8; 16], stamp_helper: TimestampHelper,
#[new(default)]
stamp_helper: TimeStampHelper,
} }
impl<ComInterface: SpiInterface, TmSender: EcssTmSender> MgmHandlerLis3Mdl<ComInterface, TmSender> { impl<
ComInterface: SpiInterface,
TmSender: EcssTmSender,
SwitchHelper: PowerSwitchInfo<PcduSwitch> + PowerSwitcherCommandSender<PcduSwitch>,
> MgmHandlerLis3Mdl<ComInterface, TmSender, SwitchHelper>
{
pub fn periodic_operation(&mut self) { pub fn periodic_operation(&mut self) {
self.stamp_helper.update_from_now(); self.stamp_helper.update_from_now();
// Handle requests. // Handle requests.
self.handle_composite_requests(); self.handle_composite_requests();
self.handle_mode_requests(); self.handle_mode_requests();
if let Some(target_mode_submode) = self.mode_helpers.target {
self.handle_mode_transition(target_mode_submode);
}
if self.mode() == DeviceMode::Normal as u32 { if self.mode() == DeviceMode::Normal as u32 {
log::trace!("polling LIS3MDL sensor {}", self.dev_str); log::trace!("polling LIS3MDL sensor {}", self.dev_str);
// Communicate with the device. self.poll_sensor();
let result = self.com_interface.transfer(&self.tx_buf, &mut self.rx_buf);
assert!(result.is_ok());
// Actual data begins on the second byte, similarly to how a lot of SPI devices behave.
let x_raw = i16::from_be_bytes(self.rx_buf[1..3].try_into().unwrap());
let y_raw = i16::from_be_bytes(self.rx_buf[3..5].try_into().unwrap());
let z_raw = i16::from_be_bytes(self.rx_buf[5..7].try_into().unwrap());
// Simple scaling to retrieve the float value, assuming a sensor resolution of
let mut mgm_guard = self.shared_mgm_set.lock().unwrap();
mgm_guard.x = x_raw as f32 * GAUSS_TO_MICROTESLA_FACTOR * FIELD_LSB_PER_GAUSS_4_SENS;
mgm_guard.y = y_raw as f32 * GAUSS_TO_MICROTESLA_FACTOR * FIELD_LSB_PER_GAUSS_4_SENS;
mgm_guard.z = z_raw as f32 * GAUSS_TO_MICROTESLA_FACTOR * FIELD_LSB_PER_GAUSS_4_SENS;
drop(mgm_guard);
} }
} }
pub fn handle_composite_requests(&mut self) { pub fn handle_composite_requests(&mut self) {
loop { loop {
match self.composite_request_receiver.try_recv() { match self.composite_request_rx.try_recv() {
Ok(ref msg) => match &msg.message { Ok(ref msg) => match &msg.message {
CompositeRequest::Hk(hk_request) => { CompositeRequest::Hk(hk_request) => {
self.handle_hk_request(&msg.requestor_info, hk_request) self.handle_hk_request(&msg.requestor_info, hk_request)
@ -139,34 +237,33 @@ impl<ComInterface: SpiInterface, TmSender: EcssTmSender> MgmHandlerLis3Mdl<ComIn
pub fn handle_hk_request(&mut self, requestor_info: &MessageMetadata, hk_request: &HkRequest) { pub fn handle_hk_request(&mut self, requestor_info: &MessageMetadata, hk_request: &HkRequest) {
match hk_request.variant { match hk_request.variant {
HkRequestVariant::OneShot => { HkRequestVariant::OneShot => {
self.hk_reply_sender let mgm_snapshot = *self.shared_mgm_set.lock().unwrap();
if let Ok(hk_tm) = self.hk_helper.generate_hk_report_packet(
self.stamp_helper.stamp(),
SetId::SensorData as u32,
&mut |hk_buf| {
hk_buf[0] = mgm_snapshot.valid as u8;
hk_buf[1..5].copy_from_slice(&mgm_snapshot.x.to_be_bytes());
hk_buf[5..9].copy_from_slice(&mgm_snapshot.y.to_be_bytes());
hk_buf[9..13].copy_from_slice(&mgm_snapshot.z.to_be_bytes());
Ok(13)
},
&mut self.bufs.tm_buf,
) {
// TODO: If sending the TM fails, we should also send a failure reply.
self.tm_sender
.send_tm(self.id.id(), PusTmVariant::Direct(hk_tm))
.expect("failed to send HK TM");
self.hk_reply_tx
.send(GenericMessage::new( .send(GenericMessage::new(
*requestor_info, *requestor_info,
HkReply::new(hk_request.unique_id, HkReplyVariant::Ack), HkReply::new(hk_request.unique_id, HkReplyVariant::Ack),
)) ))
.expect("failed to send HK reply"); .expect("failed to send HK reply");
let sec_header = PusTmSecondaryHeader::new( } else {
3, // TODO: Send back failure reply. Need result code for this.
hk::Subservice::TmHkPacket as u8, log::error!("TM buffer too small to generate HK data");
0, }
0,
self.stamp_helper.stamp(),
);
let mgm_snapshot = *self.shared_mgm_set.lock().unwrap();
// Use binary serialization here. We want the data to be tightly packed.
self.tm_buf[0] = mgm_snapshot.valid as u8;
self.tm_buf[1..5].copy_from_slice(&mgm_snapshot.x.to_be_bytes());
self.tm_buf[5..9].copy_from_slice(&mgm_snapshot.y.to_be_bytes());
self.tm_buf[9..13].copy_from_slice(&mgm_snapshot.z.to_be_bytes());
let hk_tm = PusTmCreator::new(
SpHeader::new_from_apid(self.id.apid),
sec_header,
&self.tm_buf[0..12],
true,
);
self.tm_sender
.send_tm(self.id.id(), PusTmVariant::Direct(hk_tm))
.expect("failed to send HK TM");
} }
HkRequestVariant::EnablePeriodic => todo!(), HkRequestVariant::EnablePeriodic => todo!(),
HkRequestVariant::DisablePeriodic => todo!(), HkRequestVariant::DisablePeriodic => todo!(),
@ -199,20 +296,91 @@ impl<ComInterface: SpiInterface, TmSender: EcssTmSender> MgmHandlerLis3Mdl<ComIn
} }
} }
} }
}
impl<ComInterface: SpiInterface, TmSender: EcssTmSender> ModeProvider pub fn poll_sensor(&mut self) {
for MgmHandlerLis3Mdl<ComInterface, TmSender> // Communicate with the device. This is actually how to read the data from the LIS3 device
{ // SPI interface.
fn mode_and_submode(&self) -> ModeAndSubmode { self.com_interface
self.mode_and_submode .transfer(
&self.bufs.tx_buf[0..NR_OF_DATA_AND_CFG_REGISTERS + 1],
&mut self.bufs.rx_buf[0..NR_OF_DATA_AND_CFG_REGISTERS + 1],
)
.expect("failed to transfer data");
let x_raw = i16::from_le_bytes(
self.bufs.rx_buf[X_LOWBYTE_IDX..X_LOWBYTE_IDX + 2]
.try_into()
.unwrap(),
);
let y_raw = i16::from_le_bytes(
self.bufs.rx_buf[Y_LOWBYTE_IDX..Y_LOWBYTE_IDX + 2]
.try_into()
.unwrap(),
);
let z_raw = i16::from_le_bytes(
self.bufs.rx_buf[Z_LOWBYTE_IDX..Z_LOWBYTE_IDX + 2]
.try_into()
.unwrap(),
);
// Simple scaling to retrieve the float value, assuming the best sensor resolution.
let mut mgm_guard = self.shared_mgm_set.lock().unwrap();
mgm_guard.x = x_raw as f32 * GAUSS_TO_MICROTESLA_FACTOR as f32 * FIELD_LSB_PER_GAUSS_4_SENS;
mgm_guard.y = y_raw as f32 * GAUSS_TO_MICROTESLA_FACTOR as f32 * FIELD_LSB_PER_GAUSS_4_SENS;
mgm_guard.z = z_raw as f32 * GAUSS_TO_MICROTESLA_FACTOR as f32 * FIELD_LSB_PER_GAUSS_4_SENS;
mgm_guard.valid = true;
drop(mgm_guard);
}
pub fn handle_mode_transition(&mut self, target_mode_submode: ModeAndSubmode) {
if target_mode_submode.mode() == DeviceMode::On as u32
|| target_mode_submode.mode() == DeviceMode::Normal as u32
{
if self.mode_helpers.transition_state == TransitionState::Idle {
let result = self
.switch_helper
.send_switch_on_cmd(MessageMetadata::new(0, self.id.id()), PcduSwitch::Mgm);
if result.is_err() {
// Could not send switch command.. still continue with transition.
log::error!("failed to send switch on command");
}
self.mode_helpers.transition_state = TransitionState::PowerSwitching;
}
if self.mode_helpers.transition_state == TransitionState::PowerSwitching
&& self
.switch_helper
.is_switch_on(PcduSwitch::Mgm)
.expect("switch info error")
{
self.mode_helpers.transition_state = TransitionState::Done;
}
if self.mode_helpers.transition_state == TransitionState::Done {
self.mode_helpers.current = self.mode_helpers.target.unwrap();
self.handle_mode_reached(self.mode_helpers.requestor_info)
.expect("failed to handle mode reached");
self.mode_helpers.transition_state = TransitionState::Idle;
}
}
} }
} }
impl<ComInterface: SpiInterface, TmSender: EcssTmSender> ModeRequestHandler impl<
for MgmHandlerLis3Mdl<ComInterface, TmSender> ComInterface: SpiInterface,
TmSender: EcssTmSender,
SwitchHelper: PowerSwitchInfo<PcduSwitch> + PowerSwitcherCommandSender<PcduSwitch>,
> ModeProvider for MgmHandlerLis3Mdl<ComInterface, TmSender, SwitchHelper>
{
fn mode_and_submode(&self) -> ModeAndSubmode {
self.mode_helpers.current
}
}
impl<
ComInterface: SpiInterface,
TmSender: EcssTmSender,
SwitchHelper: PowerSwitchInfo<PcduSwitch> + PowerSwitcherCommandSender<PcduSwitch>,
> ModeRequestHandler for MgmHandlerLis3Mdl<ComInterface, TmSender, SwitchHelper>
{ {
type Error = ModeError; type Error = ModeError;
fn start_transition( fn start_transition(
&mut self, &mut self,
requestor: MessageMetadata, requestor: MessageMetadata,
@ -223,8 +391,18 @@ impl<ComInterface: SpiInterface, TmSender: EcssTmSender> ModeRequestHandler
self.dev_str, self.dev_str,
mode_and_submode mode_and_submode
); );
self.mode_and_submode = mode_and_submode; self.mode_helpers.current = mode_and_submode;
if mode_and_submode.mode() == DeviceMode::Off as u32 {
self.shared_mgm_set.lock().unwrap().valid = false;
self.handle_mode_reached(Some(requestor))?; self.handle_mode_reached(Some(requestor))?;
} else if mode_and_submode.mode() == DeviceMode::Normal as u32
|| mode_and_submode.mode() == DeviceMode::On as u32
{
// TODO: Write helper method for the struct? Might help for other handlers as well..
self.mode_helpers.transition_state = TransitionState::Idle;
self.mode_helpers.requestor_info = Some(requestor);
self.mode_helpers.target = Some(mode_and_submode);
}
Ok(()) Ok(())
} }
@ -232,7 +410,7 @@ impl<ComInterface: SpiInterface, TmSender: EcssTmSender> ModeRequestHandler
log::info!( log::info!(
"{} announcing mode: {:?}", "{} announcing mode: {:?}",
self.dev_str, self.dev_str,
self.mode_and_submode self.mode_and_submode()
); );
} }
@ -240,6 +418,7 @@ impl<ComInterface: SpiInterface, TmSender: EcssTmSender> ModeRequestHandler
&mut self, &mut self,
requestor: Option<MessageMetadata>, requestor: Option<MessageMetadata>,
) -> Result<(), Self::Error> { ) -> Result<(), Self::Error> {
self.mode_helpers.target = None;
self.announce_mode(requestor, false); self.announce_mode(requestor, false);
if let Some(requestor) = requestor { if let Some(requestor) = requestor {
if requestor.sender_id() != PUS_MODE_SERVICE.id() { if requestor.sender_id() != PUS_MODE_SERVICE.id() {
@ -266,7 +445,7 @@ impl<ComInterface: SpiInterface, TmSender: EcssTmSender> ModeRequestHandler
); );
} }
self.mode_interface self.mode_interface
.reply_tx_to_pus .reply_to_pus_tx
.send(GenericMessage::new(requestor, reply)) .send(GenericMessage::new(requestor, reply))
.map_err(|_| GenericTargetedMessagingError::Send(GenericSendError::RxDisconnected))?; .map_err(|_| GenericTargetedMessagingError::Send(GenericSendError::RxDisconnected))?;
Ok(()) Ok(())
@ -280,3 +459,193 @@ impl<ComInterface: SpiInterface, TmSender: EcssTmSender> ModeRequestHandler
Ok(()) Ok(())
} }
} }
#[cfg(test)]
mod tests {
use std::sync::{mpsc, Arc};
use satrs::{
mode::{ModeReply, ModeRequest},
power::SwitchStateBinary,
request::{GenericMessage, UniqueApidTargetId},
tmtc::PacketAsVec,
};
use satrs_example::config::components::Apid;
use satrs_minisim::acs::lis3mdl::MgmLis3RawValues;
use crate::{eps::TestSwitchHelper, pus::hk::HkReply, requests::CompositeRequest};
use super::*;
#[derive(Default)]
pub struct TestSpiInterface {
pub call_count: u32,
pub next_mgm_data: MgmLis3RawValues,
}
impl SpiInterface for TestSpiInterface {
type Error = ();
fn transfer(&mut self, _tx: &[u8], rx: &mut [u8]) -> Result<(), Self::Error> {
rx[X_LOWBYTE_IDX..X_LOWBYTE_IDX + 2]
.copy_from_slice(&self.next_mgm_data.x.to_le_bytes());
rx[Y_LOWBYTE_IDX..Y_LOWBYTE_IDX + 2]
.copy_from_slice(&self.next_mgm_data.y.to_le_bytes());
rx[Z_LOWBYTE_IDX..Z_LOWBYTE_IDX + 2]
.copy_from_slice(&self.next_mgm_data.z.to_le_bytes());
self.call_count += 1;
Ok(())
}
}
pub struct MgmTestbench {
pub mode_request_tx: mpsc::Sender<GenericMessage<ModeRequest>>,
pub mode_reply_rx_to_pus: mpsc::Receiver<GenericMessage<ModeReply>>,
pub mode_reply_rx_to_parent: mpsc::Receiver<GenericMessage<ModeReply>>,
pub composite_request_tx: mpsc::Sender<GenericMessage<CompositeRequest>>,
pub hk_reply_rx: mpsc::Receiver<GenericMessage<HkReply>>,
pub tm_rx: mpsc::Receiver<PacketAsVec>,
pub handler:
MgmHandlerLis3Mdl<TestSpiInterface, mpsc::Sender<PacketAsVec>, TestSwitchHelper>,
}
impl MgmTestbench {
pub fn new() -> Self {
let (request_tx, request_rx) = mpsc::channel();
let (reply_tx_to_pus, reply_rx_to_pus) = mpsc::channel();
let (reply_tx_to_parent, reply_rx_to_parent) = mpsc::sync_channel(5);
let mode_interface = MpscModeLeafInterface {
request_rx,
reply_to_pus_tx: reply_tx_to_pus,
reply_to_parent_tx: reply_tx_to_parent,
};
let (composite_request_tx, composite_request_rx) = mpsc::channel();
let (hk_reply_tx, hk_reply_rx) = mpsc::channel();
let (tm_tx, tm_rx) = mpsc::channel::<PacketAsVec>();
let shared_mgm_set = Arc::default();
Self {
mode_request_tx: request_tx,
mode_reply_rx_to_pus: reply_rx_to_pus,
mode_reply_rx_to_parent: reply_rx_to_parent,
composite_request_tx,
tm_rx,
hk_reply_rx,
handler: MgmHandlerLis3Mdl::new(
UniqueApidTargetId::new(Apid::Acs as u16, 1),
"test-mgm",
mode_interface,
composite_request_rx,
hk_reply_tx,
TestSwitchHelper::default(),
tm_tx,
TestSpiInterface::default(),
shared_mgm_set,
),
}
}
}
#[test]
fn test_basic_handler() {
let mut testbench = MgmTestbench::new();
assert_eq!(testbench.handler.com_interface.call_count, 0);
assert_eq!(
testbench.handler.mode_and_submode().mode(),
DeviceMode::Off as u32
);
assert_eq!(testbench.handler.mode_and_submode().submode(), 0_u16);
testbench.handler.periodic_operation();
// Handler is OFF, no changes expected.
assert_eq!(testbench.handler.com_interface.call_count, 0);
assert_eq!(
testbench.handler.mode_and_submode().mode(),
DeviceMode::Off as u32
);
assert_eq!(testbench.handler.mode_and_submode().submode(), 0_u16);
}
#[test]
fn test_normal_handler() {
let mut testbench = MgmTestbench::new();
testbench
.mode_request_tx
.send(GenericMessage::new(
MessageMetadata::new(0, PUS_MODE_SERVICE.id()),
ModeRequest::SetMode(ModeAndSubmode::new(DeviceMode::Normal as u32, 0)),
))
.expect("failed to send mode request");
testbench.handler.periodic_operation();
assert_eq!(
testbench.handler.mode_and_submode().mode(),
DeviceMode::Normal as u32
);
assert_eq!(testbench.handler.mode_and_submode().submode(), 0);
// Verify power switch handling.
let mut switch_requests = testbench.handler.switch_helper.switch_requests.borrow_mut();
assert_eq!(switch_requests.len(), 1);
let switch_req = switch_requests.pop_front().expect("no switch request");
assert_eq!(switch_req.target_state, SwitchStateBinary::On);
assert_eq!(switch_req.switch_id, PcduSwitch::Mgm);
let mut switch_info_requests = testbench
.handler
.switch_helper
.switch_info_requests
.borrow_mut();
assert_eq!(switch_info_requests.len(), 1);
let switch_info_req = switch_info_requests.pop_front().expect("no switch request");
assert_eq!(switch_info_req, PcduSwitch::Mgm);
let mode_reply = testbench
.mode_reply_rx_to_pus
.try_recv()
.expect("no mode reply generated");
match mode_reply.message {
ModeReply::ModeReply(mode) => {
assert_eq!(mode.mode(), DeviceMode::Normal as u32);
assert_eq!(mode.submode(), 0);
}
_ => panic!("unexpected mode reply"),
}
// The device should have been polled once.
assert_eq!(testbench.handler.com_interface.call_count, 1);
let mgm_set = *testbench.handler.shared_mgm_set.lock().unwrap();
assert!(mgm_set.x < 0.001);
assert!(mgm_set.y < 0.001);
assert!(mgm_set.z < 0.001);
assert!(mgm_set.valid);
}
#[test]
fn test_normal_handler_mgm_set_conversion() {
let mut testbench = MgmTestbench::new();
let raw_values = MgmLis3RawValues {
x: 1000,
y: -1000,
z: 1000,
};
testbench.handler.com_interface.next_mgm_data = raw_values;
testbench
.mode_request_tx
.send(GenericMessage::new(
MessageMetadata::new(0, PUS_MODE_SERVICE.id()),
ModeRequest::SetMode(ModeAndSubmode::new(DeviceMode::Normal as u32, 0)),
))
.expect("failed to send mode request");
testbench.handler.periodic_operation();
let mgm_set = *testbench.handler.shared_mgm_set.lock().unwrap();
let expected_x =
raw_values.x as f32 * GAUSS_TO_MICROTESLA_FACTOR as f32 * FIELD_LSB_PER_GAUSS_4_SENS;
let expected_y =
raw_values.y as f32 * GAUSS_TO_MICROTESLA_FACTOR as f32 * FIELD_LSB_PER_GAUSS_4_SENS;
let expected_z =
raw_values.z as f32 * GAUSS_TO_MICROTESLA_FACTOR as f32 * FIELD_LSB_PER_GAUSS_4_SENS;
let x_diff = (mgm_set.x - expected_x).abs();
let y_diff = (mgm_set.y - expected_y).abs();
let z_diff = (mgm_set.z - expected_z).abs();
assert!(x_diff < 0.001, "x diff too large: {}", x_diff);
assert!(y_diff < 0.001, "y diff too large: {}", y_diff);
assert!(z_diff < 0.001, "z diff too large: {}", z_diff);
assert!(mgm_set.valid);
}
}

View File

@ -132,6 +132,7 @@ pub mod components {
Acs = 3, Acs = 3,
Cfdp = 4, Cfdp = 4,
Tmtc = 5, Tmtc = 5,
Eps = 6,
} }
// Component IDs for components with the PUS APID. // Component IDs for components with the PUS APID.
@ -150,6 +151,11 @@ pub mod components {
Mgm0 = 0, Mgm0 = 0,
} }
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum EpsId {
Pcdu = 0,
}
#[derive(Copy, Clone, PartialEq, Eq)] #[derive(Copy, Clone, PartialEq, Eq)]
pub enum TmtcId { pub enum TmtcId {
UdpServer = 0, UdpServer = 0,
@ -172,6 +178,8 @@ pub mod components {
UniqueApidTargetId::new(Apid::Sched as u16, 0); UniqueApidTargetId::new(Apid::Sched as u16, 0);
pub const MGM_HANDLER_0: UniqueApidTargetId = pub const MGM_HANDLER_0: UniqueApidTargetId =
UniqueApidTargetId::new(Apid::Acs as u16, AcsId::Mgm0 as u32); UniqueApidTargetId::new(Apid::Acs as u16, AcsId::Mgm0 as u32);
pub const PCDU_HANDLER: UniqueApidTargetId =
UniqueApidTargetId::new(Apid::Eps as u16, EpsId::Pcdu as u32);
pub const UDP_SERVER: UniqueApidTargetId = pub const UDP_SERVER: UniqueApidTargetId =
UniqueApidTargetId::new(Apid::Tmtc as u16, TmtcId::UdpServer as u32); UniqueApidTargetId::new(Apid::Tmtc as u16, TmtcId::UdpServer as u32);
pub const TCP_SERVER: UniqueApidTargetId = pub const TCP_SERVER: UniqueApidTargetId =
@ -224,7 +232,7 @@ pub mod pool {
pub mod tasks { pub mod tasks {
pub const FREQ_MS_UDP_TMTC: u64 = 200; pub const FREQ_MS_UDP_TMTC: u64 = 200;
pub const FREQ_MS_EVENT_HANDLING: u64 = 400;
pub const FREQ_MS_AOCS: u64 = 500; pub const FREQ_MS_AOCS: u64 = 500;
pub const FREQ_MS_PUS_STACK: u64 = 200; pub const FREQ_MS_PUS_STACK: u64 = 200;
pub const SIM_CLIENT_IDLE_DELAY_MS: u64 = 5;
} }

View File

@ -0,0 +1,195 @@
use derive_new::new;
use std::{cell::RefCell, collections::VecDeque, sync::mpsc, time::Duration};
use satrs::{
power::{
PowerSwitchInfo, PowerSwitcherCommandSender, SwitchRequest, SwitchState, SwitchStateBinary,
},
queue::GenericSendError,
request::{GenericMessage, MessageMetadata},
};
use satrs_minisim::eps::{PcduSwitch, SwitchMapWrapper};
use thiserror::Error;
use self::pcdu::SharedSwitchSet;
pub mod pcdu;
#[derive(new, Clone)]
pub struct PowerSwitchHelper {
switcher_tx: mpsc::SyncSender<GenericMessage<SwitchRequest>>,
shared_switch_set: SharedSwitchSet,
}
#[derive(Debug, Error, Copy, Clone, PartialEq, Eq)]
pub enum SwitchCommandingError {
#[error("send error: {0}")]
Send(#[from] GenericSendError),
}
#[derive(Debug, Error, Copy, Clone, PartialEq, Eq)]
pub enum SwitchInfoError {
/// This is a configuration error which should not occur.
#[error("switch ID not in map")]
SwitchIdNotInMap(PcduSwitch),
#[error("switch set invalid")]
SwitchSetInvalid,
}
impl PowerSwitchInfo<PcduSwitch> for PowerSwitchHelper {
type Error = SwitchInfoError;
fn switch_state(
&self,
switch_id: PcduSwitch,
) -> Result<satrs::power::SwitchState, Self::Error> {
let switch_set = self
.shared_switch_set
.lock()
.expect("failed to lock switch set");
if !switch_set.valid {
return Err(SwitchInfoError::SwitchSetInvalid);
}
if let Some(state) = switch_set.switch_map.get(&switch_id) {
return Ok(*state);
}
Err(SwitchInfoError::SwitchIdNotInMap(switch_id))
}
fn switch_delay_ms(&self) -> Duration {
// Here, we could set device specific switch delays theoretically. Set it to this value
// for now.
Duration::from_millis(1000)
}
}
impl PowerSwitcherCommandSender<PcduSwitch> for PowerSwitchHelper {
type Error = SwitchCommandingError;
fn send_switch_on_cmd(
&self,
requestor_info: satrs::request::MessageMetadata,
switch_id: PcduSwitch,
) -> Result<(), Self::Error> {
self.switcher_tx
.send_switch_on_cmd(requestor_info, switch_id)?;
Ok(())
}
fn send_switch_off_cmd(
&self,
requestor_info: satrs::request::MessageMetadata,
switch_id: PcduSwitch,
) -> Result<(), Self::Error> {
self.switcher_tx
.send_switch_off_cmd(requestor_info, switch_id)?;
Ok(())
}
}
#[derive(new)]
pub struct SwitchRequestInfo {
pub requestor_info: MessageMetadata,
pub switch_id: PcduSwitch,
pub target_state: satrs::power::SwitchStateBinary,
}
// Test switch helper which can be used for unittests.
pub struct TestSwitchHelper {
pub switch_requests: RefCell<VecDeque<SwitchRequestInfo>>,
pub switch_info_requests: RefCell<VecDeque<PcduSwitch>>,
pub switch_delay_request_count: u32,
pub next_switch_delay: Duration,
pub switch_map: RefCell<SwitchMapWrapper>,
pub switch_map_valid: bool,
}
impl Default for TestSwitchHelper {
fn default() -> Self {
Self {
switch_requests: Default::default(),
switch_info_requests: Default::default(),
switch_delay_request_count: Default::default(),
next_switch_delay: Duration::from_millis(1000),
switch_map: Default::default(),
switch_map_valid: true,
}
}
}
impl PowerSwitchInfo<PcduSwitch> for TestSwitchHelper {
type Error = SwitchInfoError;
fn switch_state(
&self,
switch_id: PcduSwitch,
) -> Result<satrs::power::SwitchState, Self::Error> {
let mut switch_info_requests_mut = self.switch_info_requests.borrow_mut();
switch_info_requests_mut.push_back(switch_id);
if !self.switch_map_valid {
return Err(SwitchInfoError::SwitchSetInvalid);
}
let switch_map_mut = self.switch_map.borrow_mut();
if let Some(state) = switch_map_mut.0.get(&switch_id) {
return Ok(*state);
}
Err(SwitchInfoError::SwitchIdNotInMap(switch_id))
}
fn switch_delay_ms(&self) -> Duration {
self.next_switch_delay
}
}
impl PowerSwitcherCommandSender<PcduSwitch> for TestSwitchHelper {
type Error = SwitchCommandingError;
fn send_switch_on_cmd(
&self,
requestor_info: MessageMetadata,
switch_id: PcduSwitch,
) -> Result<(), Self::Error> {
let mut switch_requests_mut = self.switch_requests.borrow_mut();
switch_requests_mut.push_back(SwitchRequestInfo {
requestor_info,
switch_id,
target_state: SwitchStateBinary::On,
});
// By default, the test helper immediately acknowledges the switch request by setting
// the appropriate switch state in the internal switch map.
let mut switch_map_mut = self.switch_map.borrow_mut();
if let Some(switch_state) = switch_map_mut.0.get_mut(&switch_id) {
*switch_state = SwitchState::On;
}
Ok(())
}
fn send_switch_off_cmd(
&self,
requestor_info: MessageMetadata,
switch_id: PcduSwitch,
) -> Result<(), Self::Error> {
let mut switch_requests_mut = self.switch_requests.borrow_mut();
switch_requests_mut.push_back(SwitchRequestInfo {
requestor_info,
switch_id,
target_state: SwitchStateBinary::Off,
});
// By default, the test helper immediately acknowledges the switch request by setting
// the appropriate switch state in the internal switch map.
let mut switch_map_mut = self.switch_map.borrow_mut();
if let Some(switch_state) = switch_map_mut.0.get_mut(&switch_id) {
*switch_state = SwitchState::Off;
}
Ok(())
}
}
#[allow(dead_code)]
impl TestSwitchHelper {
// Helper function which can be used to force a switch to another state for test purposes.
pub fn set_switch_state(&mut self, switch: PcduSwitch, state: SwitchState) {
self.switch_map.get_mut().0.insert(switch, state);
}
}

View File

@ -0,0 +1,467 @@
use std::{
cell::RefCell,
collections::VecDeque,
sync::{mpsc, Arc, Mutex},
};
use derive_new::new;
use num_enum::{IntoPrimitive, TryFromPrimitive};
use satrs::{
hk::{HkRequest, HkRequestVariant},
mode::{ModeAndSubmode, ModeError, ModeProvider, ModeReply, ModeRequestHandler},
power::SwitchRequest,
pus::{EcssTmSender, PusTmVariant},
queue::{GenericSendError, GenericTargetedMessagingError},
request::{GenericMessage, MessageMetadata, UniqueApidTargetId},
spacepackets::ByteConversionError,
};
use satrs_example::{config::components::PUS_MODE_SERVICE, DeviceMode, TimestampHelper};
use satrs_minisim::{
eps::{
PcduReply, PcduRequest, PcduSwitch, SwitchMap, SwitchMapBinaryWrapper, SwitchMapWrapper,
},
SerializableSimMsgPayload, SimReply, SimRequest,
};
use serde::{Deserialize, Serialize};
use crate::{
acs::mgm::MpscModeLeafInterface,
hk::PusHkHelper,
pus::hk::{HkReply, HkReplyVariant},
requests::CompositeRequest,
};
pub trait SerialInterface {
type Error: core::fmt::Debug;
/// Send some data via the serial interface.
fn send(&self, data: &[u8]) -> Result<(), Self::Error>;
/// Receive all replies received on the serial interface so far. This function takes a closure
/// and call its for each received packet, passing the received packet into it.
fn try_recv_replies<ReplyHandler: FnMut(&[u8])>(
&self,
f: ReplyHandler,
) -> Result<(), Self::Error>;
}
#[derive(new)]
pub struct SerialInterfaceToSim {
pub sim_request_tx: mpsc::Sender<SimRequest>,
pub sim_reply_rx: mpsc::Receiver<SimReply>,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, TryFromPrimitive, IntoPrimitive)]
#[repr(u32)]
pub enum SetId {
SwitcherSet = 0,
}
impl SerialInterface for SerialInterfaceToSim {
type Error = ();
fn send(&self, data: &[u8]) -> Result<(), Self::Error> {
let request: SimRequest = serde_json::from_slice(data).unwrap();
self.sim_request_tx
.send(request)
.expect("failed to send request to simulation");
Ok(())
}
fn try_recv_replies<ReplyHandler: FnMut(&[u8])>(
&self,
mut f: ReplyHandler,
) -> Result<(), Self::Error> {
loop {
match self.sim_reply_rx.try_recv() {
Ok(reply) => {
let reply = serde_json::to_string(&reply).unwrap();
f(reply.as_bytes());
}
Err(e) => match e {
mpsc::TryRecvError::Empty => break,
mpsc::TryRecvError::Disconnected => {
log::warn!("sim reply sender has disconnected");
break;
}
},
}
}
Ok(())
}
}
#[derive(Default)]
pub struct SerialInterfaceDummy {
// Need interior mutability here for both fields.
pub switch_map: RefCell<SwitchMapBinaryWrapper>,
pub reply_deque: RefCell<VecDeque<SimReply>>,
}
impl SerialInterface for SerialInterfaceDummy {
type Error = ();
fn send(&self, data: &[u8]) -> Result<(), Self::Error> {
let sim_req: SimRequest = serde_json::from_slice(data).unwrap();
let pcdu_req =
PcduRequest::from_sim_message(&sim_req).expect("PCDU request creation failed");
let switch_map_mut = &mut self.switch_map.borrow_mut().0;
match pcdu_req {
PcduRequest::SwitchDevice { switch, state } => {
match switch_map_mut.entry(switch) {
std::collections::hash_map::Entry::Occupied(mut val) => {
*val.get_mut() = state;
}
std::collections::hash_map::Entry::Vacant(vacant) => {
vacant.insert(state);
}
};
}
PcduRequest::RequestSwitchInfo => {
let mut reply_deque_mut = self.reply_deque.borrow_mut();
reply_deque_mut.push_back(SimReply::new(&PcduReply::SwitchInfo(
self.switch_map.borrow().0.clone(),
)));
}
};
Ok(())
}
fn try_recv_replies<ReplyHandler: FnMut(&[u8])>(
&self,
mut f: ReplyHandler,
) -> Result<(), Self::Error> {
if self.reply_deque.borrow().is_empty() {
return Ok(());
}
loop {
let mut reply_deque_mut = self.reply_deque.borrow_mut();
let next_reply = reply_deque_mut.pop_front().unwrap();
let reply = serde_json::to_string(&next_reply).unwrap();
f(reply.as_bytes());
if reply_deque_mut.is_empty() {
break;
}
}
Ok(())
}
}
pub enum SerialSimInterfaceWrapper {
Dummy(SerialInterfaceDummy),
Sim(SerialInterfaceToSim),
}
impl SerialInterface for SerialSimInterfaceWrapper {
type Error = ();
fn send(&self, data: &[u8]) -> Result<(), Self::Error> {
match self {
SerialSimInterfaceWrapper::Dummy(dummy) => dummy.send(data),
SerialSimInterfaceWrapper::Sim(sim) => sim.send(data),
}
}
fn try_recv_replies<ReplyHandler: FnMut(&[u8])>(
&self,
f: ReplyHandler,
) -> Result<(), Self::Error> {
match self {
SerialSimInterfaceWrapper::Dummy(dummy) => dummy.try_recv_replies(f),
SerialSimInterfaceWrapper::Sim(sim) => sim.try_recv_replies(f),
}
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum OpCode {
RegularOp = 0,
PollAndRecvReplies = 1,
}
#[derive(Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct SwitchSet {
pub valid: bool,
pub switch_map: SwitchMap,
}
pub type SharedSwitchSet = Arc<Mutex<SwitchSet>>;
/// Example PCDU device handler.
#[derive(new)]
#[allow(clippy::too_many_arguments)]
pub struct PcduHandler<ComInterface: SerialInterface, TmSender: EcssTmSender> {
id: UniqueApidTargetId,
dev_str: &'static str,
mode_interface: MpscModeLeafInterface,
composite_request_rx: mpsc::Receiver<GenericMessage<CompositeRequest>>,
hk_reply_tx: mpsc::Sender<GenericMessage<HkReply>>,
switch_request_rx: mpsc::Receiver<GenericMessage<SwitchRequest>>,
tm_sender: TmSender,
pub com_interface: ComInterface,
shared_switch_map: Arc<Mutex<SwitchSet>>,
#[new(value = "PusHkHelper::new(id)")]
hk_helper: PusHkHelper,
#[new(value = "ModeAndSubmode::new(satrs_example::DeviceMode::Off as u32, 0)")]
mode_and_submode: ModeAndSubmode,
#[new(default)]
stamp_helper: TimestampHelper,
#[new(value = "[0; 256]")]
tm_buf: [u8; 256],
}
impl<ComInterface: SerialInterface, TmSender: EcssTmSender> PcduHandler<ComInterface, TmSender> {
pub fn periodic_operation(&mut self, op_code: OpCode) {
match op_code {
OpCode::RegularOp => {
self.stamp_helper.update_from_now();
// Handle requests.
self.handle_composite_requests();
self.handle_mode_requests();
self.handle_switch_requests();
// Poll the switch states and/or telemetry regularly here.
if self.mode() == DeviceMode::Normal as u32 || self.mode() == DeviceMode::On as u32
{
self.handle_periodic_commands();
}
}
OpCode::PollAndRecvReplies => {
self.poll_and_handle_replies();
}
}
}
pub fn handle_composite_requests(&mut self) {
loop {
match self.composite_request_rx.try_recv() {
Ok(ref msg) => match &msg.message {
CompositeRequest::Hk(hk_request) => {
self.handle_hk_request(&msg.requestor_info, hk_request)
}
// TODO: This object does not have actions (yet).. Still send back completion failure
// reply.
CompositeRequest::Action(_action_req) => {}
},
Err(e) => {
if e != mpsc::TryRecvError::Empty {
log::warn!(
"{}: failed to receive composite request: {:?}",
self.dev_str,
e
);
} else {
break;
}
}
}
}
}
pub fn handle_hk_request(&mut self, requestor_info: &MessageMetadata, hk_request: &HkRequest) {
match hk_request.variant {
HkRequestVariant::OneShot => {
if hk_request.unique_id == SetId::SwitcherSet as u32 {
if let Ok(hk_tm) = self.hk_helper.generate_hk_report_packet(
self.stamp_helper.stamp(),
SetId::SwitcherSet as u32,
&mut |hk_buf| {
// Send TM down as JSON.
let switch_map_snapshot = self
.shared_switch_map
.lock()
.expect("failed to lock switch map")
.clone();
let switch_map_json = serde_json::to_string(&switch_map_snapshot)
.expect("failed to serialize switch map");
if switch_map_json.len() > hk_buf.len() {
log::error!("switch map JSON too large for HK buffer");
return Err(ByteConversionError::ToSliceTooSmall {
found: hk_buf.len(),
expected: switch_map_json.len(),
});
}
Ok(switch_map_json.len())
},
&mut self.tm_buf,
) {
self.tm_sender
.send_tm(self.id.id(), PusTmVariant::Direct(hk_tm))
.expect("failed to send HK TM");
self.hk_reply_tx
.send(GenericMessage::new(
*requestor_info,
HkReply::new(hk_request.unique_id, HkReplyVariant::Ack),
))
.expect("failed to send HK reply");
}
}
}
HkRequestVariant::EnablePeriodic => todo!(),
HkRequestVariant::DisablePeriodic => todo!(),
HkRequestVariant::ModifyCollectionInterval(_) => todo!(),
}
}
pub fn handle_periodic_commands(&self) {
let pcdu_req = PcduRequest::RequestSwitchInfo;
let pcdu_req_ser = serde_json::to_string(&pcdu_req).unwrap();
if let Err(_e) = self.com_interface.send(pcdu_req_ser.as_bytes()) {
log::warn!("polling PCDU switch info failed");
}
}
pub fn handle_mode_requests(&mut self) {
loop {
// TODO: Only allow one set mode request per cycle?
match self.mode_interface.request_rx.try_recv() {
Ok(msg) => {
let result = self.handle_mode_request(msg);
// TODO: Trigger event?
if result.is_err() {
log::warn!(
"{}: mode request failed with error {:?}",
self.dev_str,
result.err().unwrap()
);
}
}
Err(e) => {
if e != mpsc::TryRecvError::Empty {
log::warn!("{}: failed to receive mode request: {:?}", self.dev_str, e);
} else {
break;
}
}
}
}
}
pub fn handle_switch_requests(&mut self) {
loop {
match self.switch_request_rx.try_recv() {
Ok(switch_req) => match PcduSwitch::try_from(switch_req.message.switch_id()) {
Ok(pcdu_switch) => {
let pcdu_req = PcduRequest::SwitchDevice {
switch: pcdu_switch,
state: switch_req.message.target_state(),
};
let pcdu_req_ser = serde_json::to_string(&pcdu_req).unwrap();
self.com_interface
.send(pcdu_req_ser.as_bytes())
.expect("failed to send switch request to PCDU");
}
Err(e) => todo!("failed to convert switch ID {:?} to typed PCDU switch", e),
},
Err(e) => match e {
mpsc::TryRecvError::Empty => break,
mpsc::TryRecvError::Disconnected => {
log::warn!("switch request receiver has disconnected");
break;
}
},
};
}
}
pub fn poll_and_handle_replies(&mut self) {
if let Err(e) = self.com_interface.try_recv_replies(|reply| {
let sim_reply: SimReply = serde_json::from_slice(reply).expect("invalid reply format");
let pcdu_reply = PcduReply::from_sim_message(&sim_reply).expect("invalid reply format");
match pcdu_reply {
PcduReply::SwitchInfo(switch_info) => {
let switch_map_wrapper =
SwitchMapWrapper::from_binary_switch_map_ref(&switch_info);
self.shared_switch_map
.lock()
.expect("failed to lock switch map")
.switch_map = switch_map_wrapper.0;
}
}
}) {
log::warn!("receiving PCDU replies failed: {:?}", e);
}
}
}
impl<ComInterface: SerialInterface, TmSender: EcssTmSender> ModeProvider
for PcduHandler<ComInterface, TmSender>
{
fn mode_and_submode(&self) -> ModeAndSubmode {
self.mode_and_submode
}
}
impl<ComInterface: SerialInterface, TmSender: EcssTmSender> ModeRequestHandler
for PcduHandler<ComInterface, TmSender>
{
type Error = ModeError;
fn start_transition(
&mut self,
requestor: MessageMetadata,
mode_and_submode: ModeAndSubmode,
) -> Result<(), satrs::mode::ModeError> {
log::info!(
"{}: transitioning to mode {:?}",
self.dev_str,
mode_and_submode
);
self.mode_and_submode = mode_and_submode;
if mode_and_submode.mode() == DeviceMode::Off as u32 {
self.shared_switch_map.lock().unwrap().valid = false;
}
self.handle_mode_reached(Some(requestor))?;
Ok(())
}
fn announce_mode(&self, _requestor_info: Option<MessageMetadata>, _recursive: bool) {
log::info!(
"{} announcing mode: {:?}",
self.dev_str,
self.mode_and_submode
);
}
fn handle_mode_reached(
&mut self,
requestor: Option<MessageMetadata>,
) -> Result<(), Self::Error> {
self.announce_mode(requestor, false);
if let Some(requestor) = requestor {
if requestor.sender_id() != PUS_MODE_SERVICE.id() {
log::warn!(
"can not send back mode reply to sender {}",
requestor.sender_id()
);
} else {
self.send_mode_reply(requestor, ModeReply::ModeReply(self.mode_and_submode()))?;
}
}
Ok(())
}
fn send_mode_reply(
&self,
requestor: MessageMetadata,
reply: ModeReply,
) -> Result<(), Self::Error> {
if requestor.sender_id() != PUS_MODE_SERVICE.id() {
log::warn!(
"can not send back mode reply to sender {}",
requestor.sender_id()
);
}
self.mode_interface
.reply_to_pus_tx
.send(GenericMessage::new(requestor, reply))
.map_err(|_| GenericTargetedMessagingError::Send(GenericSendError::RxDisconnected))?;
Ok(())
}
fn handle_mode_info(
&mut self,
_requestor_info: MessageMetadata,
_info: ModeAndSubmode,
) -> Result<(), Self::Error> {
Ok(())
}
}

View File

@ -1,7 +1,9 @@
use derive_new::new; use derive_new::new;
use satrs::hk::UniqueId; use satrs::hk::UniqueId;
use satrs::request::UniqueApidTargetId; use satrs::request::UniqueApidTargetId;
use satrs::spacepackets::ByteConversionError; use satrs::spacepackets::ecss::hk;
use satrs::spacepackets::ecss::tm::{PusTmCreator, PusTmSecondaryHeader};
use satrs::spacepackets::{ByteConversionError, SpHeader};
#[derive(Debug, new, Copy, Clone)] #[derive(Debug, new, Copy, Clone)]
pub struct HkUniqueId { pub struct HkUniqueId {
@ -33,3 +35,35 @@ impl HkUniqueId {
Ok(8) Ok(8)
} }
} }
#[derive(new)]
pub struct PusHkHelper {
component_id: UniqueApidTargetId,
}
impl PusHkHelper {
pub fn generate_hk_report_packet<
'a,
'b,
HkWriter: FnMut(&mut [u8]) -> Result<usize, ByteConversionError>,
>(
&self,
timestamp: &'a [u8],
set_id: u32,
hk_data_writer: &mut HkWriter,
buf: &'b mut [u8],
) -> Result<PusTmCreator<'a, 'b>, ByteConversionError> {
let sec_header =
PusTmSecondaryHeader::new(3, hk::Subservice::TmHkPacket as u8, 0, 0, timestamp);
buf[0..4].copy_from_slice(&self.component_id.unique_id.to_be_bytes());
buf[4..8].copy_from_slice(&set_id.to_be_bytes());
let (_, second_half) = buf.split_at_mut(8);
let hk_data_len = hk_data_writer(second_half)?;
Ok(PusTmCreator::new(
SpHeader::new_from_apid(self.component_id.apid),
sec_header,
&buf[0..8 + hk_data_len],
true,
))
}
}

View File

@ -1,3 +1,4 @@
//! This module contains all component related to the direct interface of the example. //! This module contains all component related to the direct interface of the example.
pub mod sim_client_udp;
pub mod tcp; pub mod tcp;
pub mod udp; pub mod udp;

View File

@ -0,0 +1,420 @@
use std::{
collections::HashMap,
net::{Ipv4Addr, SocketAddr, SocketAddrV4, UdpSocket},
sync::mpsc,
time::Duration,
};
use satrs::pus::HandlingStatus;
use satrs_minisim::{
udp::SIM_CTRL_PORT, SerializableSimMsgPayload, SimComponent, SimMessageProvider, SimReply,
SimRequest,
};
use satrs_minisim::{SimCtrlReply, SimCtrlRequest};
struct SimReplyMap(pub HashMap<SimComponent, mpsc::Sender<SimReply>>);
pub fn create_sim_client(sim_request_rx: mpsc::Receiver<SimRequest>) -> Option<SimClientUdp> {
match SimClientUdp::new(
SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, SIM_CTRL_PORT)),
sim_request_rx,
) {
Ok(sim_client) => {
log::info!("simulator client connection success");
return Some(sim_client);
}
Err(e) => {
log::warn!("sim client creation error: {}", e);
}
}
None
}
#[derive(thiserror::Error, Debug)]
pub enum SimClientCreationError {
#[error("io error: {0}")]
Io(#[from] std::io::Error),
#[error("timeout when trying to connect to sim UDP server")]
Timeout,
#[error("invalid ping reply when trying connection to UDP sim server")]
InvalidReplyJsonError(#[from] serde_json::Error),
#[error("invalid sim reply, not pong reply as expected: {0:?}")]
ReplyIsNotPong(SimReply),
}
pub struct SimClientUdp {
udp_client: UdpSocket,
simulator_addr: SocketAddr,
sim_request_rx: mpsc::Receiver<SimRequest>,
reply_map: SimReplyMap,
reply_buf: [u8; 4096],
}
impl SimClientUdp {
pub fn new(
simulator_addr: SocketAddr,
sim_request_rx: mpsc::Receiver<SimRequest>,
) -> Result<Self, SimClientCreationError> {
let mut reply_buf: [u8; 4096] = [0; 4096];
let mut udp_client = UdpSocket::bind("127.0.0.1:0")?;
udp_client.set_read_timeout(Some(Duration::from_millis(100)))?;
Self::attempt_connection(&mut udp_client, simulator_addr, &mut reply_buf)?;
udp_client.set_nonblocking(true)?;
Ok(Self {
udp_client,
simulator_addr,
sim_request_rx,
reply_map: SimReplyMap(HashMap::new()),
reply_buf,
})
}
pub fn attempt_connection(
udp_client: &mut UdpSocket,
simulator_addr: SocketAddr,
reply_buf: &mut [u8],
) -> Result<(), SimClientCreationError> {
let sim_req = SimRequest::new_with_epoch_time(SimCtrlRequest::Ping);
let sim_req_json = serde_json::to_string(&sim_req).expect("failed to serialize SimRequest");
udp_client.send_to(sim_req_json.as_bytes(), simulator_addr)?;
match udp_client.recv(reply_buf) {
Ok(reply_len) => {
let sim_reply: SimReply = serde_json::from_slice(&reply_buf[0..reply_len])?;
if sim_reply.component() != SimComponent::SimCtrl {
return Err(SimClientCreationError::ReplyIsNotPong(sim_reply));
}
let sim_ctrl_reply =
SimCtrlReply::from_sim_message(&sim_reply).expect("invalid SIM reply");
match sim_ctrl_reply {
SimCtrlReply::InvalidRequest(_) => {
panic!("received invalid request reply from UDP sim server")
}
SimCtrlReply::Pong => Ok(()),
}
}
Err(e) => {
if e.kind() == std::io::ErrorKind::TimedOut
|| e.kind() == std::io::ErrorKind::WouldBlock
{
Err(SimClientCreationError::Timeout)
} else {
Err(SimClientCreationError::Io(e))
}
}
}
}
pub fn operation(&mut self) -> HandlingStatus {
let mut no_sim_requests_handled = true;
let mut no_data_from_udp_server_received = true;
loop {
match self.sim_request_rx.try_recv() {
Ok(request) => {
let request_json =
serde_json::to_string(&request).expect("failed to serialize SimRequest");
if let Err(e) = self
.udp_client
.send_to(request_json.as_bytes(), self.simulator_addr)
{
log::error!("error sending data to UDP SIM server: {}", e);
break;
} else {
no_sim_requests_handled = false;
}
}
Err(e) => match e {
mpsc::TryRecvError::Empty => {
break;
}
mpsc::TryRecvError::Disconnected => {
log::warn!("SIM request sender disconnected");
break;
}
},
}
}
loop {
match self.udp_client.recv(&mut self.reply_buf) {
Ok(recvd_bytes) => {
no_data_from_udp_server_received = false;
let sim_reply_result: serde_json::Result<SimReply> =
serde_json::from_slice(&self.reply_buf[0..recvd_bytes]);
match sim_reply_result {
Ok(sim_reply) => {
if let Some(sender) = self.reply_map.0.get(&sim_reply.component()) {
sender.send(sim_reply).expect("failed to send SIM reply");
} else {
log::warn!(
"no recipient for SIM reply from component {:?}",
sim_reply.component()
);
}
}
Err(e) => {
log::warn!("failed to deserialize SIM reply: {}", e);
}
}
}
Err(e) => {
if e.kind() == std::io::ErrorKind::WouldBlock
|| e.kind() == std::io::ErrorKind::TimedOut
{
break;
}
log::error!("error receiving data from UDP SIM server: {}", e);
break;
}
}
}
if no_sim_requests_handled && no_data_from_udp_server_received {
return HandlingStatus::Empty;
}
HandlingStatus::HandledOne
}
pub fn add_reply_recipient(
&mut self,
component: SimComponent,
reply_sender: mpsc::Sender<SimReply>,
) {
self.reply_map.0.insert(component, reply_sender);
}
}
#[cfg(test)]
pub mod tests {
use std::{
collections::HashMap,
net::{SocketAddr, UdpSocket},
sync::{
atomic::{AtomicBool, Ordering},
mpsc, Arc,
},
time::Duration,
};
use satrs_minisim::{
eps::{PcduReply, PcduRequest},
SerializableSimMsgPayload, SimComponent, SimCtrlReply, SimCtrlRequest, SimMessageProvider,
SimReply, SimRequest,
};
use super::SimClientUdp;
struct UdpSimTestServer {
udp_server: UdpSocket,
request_tx: mpsc::Sender<SimRequest>,
reply_rx: mpsc::Receiver<SimReply>,
last_sender: Option<SocketAddr>,
stop_signal: Arc<AtomicBool>,
recv_buf: [u8; 1024],
}
impl UdpSimTestServer {
pub fn new(
request_tx: mpsc::Sender<SimRequest>,
reply_rx: mpsc::Receiver<SimReply>,
stop_signal: Arc<AtomicBool>,
) -> Self {
let udp_server = UdpSocket::bind("127.0.0.1:0").expect("creating UDP server failed");
udp_server
.set_nonblocking(true)
.expect("failed to set UDP server to non-blocking");
Self {
udp_server,
request_tx,
reply_rx,
last_sender: None,
stop_signal,
recv_buf: [0; 1024],
}
}
pub fn operation(&mut self) {
loop {
let mut no_sim_replies_handled = true;
let mut no_data_received = true;
if self.stop_signal.load(Ordering::Relaxed) {
break;
}
if let Some(last_sender) = self.last_sender {
loop {
match self.reply_rx.try_recv() {
Ok(sim_reply) => {
let sim_reply_json = serde_json::to_string(&sim_reply)
.expect("failed to serialize SimReply");
self.udp_server
.send_to(sim_reply_json.as_bytes(), last_sender)
.expect("failed to send reply to client from UDP server");
no_sim_replies_handled = false;
}
Err(e) => match e {
mpsc::TryRecvError::Empty => break,
mpsc::TryRecvError::Disconnected => {
panic!("reply sender disconnected")
}
},
}
}
}
loop {
match self.udp_server.recv_from(&mut self.recv_buf) {
Ok((read_bytes, from)) => {
let sim_request: SimRequest =
serde_json::from_slice(&self.recv_buf[0..read_bytes])
.expect("failed to deserialize SimRequest");
if sim_request.component() == SimComponent::SimCtrl {
// For a ping, we perform the reply handling here directly
let sim_ctrl_request =
SimCtrlRequest::from_sim_message(&sim_request)
.expect("failed to convert SimRequest to SimCtrlRequest");
match sim_ctrl_request {
SimCtrlRequest::Ping => {
no_data_received = false;
self.last_sender = Some(from);
let sim_reply = SimReply::new(&SimCtrlReply::Pong);
let sim_reply_json = serde_json::to_string(&sim_reply)
.expect("failed to serialize SimReply");
self.udp_server
.send_to(sim_reply_json.as_bytes(), from)
.expect(
"failed to send reply to client from UDP server",
);
}
};
}
// Forward each SIM request for testing purposes.
self.request_tx
.send(sim_request)
.expect("failed to send request");
}
Err(e) => {
if e.kind() != std::io::ErrorKind::WouldBlock
&& e.kind() != std::io::ErrorKind::TimedOut
{
panic!("UDP server error: {}", e);
}
break;
}
}
}
if no_sim_replies_handled && no_data_received {
std::thread::sleep(Duration::from_millis(5));
}
}
}
pub fn local_addr(&self) -> SocketAddr {
self.udp_server.local_addr().unwrap()
}
}
#[test]
fn basic_connection_test() {
let (server_sim_request_tx, server_sim_request_rx) = mpsc::channel();
let (_server_sim_reply_tx, server_sim_reply_rx) = mpsc::channel();
let stop_signal = Arc::new(AtomicBool::new(false));
let mut udp_server = UdpSimTestServer::new(
server_sim_request_tx,
server_sim_reply_rx,
stop_signal.clone(),
);
let server_addr = udp_server.local_addr();
let (_client_sim_req_tx, client_sim_req_rx) = mpsc::channel();
// Need to spawn the simulator UDP server before calling the client constructor.
let jh0 = std::thread::spawn(move || {
udp_server.operation();
});
// Creating the client also performs the connection test.
SimClientUdp::new(server_addr, client_sim_req_rx).unwrap();
let sim_request = server_sim_request_rx
.recv_timeout(Duration::from_millis(50))
.expect("no SIM request received");
let ping_request = SimCtrlRequest::from_sim_message(&sim_request)
.expect("failed to create SimCtrlRequest");
assert_eq!(ping_request, SimCtrlRequest::Ping);
// Stop the server.
stop_signal.store(true, Ordering::Relaxed);
jh0.join().unwrap();
}
#[test]
fn basic_request_reply_test() {
let (server_sim_request_tx, server_sim_request_rx) = mpsc::channel();
let (server_sim_reply_tx, sever_sim_reply_rx) = mpsc::channel();
let stop_signal = Arc::new(AtomicBool::new(false));
let mut udp_server = UdpSimTestServer::new(
server_sim_request_tx,
sever_sim_reply_rx,
stop_signal.clone(),
);
let server_addr = udp_server.local_addr();
let (client_sim_req_tx, client_sim_req_rx) = mpsc::channel();
let (client_pcdu_reply_tx, client_pcdu_reply_rx) = mpsc::channel();
// Need to spawn the simulator UDP server before calling the client constructor.
let jh0 = std::thread::spawn(move || {
udp_server.operation();
});
// Creating the client also performs the connection test.
let mut client = SimClientUdp::new(server_addr, client_sim_req_rx).unwrap();
client.add_reply_recipient(SimComponent::Pcdu, client_pcdu_reply_tx);
let sim_request = server_sim_request_rx
.recv_timeout(Duration::from_millis(50))
.expect("no SIM request received");
let ping_request = SimCtrlRequest::from_sim_message(&sim_request)
.expect("failed to create SimCtrlRequest");
assert_eq!(ping_request, SimCtrlRequest::Ping);
let pcdu_req = PcduRequest::RequestSwitchInfo;
client_sim_req_tx
.send(SimRequest::new_with_epoch_time(pcdu_req))
.expect("send failed");
client.operation();
// Check that the request arrives properly at the server.
let sim_request = server_sim_request_rx
.recv_timeout(Duration::from_millis(50))
.expect("no SIM request received");
let req_recvd_on_server =
PcduRequest::from_sim_message(&sim_request).expect("failed to create SimCtrlRequest");
matches!(req_recvd_on_server, PcduRequest::RequestSwitchInfo);
// We inject the reply ourselves.
let pcdu_reply = PcduReply::SwitchInfo(HashMap::new());
server_sim_reply_tx
.send(SimReply::new(&pcdu_reply))
.expect("sending PCDU reply failed");
// Now we verify that the reply is sent by the UDP server back to the client, and then
// forwarded by the clients internal map.
let mut pcdu_reply_received = false;
for _ in 0..3 {
client.operation();
match client_pcdu_reply_rx.try_recv() {
Ok(sim_reply) => {
assert_eq!(sim_reply.component(), SimComponent::Pcdu);
let pcdu_reply_from_client = PcduReply::from_sim_message(&sim_reply)
.expect("failed to create PcduReply");
assert_eq!(pcdu_reply_from_client, pcdu_reply);
pcdu_reply_received = true;
break;
}
Err(e) => match e {
mpsc::TryRecvError::Empty => std::thread::sleep(Duration::from_millis(10)),
mpsc::TryRecvError::Disconnected => panic!("reply sender disconnected"),
},
}
}
if !pcdu_reply_received {
panic!("no reply received");
}
// Stop the server.
stop_signal.store(true, Ordering::Relaxed);
jh0.join().unwrap();
}
}

View File

@ -9,12 +9,12 @@ pub enum DeviceMode {
Normal = 2, Normal = 2,
} }
pub struct TimeStampHelper { pub struct TimestampHelper {
stamper: CdsTime, stamper: CdsTime,
time_stamp: [u8; 7], time_stamp: [u8; 7],
} }
impl TimeStampHelper { impl TimestampHelper {
pub fn stamp(&self) -> &[u8] { pub fn stamp(&self) -> &[u8] {
&self.time_stamp &self.time_stamp
} }
@ -29,7 +29,7 @@ impl TimeStampHelper {
} }
} }
impl Default for TimeStampHelper { impl Default for TimestampHelper {
fn default() -> Self { fn default() -> Self {
Self { Self {
stamper: CdsTime::now_with_u16_days().expect("creating time stamper failed"), stamper: CdsTime::now_with_u16_days().expect("creating time stamper failed"),

View File

@ -1,4 +1,5 @@
mod acs; mod acs;
mod eps;
mod events; mod events;
mod hk; mod hk;
mod interface; mod interface;
@ -7,6 +8,10 @@ mod pus;
mod requests; mod requests;
mod tmtc; mod tmtc;
use crate::eps::pcdu::{
PcduHandler, SerialInterfaceDummy, SerialInterfaceToSim, SerialSimInterfaceWrapper,
};
use crate::eps::PowerSwitchHelper;
use crate::events::EventHandler; use crate::events::EventHandler;
use crate::interface::udp::DynamicUdpTmHandler; use crate::interface::udp::DynamicUdpTmHandler;
use crate::pus::stack::PusStack; use crate::pus::stack::PusStack;
@ -16,15 +21,20 @@ use log::info;
use pus::test::create_test_service_dynamic; use pus::test::create_test_service_dynamic;
use satrs::hal::std::tcp_server::ServerConfig; use satrs::hal::std::tcp_server::ServerConfig;
use satrs::hal::std::udp_server::UdpTcServer; use satrs::hal::std::udp_server::UdpTcServer;
use satrs::pus::HandlingStatus;
use satrs::request::GenericMessage; use satrs::request::GenericMessage;
use satrs::tmtc::{PacketSenderWithSharedPool, SharedPacketPool}; use satrs::tmtc::{PacketSenderWithSharedPool, SharedPacketPool};
use satrs_example::config::pool::{create_sched_tc_pool, create_static_pools}; use satrs_example::config::pool::{create_sched_tc_pool, create_static_pools};
use satrs_example::config::tasks::{ use satrs_example::config::tasks::{
FREQ_MS_AOCS, FREQ_MS_EVENT_HANDLING, FREQ_MS_PUS_STACK, FREQ_MS_UDP_TMTC, FREQ_MS_AOCS, FREQ_MS_PUS_STACK, FREQ_MS_UDP_TMTC, SIM_CLIENT_IDLE_DELAY_MS,
}; };
use satrs_example::config::{OBSW_SERVER_ADDR, PACKET_ID_VALIDATOR, SERVER_PORT}; use satrs_example::config::{OBSW_SERVER_ADDR, PACKET_ID_VALIDATOR, SERVER_PORT};
use crate::acs::mgm::{MgmHandlerLis3Mdl, MpscModeLeafInterface, SpiDummyInterface}; use crate::acs::mgm::{
MgmHandlerLis3Mdl, MpscModeLeafInterface, SpiDummyInterface, SpiSimInterface,
SpiSimInterfaceWrapper,
};
use crate::interface::sim_client_udp::create_sim_client;
use crate::interface::tcp::{SyncTcpTmSource, TcpTask}; use crate::interface::tcp::{SyncTcpTmSource, TcpTask};
use crate::interface::udp::{StaticUdpTmHandler, UdpTmtcServer}; use crate::interface::udp::{StaticUdpTmHandler, UdpTmtcServer};
use crate::logger::setup_logger; use crate::logger::setup_logger;
@ -39,9 +49,9 @@ use crate::requests::{CompositeRequest, GenericRequestRouter};
use satrs::mode::ModeRequest; use satrs::mode::ModeRequest;
use satrs::pus::event_man::EventRequestWithToken; use satrs::pus::event_man::EventRequestWithToken;
use satrs::spacepackets::{time::cds::CdsTime, time::TimeWriter}; use satrs::spacepackets::{time::cds::CdsTime, time::TimeWriter};
use satrs_example::config::components::{MGM_HANDLER_0, TCP_SERVER, UDP_SERVER}; use satrs_example::config::components::{MGM_HANDLER_0, PCDU_HANDLER, TCP_SERVER, UDP_SERVER};
use std::net::{IpAddr, SocketAddr}; use std::net::{IpAddr, SocketAddr};
use std::sync::mpsc; use std::sync::{mpsc, Mutex};
use std::sync::{Arc, RwLock}; use std::sync::{Arc, RwLock};
use std::thread; use std::thread;
use std::time::Duration; use std::time::Duration;
@ -60,9 +70,20 @@ fn static_tmtc_pool_main() {
let tm_sink_tx_sender = let tm_sink_tx_sender =
PacketSenderWithSharedPool::new(tm_sink_tx.clone(), shared_tm_pool_wrapper.clone()); PacketSenderWithSharedPool::new(tm_sink_tx.clone(), shared_tm_pool_wrapper.clone());
let (sim_request_tx, sim_request_rx) = mpsc::channel();
let (mgm_sim_reply_tx, mgm_sim_reply_rx) = mpsc::channel();
let (pcdu_sim_reply_tx, pcdu_sim_reply_rx) = mpsc::channel();
let mut opt_sim_client = create_sim_client(sim_request_rx);
let (mgm_handler_composite_tx, mgm_handler_composite_rx) = let (mgm_handler_composite_tx, mgm_handler_composite_rx) =
mpsc::channel::<GenericMessage<CompositeRequest>>(); mpsc::sync_channel::<GenericMessage<CompositeRequest>>(10);
let (mgm_handler_mode_tx, mgm_handler_mode_rx) = mpsc::channel::<GenericMessage<ModeRequest>>(); let (pcdu_handler_composite_tx, pcdu_handler_composite_rx) =
mpsc::sync_channel::<GenericMessage<CompositeRequest>>(30);
let (mgm_handler_mode_tx, mgm_handler_mode_rx) =
mpsc::sync_channel::<GenericMessage<ModeRequest>>(5);
let (pcdu_handler_mode_tx, pcdu_handler_mode_rx) =
mpsc::sync_channel::<GenericMessage<ModeRequest>>(5);
// Some request are targetable. This map is used to retrieve sender handles based on a target ID. // Some request are targetable. This map is used to retrieve sender handles based on a target ID.
let mut request_map = GenericRequestRouter::default(); let mut request_map = GenericRequestRouter::default();
@ -72,6 +93,12 @@ fn static_tmtc_pool_main() {
request_map request_map
.mode_router_map .mode_router_map
.insert(MGM_HANDLER_0.id(), mgm_handler_mode_tx); .insert(MGM_HANDLER_0.id(), mgm_handler_mode_tx);
request_map
.composite_router_map
.insert(PCDU_HANDLER.id(), pcdu_handler_composite_tx);
request_map
.mode_router_map
.insert(PCDU_HANDLER.id(), pcdu_handler_mode_tx);
// This helper structure is used by all telecommand providers which need to send telecommands // This helper structure is used by all telecommand providers which need to send telecommands
// to the TC source. // to the TC source.
@ -195,26 +222,68 @@ fn static_tmtc_pool_main() {
); );
let (mgm_handler_mode_reply_to_parent_tx, _mgm_handler_mode_reply_to_parent_rx) = let (mgm_handler_mode_reply_to_parent_tx, _mgm_handler_mode_reply_to_parent_rx) =
mpsc::channel(); mpsc::sync_channel(5);
let shared_switch_set = Arc::new(Mutex::default());
let (switch_request_tx, switch_request_rx) = mpsc::sync_channel(20);
let switch_helper = PowerSwitchHelper::new(switch_request_tx, shared_switch_set.clone());
let dummy_spi_interface = SpiDummyInterface::default();
let shared_mgm_set = Arc::default(); let shared_mgm_set = Arc::default();
let mode_leaf_interface = MpscModeLeafInterface { let mgm_mode_leaf_interface = MpscModeLeafInterface {
request_rx: mgm_handler_mode_rx, request_rx: mgm_handler_mode_rx,
reply_tx_to_pus: pus_mode_reply_tx, reply_to_pus_tx: pus_mode_reply_tx.clone(),
reply_tx_to_parent: mgm_handler_mode_reply_to_parent_tx, reply_to_parent_tx: mgm_handler_mode_reply_to_parent_tx,
};
let mgm_spi_interface = if let Some(sim_client) = opt_sim_client.as_mut() {
sim_client.add_reply_recipient(satrs_minisim::SimComponent::MgmLis3Mdl, mgm_sim_reply_tx);
SpiSimInterfaceWrapper::Sim(SpiSimInterface {
sim_request_tx: sim_request_tx.clone(),
sim_reply_rx: mgm_sim_reply_rx,
})
} else {
SpiSimInterfaceWrapper::Dummy(SpiDummyInterface::default())
}; };
let mut mgm_handler = MgmHandlerLis3Mdl::new( let mut mgm_handler = MgmHandlerLis3Mdl::new(
MGM_HANDLER_0, MGM_HANDLER_0,
"MGM_0", "MGM_0",
mode_leaf_interface, mgm_mode_leaf_interface,
mgm_handler_composite_rx, mgm_handler_composite_rx,
pus_hk_reply_tx, pus_hk_reply_tx.clone(),
tm_sink_tx, switch_helper.clone(),
dummy_spi_interface, tm_sink_tx.clone(),
mgm_spi_interface,
shared_mgm_set, shared_mgm_set,
); );
let (pcdu_handler_mode_reply_to_parent_tx, _pcdu_handler_mode_reply_to_parent_rx) =
mpsc::sync_channel(10);
let pcdu_mode_leaf_interface = MpscModeLeafInterface {
request_rx: pcdu_handler_mode_rx,
reply_to_pus_tx: pus_mode_reply_tx,
reply_to_parent_tx: pcdu_handler_mode_reply_to_parent_tx,
};
let pcdu_serial_interface = if let Some(sim_client) = opt_sim_client.as_mut() {
sim_client.add_reply_recipient(satrs_minisim::SimComponent::Pcdu, pcdu_sim_reply_tx);
SerialSimInterfaceWrapper::Sim(SerialInterfaceToSim::new(
sim_request_tx.clone(),
pcdu_sim_reply_rx,
))
} else {
SerialSimInterfaceWrapper::Dummy(SerialInterfaceDummy::default())
};
let mut pcdu_handler = PcduHandler::new(
PCDU_HANDLER,
"PCDU",
pcdu_mode_leaf_interface,
pcdu_handler_composite_rx,
pus_hk_reply_tx,
switch_request_rx,
tm_sink_tx,
pcdu_serial_interface,
shared_switch_set,
);
info!("Starting TMTC and UDP task"); info!("Starting TMTC and UDP task");
let jh_udp_tmtc = thread::Builder::new() let jh_udp_tmtc = thread::Builder::new()
.name("SATRS tmtc-udp".to_string()) .name("SATRS tmtc-udp".to_string())
@ -247,14 +316,20 @@ fn static_tmtc_pool_main() {
}) })
.unwrap(); .unwrap();
info!("Starting event handling task"); let mut opt_jh_sim_client = None;
let jh_event_handling = thread::Builder::new() if let Some(mut sim_client) = opt_sim_client {
.name("sat-rs events".to_string()) info!("Starting UDP sim client task");
opt_jh_sim_client = Some(
thread::Builder::new()
.name("sat-rs sim adapter".to_string())
.spawn(move || loop { .spawn(move || loop {
event_handler.periodic_operation(); if sim_client.operation() == HandlingStatus::Empty {
thread::sleep(Duration::from_millis(FREQ_MS_EVENT_HANDLING)); std::thread::sleep(Duration::from_millis(SIM_CLIENT_IDLE_DELAY_MS));
}
}) })
.unwrap(); .unwrap(),
);
}
info!("Starting AOCS thread"); info!("Starting AOCS thread");
let jh_aocs = thread::Builder::new() let jh_aocs = thread::Builder::new()
@ -265,10 +340,26 @@ fn static_tmtc_pool_main() {
}) })
.unwrap(); .unwrap();
info!("Starting EPS thread");
let jh_eps = thread::Builder::new()
.name("sat-rs eps".to_string())
.spawn(move || loop {
// TODO: We should introduce something like a fixed timeslot helper to allow a more
// declarative API. It would also be very useful for the AOCS task.
pcdu_handler.periodic_operation(eps::pcdu::OpCode::RegularOp);
thread::sleep(Duration::from_millis(50));
pcdu_handler.periodic_operation(eps::pcdu::OpCode::PollAndRecvReplies);
thread::sleep(Duration::from_millis(50));
pcdu_handler.periodic_operation(eps::pcdu::OpCode::PollAndRecvReplies);
thread::sleep(Duration::from_millis(300));
})
.unwrap();
info!("Starting PUS handler thread"); info!("Starting PUS handler thread");
let jh_pus_handler = thread::Builder::new() let jh_pus_handler = thread::Builder::new()
.name("sat-rs pus".to_string()) .name("sat-rs pus".to_string())
.spawn(move || loop { .spawn(move || loop {
event_handler.periodic_operation();
pus_stack.periodic_operation(); pus_stack.periodic_operation();
thread::sleep(Duration::from_millis(FREQ_MS_PUS_STACK)); thread::sleep(Duration::from_millis(FREQ_MS_PUS_STACK));
}) })
@ -283,10 +374,13 @@ fn static_tmtc_pool_main() {
jh_tm_funnel jh_tm_funnel
.join() .join()
.expect("Joining TM Funnel thread failed"); .expect("Joining TM Funnel thread failed");
jh_event_handling if let Some(jh_sim_client) = opt_jh_sim_client {
jh_sim_client
.join() .join()
.expect("Joining Event Manager thread failed"); .expect("Joining SIM client thread failed");
}
jh_aocs.join().expect("Joining AOCS thread failed"); jh_aocs.join().expect("Joining AOCS thread failed");
jh_eps.join().expect("Joining EPS thread failed");
jh_pus_handler jh_pus_handler
.join() .join()
.expect("Joining PUS handler thread failed"); .expect("Joining PUS handler thread failed");
@ -295,22 +389,38 @@ fn static_tmtc_pool_main() {
#[allow(dead_code)] #[allow(dead_code)]
fn dyn_tmtc_pool_main() { fn dyn_tmtc_pool_main() {
let (tc_source_tx, tc_source_rx) = mpsc::channel(); let (tc_source_tx, tc_source_rx) = mpsc::channel();
let (tm_funnel_tx, tm_funnel_rx) = mpsc::channel(); let (tm_sink_tx, tm_sink_rx) = mpsc::channel();
let (tm_server_tx, tm_server_rx) = mpsc::channel(); let (tm_server_tx, tm_server_rx) = mpsc::channel();
let (sim_request_tx, sim_request_rx) = mpsc::channel();
let (mgm_sim_reply_tx, mgm_sim_reply_rx) = mpsc::channel();
let (pcdu_sim_reply_tx, pcdu_sim_reply_rx) = mpsc::channel();
let mut opt_sim_client = create_sim_client(sim_request_rx);
// Some request are targetable. This map is used to retrieve sender handles based on a target ID. // Some request are targetable. This map is used to retrieve sender handles based on a target ID.
let (mgm_handler_composite_tx, mgm_handler_composite_rx) = let (mgm_handler_composite_tx, mgm_handler_composite_rx) =
mpsc::channel::<GenericMessage<CompositeRequest>>(); mpsc::sync_channel::<GenericMessage<CompositeRequest>>(5);
let (mgm_handler_mode_tx, mgm_handler_mode_rx) = mpsc::channel::<GenericMessage<ModeRequest>>(); let (pcdu_handler_composite_tx, pcdu_handler_composite_rx) =
mpsc::sync_channel::<GenericMessage<CompositeRequest>>(10);
let (mgm_handler_mode_tx, mgm_handler_mode_rx) =
mpsc::sync_channel::<GenericMessage<ModeRequest>>(5);
let (pcdu_handler_mode_tx, pcdu_handler_mode_rx) =
mpsc::sync_channel::<GenericMessage<ModeRequest>>(10);
// Some request are targetable. This map is used to retrieve sender handles based on a target ID. // Some request are targetable. This map is used to retrieve sender handles based on a target ID.
let mut request_map = GenericRequestRouter::default(); let mut request_map = GenericRequestRouter::default();
request_map request_map
.composite_router_map .composite_router_map
.insert(MGM_HANDLER_0.raw(), mgm_handler_composite_tx); .insert(MGM_HANDLER_0.id(), mgm_handler_composite_tx);
request_map request_map
.mode_router_map .mode_router_map
.insert(MGM_HANDLER_0.raw(), mgm_handler_mode_tx); .insert(MGM_HANDLER_0.id(), mgm_handler_mode_tx);
request_map
.composite_router_map
.insert(PCDU_HANDLER.id(), pcdu_handler_composite_tx);
request_map
.mode_router_map
.insert(PCDU_HANDLER.id(), pcdu_handler_mode_tx);
// Create event handling components // Create event handling components
// These sender handles are used to send event requests, for example to enable or disable // These sender handles are used to send event requests, for example to enable or disable
@ -319,7 +429,7 @@ fn dyn_tmtc_pool_main() {
let (event_request_tx, event_request_rx) = mpsc::channel::<EventRequestWithToken>(); let (event_request_tx, event_request_rx) = mpsc::channel::<EventRequestWithToken>();
// The event task is the core handler to perform the event routing and TM handling as specified // The event task is the core handler to perform the event routing and TM handling as specified
// in the sat-rs documentation. // in the sat-rs documentation.
let mut event_handler = EventHandler::new(tm_funnel_tx.clone(), event_rx, event_request_rx); let mut event_handler = EventHandler::new(tm_sink_tx.clone(), event_rx, event_request_rx);
let (pus_test_tx, pus_test_rx) = mpsc::channel(); let (pus_test_tx, pus_test_rx) = mpsc::channel();
let (pus_event_tx, pus_event_rx) = mpsc::channel(); let (pus_event_tx, pus_event_rx) = mpsc::channel();
@ -342,30 +452,30 @@ fn dyn_tmtc_pool_main() {
}; };
let pus_test_service = let pus_test_service =
create_test_service_dynamic(tm_funnel_tx.clone(), event_tx.clone(), pus_test_rx); create_test_service_dynamic(tm_sink_tx.clone(), event_tx.clone(), pus_test_rx);
let pus_scheduler_service = create_scheduler_service_dynamic( let pus_scheduler_service = create_scheduler_service_dynamic(
tm_funnel_tx.clone(), tm_sink_tx.clone(),
tc_source_tx.clone(), tc_source_tx.clone(),
pus_sched_rx, pus_sched_rx,
create_sched_tc_pool(), create_sched_tc_pool(),
); );
let pus_event_service = let pus_event_service =
create_event_service_dynamic(tm_funnel_tx.clone(), pus_event_rx, event_request_tx); create_event_service_dynamic(tm_sink_tx.clone(), pus_event_rx, event_request_tx);
let pus_action_service = create_action_service_dynamic( let pus_action_service = create_action_service_dynamic(
tm_funnel_tx.clone(), tm_sink_tx.clone(),
pus_action_rx, pus_action_rx,
request_map.clone(), request_map.clone(),
pus_action_reply_rx, pus_action_reply_rx,
); );
let pus_hk_service = create_hk_service_dynamic( let pus_hk_service = create_hk_service_dynamic(
tm_funnel_tx.clone(), tm_sink_tx.clone(),
pus_hk_rx, pus_hk_rx,
request_map.clone(), request_map.clone(),
pus_hk_reply_rx, pus_hk_reply_rx,
); );
let pus_mode_service = create_mode_service_dynamic( let pus_mode_service = create_mode_service_dynamic(
tm_funnel_tx.clone(), tm_sink_tx.clone(),
pus_mode_rx, pus_mode_rx,
request_map, request_map,
pus_mode_reply_rx, pus_mode_reply_rx,
@ -381,7 +491,7 @@ fn dyn_tmtc_pool_main() {
let mut tmtc_task = TcSourceTaskDynamic::new( let mut tmtc_task = TcSourceTaskDynamic::new(
tc_source_rx, tc_source_rx,
PusTcDistributor::new(tm_funnel_tx.clone(), pus_router), PusTcDistributor::new(tm_sink_tx.clone(), pus_router),
); );
let sock_addr = SocketAddr::new(IpAddr::V4(OBSW_SERVER_ADDR), SERVER_PORT); let sock_addr = SocketAddr::new(IpAddr::V4(OBSW_SERVER_ADDR), SERVER_PORT);
@ -410,28 +520,70 @@ fn dyn_tmtc_pool_main() {
) )
.expect("tcp server creation failed"); .expect("tcp server creation failed");
let mut tm_funnel = TmSinkDynamic::new(sync_tm_tcp_source, tm_funnel_rx, tm_server_tx); let mut tm_funnel = TmSinkDynamic::new(sync_tm_tcp_source, tm_sink_rx, tm_server_tx);
let shared_switch_set = Arc::new(Mutex::default());
let (switch_request_tx, switch_request_rx) = mpsc::sync_channel(20);
let switch_helper = PowerSwitchHelper::new(switch_request_tx, shared_switch_set.clone());
let (mgm_handler_mode_reply_to_parent_tx, _mgm_handler_mode_reply_to_parent_rx) = let (mgm_handler_mode_reply_to_parent_tx, _mgm_handler_mode_reply_to_parent_rx) =
mpsc::channel(); mpsc::sync_channel(5);
let dummy_spi_interface = SpiDummyInterface::default();
let shared_mgm_set = Arc::default(); let shared_mgm_set = Arc::default();
let mode_leaf_interface = MpscModeLeafInterface { let mode_leaf_interface = MpscModeLeafInterface {
request_rx: mgm_handler_mode_rx, request_rx: mgm_handler_mode_rx,
reply_tx_to_pus: pus_mode_reply_tx, reply_to_pus_tx: pus_mode_reply_tx.clone(),
reply_tx_to_parent: mgm_handler_mode_reply_to_parent_tx, reply_to_parent_tx: mgm_handler_mode_reply_to_parent_tx,
};
let mgm_spi_interface = if let Some(sim_client) = opt_sim_client.as_mut() {
sim_client.add_reply_recipient(satrs_minisim::SimComponent::MgmLis3Mdl, mgm_sim_reply_tx);
SpiSimInterfaceWrapper::Sim(SpiSimInterface {
sim_request_tx: sim_request_tx.clone(),
sim_reply_rx: mgm_sim_reply_rx,
})
} else {
SpiSimInterfaceWrapper::Dummy(SpiDummyInterface::default())
}; };
let mut mgm_handler = MgmHandlerLis3Mdl::new( let mut mgm_handler = MgmHandlerLis3Mdl::new(
MGM_HANDLER_0, MGM_HANDLER_0,
"MGM_0", "MGM_0",
mode_leaf_interface, mode_leaf_interface,
mgm_handler_composite_rx, mgm_handler_composite_rx,
pus_hk_reply_tx, pus_hk_reply_tx.clone(),
tm_funnel_tx, switch_helper.clone(),
dummy_spi_interface, tm_sink_tx.clone(),
mgm_spi_interface,
shared_mgm_set, shared_mgm_set,
); );
let (pcdu_handler_mode_reply_to_parent_tx, _pcdu_handler_mode_reply_to_parent_rx) =
mpsc::sync_channel(10);
let pcdu_mode_leaf_interface = MpscModeLeafInterface {
request_rx: pcdu_handler_mode_rx,
reply_to_pus_tx: pus_mode_reply_tx,
reply_to_parent_tx: pcdu_handler_mode_reply_to_parent_tx,
};
let pcdu_serial_interface = if let Some(sim_client) = opt_sim_client.as_mut() {
sim_client.add_reply_recipient(satrs_minisim::SimComponent::Pcdu, pcdu_sim_reply_tx);
SerialSimInterfaceWrapper::Sim(SerialInterfaceToSim::new(
sim_request_tx.clone(),
pcdu_sim_reply_rx,
))
} else {
SerialSimInterfaceWrapper::Dummy(SerialInterfaceDummy::default())
};
let mut pcdu_handler = PcduHandler::new(
PCDU_HANDLER,
"PCDU",
pcdu_mode_leaf_interface,
pcdu_handler_composite_rx,
pus_hk_reply_tx,
switch_request_rx,
tm_sink_tx,
pcdu_serial_interface,
shared_switch_set,
);
info!("Starting TMTC and UDP task"); info!("Starting TMTC and UDP task");
let jh_udp_tmtc = thread::Builder::new() let jh_udp_tmtc = thread::Builder::new()
.name("sat-rs tmtc-udp".to_string()) .name("sat-rs tmtc-udp".to_string())
@ -464,14 +616,20 @@ fn dyn_tmtc_pool_main() {
}) })
.unwrap(); .unwrap();
info!("Starting event handling task"); let mut opt_jh_sim_client = None;
let jh_event_handling = thread::Builder::new() if let Some(mut sim_client) = opt_sim_client {
.name("sat-rs events".to_string()) info!("Starting UDP sim client task");
opt_jh_sim_client = Some(
thread::Builder::new()
.name("sat-rs sim adapter".to_string())
.spawn(move || loop { .spawn(move || loop {
event_handler.periodic_operation(); if sim_client.operation() == HandlingStatus::Empty {
thread::sleep(Duration::from_millis(FREQ_MS_EVENT_HANDLING)); std::thread::sleep(Duration::from_millis(SIM_CLIENT_IDLE_DELAY_MS));
}
}) })
.unwrap(); .unwrap(),
);
}
info!("Starting AOCS thread"); info!("Starting AOCS thread");
let jh_aocs = thread::Builder::new() let jh_aocs = thread::Builder::new()
@ -482,11 +640,27 @@ fn dyn_tmtc_pool_main() {
}) })
.unwrap(); .unwrap();
info!("Starting EPS thread");
let jh_eps = thread::Builder::new()
.name("sat-rs eps".to_string())
.spawn(move || loop {
// TODO: We should introduce something like a fixed timeslot helper to allow a more
// declarative API. It would also be very useful for the AOCS task.
pcdu_handler.periodic_operation(eps::pcdu::OpCode::RegularOp);
thread::sleep(Duration::from_millis(50));
pcdu_handler.periodic_operation(eps::pcdu::OpCode::PollAndRecvReplies);
thread::sleep(Duration::from_millis(50));
pcdu_handler.periodic_operation(eps::pcdu::OpCode::PollAndRecvReplies);
thread::sleep(Duration::from_millis(300));
})
.unwrap();
info!("Starting PUS handler thread"); info!("Starting PUS handler thread");
let jh_pus_handler = thread::Builder::new() let jh_pus_handler = thread::Builder::new()
.name("sat-rs pus".to_string()) .name("sat-rs pus".to_string())
.spawn(move || loop { .spawn(move || loop {
pus_stack.periodic_operation(); pus_stack.periodic_operation();
event_handler.periodic_operation();
thread::sleep(Duration::from_millis(FREQ_MS_PUS_STACK)); thread::sleep(Duration::from_millis(FREQ_MS_PUS_STACK));
}) })
.unwrap(); .unwrap();
@ -500,10 +674,13 @@ fn dyn_tmtc_pool_main() {
jh_tm_funnel jh_tm_funnel
.join() .join()
.expect("Joining TM Funnel thread failed"); .expect("Joining TM Funnel thread failed");
jh_event_handling if let Some(jh_sim_client) = opt_jh_sim_client {
jh_sim_client
.join() .join()
.expect("Joining Event Manager thread failed"); .expect("Joining SIM client thread failed");
}
jh_aocs.join().expect("Joining AOCS thread failed"); jh_aocs.join().expect("Joining AOCS thread failed");
jh_eps.join().expect("Joining EPS thread failed");
jh_pus_handler jh_pus_handler
.join() .join()
.expect("Joining PUS handler thread failed"); .expect("Joining PUS handler thread failed");

View File

@ -341,7 +341,7 @@ mod tests {
let (tm_funnel_tx, tm_funnel_rx) = mpsc::channel(); let (tm_funnel_tx, tm_funnel_rx) = mpsc::channel();
let (pus_action_tx, pus_action_rx) = mpsc::channel(); let (pus_action_tx, pus_action_rx) = mpsc::channel();
let (action_reply_tx, action_reply_rx) = mpsc::channel(); let (action_reply_tx, action_reply_rx) = mpsc::channel();
let (action_req_tx, action_req_rx) = mpsc::channel(); let (action_req_tx, action_req_rx) = mpsc::sync_channel(10);
let verif_reporter = TestVerificationReporter::new(owner_id); let verif_reporter = TestVerificationReporter::new(owner_id);
let mut generic_req_router = GenericRequestRouter::default(); let mut generic_req_router = GenericRequestRouter::default();
generic_req_router generic_req_router

View File

@ -12,6 +12,7 @@ use satrs::pus::{
PusPacketHandlingError, PusReplyHandler, PusServiceHelper, PusTcToRequestConverter, PusPacketHandlingError, PusReplyHandler, PusServiceHelper, PusTcToRequestConverter,
}; };
use satrs::request::{GenericMessage, UniqueApidTargetId}; use satrs::request::{GenericMessage, UniqueApidTargetId};
use satrs::res_code::ResultU16;
use satrs::spacepackets::ecss::tc::PusTcReader; use satrs::spacepackets::ecss::tc::PusTcReader;
use satrs::spacepackets::ecss::{hk, PusPacket, PusServiceId}; use satrs::spacepackets::ecss::{hk, PusPacket, PusServiceId};
use satrs::tmtc::{PacketAsVec, PacketSenderWithSharedPool}; use satrs::tmtc::{PacketAsVec, PacketSenderWithSharedPool};
@ -34,6 +35,7 @@ pub struct HkReply {
#[derive(Clone, PartialEq, Debug)] #[derive(Clone, PartialEq, Debug)]
pub enum HkReplyVariant { pub enum HkReplyVariant {
Ack, Ack,
Failed(ResultU16),
} }
#[derive(Default)] #[derive(Default)]
@ -69,6 +71,15 @@ impl PusReplyHandler<ActivePusRequestStd, HkReply> for HkReplyHandler {
.completion_success(tm_sender, started_token, time_stamp) .completion_success(tm_sender, started_token, time_stamp)
.expect("sending completion success verification failed"); .expect("sending completion success verification failed");
} }
HkReplyVariant::Failed(failure_code) => {
verification_handler
.completion_failure(
tm_sender,
started_token,
FailParams::new(time_stamp, &failure_code, &[]),
)
.expect("sending completion success verification failed");
}
}; };
Ok(true) Ok(true)
} }

View File

@ -19,7 +19,7 @@ use satrs::tmtc::{PacketAsVec, PacketInPool};
use satrs::ComponentId; use satrs::ComponentId;
use satrs_example::config::components::PUS_ROUTING_SERVICE; use satrs_example::config::components::PUS_ROUTING_SERVICE;
use satrs_example::config::{tmtc_err, CustomPusServiceId}; use satrs_example::config::{tmtc_err, CustomPusServiceId};
use satrs_example::TimeStampHelper; use satrs_example::TimestampHelper;
use std::fmt::Debug; use std::fmt::Debug;
use std::sync::mpsc::{self, Sender}; use std::sync::mpsc::{self, Sender};
@ -53,7 +53,7 @@ pub struct PusTcDistributor<TmSender: EcssTmSender> {
pub tm_sender: TmSender, pub tm_sender: TmSender,
pub verif_reporter: VerificationReporter, pub verif_reporter: VerificationReporter,
pub pus_router: PusTcMpscRouter, pub pus_router: PusTcMpscRouter,
stamp_helper: TimeStampHelper, stamp_helper: TimestampHelper,
} }
impl<TmSender: EcssTmSender> PusTcDistributor<TmSender> { impl<TmSender: EcssTmSender> PusTcDistributor<TmSender> {
@ -66,7 +66,7 @@ impl<TmSender: EcssTmSender> PusTcDistributor<TmSender> {
PUS_ROUTING_SERVICE.apid, PUS_ROUTING_SERVICE.apid,
), ),
pus_router, pus_router,
stamp_helper: TimeStampHelper::default(), stamp_helper: TimestampHelper::default(),
} }
} }

View File

@ -28,8 +28,9 @@ pub enum CompositeRequest {
pub struct GenericRequestRouter { pub struct GenericRequestRouter {
pub id: ComponentId, pub id: ComponentId,
// All messages which do not have a dedicated queue. // All messages which do not have a dedicated queue.
pub composite_router_map: HashMap<ComponentId, mpsc::Sender<GenericMessage<CompositeRequest>>>, pub composite_router_map:
pub mode_router_map: HashMap<ComponentId, mpsc::Sender<GenericMessage<ModeRequest>>>, HashMap<ComponentId, mpsc::SyncSender<GenericMessage<CompositeRequest>>>,
pub mode_router_map: HashMap<ComponentId, mpsc::SyncSender<GenericMessage<ModeRequest>>>,
} }
impl Default for GenericRequestRouter { impl Default for GenericRequestRouter {

View File

@ -11,6 +11,8 @@ serde_json = "1"
log = "0.4" log = "0.4"
thiserror = "1" thiserror = "1"
fern = "0.5" fern = "0.5"
strum = { version = "0.26", features = ["derive"] }
num_enum = "0.7"
humantime = "2" humantime = "2"
[dependencies.asynchronix] [dependencies.asynchronix]

View File

@ -6,14 +6,17 @@ use asynchronix::{
}; };
use satrs::power::SwitchStateBinary; use satrs::power::SwitchStateBinary;
use satrs_minisim::{ use satrs_minisim::{
acs::{MgmReply, MgmSensorValues, MgtDipole, MgtHkSet, MgtReply, MGT_GEN_MAGNETIC_FIELD}, acs::{
lis3mdl::MgmLis3MdlReply, MgmReplyCommon, MgmReplyProvider, MgmSensorValuesMicroTesla,
MgtDipole, MgtHkSet, MgtReply, MGT_GEN_MAGNETIC_FIELD,
},
SimReply, SimReply,
}; };
use crate::time::current_millis; use crate::time::current_millis;
// Earth magnetic field varies between -30 uT and 30 uT // Earth magnetic field varies between roughly -30 uT and 30 uT
const AMPLITUDE_MGM: f32 = 0.03; const AMPLITUDE_MGM_UT: f32 = 30.0;
// Lets start with a simple frequency here. // Lets start with a simple frequency here.
const FREQUENCY_MGM: f32 = 1.0; const FREQUENCY_MGM: f32 = 1.0;
const PHASE_X: f32 = 0.0; const PHASE_X: f32 = 0.0;
@ -23,38 +26,37 @@ const PHASE_Z: f32 = 0.2;
/// Simple model for a magnetometer where the measure magnetic fields are modeled with sine waves. /// Simple model for a magnetometer where the measure magnetic fields are modeled with sine waves.
/// ///
/// Please note that that a more realistic MGM model wouold include the following components /// An ideal sensor would sample the magnetic field at a high fixed rate. This might not be
/// which are not included here to simplify the model: /// possible for a general purpose OS, but self self-sampling at a relatively high rate (20-40 ms)
/// /// might still be possible and is probably sufficient for many OBSW needs.
/// 1. It would probably generate signed [i16] values which need to be converted to SI units pub struct MagnetometerModel<ReplyProvider: MgmReplyProvider> {
/// because it is a digital sensor
/// 2. It would sample the magnetic field at a high fixed rate. This might not be possible for
/// a general purpose OS, but self self-sampling at a relatively high rate (20-40 ms) might
/// stil lbe possible.
pub struct MagnetometerModel {
pub switch_state: SwitchStateBinary, pub switch_state: SwitchStateBinary,
pub periodicity: Duration, pub periodicity: Duration,
pub external_mag_field: Option<MgmSensorValues>, pub external_mag_field: Option<MgmSensorValuesMicroTesla>,
pub reply_sender: mpsc::Sender<SimReply>, pub reply_sender: mpsc::Sender<SimReply>,
pub phatom: std::marker::PhantomData<ReplyProvider>,
} }
impl MagnetometerModel { impl MagnetometerModel<MgmLis3MdlReply> {
pub fn new(periodicity: Duration, reply_sender: mpsc::Sender<SimReply>) -> Self { pub fn new_for_lis3mdl(periodicity: Duration, reply_sender: mpsc::Sender<SimReply>) -> Self {
Self { Self {
switch_state: SwitchStateBinary::Off, switch_state: SwitchStateBinary::Off,
periodicity, periodicity,
external_mag_field: None, external_mag_field: None,
reply_sender, reply_sender,
phatom: std::marker::PhantomData,
} }
} }
}
impl<ReplyProvider: MgmReplyProvider> MagnetometerModel<ReplyProvider> {
pub async fn switch_device(&mut self, switch_state: SwitchStateBinary) { pub async fn switch_device(&mut self, switch_state: SwitchStateBinary) {
self.switch_state = switch_state; self.switch_state = switch_state;
} }
pub async fn send_sensor_values(&mut self, _: (), scheduler: &Scheduler<Self>) { pub async fn send_sensor_values(&mut self, _: (), scheduler: &Scheduler<Self>) {
self.reply_sender self.reply_sender
.send(SimReply::new(MgmReply { .send(ReplyProvider::create_mgm_reply(MgmReplyCommon {
switch_state: self.switch_state, switch_state: self.switch_state,
sensor_values: self.calculate_current_mgm_tuple(current_millis(scheduler.time())), sensor_values: self.calculate_current_mgm_tuple(current_millis(scheduler.time())),
})) }))
@ -63,23 +65,23 @@ impl MagnetometerModel {
// Devices like magnetorquers generate a strong magnetic field which overrides the default // Devices like magnetorquers generate a strong magnetic field which overrides the default
// model for the measured magnetic field. // model for the measured magnetic field.
pub async fn apply_external_magnetic_field(&mut self, field: MgmSensorValues) { pub async fn apply_external_magnetic_field(&mut self, field: MgmSensorValuesMicroTesla) {
self.external_mag_field = Some(field); self.external_mag_field = Some(field);
} }
fn calculate_current_mgm_tuple(&self, time_ms: u64) -> MgmSensorValues { fn calculate_current_mgm_tuple(&self, time_ms: u64) -> MgmSensorValuesMicroTesla {
if SwitchStateBinary::On == self.switch_state { if SwitchStateBinary::On == self.switch_state {
if let Some(ext_field) = self.external_mag_field { if let Some(ext_field) = self.external_mag_field {
return ext_field; return ext_field;
} }
let base_sin_val = 2.0 * PI * FREQUENCY_MGM * (time_ms as f32 / 1000.0); let base_sin_val = 2.0 * PI * FREQUENCY_MGM * (time_ms as f32 / 1000.0);
return MgmSensorValues { return MgmSensorValuesMicroTesla {
x: AMPLITUDE_MGM * (base_sin_val + PHASE_X).sin(), x: AMPLITUDE_MGM_UT * (base_sin_val + PHASE_X).sin(),
y: AMPLITUDE_MGM * (base_sin_val + PHASE_Y).sin(), y: AMPLITUDE_MGM_UT * (base_sin_val + PHASE_Y).sin(),
z: AMPLITUDE_MGM * (base_sin_val + PHASE_Z).sin(), z: AMPLITUDE_MGM_UT * (base_sin_val + PHASE_Z).sin(),
}; };
} }
MgmSensorValues { MgmSensorValuesMicroTesla {
x: 0.0, x: 0.0,
y: 0.0, y: 0.0,
z: 0.0, z: 0.0,
@ -87,13 +89,13 @@ impl MagnetometerModel {
} }
} }
impl Model for MagnetometerModel {} impl<ReplyProvider: MgmReplyProvider> Model for MagnetometerModel<ReplyProvider> {}
pub struct MagnetorquerModel { pub struct MagnetorquerModel {
switch_state: SwitchStateBinary, switch_state: SwitchStateBinary,
torquing: bool, torquing: bool,
torque_dipole: MgtDipole, torque_dipole: MgtDipole,
pub gen_magnetic_field: Output<MgmSensorValues>, pub gen_magnetic_field: Output<MgmSensorValuesMicroTesla>,
reply_sender: mpsc::Sender<SimReply>, reply_sender: mpsc::Sender<SimReply>,
} }
@ -146,14 +148,14 @@ impl MagnetorquerModel {
pub fn send_housekeeping_data(&mut self) { pub fn send_housekeeping_data(&mut self) {
self.reply_sender self.reply_sender
.send(SimReply::new(MgtReply::Hk(MgtHkSet { .send(SimReply::new(&MgtReply::Hk(MgtHkSet {
dipole: self.torque_dipole, dipole: self.torque_dipole,
torquing: self.torquing, torquing: self.torquing,
}))) })))
.unwrap(); .unwrap();
} }
fn calc_magnetic_field(&self, _: MgtDipole) -> MgmSensorValues { fn calc_magnetic_field(&self, _: MgtDipole) -> MgmSensorValuesMicroTesla {
// Simplified model: Just returns some fixed magnetic field for now. // Simplified model: Just returns some fixed magnetic field for now.
// Later, we could make this more fancy by incorporating the commanded dipole. // Later, we could make this more fancy by incorporating the commanded dipole.
MGT_GEN_MAGNETIC_FIELD MGT_GEN_MAGNETIC_FIELD
@ -179,9 +181,12 @@ pub mod tests {
use satrs::power::SwitchStateBinary; use satrs::power::SwitchStateBinary;
use satrs_minisim::{ use satrs_minisim::{
acs::{MgmReply, MgmRequest, MgtDipole, MgtHkSet, MgtReply, MgtRequest}, acs::{
lis3mdl::{self, MgmLis3MdlReply},
MgmRequestLis3Mdl, MgtDipole, MgtHkSet, MgtReply, MgtRequest,
},
eps::PcduSwitch, eps::PcduSwitch,
SerializableSimMsgPayload, SimMessageProvider, SimRequest, SimTarget, SerializableSimMsgPayload, SimComponent, SimMessageProvider, SimRequest,
}; };
use crate::{eps::tests::switch_device_on, test_helpers::SimTestbench}; use crate::{eps::tests::switch_device_on, test_helpers::SimTestbench};
@ -189,7 +194,7 @@ pub mod tests {
#[test] #[test]
fn test_basic_mgm_request() { fn test_basic_mgm_request() {
let mut sim_testbench = SimTestbench::new(); let mut sim_testbench = SimTestbench::new();
let request = SimRequest::new_with_epoch_time(MgmRequest::RequestSensorData); let request = SimRequest::new_with_epoch_time(MgmRequestLis3Mdl::RequestSensorData);
sim_testbench sim_testbench
.send_request(request) .send_request(request)
.expect("sending MGM request failed"); .expect("sending MGM request failed");
@ -198,13 +203,13 @@ pub mod tests {
let sim_reply = sim_testbench.try_receive_next_reply(); let sim_reply = sim_testbench.try_receive_next_reply();
assert!(sim_reply.is_some()); assert!(sim_reply.is_some());
let sim_reply = sim_reply.unwrap(); let sim_reply = sim_reply.unwrap();
assert_eq!(sim_reply.target(), SimTarget::Mgm); assert_eq!(sim_reply.component(), SimComponent::MgmLis3Mdl);
let reply = MgmReply::from_sim_message(&sim_reply) let reply = MgmLis3MdlReply::from_sim_message(&sim_reply)
.expect("failed to deserialize MGM sensor values"); .expect("failed to deserialize MGM sensor values");
assert_eq!(reply.switch_state, SwitchStateBinary::Off); assert_eq!(reply.common.switch_state, SwitchStateBinary::Off);
assert_eq!(reply.sensor_values.x, 0.0); assert_eq!(reply.common.sensor_values.x, 0.0);
assert_eq!(reply.sensor_values.y, 0.0); assert_eq!(reply.common.sensor_values.y, 0.0);
assert_eq!(reply.sensor_values.z, 0.0); assert_eq!(reply.common.sensor_values.z, 0.0);
} }
#[test] #[test]
@ -212,7 +217,7 @@ pub mod tests {
let mut sim_testbench = SimTestbench::new(); let mut sim_testbench = SimTestbench::new();
switch_device_on(&mut sim_testbench, PcduSwitch::Mgm); switch_device_on(&mut sim_testbench, PcduSwitch::Mgm);
let mut request = SimRequest::new_with_epoch_time(MgmRequest::RequestSensorData); let mut request = SimRequest::new_with_epoch_time(MgmRequestLis3Mdl::RequestSensorData);
sim_testbench sim_testbench
.send_request(request) .send_request(request)
.expect("sending MGM request failed"); .expect("sending MGM request failed");
@ -221,12 +226,12 @@ pub mod tests {
let mut sim_reply_res = sim_testbench.try_receive_next_reply(); let mut sim_reply_res = sim_testbench.try_receive_next_reply();
assert!(sim_reply_res.is_some()); assert!(sim_reply_res.is_some());
let mut sim_reply = sim_reply_res.unwrap(); let mut sim_reply = sim_reply_res.unwrap();
assert_eq!(sim_reply.target(), SimTarget::Mgm); assert_eq!(sim_reply.component(), SimComponent::MgmLis3Mdl);
let first_reply = MgmReply::from_sim_message(&sim_reply) let first_reply = MgmLis3MdlReply::from_sim_message(&sim_reply)
.expect("failed to deserialize MGM sensor values"); .expect("failed to deserialize MGM sensor values");
sim_testbench.step_by(Duration::from_millis(50)); sim_testbench.step_by(Duration::from_millis(50));
request = SimRequest::new_with_epoch_time(MgmRequest::RequestSensorData); request = SimRequest::new_with_epoch_time(MgmRequestLis3Mdl::RequestSensorData);
sim_testbench sim_testbench
.send_request(request) .send_request(request)
.expect("sending MGM request failed"); .expect("sending MGM request failed");
@ -236,8 +241,24 @@ pub mod tests {
assert!(sim_reply_res.is_some()); assert!(sim_reply_res.is_some());
sim_reply = sim_reply_res.unwrap(); sim_reply = sim_reply_res.unwrap();
let second_reply = MgmReply::from_sim_message(&sim_reply) let second_reply = MgmLis3MdlReply::from_sim_message(&sim_reply)
.expect("failed to deserialize MGM sensor values"); .expect("failed to deserialize MGM sensor values");
let x_conv_back = second_reply.raw.x as f32
* lis3mdl::FIELD_LSB_PER_GAUSS_4_SENS
* lis3mdl::GAUSS_TO_MICROTESLA_FACTOR as f32;
let y_conv_back = second_reply.raw.y as f32
* lis3mdl::FIELD_LSB_PER_GAUSS_4_SENS
* lis3mdl::GAUSS_TO_MICROTESLA_FACTOR as f32;
let z_conv_back = second_reply.raw.z as f32
* lis3mdl::FIELD_LSB_PER_GAUSS_4_SENS
* lis3mdl::GAUSS_TO_MICROTESLA_FACTOR as f32;
let diff_x = (second_reply.common.sensor_values.x - x_conv_back).abs();
assert!(diff_x < 0.01, "diff x too large: {}", diff_x);
let diff_y = (second_reply.common.sensor_values.y - y_conv_back).abs();
assert!(diff_y < 0.01, "diff y too large: {}", diff_y);
let diff_z = (second_reply.common.sensor_values.z - z_conv_back).abs();
assert!(diff_z < 0.01, "diff z too large: {}", diff_z);
// assert_eq!(second_reply.raw_reply, SwitchStateBinary::On);
// Check that the values are changing. // Check that the values are changing.
assert!(first_reply != second_reply); assert!(first_reply != second_reply);
} }

View File

@ -5,10 +5,10 @@ use asynchronix::{
time::{Clock, MonotonicTime, SystemClock}, time::{Clock, MonotonicTime, SystemClock},
}; };
use satrs_minisim::{ use satrs_minisim::{
acs::{MgmRequest, MgtRequest}, acs::{lis3mdl::MgmLis3MdlReply, MgmRequestLis3Mdl, MgtRequest},
eps::PcduRequest, eps::PcduRequest,
SerializableSimMsgPayload, SimCtrlReply, SimCtrlRequest, SimMessageProvider, SimReply, SerializableSimMsgPayload, SimComponent, SimCtrlReply, SimCtrlRequest, SimMessageProvider,
SimRequest, SimRequestError, SimTarget, SimReply, SimRequest, SimRequestError,
}; };
use crate::{ use crate::{
@ -16,13 +16,18 @@ use crate::{
eps::PcduModel, eps::PcduModel,
}; };
const SIM_CTRL_REQ_WIRETAPPING: bool = true;
const MGM_REQ_WIRETAPPING: bool = true;
const PCDU_REQ_WIRETAPPING: bool = true;
const MGT_REQ_WIRETAPPING: bool = true;
// The simulation controller processes requests and drives the simulation. // The simulation controller processes requests and drives the simulation.
pub struct SimController { pub struct SimController {
pub sys_clock: SystemClock, pub sys_clock: SystemClock,
pub request_receiver: mpsc::Receiver<SimRequest>, pub request_receiver: mpsc::Receiver<SimRequest>,
pub reply_sender: mpsc::Sender<SimReply>, pub reply_sender: mpsc::Sender<SimReply>,
pub simulation: Simulation, pub simulation: Simulation,
pub mgm_addr: Address<MagnetometerModel>, pub mgm_addr: Address<MagnetometerModel<MgmLis3MdlReply>>,
pub pcdu_addr: Address<PcduModel>, pub pcdu_addr: Address<PcduModel>,
pub mgt_addr: Address<MagnetorquerModel>, pub mgt_addr: Address<MagnetorquerModel>,
} }
@ -33,7 +38,7 @@ impl SimController {
request_receiver: mpsc::Receiver<SimRequest>, request_receiver: mpsc::Receiver<SimRequest>,
reply_sender: mpsc::Sender<SimReply>, reply_sender: mpsc::Sender<SimReply>,
simulation: Simulation, simulation: Simulation,
mgm_addr: Address<MagnetometerModel>, mgm_addr: Address<MagnetometerModel<MgmLis3MdlReply>>,
pcdu_addr: Address<PcduModel>, pcdu_addr: Address<PcduModel>,
mgt_addr: Address<MagnetorquerModel>, mgt_addr: Address<MagnetorquerModel>,
) -> Self { ) -> Self {
@ -70,11 +75,11 @@ impl SimController {
if request.timestamp < old_timestamp { if request.timestamp < old_timestamp {
log::warn!("stale data with timestamp {:?} received", request.timestamp); log::warn!("stale data with timestamp {:?} received", request.timestamp);
} }
if let Err(e) = match request.target() { if let Err(e) = match request.component() {
SimTarget::SimCtrl => self.handle_ctrl_request(&request), SimComponent::SimCtrl => self.handle_ctrl_request(&request),
SimTarget::Mgm => self.handle_mgm_request(&request), SimComponent::MgmLis3Mdl => self.handle_mgm_request(&request),
SimTarget::Mgt => self.handle_mgt_request(&request), SimComponent::Mgt => self.handle_mgt_request(&request),
SimTarget::Pcdu => self.handle_pcdu_request(&request), SimComponent::Pcdu => self.handle_pcdu_request(&request),
} { } {
self.handle_invalid_request_with_valid_target(e, &request) self.handle_invalid_request_with_valid_target(e, &request)
} }
@ -91,19 +96,26 @@ impl SimController {
fn handle_ctrl_request(&mut self, request: &SimRequest) -> Result<(), SimRequestError> { fn handle_ctrl_request(&mut self, request: &SimRequest) -> Result<(), SimRequestError> {
let sim_ctrl_request = SimCtrlRequest::from_sim_message(request)?; let sim_ctrl_request = SimCtrlRequest::from_sim_message(request)?;
if SIM_CTRL_REQ_WIRETAPPING {
log::info!("received sim ctrl request: {:?}", sim_ctrl_request);
}
match sim_ctrl_request { match sim_ctrl_request {
SimCtrlRequest::Ping => { SimCtrlRequest::Ping => {
self.reply_sender self.reply_sender
.send(SimReply::new(SimCtrlReply::Pong)) .send(SimReply::new(&SimCtrlReply::Pong))
.expect("sending reply from sim controller failed"); .expect("sending reply from sim controller failed");
} }
} }
Ok(()) Ok(())
} }
fn handle_mgm_request(&mut self, request: &SimRequest) -> Result<(), SimRequestError> { fn handle_mgm_request(&mut self, request: &SimRequest) -> Result<(), SimRequestError> {
let mgm_request = MgmRequest::from_sim_message(request)?; let mgm_request = MgmRequestLis3Mdl::from_sim_message(request)?;
if MGM_REQ_WIRETAPPING {
log::info!("received MGM request: {:?}", mgm_request);
}
match mgm_request { match mgm_request {
MgmRequest::RequestSensorData => { MgmRequestLis3Mdl::RequestSensorData => {
self.simulation.send_event( self.simulation.send_event(
MagnetometerModel::send_sensor_values, MagnetometerModel::send_sensor_values,
(), (),
@ -116,6 +128,9 @@ impl SimController {
fn handle_pcdu_request(&mut self, request: &SimRequest) -> Result<(), SimRequestError> { fn handle_pcdu_request(&mut self, request: &SimRequest) -> Result<(), SimRequestError> {
let pcdu_request = PcduRequest::from_sim_message(request)?; let pcdu_request = PcduRequest::from_sim_message(request)?;
if PCDU_REQ_WIRETAPPING {
log::info!("received PCDU request: {:?}", pcdu_request);
}
match pcdu_request { match pcdu_request {
PcduRequest::RequestSwitchInfo => { PcduRequest::RequestSwitchInfo => {
self.simulation self.simulation
@ -134,6 +149,9 @@ impl SimController {
fn handle_mgt_request(&mut self, request: &SimRequest) -> Result<(), SimRequestError> { fn handle_mgt_request(&mut self, request: &SimRequest) -> Result<(), SimRequestError> {
let mgt_request = MgtRequest::from_sim_message(request)?; let mgt_request = MgtRequest::from_sim_message(request)?;
if MGT_REQ_WIRETAPPING {
log::info!("received MGT request: {:?}", mgt_request);
}
match mgt_request { match mgt_request {
MgtRequest::ApplyTorque { duration, dipole } => self.simulation.send_event( MgtRequest::ApplyTorque { duration, dipole } => self.simulation.send_event(
MagnetorquerModel::apply_torque, MagnetorquerModel::apply_torque,
@ -156,11 +174,11 @@ impl SimController {
) { ) {
log::warn!( log::warn!(
"received invalid {:?} request: {:?}", "received invalid {:?} request: {:?}",
request.target(), request.component(),
error error
); );
self.reply_sender self.reply_sender
.send(SimReply::new(SimCtrlReply::from(error))) .send(SimReply::new(&SimCtrlReply::from(error)))
.expect("sending reply from sim controller failed"); .expect("sending reply from sim controller failed");
} }
} }
@ -183,7 +201,7 @@ mod tests {
let sim_reply = sim_testbench.try_receive_next_reply(); let sim_reply = sim_testbench.try_receive_next_reply();
assert!(sim_reply.is_some()); assert!(sim_reply.is_some());
let sim_reply = sim_reply.unwrap(); let sim_reply = sim_reply.unwrap();
assert_eq!(sim_reply.target(), SimTarget::SimCtrl); assert_eq!(sim_reply.component(), SimComponent::SimCtrl);
let reply = SimCtrlReply::from_sim_message(&sim_reply) let reply = SimCtrlReply::from_sim_message(&sim_reply)
.expect("failed to deserialize MGM sensor values"); .expect("failed to deserialize MGM sensor values");
assert_eq!(reply, SimCtrlReply::Pong); assert_eq!(reply, SimCtrlReply::Pong);

View File

@ -1,4 +1,4 @@
use std::{collections::HashMap, sync::mpsc, time::Duration}; use std::{sync::mpsc, time::Duration};
use asynchronix::{ use asynchronix::{
model::{Model, Output}, model::{Model, Output},
@ -6,14 +6,14 @@ use asynchronix::{
}; };
use satrs::power::SwitchStateBinary; use satrs::power::SwitchStateBinary;
use satrs_minisim::{ use satrs_minisim::{
eps::{PcduReply, PcduSwitch, SwitchMap}, eps::{PcduReply, PcduSwitch, SwitchMapBinaryWrapper},
SimReply, SimReply,
}; };
pub const SWITCH_INFO_DELAY_MS: u64 = 10; pub const SWITCH_INFO_DELAY_MS: u64 = 10;
pub struct PcduModel { pub struct PcduModel {
pub switcher_map: SwitchMap, pub switcher_map: SwitchMapBinaryWrapper,
pub mgm_switch: Output<SwitchStateBinary>, pub mgm_switch: Output<SwitchStateBinary>,
pub mgt_switch: Output<SwitchStateBinary>, pub mgt_switch: Output<SwitchStateBinary>,
pub reply_sender: mpsc::Sender<SimReply>, pub reply_sender: mpsc::Sender<SimReply>,
@ -21,12 +21,8 @@ pub struct PcduModel {
impl PcduModel { impl PcduModel {
pub fn new(reply_sender: mpsc::Sender<SimReply>) -> Self { pub fn new(reply_sender: mpsc::Sender<SimReply>) -> Self {
let mut switcher_map = HashMap::new();
switcher_map.insert(PcduSwitch::Mgm, SwitchStateBinary::Off);
switcher_map.insert(PcduSwitch::Mgt, SwitchStateBinary::Off);
Self { Self {
switcher_map, switcher_map: Default::default(),
mgm_switch: Output::new(), mgm_switch: Output::new(),
mgt_switch: Output::new(), mgt_switch: Output::new(),
reply_sender, reply_sender,
@ -44,7 +40,7 @@ impl PcduModel {
} }
pub fn send_switch_info(&mut self) { pub fn send_switch_info(&mut self) {
let reply = SimReply::new(PcduReply::SwitchInfo(self.switcher_map.clone())); let reply = SimReply::new(&PcduReply::SwitchInfo(self.switcher_map.0.clone()));
self.reply_sender.send(reply).unwrap(); self.reply_sender.send(reply).unwrap();
} }
@ -54,6 +50,7 @@ impl PcduModel {
) { ) {
let val = self let val = self
.switcher_map .switcher_map
.0
.get_mut(&switch_and_target_state.0) .get_mut(&switch_and_target_state.0)
.unwrap_or_else(|| panic!("switch {:?} not found", switch_and_target_state.0)); .unwrap_or_else(|| panic!("switch {:?} not found", switch_and_target_state.0));
*val = switch_and_target_state.1; *val = switch_and_target_state.1;
@ -76,7 +73,8 @@ pub(crate) mod tests {
use std::time::Duration; use std::time::Duration;
use satrs_minisim::{ use satrs_minisim::{
eps::PcduRequest, SerializableSimMsgPayload, SimMessageProvider, SimRequest, SimTarget, eps::{PcduRequest, SwitchMapBinary},
SerializableSimMsgPayload, SimComponent, SimMessageProvider, SimRequest,
}; };
use crate::test_helpers::SimTestbench; use crate::test_helpers::SimTestbench;
@ -105,14 +103,11 @@ pub(crate) mod tests {
switch_device(sim_testbench, switch, SwitchStateBinary::On); switch_device(sim_testbench, switch, SwitchStateBinary::On);
} }
pub(crate) fn get_all_off_switch_map() -> SwitchMap { pub(crate) fn get_all_off_switch_map() -> SwitchMapBinary {
let mut switcher_map = SwitchMap::new(); SwitchMapBinaryWrapper::default().0
switcher_map.insert(super::PcduSwitch::Mgm, super::SwitchStateBinary::Off);
switcher_map.insert(super::PcduSwitch::Mgt, super::SwitchStateBinary::Off);
switcher_map
} }
fn check_switch_state(sim_testbench: &mut SimTestbench, expected_switch_map: &SwitchMap) { fn check_switch_state(sim_testbench: &mut SimTestbench, expected_switch_map: &SwitchMapBinary) {
let request = SimRequest::new_with_epoch_time(PcduRequest::RequestSwitchInfo); let request = SimRequest::new_with_epoch_time(PcduRequest::RequestSwitchInfo);
sim_testbench sim_testbench
.send_request(request) .send_request(request)
@ -122,7 +117,7 @@ pub(crate) mod tests {
let sim_reply = sim_testbench.try_receive_next_reply(); let sim_reply = sim_testbench.try_receive_next_reply();
assert!(sim_reply.is_some()); assert!(sim_reply.is_some());
let sim_reply = sim_reply.unwrap(); let sim_reply = sim_reply.unwrap();
assert_eq!(sim_reply.target(), SimTarget::Pcdu); assert_eq!(sim_reply.component(), SimComponent::Pcdu);
let pcdu_reply = PcduReply::from_sim_message(&sim_reply) let pcdu_reply = PcduReply::from_sim_message(&sim_reply)
.expect("failed to deserialize PCDU switch info"); .expect("failed to deserialize PCDU switch info");
match pcdu_reply { match pcdu_reply {
@ -157,7 +152,7 @@ pub(crate) mod tests {
let sim_reply = sim_testbench.try_receive_next_reply(); let sim_reply = sim_testbench.try_receive_next_reply();
assert!(sim_reply.is_some()); assert!(sim_reply.is_some());
let sim_reply = sim_reply.unwrap(); let sim_reply = sim_reply.unwrap();
assert_eq!(sim_reply.target(), SimTarget::Pcdu); assert_eq!(sim_reply.component(), SimComponent::Pcdu);
let pcdu_reply = PcduReply::from_sim_message(&sim_reply) let pcdu_reply = PcduReply::from_sim_message(&sim_reply)
.expect("failed to deserialize PCDU switch info"); .expect("failed to deserialize PCDU switch info");
match pcdu_reply { match pcdu_reply {

View File

@ -1,19 +1,18 @@
use asynchronix::time::MonotonicTime; use asynchronix::time::MonotonicTime;
use num_enum::{IntoPrimitive, TryFromPrimitive};
use serde::{de::DeserializeOwned, Deserialize, Serialize}; use serde::{de::DeserializeOwned, Deserialize, Serialize};
pub const SIM_CTRL_UDP_PORT: u16 = 7303; #[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)]
pub enum SimComponent {
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum SimTarget {
SimCtrl, SimCtrl,
Mgm, MgmLis3Mdl,
Mgt, Mgt,
Pcdu, Pcdu,
} }
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SimMessage { pub struct SimMessage {
pub target: SimTarget, pub target: SimComponent,
pub payload: String, pub payload: String,
} }
@ -37,10 +36,10 @@ pub enum SimMessageType {
pub trait SerializableSimMsgPayload<P: SimMessageProvider>: pub trait SerializableSimMsgPayload<P: SimMessageProvider>:
Serialize + DeserializeOwned + Sized Serialize + DeserializeOwned + Sized
{ {
const TARGET: SimTarget; const TARGET: SimComponent;
fn from_sim_message(sim_message: &P) -> Result<Self, SimMessageError<P>> { fn from_sim_message(sim_message: &P) -> Result<Self, SimMessageError<P>> {
if sim_message.target() == Self::TARGET { if sim_message.component() == Self::TARGET {
return Ok(serde_json::from_str(sim_message.payload())?); return Ok(serde_json::from_str(sim_message.payload())?);
} }
Err(SimMessageError::TargetRequestMissmatch(sim_message.clone())) Err(SimMessageError::TargetRequestMissmatch(sim_message.clone()))
@ -49,7 +48,7 @@ pub trait SerializableSimMsgPayload<P: SimMessageProvider>:
pub trait SimMessageProvider: Serialize + DeserializeOwned + Clone + Sized { pub trait SimMessageProvider: Serialize + DeserializeOwned + Clone + Sized {
fn msg_type(&self) -> SimMessageType; fn msg_type(&self) -> SimMessageType;
fn target(&self) -> SimTarget; fn component(&self) -> SimComponent;
fn payload(&self) -> &String; fn payload(&self) -> &String;
fn from_raw_data(data: &[u8]) -> serde_json::Result<Self> { fn from_raw_data(data: &[u8]) -> serde_json::Result<Self> {
serde_json::from_slice(data) serde_json::from_slice(data)
@ -78,7 +77,7 @@ impl SimRequest {
} }
impl SimMessageProvider for SimRequest { impl SimMessageProvider for SimRequest {
fn target(&self) -> SimTarget { fn component(&self) -> SimComponent {
self.inner.target self.inner.target
} }
fn payload(&self) -> &String { fn payload(&self) -> &String {
@ -91,25 +90,25 @@ impl SimMessageProvider for SimRequest {
} }
/// A generic simulation reply type. Right now, the payload data is expected to be /// A generic simulation reply type. Right now, the payload data is expected to be
/// JSON, which might be changed inthe future. /// JSON, which might be changed in the future.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SimReply { pub struct SimReply {
inner: SimMessage, inner: SimMessage,
} }
impl SimReply { impl SimReply {
pub fn new<T: SerializableSimMsgPayload<SimReply>>(serializable_reply: T) -> Self { pub fn new<T: SerializableSimMsgPayload<SimReply>>(serializable_reply: &T) -> Self {
Self { Self {
inner: SimMessage { inner: SimMessage {
target: T::TARGET, target: T::TARGET,
payload: serde_json::to_string(&serializable_reply).unwrap(), payload: serde_json::to_string(serializable_reply).unwrap(),
}, },
} }
} }
} }
impl SimMessageProvider for SimReply { impl SimMessageProvider for SimReply {
fn target(&self) -> SimTarget { fn component(&self) -> SimComponent {
self.inner.target self.inner.target
} }
fn payload(&self) -> &String { fn payload(&self) -> &String {
@ -126,7 +125,7 @@ pub enum SimCtrlRequest {
} }
impl SerializableSimMsgPayload<SimRequest> for SimCtrlRequest { impl SerializableSimMsgPayload<SimRequest> for SimCtrlRequest {
const TARGET: SimTarget = SimTarget::SimCtrl; const TARGET: SimComponent = SimComponent::SimCtrl;
} }
pub type SimReplyError = SimMessageError<SimReply>; pub type SimReplyError = SimMessageError<SimReply>;
@ -151,7 +150,7 @@ pub enum SimCtrlReply {
} }
impl SerializableSimMsgPayload<SimReply> for SimCtrlReply { impl SerializableSimMsgPayload<SimReply> for SimCtrlReply {
const TARGET: SimTarget = SimTarget::SimCtrl; const TARGET: SimComponent = SimComponent::SimCtrl;
} }
impl From<SimRequestError> for SimCtrlReply { impl From<SimRequestError> for SimCtrlReply {
@ -162,18 +161,81 @@ impl From<SimRequestError> for SimCtrlReply {
pub mod eps { pub mod eps {
use super::*; use super::*;
use satrs::power::{SwitchState, SwitchStateBinary};
use std::collections::HashMap; use std::collections::HashMap;
use strum::{EnumIter, IntoEnumIterator};
use satrs::power::SwitchStateBinary; pub type SwitchMap = HashMap<PcduSwitch, SwitchState>;
pub type SwitchMapBinary = HashMap<PcduSwitch, SwitchStateBinary>;
pub type SwitchMap = HashMap<PcduSwitch, SwitchStateBinary>; pub struct SwitchMapWrapper(pub SwitchMap);
pub struct SwitchMapBinaryWrapper(pub SwitchMapBinary);
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)] #[derive(
Debug,
Copy,
Clone,
PartialEq,
Eq,
Serialize,
Deserialize,
Hash,
EnumIter,
IntoPrimitive,
TryFromPrimitive,
)]
#[repr(u16)]
pub enum PcduSwitch { pub enum PcduSwitch {
Mgm = 0, Mgm = 0,
Mgt = 1, Mgt = 1,
} }
impl Default for SwitchMapBinaryWrapper {
fn default() -> Self {
let mut switch_map = SwitchMapBinary::default();
for entry in PcduSwitch::iter() {
switch_map.insert(entry, SwitchStateBinary::Off);
}
Self(switch_map)
}
}
impl Default for SwitchMapWrapper {
fn default() -> Self {
let mut switch_map = SwitchMap::default();
for entry in PcduSwitch::iter() {
switch_map.insert(entry, SwitchState::Unknown);
}
Self(switch_map)
}
}
impl SwitchMapWrapper {
pub fn new_with_init_switches_off() -> Self {
let mut switch_map = SwitchMap::default();
for entry in PcduSwitch::iter() {
switch_map.insert(entry, SwitchState::Off);
}
Self(switch_map)
}
pub fn from_binary_switch_map_ref(switch_map: &SwitchMapBinary) -> Self {
Self(
switch_map
.iter()
.map(|(key, value)| (*key, SwitchState::from(*value)))
.collect(),
)
}
}
#[derive(Debug, Copy, Clone)]
#[repr(u8)]
pub enum PcduRequestId {
SwitchDevice = 0,
RequestSwitchInfo = 1,
}
#[derive(Debug, Copy, Clone, Serialize, Deserialize)] #[derive(Debug, Copy, Clone, Serialize, Deserialize)]
pub enum PcduRequest { pub enum PcduRequest {
SwitchDevice { SwitchDevice {
@ -184,16 +246,17 @@ pub mod eps {
} }
impl SerializableSimMsgPayload<SimRequest> for PcduRequest { impl SerializableSimMsgPayload<SimRequest> for PcduRequest {
const TARGET: SimTarget = SimTarget::Pcdu; const TARGET: SimComponent = SimComponent::Pcdu;
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum PcduReply { pub enum PcduReply {
SwitchInfo(SwitchMap), // Ack,
SwitchInfo(SwitchMapBinary),
} }
impl SerializableSimMsgPayload<SimReply> for PcduReply { impl SerializableSimMsgPayload<SimReply> for PcduReply {
const TARGET: SimTarget = SimTarget::Pcdu; const TARGET: SimComponent = SimComponent::Pcdu;
} }
} }
@ -204,40 +267,116 @@ pub mod acs {
use super::*; use super::*;
pub trait MgmReplyProvider: Send + 'static {
fn create_mgm_reply(common: MgmReplyCommon) -> SimReply;
}
#[derive(Debug, Copy, Clone, Serialize, Deserialize)] #[derive(Debug, Copy, Clone, Serialize, Deserialize)]
pub enum MgmRequest { pub enum MgmRequestLis3Mdl {
RequestSensorData, RequestSensorData,
} }
impl SerializableSimMsgPayload<SimRequest> for MgmRequest { impl SerializableSimMsgPayload<SimRequest> for MgmRequestLis3Mdl {
const TARGET: SimTarget = SimTarget::Mgm; const TARGET: SimComponent = SimComponent::MgmLis3Mdl;
} }
// Normally, small magnetometers generate their output as a signed 16 bit raw format or something // Normally, small magnetometers generate their output as a signed 16 bit raw format or something
// similar which needs to be converted to a signed float value with physical units. We will // similar which needs to be converted to a signed float value with physical units. We will
// simplify this now and generate the signed float values directly. // simplify this now and generate the signed float values directly. The unit is micro tesla.
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
pub struct MgmSensorValues { pub struct MgmSensorValuesMicroTesla {
pub x: f32, pub x: f32,
pub y: f32, pub y: f32,
pub z: f32, pub z: f32,
} }
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
pub struct MgmReply { pub struct MgmReplyCommon {
pub switch_state: SwitchStateBinary, pub switch_state: SwitchStateBinary,
pub sensor_values: MgmSensorValues, pub sensor_values: MgmSensorValuesMicroTesla,
} }
impl SerializableSimMsgPayload<SimReply> for MgmReply { pub const MGT_GEN_MAGNETIC_FIELD: MgmSensorValuesMicroTesla = MgmSensorValuesMicroTesla {
const TARGET: SimTarget = SimTarget::Mgm; x: 30.0,
} y: -30.0,
z: 30.0,
pub const MGT_GEN_MAGNETIC_FIELD: MgmSensorValues = MgmSensorValues {
x: 0.03,
y: -0.03,
z: 0.03,
}; };
pub const ALL_ONES_SENSOR_VAL: i16 = 0xffff_u16 as i16;
pub mod lis3mdl {
use super::*;
// Field data register scaling
pub const GAUSS_TO_MICROTESLA_FACTOR: u32 = 100;
pub const FIELD_LSB_PER_GAUSS_4_SENS: f32 = 1.0 / 6842.0;
pub const FIELD_LSB_PER_GAUSS_8_SENS: f32 = 1.0 / 3421.0;
pub const FIELD_LSB_PER_GAUSS_12_SENS: f32 = 1.0 / 2281.0;
pub const FIELD_LSB_PER_GAUSS_16_SENS: f32 = 1.0 / 1711.0;
#[derive(Default, Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
pub struct MgmLis3RawValues {
pub x: i16,
pub y: i16,
pub z: i16,
}
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
pub struct MgmLis3MdlReply {
pub common: MgmReplyCommon,
// Raw sensor values which are transmitted by the LIS3 device in little-endian
// order.
pub raw: MgmLis3RawValues,
}
impl MgmLis3MdlReply {
pub fn new(common: MgmReplyCommon) -> Self {
match common.switch_state {
SwitchStateBinary::Off => Self {
common,
raw: MgmLis3RawValues {
x: ALL_ONES_SENSOR_VAL,
y: ALL_ONES_SENSOR_VAL,
z: ALL_ONES_SENSOR_VAL,
},
},
SwitchStateBinary::On => {
let mut raw_reply: [u8; 7] = [0; 7];
let raw_x: i16 = (common.sensor_values.x
/ (GAUSS_TO_MICROTESLA_FACTOR as f32 * FIELD_LSB_PER_GAUSS_4_SENS))
.round() as i16;
let raw_y: i16 = (common.sensor_values.y
/ (GAUSS_TO_MICROTESLA_FACTOR as f32 * FIELD_LSB_PER_GAUSS_4_SENS))
.round() as i16;
let raw_z: i16 = (common.sensor_values.z
/ (GAUSS_TO_MICROTESLA_FACTOR as f32 * FIELD_LSB_PER_GAUSS_4_SENS))
.round() as i16;
// The first byte is a dummy byte.
raw_reply[1..3].copy_from_slice(&raw_x.to_be_bytes());
raw_reply[3..5].copy_from_slice(&raw_y.to_be_bytes());
raw_reply[5..7].copy_from_slice(&raw_z.to_be_bytes());
Self {
common,
raw: MgmLis3RawValues {
x: raw_x,
y: raw_y,
z: raw_z,
},
}
}
}
}
}
impl SerializableSimMsgPayload<SimReply> for MgmLis3MdlReply {
const TARGET: SimComponent = SimComponent::MgmLis3Mdl;
}
impl MgmReplyProvider for MgmLis3MdlReply {
fn create_mgm_reply(common: MgmReplyCommon) -> SimReply {
SimReply::new(&Self::new(common))
}
}
}
// Simple model using i16 values. // Simple model using i16 values.
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
@ -262,7 +401,7 @@ pub mod acs {
} }
impl SerializableSimMsgPayload<SimRequest> for MgtRequest { impl SerializableSimMsgPayload<SimRequest> for MgtRequest {
const TARGET: SimTarget = SimTarget::Mgt; const TARGET: SimComponent = SimComponent::Mgt;
} }
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
@ -279,78 +418,12 @@ pub mod acs {
} }
impl SerializableSimMsgPayload<SimReply> for MgtReply { impl SerializableSimMsgPayload<SimReply> for MgtReply {
const TARGET: SimTarget = SimTarget::Mgm; const TARGET: SimComponent = SimComponent::MgmLis3Mdl;
} }
} }
pub mod udp { pub mod udp {
use std::{ pub const SIM_CTRL_PORT: u16 = 7303;
net::{SocketAddr, UdpSocket},
time::Duration,
};
use thiserror::Error;
use crate::{SimReply, SimRequest};
#[derive(Error, Debug)]
pub enum ReceptionError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Serde JSON error: {0}")]
SerdeJson(#[from] serde_json::Error),
}
pub struct SimUdpClient {
socket: UdpSocket,
pub reply_buf: [u8; 4096],
}
impl SimUdpClient {
pub fn new(
server_addr: &SocketAddr,
non_blocking: bool,
read_timeot_ms: Option<u64>,
) -> std::io::Result<Self> {
let socket = UdpSocket::bind("127.0.0.1:0")?;
socket.set_nonblocking(non_blocking)?;
socket
.connect(server_addr)
.expect("could not connect to server addr");
if let Some(read_timeout) = read_timeot_ms {
// Set a read timeout so the test does not hang on failures.
socket.set_read_timeout(Some(Duration::from_millis(read_timeout)))?;
}
Ok(Self {
socket,
reply_buf: [0; 4096],
})
}
pub fn set_nonblocking(&self, non_blocking: bool) -> std::io::Result<()> {
self.socket.set_nonblocking(non_blocking)
}
pub fn set_read_timeout(&self, read_timeout_ms: u64) -> std::io::Result<()> {
self.socket
.set_read_timeout(Some(Duration::from_millis(read_timeout_ms)))
}
pub fn send_request(&self, sim_request: &SimRequest) -> std::io::Result<usize> {
self.socket.send(
&serde_json::to_vec(sim_request).expect("conversion of request to vector failed"),
)
}
pub fn recv_raw(&mut self) -> std::io::Result<usize> {
self.socket.recv(&mut self.reply_buf)
}
pub fn recv_sim_reply(&mut self) -> Result<SimReply, ReceptionError> {
let read_len = self.recv_raw()?;
Ok(serde_json::from_slice(&self.reply_buf[0..read_len])?)
}
}
} }
#[cfg(test)] #[cfg(test)]
@ -363,7 +436,7 @@ pub mod tests {
} }
impl SerializableSimMsgPayload<SimRequest> for DummyRequest { impl SerializableSimMsgPayload<SimRequest> for DummyRequest {
const TARGET: SimTarget = SimTarget::SimCtrl; const TARGET: SimComponent = SimComponent::SimCtrl;
} }
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
@ -372,13 +445,13 @@ pub mod tests {
} }
impl SerializableSimMsgPayload<SimReply> for DummyReply { impl SerializableSimMsgPayload<SimReply> for DummyReply {
const TARGET: SimTarget = SimTarget::SimCtrl; const TARGET: SimComponent = SimComponent::SimCtrl;
} }
#[test] #[test]
fn test_basic_request() { fn test_basic_request() {
let sim_request = SimRequest::new_with_epoch_time(DummyRequest::Ping); let sim_request = SimRequest::new_with_epoch_time(DummyRequest::Ping);
assert_eq!(sim_request.target(), SimTarget::SimCtrl); assert_eq!(sim_request.component(), SimComponent::SimCtrl);
assert_eq!(sim_request.msg_type(), SimMessageType::Request); assert_eq!(sim_request.msg_type(), SimMessageType::Request);
let dummy_request = let dummy_request =
DummyRequest::from_sim_message(&sim_request).expect("deserialization failed"); DummyRequest::from_sim_message(&sim_request).expect("deserialization failed");
@ -387,8 +460,8 @@ pub mod tests {
#[test] #[test]
fn test_basic_reply() { fn test_basic_reply() {
let sim_reply = SimReply::new(DummyReply::Pong); let sim_reply = SimReply::new(&DummyReply::Pong);
assert_eq!(sim_reply.target(), SimTarget::SimCtrl); assert_eq!(sim_reply.component(), SimComponent::SimCtrl);
assert_eq!(sim_reply.msg_type(), SimMessageType::Reply); assert_eq!(sim_reply.msg_type(), SimMessageType::Reply);
let dummy_request = let dummy_request =
DummyReply::from_sim_message(&sim_reply).expect("deserialization failed"); DummyReply::from_sim_message(&sim_reply).expect("deserialization failed");

View File

@ -3,7 +3,8 @@ use asynchronix::simulation::{Mailbox, SimInit};
use asynchronix::time::{MonotonicTime, SystemClock}; use asynchronix::time::{MonotonicTime, SystemClock};
use controller::SimController; use controller::SimController;
use eps::PcduModel; use eps::PcduModel;
use satrs_minisim::{SimReply, SimRequest, SIM_CTRL_UDP_PORT}; use satrs_minisim::udp::SIM_CTRL_PORT;
use satrs_minisim::{SimReply, SimRequest};
use std::sync::mpsc; use std::sync::mpsc;
use std::thread; use std::thread;
use std::time::{Duration, SystemTime}; use std::time::{Duration, SystemTime};
@ -30,7 +31,8 @@ fn create_sim_controller(
request_receiver: mpsc::Receiver<SimRequest>, request_receiver: mpsc::Receiver<SimRequest>,
) -> SimController { ) -> SimController {
// Instantiate models and their mailboxes. // Instantiate models and their mailboxes.
let mgm_model = MagnetometerModel::new(Duration::from_millis(50), reply_sender.clone()); let mgm_model =
MagnetometerModel::new_for_lis3mdl(Duration::from_millis(50), reply_sender.clone());
let mgm_mailbox = Mailbox::new(); let mgm_mailbox = Mailbox::new();
let mgm_addr = mgm_mailbox.address(); let mgm_addr = mgm_mailbox.address();
@ -112,9 +114,9 @@ fn main() {
}); });
let mut udp_server = let mut udp_server =
SimUdpServer::new(SIM_CTRL_UDP_PORT, request_sender, reply_receiver, 200, None) SimUdpServer::new(SIM_CTRL_PORT, request_sender, reply_receiver, 200, None)
.expect("could not create UDP request server"); .expect("could not create UDP request server");
log::info!("starting UDP server on port {}", SIM_CTRL_UDP_PORT); log::info!("starting UDP server on port {}", SIM_CTRL_PORT);
// This thread manages the simulator UDP server. // This thread manages the simulator UDP server.
let udp_tc_thread = thread::spawn(move || { let udp_tc_thread = thread::spawn(move || {
udp_server.run(); udp_server.run();

View File

@ -150,6 +150,7 @@ impl SimUdpServer {
mod tests { mod tests {
use std::{ use std::{
io::ErrorKind, io::ErrorKind,
net::{SocketAddr, UdpSocket},
sync::{ sync::{
atomic::{AtomicBool, Ordering}, atomic::{AtomicBool, Ordering},
mpsc, Arc, mpsc, Arc,
@ -159,7 +160,6 @@ mod tests {
use satrs_minisim::{ use satrs_minisim::{
eps::{PcduReply, PcduRequest}, eps::{PcduReply, PcduRequest},
udp::{ReceptionError, SimUdpClient},
SimCtrlReply, SimCtrlRequest, SimReply, SimRequest, SimCtrlReply, SimCtrlRequest, SimReply, SimRequest,
}; };
@ -171,8 +171,57 @@ mod tests {
// Wait time to ensure even possibly laggy systems like CI servers can run the tests. // Wait time to ensure even possibly laggy systems like CI servers can run the tests.
const SERVER_WAIT_TIME_MS: u64 = 50; const SERVER_WAIT_TIME_MS: u64 = 50;
#[derive(thiserror::Error, Debug)]
pub enum ReceptionError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Serde JSON error: {0}")]
SerdeJson(#[from] serde_json::Error),
}
pub struct SimUdpTestClient {
socket: UdpSocket,
pub reply_buf: [u8; 4096],
}
impl SimUdpTestClient {
pub fn new(
server_addr: &SocketAddr,
non_blocking: bool,
read_timeot_ms: Option<u64>,
) -> std::io::Result<Self> {
let socket = UdpSocket::bind("127.0.0.1:0")?;
socket.set_nonblocking(non_blocking)?;
socket
.connect(server_addr)
.expect("could not connect to server addr");
if let Some(read_timeout) = read_timeot_ms {
// Set a read timeout so the test does not hang on failures.
socket.set_read_timeout(Some(Duration::from_millis(read_timeout)))?;
}
Ok(Self {
socket,
reply_buf: [0; 4096],
})
}
pub fn send_request(&self, sim_request: &SimRequest) -> std::io::Result<usize> {
self.socket.send(
&serde_json::to_vec(sim_request).expect("conversion of request to vector failed"),
)
}
pub fn recv_raw(&mut self) -> std::io::Result<usize> {
self.socket.recv(&mut self.reply_buf)
}
pub fn recv_sim_reply(&mut self) -> Result<SimReply, ReceptionError> {
let read_len = self.recv_raw()?;
Ok(serde_json::from_slice(&self.reply_buf[0..read_len])?)
}
}
struct UdpTestbench { struct UdpTestbench {
client: SimUdpClient, client: SimUdpTestClient,
stop_signal: Arc<AtomicBool>, stop_signal: Arc<AtomicBool>,
request_receiver: mpsc::Receiver<SimRequest>, request_receiver: mpsc::Receiver<SimRequest>,
reply_sender: mpsc::Sender<SimReply>, reply_sender: mpsc::Sender<SimReply>,
@ -197,7 +246,7 @@ mod tests {
let server_addr = server.server_addr()?; let server_addr = server.server_addr()?;
Ok(( Ok((
Self { Self {
client: SimUdpClient::new( client: SimUdpTestClient::new(
&server_addr, &server_addr,
client_non_blocking, client_non_blocking,
client_read_timeout_ms, client_read_timeout_ms,
@ -295,7 +344,7 @@ mod tests {
.send_request(&SimRequest::new_with_epoch_time(SimCtrlRequest::Ping)) .send_request(&SimRequest::new_with_epoch_time(SimCtrlRequest::Ping))
.expect("sending request failed"); .expect("sending request failed");
let sim_reply = SimReply::new(PcduReply::SwitchInfo(get_all_off_switch_map())); let sim_reply = SimReply::new(&PcduReply::SwitchInfo(get_all_off_switch_map()));
udp_testbench.send_reply(&sim_reply); udp_testbench.send_reply(&sim_reply);
udp_testbench.check_next_sim_reply(&sim_reply); udp_testbench.check_next_sim_reply(&sim_reply);
@ -320,7 +369,7 @@ mod tests {
.expect("sending request failed"); .expect("sending request failed");
// Send a reply to the server, ensure it gets forwarded to the client. // Send a reply to the server, ensure it gets forwarded to the client.
let sim_reply = SimReply::new(PcduReply::SwitchInfo(get_all_off_switch_map())); let sim_reply = SimReply::new(&PcduReply::SwitchInfo(get_all_off_switch_map()));
udp_testbench.send_reply(&sim_reply); udp_testbench.send_reply(&sim_reply);
std::thread::sleep(Duration::from_millis(SERVER_WAIT_TIME_MS)); std::thread::sleep(Duration::from_millis(SERVER_WAIT_TIME_MS));
@ -339,7 +388,7 @@ mod tests {
let server_thread = std::thread::spawn(move || udp_server.run()); let server_thread = std::thread::spawn(move || udp_server.run());
// Send a reply to the server. The client is not connected, so it won't get forwarded. // Send a reply to the server. The client is not connected, so it won't get forwarded.
let sim_reply = SimReply::new(PcduReply::SwitchInfo(get_all_off_switch_map())); let sim_reply = SimReply::new(&PcduReply::SwitchInfo(get_all_off_switch_map()));
udp_testbench.send_reply(&sim_reply); udp_testbench.send_reply(&sim_reply);
std::thread::sleep(Duration::from_millis(10)); std::thread::sleep(Duration::from_millis(10));
@ -366,7 +415,7 @@ mod tests {
let server_thread = std::thread::spawn(move || udp_server.run()); let server_thread = std::thread::spawn(move || udp_server.run());
// The server only caches up to 3 replies. // The server only caches up to 3 replies.
let sim_reply = SimReply::new(SimCtrlReply::Pong); let sim_reply = SimReply::new(&SimCtrlReply::Pong);
for _ in 0..4 { for _ in 0..4 {
udp_testbench.send_reply(&sim_reply); udp_testbench.send_reply(&sim_reply);
} }

View File

@ -1,22 +1,17 @@
use core::time::Duration;
use derive_new::new;
#[cfg(feature = "serde")] #[cfg(feature = "serde")]
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[cfg(feature = "std")]
#[allow(unused_imports)]
pub use std_mod::*;
/// Generic trait for a device capable of switching itself on or off. use crate::request::MessageMetadata;
pub trait PowerSwitch {
type Error;
fn switch_on(&mut self) -> Result<(), Self::Error>;
fn switch_off(&mut self) -> Result<(), Self::Error>;
fn is_switch_on(&self) -> bool {
self.switch_state() == SwitchState::On
}
fn switch_state(&self) -> SwitchState;
}
#[derive(Debug, Eq, PartialEq, Copy, Clone)] #[derive(Debug, Eq, PartialEq, Copy, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum SwitchState { pub enum SwitchState {
Off = 0, Off = 0,
On = 1, On = 1,
@ -26,6 +21,7 @@ pub enum SwitchState {
#[derive(Debug, Eq, PartialEq, Copy, Clone)] #[derive(Debug, Eq, PartialEq, Copy, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum SwitchStateBinary { pub enum SwitchStateBinary {
Off = 0, Off = 0,
On = 1, On = 1,
@ -63,76 +59,254 @@ impl From<SwitchStateBinary> for SwitchState {
pub type SwitchId = u16; pub type SwitchId = u16;
/// Generic trait for a device capable of turning on and off switches. /// Generic trait for a device capable of turning on and off switches.
pub trait PowerSwitcherCommandSender { pub trait PowerSwitcherCommandSender<SwitchType: Into<u16>> {
type Error; type Error: core::fmt::Debug;
fn send_switch_on_cmd(&mut self, switch_id: SwitchId) -> Result<(), Self::Error>; fn send_switch_on_cmd(
fn send_switch_off_cmd(&mut self, switch_id: SwitchId) -> Result<(), Self::Error>; &self,
requestor_info: MessageMetadata,
switch_id: SwitchType,
) -> Result<(), Self::Error>;
fn send_switch_off_cmd(
&self,
requestor_info: MessageMetadata,
switch_id: SwitchType,
) -> Result<(), Self::Error>;
} }
pub trait PowerSwitchInfo { pub trait PowerSwitchInfo<SwitchType> {
type Error; type Error: core::fmt::Debug;
/// Retrieve the switch state /// Retrieve the switch state
fn get_switch_state(&mut self, switch_id: SwitchId) -> Result<SwitchState, Self::Error>; fn switch_state(&self, switch_id: SwitchType) -> Result<SwitchState, Self::Error>;
fn get_is_switch_on(&mut self, switch_id: SwitchId) -> Result<bool, Self::Error> { fn is_switch_on(&self, switch_id: SwitchType) -> Result<bool, Self::Error> {
Ok(self.get_switch_state(switch_id)? == SwitchState::On) Ok(self.switch_state(switch_id)? == SwitchState::On)
} }
/// The maximum delay it will take to change a switch. /// The maximum delay it will take to change a switch.
/// ///
/// This may take into account the time to send a command, wait for it to be executed, and /// This may take into account the time to send a command, wait for it to be executed, and
/// see the switch changed. /// see the switch changed.
fn switch_delay_ms(&self) -> u32; fn switch_delay_ms(&self) -> Duration;
}
#[derive(new)]
pub struct SwitchRequest {
switch_id: SwitchId,
target_state: SwitchStateBinary,
}
impl SwitchRequest {
pub fn switch_id(&self) -> SwitchId {
self.switch_id
}
pub fn target_state(&self) -> SwitchStateBinary {
self.target_state
}
}
#[cfg(feature = "std")]
pub mod std_mod {
use std::sync::mpsc;
use crate::{
queue::GenericSendError,
request::{GenericMessage, MessageMetadata},
};
use super::*;
pub type MpscSwitchCmdSender = mpsc::Sender<GenericMessage<SwitchRequest>>;
pub type MpscSwitchCmdSenderBounded = mpsc::SyncSender<GenericMessage<SwitchRequest>>;
impl<SwitchType: Into<u16>> PowerSwitcherCommandSender<SwitchType> for MpscSwitchCmdSender {
type Error = GenericSendError;
fn send_switch_on_cmd(
&self,
requestor_info: MessageMetadata,
switch_id: SwitchType,
) -> Result<(), Self::Error> {
self.send(GenericMessage::new(
requestor_info,
SwitchRequest::new(switch_id.into(), SwitchStateBinary::On),
))
.map_err(|_| GenericSendError::RxDisconnected)
}
fn send_switch_off_cmd(
&self,
requestor_info: MessageMetadata,
switch_id: SwitchType,
) -> Result<(), Self::Error> {
self.send(GenericMessage::new(
requestor_info,
SwitchRequest::new(switch_id.into(), SwitchStateBinary::Off),
))
.map_err(|_| GenericSendError::RxDisconnected)
}
}
impl<SwitchType: Into<u16>> PowerSwitcherCommandSender<SwitchType> for MpscSwitchCmdSenderBounded {
type Error = GenericSendError;
fn send_switch_on_cmd(
&self,
requestor_info: MessageMetadata,
switch_id: SwitchType,
) -> Result<(), Self::Error> {
self.try_send(GenericMessage::new(
requestor_info,
SwitchRequest::new(switch_id.into(), SwitchStateBinary::On),
))
.map_err(|e| match e {
mpsc::TrySendError::Full(_) => GenericSendError::QueueFull(None),
mpsc::TrySendError::Disconnected(_) => GenericSendError::RxDisconnected,
})
}
fn send_switch_off_cmd(
&self,
requestor_info: MessageMetadata,
switch_id: SwitchType,
) -> Result<(), Self::Error> {
self.try_send(GenericMessage::new(
requestor_info,
SwitchRequest::new(switch_id.into(), SwitchStateBinary::Off),
))
.map_err(|e| match e {
mpsc::TrySendError::Full(_) => GenericSendError::QueueFull(None),
mpsc::TrySendError::Disconnected(_) => GenericSendError::RxDisconnected,
})
}
}
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
#![allow(dead_code)] // TODO: Add unittests for PowerSwitcherCommandSender impls for mpsc.
use std::sync::mpsc::{self, TryRecvError};
use crate::{queue::GenericSendError, request::GenericMessage, ComponentId};
use super::*; use super::*;
use std::boxed::Box;
struct Pcdu { const TEST_REQ_ID: u32 = 2;
switch_rx: std::sync::mpsc::Receiver<(SwitchId, u16)>, const TEST_SENDER_ID: ComponentId = 5;
const TEST_SWITCH_ID: u16 = 0x1ff;
fn common_checks(request: &GenericMessage<SwitchRequest>) {
assert_eq!(request.requestor_info.sender_id(), TEST_SENDER_ID);
assert_eq!(request.requestor_info.request_id(), TEST_REQ_ID);
assert_eq!(request.message.switch_id(), TEST_SWITCH_ID);
} }
#[derive(Eq, PartialEq)] #[test]
enum DeviceState { fn test_comand_switch_sending_mpsc_regular_on_cmd() {
OFF, let (switch_cmd_tx, switch_cmd_rx) = mpsc::channel::<GenericMessage<SwitchRequest>>();
SwitchingPower, switch_cmd_tx
ON, .send_switch_on_cmd(
SETUP, MessageMetadata::new(TEST_REQ_ID, TEST_SENDER_ID),
IDLE, TEST_SWITCH_ID,
} )
struct MyComplexDevice { .expect("sending switch cmd failed");
power_switcher: Box<dyn PowerSwitcherCommandSender<Error = ()>>, let request = switch_cmd_rx
power_info: Box<dyn PowerSwitchInfo<Error = ()>>, .recv()
switch_id: SwitchId, .expect("receiving switch request failed");
some_state: u16, common_checks(&request);
dev_state: DeviceState, assert_eq!(request.message.target_state(), SwitchStateBinary::On);
mode: u32,
submode: u16,
} }
impl MyComplexDevice { #[test]
pub fn periodic_op(&mut self) { fn test_comand_switch_sending_mpsc_regular_off_cmd() {
// .. mode command coming in let (switch_cmd_tx, switch_cmd_rx) = mpsc::channel::<GenericMessage<SwitchRequest>>();
let mode = 1; switch_cmd_tx
if mode == 1 { .send_switch_off_cmd(
if self.dev_state == DeviceState::OFF { MessageMetadata::new(TEST_REQ_ID, TEST_SENDER_ID),
self.power_switcher TEST_SWITCH_ID,
.send_switch_on_cmd(self.switch_id) )
.expect("sending siwthc cmd failed"); .expect("sending switch cmd failed");
self.dev_state = DeviceState::SwitchingPower; let request = switch_cmd_rx
.recv()
.expect("receiving switch request failed");
common_checks(&request);
assert_eq!(request.message.target_state(), SwitchStateBinary::Off);
} }
if self.dev_state == DeviceState::SwitchingPower {
if self.power_info.get_is_switch_on(0).unwrap() { #[test]
self.dev_state = DeviceState::ON; fn test_comand_switch_sending_mpsc_regular_rx_disconnected() {
self.mode = 1; let (switch_cmd_tx, switch_cmd_rx) = mpsc::channel::<GenericMessage<SwitchRequest>>();
drop(switch_cmd_rx);
let result = switch_cmd_tx.send_switch_off_cmd(
MessageMetadata::new(TEST_REQ_ID, TEST_SENDER_ID),
TEST_SWITCH_ID,
);
assert!(result.is_err());
matches!(result.unwrap_err(), GenericSendError::RxDisconnected);
} }
#[test]
fn test_comand_switch_sending_mpsc_sync_on_cmd() {
let (switch_cmd_tx, switch_cmd_rx) = mpsc::sync_channel::<GenericMessage<SwitchRequest>>(3);
switch_cmd_tx
.send_switch_on_cmd(
MessageMetadata::new(TEST_REQ_ID, TEST_SENDER_ID),
TEST_SWITCH_ID,
)
.expect("sending switch cmd failed");
let request = switch_cmd_rx
.recv()
.expect("receiving switch request failed");
common_checks(&request);
assert_eq!(request.message.target_state(), SwitchStateBinary::On);
} }
#[test]
fn test_comand_switch_sending_mpsc_sync_off_cmd() {
let (switch_cmd_tx, switch_cmd_rx) = mpsc::sync_channel::<GenericMessage<SwitchRequest>>(3);
switch_cmd_tx
.send_switch_off_cmd(
MessageMetadata::new(TEST_REQ_ID, TEST_SENDER_ID),
TEST_SWITCH_ID,
)
.expect("sending switch cmd failed");
let request = switch_cmd_rx
.recv()
.expect("receiving switch request failed");
common_checks(&request);
assert_eq!(request.message.target_state(), SwitchStateBinary::Off);
} }
#[test]
fn test_comand_switch_sending_mpsc_sync_rx_disconnected() {
let (switch_cmd_tx, switch_cmd_rx) = mpsc::sync_channel::<GenericMessage<SwitchRequest>>(1);
drop(switch_cmd_rx);
let result = switch_cmd_tx.send_switch_off_cmd(
MessageMetadata::new(TEST_REQ_ID, TEST_SENDER_ID),
TEST_SWITCH_ID,
);
assert!(result.is_err());
matches!(result.unwrap_err(), GenericSendError::RxDisconnected);
} }
#[test]
fn test_comand_switch_sending_mpsc_sync_queue_full() {
let (switch_cmd_tx, switch_cmd_rx) = mpsc::sync_channel::<GenericMessage<SwitchRequest>>(1);
let mut result = switch_cmd_tx.send_switch_off_cmd(
MessageMetadata::new(TEST_REQ_ID, TEST_SENDER_ID),
TEST_SWITCH_ID,
);
assert!(result.is_ok());
result = switch_cmd_tx.send_switch_off_cmd(
MessageMetadata::new(TEST_REQ_ID, TEST_SENDER_ID),
TEST_SWITCH_ID,
);
assert!(result.is_err());
matches!(result.unwrap_err(), GenericSendError::QueueFull(None));
matches!(switch_cmd_rx.try_recv(), Err(TryRecvError::Empty));
} }
} }