Compare commits
25 Commits
serializat
...
satrs-shar
Author | SHA1 | Date | |
---|---|---|---|
6ffd55cec2 | |||
5e51b3de42
|
|||
bd059a2541 | |||
1a4d764f25 | |||
b38c617fae | |||
edcd5491f1 | |||
b4cb034b73 | |||
47c86aea5c | |||
c8bba48e76 | |||
176da4838a
|
|||
8114195bc6 | |||
1726e34fa7 | |||
1f2d6c9474 | |||
9960930339
|
|||
cb270964a1 | |||
a45e634214
|
|||
9e4132706c | |||
36d889a504
|
|||
97a6510af7 | |||
2e5d6a5c41
|
|||
29167736db
|
|||
a4c433a7be | |||
472a8ce0f9 | |||
e753319dac | |||
99dddf36f3 |
@ -12,4 +12,3 @@ exclude = [
|
|||||||
"embedded-examples/stm32f3-disco-rtic",
|
"embedded-examples/stm32f3-disco-rtic",
|
||||||
"embedded-examples/stm32h7-rtic",
|
"embedded-examples/stm32h7-rtic",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
11
README.md
11
README.md
@ -37,13 +37,16 @@ This project currently contains following crates:
|
|||||||
* [`satrs-example`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-example):
|
* [`satrs-example`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-example):
|
||||||
Example of a simple example on-board software using various sat-rs components which can be run
|
Example of a simple example on-board software using various sat-rs components which can be run
|
||||||
on a host computer or on any system with a standard runtime like a Raspberry Pi.
|
on a host computer or on any system with a standard runtime like a Raspberry Pi.
|
||||||
|
* [`satrs-minisim`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-minisim):
|
||||||
|
Mini-Simulator based on [asynchronix](https://github.com/asynchronics/asynchronix) which
|
||||||
|
simulates some physical devices for the `satrs-example` application device handlers.
|
||||||
* [`satrs-mib`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-mib):
|
* [`satrs-mib`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-mib):
|
||||||
Components to build a mission information base from the on-board software directly.
|
Components to build a mission information base from the on-board software directly.
|
||||||
* [`satrs-stm32f3-disco-rtic`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/embedded-examples/satrs-stm32f3-disco-rtic):
|
* [`satrs-stm32f3-disco-rtic`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/embedded-examples/stm32f3-disco-rtic):
|
||||||
Example of a simple example using low-level sat-rs components on a bare-metal system
|
Example of a simple example using low-level sat-rs components on a bare-metal system
|
||||||
with constrained resources. This example uses the [RTIC](https://github.com/rtic-rs/rtic)
|
with constrained resources. This example uses the [RTIC](https://github.com/rtic-rs/rtic)
|
||||||
framework on the STM32F3-Discovery device.
|
framework on the STM32F3-Discovery device.
|
||||||
* [`satrs-stm32h-nucleo-rtic`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/embedded-examples/satrs-stm32h7-nucleo-rtic):
|
* [`satrs-stm32h-nucleo-rtic`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/embedded-examples/stm32h7-nucleo-rtic):
|
||||||
Example of a simple example using sat-rs components on a bare-metal system
|
Example of a simple example using sat-rs components on a bare-metal system
|
||||||
with constrained resources. This example uses the [RTIC](https://github.com/rtic-rs/rtic)
|
with constrained resources. This example uses the [RTIC](https://github.com/rtic-rs/rtic)
|
||||||
framework on the STM32H743ZIT device.
|
framework on the STM32H743ZIT device.
|
||||||
@ -69,6 +72,10 @@ Currently this library has the following flight heritage:
|
|||||||
[flown on the satellite](https://blogs.esa.int/rocketscience/2024/05/21/ops-sat-reentry-tomorrow-final-experiments-continue/).
|
[flown on the satellite](https://blogs.esa.int/rocketscience/2024/05/21/ops-sat-reentry-tomorrow-final-experiments-continue/).
|
||||||
The application is strongly based on the sat-rs example application. You can find the repository
|
The application is strongly based on the sat-rs example application. You can find the repository
|
||||||
of the experiment [here](https://egit.irs.uni-stuttgart.de/rust/ops-sat-rs).
|
of the experiment [here](https://egit.irs.uni-stuttgart.de/rust/ops-sat-rs).
|
||||||
|
- Development and use of a sat-rs-based [demonstration on-board software](https://egit.irs.uni-stuttgart.de/rust/eurosim-obsw)
|
||||||
|
alongside a Flight System Simulator in the context of a
|
||||||
|
[Bachelors Thesis](https://www.researchgate.net/publication/380785984_Design_and_Development_of_a_Hardware-in-the-Loop_EuroSim_Demonstrator)
|
||||||
|
at [Airbus Netherlands](https://www.airbusdefenceandspacenetherlands.nl/).
|
||||||
|
|
||||||
# Coverage
|
# Coverage
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
sat-rs example for the STM32F3-Discovery board
|
sat-rs example for the STM32H73ZI-Nucleo board
|
||||||
=======
|
=======
|
||||||
|
|
||||||
This example application shows how the [sat-rs library](https://egit.irs.uni-stuttgart.de/rust/sat-rs)
|
This example application shows how the [sat-rs library](https://egit.irs.uni-stuttgart.de/rust/sat-rs)
|
||||||
|
260
images/minisim-arch/minisim-arch.graphml
Normal file
260
images/minisim-arch/minisim-arch.graphml
Normal file
@ -0,0 +1,260 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<graphml xmlns="http://graphml.graphdrawing.org/xmlns" xmlns:java="http://www.yworks.com/xml/yfiles-common/1.0/java" xmlns:sys="http://www.yworks.com/xml/yfiles-common/markup/primitives/2.0" xmlns:x="http://www.yworks.com/xml/yfiles-common/markup/2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:y="http://www.yworks.com/xml/graphml" xmlns:yed="http://www.yworks.com/xml/yed/3" xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns http://www.yworks.com/xml/schema/graphml/1.1/ygraphml.xsd">
|
||||||
|
<!--Created by yEd 3.23.2-->
|
||||||
|
<key attr.name="Description" attr.type="string" for="graph" id="d0"/>
|
||||||
|
<key for="port" id="d1" yfiles.type="portgraphics"/>
|
||||||
|
<key for="port" id="d2" yfiles.type="portgeometry"/>
|
||||||
|
<key for="port" id="d3" yfiles.type="portuserdata"/>
|
||||||
|
<key attr.name="url" attr.type="string" for="node" id="d4"/>
|
||||||
|
<key attr.name="description" attr.type="string" for="node" id="d5"/>
|
||||||
|
<key for="node" id="d6" yfiles.type="nodegraphics"/>
|
||||||
|
<key for="graphml" id="d7" yfiles.type="resources"/>
|
||||||
|
<key attr.name="url" attr.type="string" for="edge" id="d8"/>
|
||||||
|
<key attr.name="description" attr.type="string" for="edge" id="d9"/>
|
||||||
|
<key for="edge" id="d10" yfiles.type="edgegraphics"/>
|
||||||
|
<graph edgedefault="directed" id="G">
|
||||||
|
<data key="d0" xml:space="preserve"/>
|
||||||
|
<node id="n0">
|
||||||
|
<data key="d5"/>
|
||||||
|
<data key="d6">
|
||||||
|
<y:ShapeNode>
|
||||||
|
<y:Geometry height="360.0" width="479.0" x="771.3047672479152" y="458.0"/>
|
||||||
|
<y:Fill hasColor="false" transparent="false"/>
|
||||||
|
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||||
|
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" hasText="false" height="4.0" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="4.0" x="237.5" y="178.0">
|
||||||
|
<y:LabelModel>
|
||||||
|
<y:SmartNodeLabelModel distance="4.0"/>
|
||||||
|
</y:LabelModel>
|
||||||
|
<y:ModelParameter>
|
||||||
|
<y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/>
|
||||||
|
</y:ModelParameter>
|
||||||
|
</y:NodeLabel>
|
||||||
|
<y:Shape type="rectangle"/>
|
||||||
|
</y:ShapeNode>
|
||||||
|
</data>
|
||||||
|
</node>
|
||||||
|
<node id="n1">
|
||||||
|
<data key="d6">
|
||||||
|
<y:ShapeNode>
|
||||||
|
<y:Geometry height="177.64799999999997" width="200.75199999999973" x="1037.5527672479152" y="470.15200000000027"/>
|
||||||
|
<y:Fill hasColor="false" transparent="false"/>
|
||||||
|
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||||
|
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="67.919921875" x="13.264464667588754" xml:space="preserve" y="8.302185845943427">Simulation<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="-0.5" labelRatioY="-0.5" nodeRatioX="-0.433926114471642" nodeRatioY="-0.45326608886143704" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||||
|
<y:Shape type="rectangle"/>
|
||||||
|
</y:ShapeNode>
|
||||||
|
</data>
|
||||||
|
</node>
|
||||||
|
<node id="n2">
|
||||||
|
<data key="d6">
|
||||||
|
<y:ShapeNode>
|
||||||
|
<y:Geometry height="34.0" width="84.39999999999986" x="1068.8351781652768" y="508.2800000000002"/>
|
||||||
|
<y:Fill color="#FFCC00" transparent="false"/>
|
||||||
|
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||||
|
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="37.638671875" x="23.380664062499818" xml:space="preserve" y="8.015625">PCDU<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||||
|
<y:Shape type="rectangle"/>
|
||||||
|
</y:ShapeNode>
|
||||||
|
</data>
|
||||||
|
</node>
|
||||||
|
<node id="n3">
|
||||||
|
<data key="d6">
|
||||||
|
<y:ShapeNode>
|
||||||
|
<y:Geometry height="34.0" width="120.39999999999986" x="1068.8351781652768" y="550.4800000000001"/>
|
||||||
|
<y:Fill color="#FFCC00" transparent="false"/>
|
||||||
|
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||||
|
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="92.453125" x="13.973437499999818" xml:space="preserve" y="8.015625">Magnetometer<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||||
|
<y:Shape type="rectangle"/>
|
||||||
|
</y:ShapeNode>
|
||||||
|
</data>
|
||||||
|
</node>
|
||||||
|
<node id="n4">
|
||||||
|
<data key="d6">
|
||||||
|
<y:ShapeNode>
|
||||||
|
<y:Geometry height="34.0" width="120.39999999999986" x="1068.8351781652768" y="594.9000000000001"/>
|
||||||
|
<y:Fill color="#FFCC00" transparent="false"/>
|
||||||
|
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||||
|
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="88.83203125" x="15.783984374999818" xml:space="preserve" y="8.015625">Magnetorquer<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||||
|
<y:Shape type="rectangle"/>
|
||||||
|
</y:ShapeNode>
|
||||||
|
</data>
|
||||||
|
</node>
|
||||||
|
<node id="n5">
|
||||||
|
<data key="d6">
|
||||||
|
<y:ShapeNode>
|
||||||
|
<y:Geometry height="34.0" width="120.39999999999986" x="783.4063563305535" y="545.2800000000002"/>
|
||||||
|
<y:Fill color="#FFCC00" transparent="false"/>
|
||||||
|
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||||
|
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="85.931640625" x="17.234179687499932" xml:space="preserve" y="8.015625">SimController<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||||
|
<y:Shape type="rectangle"/>
|
||||||
|
</y:ShapeNode>
|
||||||
|
</data>
|
||||||
|
</node>
|
||||||
|
<node id="n6">
|
||||||
|
<data key="d6">
|
||||||
|
<y:ShapeNode>
|
||||||
|
<y:Geometry height="34.0" width="120.39999999999986" x="840.5407126611072" y="677.8000000000002"/>
|
||||||
|
<y:Fill color="#FFCC00" transparent="false"/>
|
||||||
|
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||||
|
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="105.05078125" x="7.674609374999932" xml:space="preserve" y="8.015625">UDP TC Receiver<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||||
|
<y:Shape type="rectangle"/>
|
||||||
|
</y:ShapeNode>
|
||||||
|
</data>
|
||||||
|
</node>
|
||||||
|
<node id="n7">
|
||||||
|
<data key="d6">
|
||||||
|
<y:ShapeNode>
|
||||||
|
<y:Geometry height="34.0" width="120.39999999999986" x="1005.2814253222144" y="677.8000000000002"/>
|
||||||
|
<y:Fill color="#FFCC00" transparent="false"/>
|
||||||
|
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||||
|
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="97.111328125" x="11.644335937499932" xml:space="preserve" y="8.015625">UDP TM Sender<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||||
|
<y:Shape type="rectangle"/>
|
||||||
|
</y:ShapeNode>
|
||||||
|
</data>
|
||||||
|
</node>
|
||||||
|
<node id="n8">
|
||||||
|
<data key="d6">
|
||||||
|
<y:ShapeNode>
|
||||||
|
<y:Geometry height="34.0" width="120.39999999999986" x="931.6174253222144" y="775.5920000000002"/>
|
||||||
|
<y:Fill color="#FFCC00" transparent="false"/>
|
||||||
|
<y:BorderStyle color="#000000" raised="false" type="line" width="1.0"/>
|
||||||
|
<y:NodeLabel alignment="center" autoSizePolicy="content" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" textColor="#000000" verticalTextPosition="bottom" visible="true" width="38.740234375" x="40.82988281249993" xml:space="preserve" y="8.015625">Client<y:LabelModel><y:SmartNodeLabelModel distance="4.0"/></y:LabelModel><y:ModelParameter><y:SmartNodeLabelModelParameter labelRatioX="0.0" labelRatioY="0.0" nodeRatioX="0.0" nodeRatioY="0.0" offsetX="0.0" offsetY="0.0" upX="0.0" upY="-1.0"/></y:ModelParameter></y:NodeLabel>
|
||||||
|
<y:Shape type="rectangle"/>
|
||||||
|
</y:ShapeNode>
|
||||||
|
</data>
|
||||||
|
</node>
|
||||||
|
<edge id="e0" source="n5" target="n3">
|
||||||
|
<data key="d10">
|
||||||
|
<y:PolyLineEdge>
|
||||||
|
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="-5.199999999999932"/>
|
||||||
|
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||||
|
<y:Arrows source="none" target="standard"/>
|
||||||
|
<y:BendStyle smoothed="false"/>
|
||||||
|
</y:PolyLineEdge>
|
||||||
|
</data>
|
||||||
|
</edge>
|
||||||
|
<edge id="e1" source="n5" target="n2">
|
||||||
|
<data key="d10">
|
||||||
|
<y:PolyLineEdge>
|
||||||
|
<y:Path sx="60.19999999999993" sy="0.0" tx="0.0" ty="0.0">
|
||||||
|
<y:Point x="1023.8695890826383" y="562.2800000000002"/>
|
||||||
|
<y:Point x="1023.8695890826383" y="525.2800000000002"/>
|
||||||
|
</y:Path>
|
||||||
|
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||||
|
<y:Arrows source="none" target="standard"/>
|
||||||
|
<y:BendStyle smoothed="false"/>
|
||||||
|
</y:PolyLineEdge>
|
||||||
|
</data>
|
||||||
|
</edge>
|
||||||
|
<edge id="e2" source="n5" target="n4">
|
||||||
|
<data key="d10">
|
||||||
|
<y:PolyLineEdge>
|
||||||
|
<y:Path sx="48.72964366944643" sy="0.0" tx="0.0" ty="0.0">
|
||||||
|
<y:Point x="1023.8695890826383" y="562.2800000000002"/>
|
||||||
|
<y:Point x="1023.8695890826383" y="611.9000000000001"/>
|
||||||
|
</y:Path>
|
||||||
|
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||||
|
<y:Arrows source="none" target="standard"/>
|
||||||
|
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="97.955078125" x="12.686124396959713" xml:space="preserve" y="-22.50440429687478">schedule_event<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="13.519999999999978" distanceToCenter="true" position="left" ratio="0.11621274698385183" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||||
|
<y:BendStyle smoothed="false"/>
|
||||||
|
</y:PolyLineEdge>
|
||||||
|
</data>
|
||||||
|
</edge>
|
||||||
|
<edge id="e3" source="n6" target="n5">
|
||||||
|
<data key="d10">
|
||||||
|
<y:PolyLineEdge>
|
||||||
|
<y:Path sx="0.0" sy="0.0" tx="-5.329643669446341" ty="0.0">
|
||||||
|
<y:Point x="900.7407126611072" y="628.5400000000002"/>
|
||||||
|
<y:Point x="838.2767126611071" y="628.5400000000002"/>
|
||||||
|
</y:Path>
|
||||||
|
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||||
|
<y:Arrows source="none" target="standard"/>
|
||||||
|
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="75.923828125" x="-87.89792405764274" xml:space="preserve" y="-40.606550292968564">SimRequest<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="49.935999999999936" distanceToCenter="true" position="left" ratio="0.5" segment="0"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||||
|
<y:BendStyle smoothed="false"/>
|
||||||
|
</y:PolyLineEdge>
|
||||||
|
</data>
|
||||||
|
</edge>
|
||||||
|
<edge id="e4" source="n4" target="n7">
|
||||||
|
<data key="d10">
|
||||||
|
<y:PolyLineEdge>
|
||||||
|
<y:Path sx="60.200000000000045" sy="0.0" tx="0.0" ty="0.0">
|
||||||
|
<y:Point x="1223.8814253222142" y="611.9000000000001"/>
|
||||||
|
<y:Point x="1223.8814253222142" y="694.8000000000002"/>
|
||||||
|
</y:Path>
|
||||||
|
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||||
|
<y:Arrows source="none" target="standard"/>
|
||||||
|
<y:BendStyle smoothed="false"/>
|
||||||
|
</y:PolyLineEdge>
|
||||||
|
</data>
|
||||||
|
</edge>
|
||||||
|
<edge id="e5" source="n3" target="n7">
|
||||||
|
<data key="d10">
|
||||||
|
<y:PolyLineEdge>
|
||||||
|
<y:Path sx="0.0" sy="0.0" tx="0.0" ty="0.0">
|
||||||
|
<y:Point x="1223.8814253222142" y="567.4800000000001"/>
|
||||||
|
<y:Point x="1223.8814253222142" y="694.8000000000002"/>
|
||||||
|
</y:Path>
|
||||||
|
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||||
|
<y:Arrows source="none" target="standard"/>
|
||||||
|
<y:BendStyle smoothed="false"/>
|
||||||
|
</y:PolyLineEdge>
|
||||||
|
</data>
|
||||||
|
</edge>
|
||||||
|
<edge id="e6" source="n2" target="n7">
|
||||||
|
<data key="d10">
|
||||||
|
<y:PolyLineEdge>
|
||||||
|
<y:Path sx="11.514125426161627" sy="-2.5781798912005343" tx="45.553752843062284" ty="0.0">
|
||||||
|
<y:Point x="1223.8814253222142" y="522.7018201087997"/>
|
||||||
|
<y:Point x="1223.8814253222142" y="694.8000000000002"/>
|
||||||
|
</y:Path>
|
||||||
|
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||||
|
<y:Arrows source="none" target="standard"/>
|
||||||
|
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="60.4140625" x="-2.4087265765670054" xml:space="preserve" y="145.1356018470808">SimReply<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="17.97817989120062" distanceToCenter="true" position="right" ratio="0.679561684469248" segment="-1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||||
|
<y:BendStyle smoothed="false"/>
|
||||||
|
</y:PolyLineEdge>
|
||||||
|
</data>
|
||||||
|
</edge>
|
||||||
|
<edge id="e7" source="n8" target="n6">
|
||||||
|
<data key="d10">
|
||||||
|
<y:PolyLineEdge>
|
||||||
|
<y:Path sx="-25.212712661107275" sy="0.0" tx="-11.264000000000124" ty="0.0">
|
||||||
|
<y:Point x="966.6047126611071" y="731.8000000000002"/>
|
||||||
|
<y:Point x="889.4767126611071" y="731.8000000000002"/>
|
||||||
|
</y:Path>
|
||||||
|
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||||
|
<y:Arrows source="none" target="standard"/>
|
||||||
|
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="119.751953125" x="-132.27600022951788" xml:space="preserve" y="-32.03587548828091">SimRequest in UDP<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="20.73181250000017" distanceToCenter="true" position="left" ratio="0.9386993050513424" segment="1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||||
|
<y:BendStyle smoothed="false"/>
|
||||||
|
</y:PolyLineEdge>
|
||||||
|
</data>
|
||||||
|
</edge>
|
||||||
|
<edge id="e8" source="n7" target="n8">
|
||||||
|
<data key="d10">
|
||||||
|
<y:PolyLineEdge>
|
||||||
|
<y:Path sx="29.18399999999997" sy="0.0" tx="24.28800000000001" ty="0.0">
|
||||||
|
<y:Point x="1094.6654253222143" y="731.8000000000002"/>
|
||||||
|
<y:Point x="1016.1054253222144" y="731.8000000000002"/>
|
||||||
|
</y:Path>
|
||||||
|
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||||
|
<y:Arrows source="none" target="standard"/>
|
||||||
|
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="104.2421875" x="-62.15307370122309" xml:space="preserve" y="34.80927001953137">SimReply in UDP<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="23.81218750000005" distanceToCenter="true" position="left" ratio="0.12769857433808468" segment="1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||||
|
<y:BendStyle smoothed="false"/>
|
||||||
|
</y:PolyLineEdge>
|
||||||
|
</data>
|
||||||
|
</edge>
|
||||||
|
<edge id="e9" source="n5" target="n1">
|
||||||
|
<data key="d10">
|
||||||
|
<y:PolyLineEdge>
|
||||||
|
<y:Path sx="23.921741802203996" sy="-3.0501798912007416" tx="0.0" ty="-56.27417989120056">
|
||||||
|
<y:Point x="867.5280981327575" y="502.70182010879967"/>
|
||||||
|
</y:Path>
|
||||||
|
<y:LineStyle color="#000000" type="line" width="1.0"/>
|
||||||
|
<y:Arrows source="none" target="standard"/>
|
||||||
|
<y:EdgeLabel alignment="center" configuration="AutoFlippingLabel" distance="2.0" fontFamily="Dialog" fontSize="12" fontStyle="plain" hasBackgroundColor="false" hasLineColor="false" height="17.96875" horizontalTextPosition="center" iconTextGap="4" modelName="custom" preferredPlacement="anywhere" ratio="0.5" textColor="#000000" verticalTextPosition="bottom" visible="true" width="29.95703125" x="73.38950633588263" xml:space="preserve" y="-62.699758016200235">step<y:LabelModel><y:SmartEdgeLabelModel autoRotationEnabled="false" defaultAngle="0.0" defaultDistance="10.0"/></y:LabelModel><y:ModelParameter><y:SmartEdgeLabelModelParameter angle="6.283185307179586" distance="11.126187499999986" distanceToCenter="true" position="left" ratio="0.5889387894625147" segment="-1"/></y:ModelParameter><y:PreferredPlacementDescriptor angle="0.0" angleOffsetOnRightSide="0" angleReference="absolute" angleRotationOnRightSide="co" distance="-1.0" frozen="true" placement="anywhere" side="anywhere" sideReference="relative_to_edge_flow"/></y:EdgeLabel>
|
||||||
|
<y:BendStyle smoothed="false"/>
|
||||||
|
</y:PolyLineEdge>
|
||||||
|
</data>
|
||||||
|
</edge>
|
||||||
|
</graph>
|
||||||
|
<data key="d7">
|
||||||
|
<y:Resources/>
|
||||||
|
</data>
|
||||||
|
</graphml>
|
BIN
images/minisim-arch/minisim-arch.png
Normal file
BIN
images/minisim-arch/minisim-arch.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 98 KiB |
@ -5,10 +5,8 @@ This book is the primary information resource for the [sat-rs library](https://e
|
|||||||
in addition to the regular API documentation. It contains the following resources:
|
in addition to the regular API documentation. It contains the following resources:
|
||||||
|
|
||||||
1. Architecture informations and consideration which would exceeds the scope of the regular API.
|
1. Architecture informations and consideration which would exceeds the scope of the regular API.
|
||||||
2. General information on how to build On-Board Software and how `sat-rs` can help to fulfill
|
2. General information on how to build on-board Software and how `sat-rs` can help to fulfill
|
||||||
the unique requirements of writing software for remote systems.
|
the unique requirements of writing software for remote systems.
|
||||||
2. A Getting-Started workshop where a small On-Board Software is built from scratch using
|
|
||||||
sat-rs components.
|
|
||||||
|
|
||||||
# Introduction
|
# Introduction
|
||||||
|
|
||||||
@ -31,7 +29,9 @@ and [EIVE](https://www.irs.uni-stuttgart.de/en/research/satellitetechnology-and-
|
|||||||
The [`satrs-example`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-example)
|
The [`satrs-example`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-example)
|
||||||
provides various practical usage examples of the `sat-rs` framework. If you are more interested in
|
provides various practical usage examples of the `sat-rs` framework. If you are more interested in
|
||||||
the practical application of `sat-rs` inside an application, it is recommended to have a look at
|
the practical application of `sat-rs` inside an application, it is recommended to have a look at
|
||||||
the example application.
|
the example application. The [`satrs-minisim`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-minisim)
|
||||||
|
applicatin complements the example application and can be used to simulate some physical devices
|
||||||
|
for the `satrs-example` device handlers.
|
||||||
|
|
||||||
# Flight Heritage
|
# Flight Heritage
|
||||||
|
|
||||||
@ -43,3 +43,7 @@ Currently this library has the following flight heritage:
|
|||||||
[flown on the satellite](https://blogs.esa.int/rocketscience/2024/05/21/ops-sat-reentry-tomorrow-final-experiments-continue/).
|
[flown on the satellite](https://blogs.esa.int/rocketscience/2024/05/21/ops-sat-reentry-tomorrow-final-experiments-continue/).
|
||||||
The application is strongly based on the sat-rs example application. You can find the repository
|
The application is strongly based on the sat-rs example application. You can find the repository
|
||||||
of the experiment [here](https://egit.irs.uni-stuttgart.de/rust/ops-sat-rs).
|
of the experiment [here](https://egit.irs.uni-stuttgart.de/rust/ops-sat-rs).
|
||||||
|
- Development and use of a sat-rs-based [demonstration on-board software](https://egit.irs.uni-stuttgart.de/rust/eurosim-obsw)
|
||||||
|
alongside a Flight System Simulator in the context of a
|
||||||
|
[Bachelors Thesis](https://www.researchgate.net/publication/380785984_Design_and_Development_of_a_Hardware-in-the-Loop_EuroSim_Demonstrator)
|
||||||
|
at [Airbus Netherlands](https://www.airbusdefenceandspacenetherlands.nl/).
|
||||||
|
@ -7,3 +7,13 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/)
|
|||||||
and this project adheres to [Semantic Versioning](http://semver.org/).
|
and this project adheres to [Semantic Versioning](http://semver.org/).
|
||||||
|
|
||||||
# [unreleased]
|
# [unreleased]
|
||||||
|
|
||||||
|
# [v0.1.1] 2024-02-21
|
||||||
|
|
||||||
|
satrs v0.2.0-rc.0
|
||||||
|
satrs-mib v0.1.1
|
||||||
|
|
||||||
|
# [v0.1.0] 2024-02-13
|
||||||
|
|
||||||
|
satrs v0.1.1
|
||||||
|
satrs-mib v0.1.0
|
||||||
|
@ -8,18 +8,18 @@ homepage = "https://egit.irs.uni-stuttgart.de/rust/sat-rs"
|
|||||||
repository = "https://egit.irs.uni-stuttgart.de/rust/sat-rs"
|
repository = "https://egit.irs.uni-stuttgart.de/rust/sat-rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
fern = "0.6"
|
fern = "0.7"
|
||||||
chrono = "0.4"
|
chrono = "0.4"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
crossbeam-channel = "0.5"
|
crossbeam-channel = "0.5"
|
||||||
delegate = "0.10"
|
delegate = "0.13"
|
||||||
zerocopy = "0.6"
|
zerocopy = "0.8"
|
||||||
csv = "1"
|
csv = "1"
|
||||||
num_enum = "0.7"
|
num_enum = "0.7"
|
||||||
thiserror = "1"
|
thiserror = "2"
|
||||||
lazy_static = "1"
|
lazy_static = "1"
|
||||||
strum = { version = "0.26", features = ["derive"] }
|
strum = { version = "0.26", features = ["derive"] }
|
||||||
derive-new = "0.5"
|
derive-new = "0.7"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
|
|
||||||
@ -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"
|
||||||
|
@ -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]
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -72,3 +73,22 @@ the `simpleclient`:
|
|||||||
```
|
```
|
||||||
|
|
||||||
You can also simply call the script without any arguments to view the command tree.
|
You can also simply call the script without any arguments to view the command tree.
|
||||||
|
|
||||||
|
## Adding the mini simulator application
|
||||||
|
|
||||||
|
This example application features a few device handlers. The
|
||||||
|
[`satrs-minisim`](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-minisim)
|
||||||
|
can be used to simulate the physical devices managed by these device handlers.
|
||||||
|
|
||||||
|
The example application will attempt communication with the mini simulator on UDP port 7303.
|
||||||
|
If this works, the device handlers will use communication interfaces dedicated to the communication
|
||||||
|
with the mini simulator. Otherwise, they will be replaced by dummy interfaces which either
|
||||||
|
return constant values or behave like ideal devices.
|
||||||
|
|
||||||
|
In summary, you can use the following command command to run the mini-simulator first:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo run -p satrs-minisim
|
||||||
|
```
|
||||||
|
|
||||||
|
and then start the example using `cargo run -p satrs-example`.
|
||||||
|
134
satrs-example/pytmtc/.gitignore
vendored
134
satrs-example/pytmtc/.gitignore
vendored
@ -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
|
||||||
|
@ -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()
|
||||||
|
27
satrs-example/pytmtc/pyproject.toml
Normal file
27
satrs-example/pytmtc/pyproject.toml
Normal 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"]
|
47
satrs-example/pytmtc/pytmtc/config.py
Normal file
47
satrs-example/pytmtc/pytmtc/config.py
Normal 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()
|
42
satrs-example/pytmtc/pytmtc/hk.py
Normal file
42
satrs-example/pytmtc/pytmtc/hk.py
Normal 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"
|
||||||
|
)
|
16
satrs-example/pytmtc/pytmtc/hk_common.py
Normal file
16
satrs-example/pytmtc/pytmtc/hk_common.py
Normal 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,
|
||||||
|
)
|
45
satrs-example/pytmtc/pytmtc/mgms.py
Normal file
45
satrs-example/pytmtc/pytmtc/mgms.py
Normal 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
|
31
satrs-example/pytmtc/pytmtc/mode.py
Normal file
31
satrs-example/pytmtc/pytmtc/mode.py
Normal 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))
|
@ -1,33 +1,69 @@
|
|||||||
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))
|
|
93
satrs-example/pytmtc/pytmtc/pus_tm.py
Normal file
93
satrs-example/pytmtc/pytmtc/pus_tm.py
Normal 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)
|
@ -1,2 +1 @@
|
|||||||
tmtccmd == 8.0.0rc2
|
.
|
||||||
# -e git+https://github.com/robamu-org/tmtccmd@97e5e51101a08b21472b3ddecc2063359f7e307a#egg=tmtccmd
|
|
||||||
|
@ -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
|
|
0
satrs-example/pytmtc/tests/__init__.py
Normal file
0
satrs-example/pytmtc/tests/__init__.py
Normal file
48
satrs-example/pytmtc/tests/test_tc_mods.py
Normal file
48
satrs-example/pytmtc/tests/test_tc_mods.py
Normal 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)
|
@ -1,52 +1,126 @@
|
|||||||
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,
|
||||||
};
|
};
|
||||||
use satrs::pus::{EcssTmSender, PusTmVariant};
|
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::{NO_SENDER, 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,86 @@ 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>>,
|
#[allow(dead_code)]
|
||||||
|
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 +238,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
|
|
||||||
.send(GenericMessage::new(
|
|
||||||
*requestor_info,
|
|
||||||
HkReply::new(hk_request.unique_id, HkReplyVariant::Ack),
|
|
||||||
))
|
|
||||||
.expect("failed to send HK reply");
|
|
||||||
let sec_header = PusTmSecondaryHeader::new(
|
|
||||||
3,
|
|
||||||
hk::Subservice::TmHkPacket as u8,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
self.stamp_helper.stamp(),
|
|
||||||
);
|
|
||||||
let mgm_snapshot = *self.shared_mgm_set.lock().unwrap();
|
let mgm_snapshot = *self.shared_mgm_set.lock().unwrap();
|
||||||
// Use binary serialization here. We want the data to be tightly packed.
|
if let Ok(hk_tm) = self.hk_helper.generate_hk_report_packet(
|
||||||
self.tm_buf[0] = mgm_snapshot.valid as u8;
|
self.stamp_helper.stamp(),
|
||||||
self.tm_buf[1..5].copy_from_slice(&mgm_snapshot.x.to_be_bytes());
|
SetId::SensorData as u32,
|
||||||
self.tm_buf[5..9].copy_from_slice(&mgm_snapshot.y.to_be_bytes());
|
&mut |hk_buf| {
|
||||||
self.tm_buf[9..13].copy_from_slice(&mgm_snapshot.z.to_be_bytes());
|
hk_buf[0] = mgm_snapshot.valid as u8;
|
||||||
let hk_tm = PusTmCreator::new(
|
hk_buf[1..5].copy_from_slice(&mgm_snapshot.x.to_be_bytes());
|
||||||
SpHeader::new_from_apid(self.id.apid),
|
hk_buf[5..9].copy_from_slice(&mgm_snapshot.y.to_be_bytes());
|
||||||
sec_header,
|
hk_buf[9..13].copy_from_slice(&mgm_snapshot.z.to_be_bytes());
|
||||||
&self.tm_buf[0..12],
|
Ok(13)
|
||||||
true,
|
},
|
||||||
);
|
&mut self.bufs.tm_buf,
|
||||||
self.tm_sender
|
) {
|
||||||
.send_tm(self.id.id(), PusTmVariant::Direct(hk_tm))
|
// TODO: If sending the TM fails, we should also send a failure reply.
|
||||||
.expect("failed to send HK TM");
|
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");
|
||||||
|
} else {
|
||||||
|
// TODO: Send back failure reply. Need result code for this.
|
||||||
|
log::error!("TM buffer too small to generate HK data");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
HkRequestVariant::EnablePeriodic => todo!(),
|
HkRequestVariant::EnablePeriodic => todo!(),
|
||||||
HkRequestVariant::DisablePeriodic => todo!(),
|
HkRequestVariant::DisablePeriodic => todo!(),
|
||||||
@ -199,20 +297,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 +392,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;
|
||||||
self.handle_mode_reached(Some(requestor))?;
|
if mode_and_submode.mode() == DeviceMode::Off as u32 {
|
||||||
|
self.shared_mgm_set.lock().unwrap().valid = false;
|
||||||
|
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 +411,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,11 +419,15 @@ 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() == NO_SENDER {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
if requestor.sender_id() != PUS_MODE_SERVICE.id() {
|
if requestor.sender_id() != PUS_MODE_SERVICE.id() {
|
||||||
log::warn!(
|
log::warn!(
|
||||||
"can not send back mode reply to sender {}",
|
"can not send back mode reply to sender {:x}",
|
||||||
requestor.sender_id()
|
requestor.sender_id()
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@ -266,7 +449,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 +463,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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
use crossbeam_channel::{bounded, Receiver, Sender};
|
use crossbeam_channel::{bounded, Receiver, Sender};
|
||||||
use std::sync::atomic::{AtomicU16, Ordering};
|
use std::sync::atomic::{AtomicU16, Ordering};
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use zerocopy::{AsBytes, FromBytes, NetworkEndian, Unaligned, U16};
|
use zerocopy::{FromBytes, Immutable, IntoBytes, NetworkEndian, Unaligned, U16};
|
||||||
|
|
||||||
trait FieldDataProvider: Send {
|
trait FieldDataProvider: Send {
|
||||||
fn get_data(&self) -> &[u8];
|
fn get_data(&self) -> &[u8];
|
||||||
@ -35,7 +35,7 @@ struct ExampleMgmSet {
|
|||||||
temperature: u16,
|
temperature: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(FromBytes, AsBytes, Unaligned)]
|
#[derive(FromBytes, IntoBytes, Immutable, Unaligned)]
|
||||||
#[repr(C)]
|
#[repr(C)]
|
||||||
struct ExampleMgmSetZc {
|
struct ExampleMgmSetZc {
|
||||||
mgm_vec: [u8; 12],
|
mgm_vec: [u8; 12],
|
||||||
|
@ -122,7 +122,7 @@ pub mod mode_err {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub mod components {
|
pub mod components {
|
||||||
use satrs::request::UniqueApidTargetId;
|
use satrs::{request::UniqueApidTargetId, ComponentId};
|
||||||
use strum::EnumIter;
|
use strum::EnumIter;
|
||||||
|
|
||||||
#[derive(Copy, Clone, PartialEq, Eq, EnumIter)]
|
#[derive(Copy, Clone, PartialEq, Eq, EnumIter)]
|
||||||
@ -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,10 +178,13 @@ 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 =
|
||||||
UniqueApidTargetId::new(Apid::Tmtc as u16, TmtcId::TcpServer as u32);
|
UniqueApidTargetId::new(Apid::Tmtc as u16, TmtcId::TcpServer as u32);
|
||||||
|
pub const NO_SENDER: ComponentId = ComponentId::MAX;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub mod pool {
|
pub mod pool {
|
||||||
@ -224,7 +233,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;
|
||||||
}
|
}
|
||||||
|
197
satrs-example/src/eps/mod.rs
Normal file
197
satrs-example/src/eps/mod.rs
Normal file
@ -0,0 +1,197 @@
|
|||||||
|
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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[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>>,
|
||||||
|
#[allow(dead_code)]
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
722
satrs-example/src/eps/pcdu.rs
Normal file
722
satrs-example/src/eps/pcdu.rs
Normal file
@ -0,0 +1,722 @@
|
|||||||
|
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::{NO_SENDER, 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: PcduRequest = serde_json::from_slice(data).expect("expected a PCDU request");
|
||||||
|
self.sim_request_tx
|
||||||
|
.send(SimRequest::new_with_epoch_time(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 pcdu_req: PcduRequest = serde_json::from_slice(data).unwrap();
|
||||||
|
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(
|
||||||
|
switch_map_mut.clone(),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_recv_replies<ReplyHandler: FnMut(&[u8])>(
|
||||||
|
&self,
|
||||||
|
mut f: ReplyHandler,
|
||||||
|
) -> Result<(), Self::Error> {
|
||||||
|
if self.reply_queue_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
loop {
|
||||||
|
let reply = self.get_next_reply_as_string();
|
||||||
|
f(reply.as_bytes());
|
||||||
|
if self.reply_queue_empty() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SerialInterfaceDummy {
|
||||||
|
fn get_next_reply_as_string(&self) -> String {
|
||||||
|
let mut reply_deque_mut = self.reply_deque.borrow_mut();
|
||||||
|
let next_reply = reply_deque_mut.pop_front().unwrap();
|
||||||
|
serde_json::to_string(&next_reply).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reply_queue_empty(&self) -> bool {
|
||||||
|
self.reply_deque.borrow().is_empty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
let mut shared_switch_map = self
|
||||||
|
.shared_switch_map
|
||||||
|
.lock()
|
||||||
|
.expect("failed to lock switch map");
|
||||||
|
shared_switch_map.switch_map = switch_map_wrapper.0;
|
||||||
|
shared_switch_map.valid = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
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() == NO_SENDER {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::sync::mpsc;
|
||||||
|
|
||||||
|
use satrs::{
|
||||||
|
mode::ModeRequest, power::SwitchStateBinary, request::GenericMessage, tmtc::PacketAsVec,
|
||||||
|
};
|
||||||
|
use satrs_example::config::components::{Apid, MGM_HANDLER_0};
|
||||||
|
use satrs_minisim::eps::SwitchMapBinary;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct SerialInterfaceTest {
|
||||||
|
pub inner: SerialInterfaceDummy,
|
||||||
|
pub send_queue: RefCell<VecDeque<Vec<u8>>>,
|
||||||
|
pub reply_queue: RefCell<VecDeque<String>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SerialInterface for SerialInterfaceTest {
|
||||||
|
type Error = ();
|
||||||
|
|
||||||
|
fn send(&self, data: &[u8]) -> Result<(), Self::Error> {
|
||||||
|
let mut send_queue_mut = self.send_queue.borrow_mut();
|
||||||
|
send_queue_mut.push_back(data.to_vec());
|
||||||
|
self.inner.send(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_recv_replies<ReplyHandler: FnMut(&[u8])>(
|
||||||
|
&self,
|
||||||
|
mut f: ReplyHandler,
|
||||||
|
) -> Result<(), Self::Error> {
|
||||||
|
if self.inner.reply_queue_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
loop {
|
||||||
|
let reply = self.inner.get_next_reply_as_string();
|
||||||
|
self.reply_queue.borrow_mut().push_back(reply.clone());
|
||||||
|
f(reply.as_bytes());
|
||||||
|
if self.inner.reply_queue_empty() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct PcduTestbench {
|
||||||
|
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 switch_request_tx: mpsc::Sender<GenericMessage<SwitchRequest>>,
|
||||||
|
pub handler: PcduHandler<SerialInterfaceTest, mpsc::Sender<PacketAsVec>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PcduTestbench {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let (mode_request_tx, mode_request_rx) = mpsc::channel();
|
||||||
|
let (mode_reply_tx_to_pus, mode_reply_rx_to_pus) = mpsc::channel();
|
||||||
|
let (mode_reply_tx_to_parent, mode_reply_rx_to_parent) = mpsc::sync_channel(5);
|
||||||
|
let mode_interface = MpscModeLeafInterface {
|
||||||
|
request_rx: mode_request_rx,
|
||||||
|
reply_to_pus_tx: mode_reply_tx_to_pus,
|
||||||
|
reply_to_parent_tx: mode_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 (switch_request_tx, switch_reqest_rx) = mpsc::channel();
|
||||||
|
let shared_switch_map = Arc::new(Mutex::new(SwitchSet::default()));
|
||||||
|
Self {
|
||||||
|
mode_request_tx,
|
||||||
|
mode_reply_rx_to_pus,
|
||||||
|
mode_reply_rx_to_parent,
|
||||||
|
composite_request_tx,
|
||||||
|
hk_reply_rx,
|
||||||
|
tm_rx,
|
||||||
|
switch_request_tx,
|
||||||
|
handler: PcduHandler::new(
|
||||||
|
UniqueApidTargetId::new(Apid::Eps as u16, 0),
|
||||||
|
"TEST_PCDU",
|
||||||
|
mode_interface,
|
||||||
|
composite_request_rx,
|
||||||
|
hk_reply_tx,
|
||||||
|
switch_reqest_rx,
|
||||||
|
tm_tx,
|
||||||
|
SerialInterfaceTest::default(),
|
||||||
|
shared_switch_map,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn verify_switch_info_req_was_sent(&self, expected_queue_len: usize) {
|
||||||
|
// Check that there is now communication happening.
|
||||||
|
let mut send_queue_mut = self.handler.com_interface.send_queue.borrow_mut();
|
||||||
|
assert_eq!(send_queue_mut.len(), expected_queue_len);
|
||||||
|
let packet_sent = send_queue_mut.pop_front().unwrap();
|
||||||
|
drop(send_queue_mut);
|
||||||
|
let pcdu_req: PcduRequest = serde_json::from_slice(&packet_sent).unwrap();
|
||||||
|
assert_eq!(pcdu_req, PcduRequest::RequestSwitchInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn verify_switch_req_was_sent(
|
||||||
|
&self,
|
||||||
|
expected_queue_len: usize,
|
||||||
|
switch_id: PcduSwitch,
|
||||||
|
target_state: SwitchStateBinary,
|
||||||
|
) {
|
||||||
|
// Check that there is now communication happening.
|
||||||
|
let mut send_queue_mut = self.handler.com_interface.send_queue.borrow_mut();
|
||||||
|
assert_eq!(send_queue_mut.len(), expected_queue_len);
|
||||||
|
let packet_sent = send_queue_mut.pop_front().unwrap();
|
||||||
|
drop(send_queue_mut);
|
||||||
|
let pcdu_req: PcduRequest = serde_json::from_slice(&packet_sent).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
pcdu_req,
|
||||||
|
PcduRequest::SwitchDevice {
|
||||||
|
switch: switch_id,
|
||||||
|
state: target_state
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn verify_switch_reply_received(
|
||||||
|
&self,
|
||||||
|
expected_queue_len: usize,
|
||||||
|
expected_map: SwitchMapBinary,
|
||||||
|
) {
|
||||||
|
// Check that a switch reply was read back.
|
||||||
|
let mut reply_received_mut = self.handler.com_interface.reply_queue.borrow_mut();
|
||||||
|
assert_eq!(reply_received_mut.len(), expected_queue_len);
|
||||||
|
let reply_received = reply_received_mut.pop_front().unwrap();
|
||||||
|
let sim_reply: SimReply = serde_json::from_str(&reply_received).unwrap();
|
||||||
|
let pcdu_reply = PcduReply::from_sim_message(&sim_reply).unwrap();
|
||||||
|
assert_eq!(pcdu_reply, PcduReply::SwitchInfo(expected_map));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_basic_handler() {
|
||||||
|
let mut testbench = PcduTestbench::new();
|
||||||
|
assert_eq!(testbench.handler.com_interface.send_queue.borrow().len(), 0);
|
||||||
|
assert_eq!(
|
||||||
|
testbench.handler.com_interface.reply_queue.borrow().len(),
|
||||||
|
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(OpCode::RegularOp);
|
||||||
|
testbench
|
||||||
|
.handler
|
||||||
|
.periodic_operation(OpCode::PollAndRecvReplies);
|
||||||
|
// Handler is OFF, no changes expected.
|
||||||
|
assert_eq!(testbench.handler.com_interface.send_queue.borrow().len(), 0);
|
||||||
|
assert_eq!(
|
||||||
|
testbench.handler.com_interface.reply_queue.borrow().len(),
|
||||||
|
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_mode() {
|
||||||
|
let mut testbench = PcduTestbench::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");
|
||||||
|
let switch_map_shared = testbench.handler.shared_switch_map.lock().unwrap();
|
||||||
|
assert!(!switch_map_shared.valid);
|
||||||
|
drop(switch_map_shared);
|
||||||
|
testbench.handler.periodic_operation(OpCode::RegularOp);
|
||||||
|
testbench
|
||||||
|
.handler
|
||||||
|
.periodic_operation(OpCode::PollAndRecvReplies);
|
||||||
|
// Check correctness of mode.
|
||||||
|
assert_eq!(
|
||||||
|
testbench.handler.mode_and_submode().mode(),
|
||||||
|
DeviceMode::Normal as u32
|
||||||
|
);
|
||||||
|
assert_eq!(testbench.handler.mode_and_submode().submode(), 0);
|
||||||
|
|
||||||
|
testbench.verify_switch_info_req_was_sent(1);
|
||||||
|
testbench.verify_switch_reply_received(1, SwitchMapBinaryWrapper::default().0);
|
||||||
|
|
||||||
|
let switch_map_shared = testbench.handler.shared_switch_map.lock().unwrap();
|
||||||
|
assert!(switch_map_shared.valid);
|
||||||
|
drop(switch_map_shared);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_switch_request_handling() {
|
||||||
|
let mut testbench = PcduTestbench::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
|
||||||
|
.switch_request_tx
|
||||||
|
.send(GenericMessage::new(
|
||||||
|
MessageMetadata::new(0, MGM_HANDLER_0.id()),
|
||||||
|
SwitchRequest::new(0, SwitchStateBinary::On),
|
||||||
|
))
|
||||||
|
.expect("failed to send switch request");
|
||||||
|
testbench.handler.periodic_operation(OpCode::RegularOp);
|
||||||
|
testbench
|
||||||
|
.handler
|
||||||
|
.periodic_operation(OpCode::PollAndRecvReplies);
|
||||||
|
|
||||||
|
testbench.verify_switch_req_was_sent(2, PcduSwitch::Mgm, SwitchStateBinary::On);
|
||||||
|
testbench.verify_switch_info_req_was_sent(1);
|
||||||
|
let mut switch_map = SwitchMapBinaryWrapper::default().0;
|
||||||
|
*switch_map
|
||||||
|
.get_mut(&PcduSwitch::Mgm)
|
||||||
|
.expect("switch state setting failed") = SwitchStateBinary::On;
|
||||||
|
testbench.verify_switch_reply_received(1, switch_map);
|
||||||
|
|
||||||
|
let switch_map_shared = testbench.handler.shared_switch_map.lock().unwrap();
|
||||||
|
assert!(switch_map_shared.valid);
|
||||||
|
drop(switch_map_shared);
|
||||||
|
}
|
||||||
|
}
|
@ -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,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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;
|
||||||
|
420
satrs-example/src/interface/sim_client_udp.rs
Normal file
420
satrs-example/src/interface/sim_client_udp.rs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
@ -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"),
|
||||||
|
@ -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,21 @@ 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::request::GenericMessage;
|
use satrs::pus::HandlingStatus;
|
||||||
|
use satrs::request::{GenericMessage, MessageMetadata};
|
||||||
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 satrs_example::DeviceMode;
|
||||||
|
|
||||||
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;
|
||||||
@ -36,12 +47,14 @@ use crate::pus::scheduler::{create_scheduler_service_dynamic, create_scheduler_s
|
|||||||
use crate::pus::test::create_test_service_static;
|
use crate::pus::test::create_test_service_static;
|
||||||
use crate::pus::{PusTcDistributor, PusTcMpscRouter};
|
use crate::pus::{PusTcDistributor, PusTcMpscRouter};
|
||||||
use crate::requests::{CompositeRequest, GenericRequestRouter};
|
use crate::requests::{CompositeRequest, GenericRequestRouter};
|
||||||
use satrs::mode::ModeRequest;
|
use satrs::mode::{Mode, ModeAndSubmode, 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, NO_SENDER, 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 +73,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 +96,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.clone());
|
||||||
|
|
||||||
// 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 +225,76 @@ 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,
|
||||||
|
);
|
||||||
|
// The PCDU is a critical component which should be in normal mode immediately.
|
||||||
|
pcdu_handler_mode_tx
|
||||||
|
.send(GenericMessage::new(
|
||||||
|
MessageMetadata::new(0, NO_SENDER),
|
||||||
|
ModeRequest::SetMode(ModeAndSubmode::new(DeviceMode::Normal as Mode, 0)),
|
||||||
|
))
|
||||||
|
.expect("sending initial mode request failed");
|
||||||
|
|
||||||
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 +327,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");
|
||||||
.spawn(move || loop {
|
opt_jh_sim_client = Some(
|
||||||
event_handler.periodic_operation();
|
thread::Builder::new()
|
||||||
thread::sleep(Duration::from_millis(FREQ_MS_EVENT_HANDLING));
|
.name("sat-rs sim adapter".to_string())
|
||||||
})
|
.spawn(move || loop {
|
||||||
.unwrap();
|
if sim_client.operation() == HandlingStatus::Empty {
|
||||||
|
std::thread::sleep(Duration::from_millis(SIM_CLIENT_IDLE_DELAY_MS));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
info!("Starting AOCS thread");
|
info!("Starting AOCS thread");
|
||||||
let jh_aocs = thread::Builder::new()
|
let jh_aocs = thread::Builder::new()
|
||||||
@ -265,10 +351,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 +385,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 {
|
||||||
.join()
|
jh_sim_client
|
||||||
.expect("Joining Event Manager thread failed");
|
.join()
|
||||||
|
.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 +400,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.clone());
|
||||||
|
|
||||||
// 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 +440,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 +463,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 +502,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 +531,77 @@ 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,
|
||||||
|
);
|
||||||
|
// The PCDU is a critical component which should be in normal mode immediately.
|
||||||
|
pcdu_handler_mode_tx
|
||||||
|
.send(GenericMessage::new(
|
||||||
|
MessageMetadata::new(0, NO_SENDER),
|
||||||
|
ModeRequest::SetMode(ModeAndSubmode::new(DeviceMode::Normal as Mode, 0)),
|
||||||
|
))
|
||||||
|
.expect("sending initial mode request failed");
|
||||||
|
|
||||||
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 +634,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");
|
||||||
.spawn(move || loop {
|
opt_jh_sim_client = Some(
|
||||||
event_handler.periodic_operation();
|
thread::Builder::new()
|
||||||
thread::sleep(Duration::from_millis(FREQ_MS_EVENT_HANDLING));
|
.name("sat-rs sim adapter".to_string())
|
||||||
})
|
.spawn(move || loop {
|
||||||
.unwrap();
|
if sim_client.operation() == HandlingStatus::Empty {
|
||||||
|
std::thread::sleep(Duration::from_millis(SIM_CLIENT_IDLE_DELAY_MS));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
info!("Starting AOCS thread");
|
info!("Starting AOCS thread");
|
||||||
let jh_aocs = thread::Builder::new()
|
let jh_aocs = thread::Builder::new()
|
||||||
@ -482,11 +658,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 +692,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 {
|
||||||
.join()
|
jh_sim_client
|
||||||
.expect("Joining Event Manager thread failed");
|
.join()
|
||||||
|
.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");
|
||||||
|
@ -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
|
||||||
|
@ -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};
|
||||||
@ -32,8 +33,10 @@ pub struct HkReply {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Debug)]
|
#[derive(Clone, PartialEq, Debug)]
|
||||||
|
#[allow(dead_code)]
|
||||||
pub enum HkReplyVariant {
|
pub enum HkReplyVariant {
|
||||||
Ack,
|
Ack,
|
||||||
|
Failed(ResultU16),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
@ -69,6 +72,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)
|
||||||
}
|
}
|
||||||
|
@ -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};
|
||||||
|
|
||||||
@ -44,16 +44,18 @@ pub struct PusTcMpscRouter {
|
|||||||
pub event_tc_sender: Sender<EcssTcAndToken>,
|
pub event_tc_sender: Sender<EcssTcAndToken>,
|
||||||
pub sched_tc_sender: Sender<EcssTcAndToken>,
|
pub sched_tc_sender: Sender<EcssTcAndToken>,
|
||||||
pub hk_tc_sender: Sender<EcssTcAndToken>,
|
pub hk_tc_sender: Sender<EcssTcAndToken>,
|
||||||
|
#[allow(dead_code)]
|
||||||
pub action_tc_sender: Sender<EcssTcAndToken>,
|
pub action_tc_sender: Sender<EcssTcAndToken>,
|
||||||
pub mode_tc_sender: Sender<EcssTcAndToken>,
|
pub mode_tc_sender: Sender<EcssTcAndToken>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct PusTcDistributor<TmSender: EcssTmSender> {
|
pub struct PusTcDistributor<TmSender: EcssTmSender> {
|
||||||
|
#[allow(dead_code)]
|
||||||
pub id: ComponentId,
|
pub id: ComponentId,
|
||||||
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 +68,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(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,10 +26,12 @@ pub enum CompositeRequest {
|
|||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct GenericRequestRouter {
|
pub struct GenericRequestRouter {
|
||||||
|
#[allow(dead_code)]
|
||||||
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 {
|
||||||
|
@ -4,16 +4,19 @@ use std::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use log::info;
|
use log::info;
|
||||||
use satrs::tmtc::{PacketAsVec, PacketInPool, SharedPacketPool};
|
|
||||||
use satrs::{
|
use satrs::{
|
||||||
pool::PoolProvider,
|
pool::PoolProvider,
|
||||||
seq_count::{CcsdsSimpleSeqCountProvider, SequenceCountProviderCore},
|
|
||||||
spacepackets::{
|
spacepackets::{
|
||||||
ecss::{tm::PusTmZeroCopyWriter, PusPacket},
|
ecss::{tm::PusTmZeroCopyWriter, PusPacket},
|
||||||
|
seq_count::CcsdsSimpleSeqCountProvider,
|
||||||
time::cds::MIN_CDS_FIELD_LEN,
|
time::cds::MIN_CDS_FIELD_LEN,
|
||||||
CcsdsPacket,
|
CcsdsPacket,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
use satrs::{
|
||||||
|
spacepackets::seq_count::SequenceCountProvider,
|
||||||
|
tmtc::{PacketAsVec, PacketInPool, SharedPacketPool},
|
||||||
|
};
|
||||||
|
|
||||||
use crate::interface::tcp::SyncTcpTmSource;
|
use crate::interface::tcp::SyncTcpTmSource;
|
||||||
|
|
||||||
|
@ -23,8 +23,9 @@ version = "1"
|
|||||||
optional = true
|
optional = true
|
||||||
|
|
||||||
[dependencies.satrs-shared]
|
[dependencies.satrs-shared]
|
||||||
version = ">=0.1.3, <0.2"
|
version = ">=0.1.3, <=0.2"
|
||||||
features = ["serde"]
|
features = ["serde"]
|
||||||
|
path = "../satrs-shared"
|
||||||
|
|
||||||
[dependencies.satrs-mib-codegen]
|
[dependencies.satrs-mib-codegen]
|
||||||
path = "codegen"
|
path = "codegen"
|
||||||
|
@ -28,7 +28,8 @@ features = ["full"]
|
|||||||
trybuild = { version = "1", features = ["diff"] }
|
trybuild = { version = "1", features = ["diff"] }
|
||||||
|
|
||||||
[dev-dependencies.satrs-shared]
|
[dev-dependencies.satrs-shared]
|
||||||
version = ">=0.1.3, <0.2"
|
version = ">=0.1.3, <=0.2"
|
||||||
|
path = "../../satrs-shared"
|
||||||
|
|
||||||
[dev-dependencies.satrs-mib]
|
[dev-dependencies.satrs-mib]
|
||||||
path = ".."
|
path = ".."
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
#![no_std]
|
#![no_std]
|
||||||
#[cfg(feature = "alloc")]
|
|
||||||
extern crate alloc;
|
|
||||||
#[cfg(any(feature = "std", test))]
|
#[cfg(any(feature = "std", test))]
|
||||||
extern crate std;
|
extern crate std;
|
||||||
|
|
||||||
|
@ -9,18 +9,20 @@ edition = "2021"
|
|||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
thiserror = "1"
|
thiserror = "2"
|
||||||
fern = "0.5"
|
fern = "0.7"
|
||||||
|
strum = { version = "0.26", features = ["derive"] }
|
||||||
|
num_enum = "0.7"
|
||||||
humantime = "2"
|
humantime = "2"
|
||||||
|
|
||||||
[dependencies.asynchronix]
|
[dependencies.asynchronix]
|
||||||
version = "0.2.1"
|
version = "0.2.2"
|
||||||
git = "https://github.com/asynchronics/asynchronix.git"
|
# git = "https://github.com/asynchronics/asynchronix.git"
|
||||||
branch = "main"
|
# branch = "main"
|
||||||
features = ["serde"]
|
features = ["serde"]
|
||||||
|
|
||||||
[dependencies.satrs]
|
[dependencies.satrs]
|
||||||
path = "../satrs"
|
path = "../satrs"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
delegate = "0.12"
|
delegate = "0.13"
|
||||||
|
32
satrs-minisim/README.md
Normal file
32
satrs-minisim/README.md
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
sat-rs minisim
|
||||||
|
======
|
||||||
|
|
||||||
|
This crate contains a mini-simulator based on the open-source discrete-event simulation framework
|
||||||
|
[asynchronix](https://github.com/asynchronics/asynchronix).
|
||||||
|
|
||||||
|
Right now, this crate is primarily used together with the
|
||||||
|
[`satrs-example` application](https://egit.irs.uni-stuttgart.de/rust/sat-rs/src/branch/main/satrs-example)
|
||||||
|
to simulate the devices connected to the example application.
|
||||||
|
|
||||||
|
You can simply run this application using
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo run
|
||||||
|
```
|
||||||
|
|
||||||
|
or
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo run -p satrs-minisim
|
||||||
|
```
|
||||||
|
|
||||||
|
in the workspace. The mini simulator uses the UDP port 7303 to exchange simulation requests and
|
||||||
|
simulation replies with any other application.
|
||||||
|
|
||||||
|
The simulator was designed in a modular way to be scalable and adaptable to other communication
|
||||||
|
schemes. This might allow it to serve a mini-simulator for other example applications which
|
||||||
|
still have similar device handlers.
|
||||||
|
|
||||||
|
The following graph shows the high-level architecture of the mini-simulator.
|
||||||
|
|
||||||
|
<img src="../images/minisim-arch/minisim-arch.png" alt="Mini simulator architecture" width="500" class="center"/>
|
@ -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,38 @@ 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,
|
||||||
|
#[allow(dead_code)]
|
||||||
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 +66,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 +90,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 +149,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 +182,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 +195,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 +204,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 +218,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 +227,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 +242,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);
|
||||||
}
|
}
|
||||||
|
@ -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,20 @@ use crate::{
|
|||||||
eps::PcduModel,
|
eps::PcduModel,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const WARNING_FOR_STALE_DATA: bool = false;
|
||||||
|
|
||||||
|
const SIM_CTRL_REQ_WIRETAPPING: bool = false;
|
||||||
|
const MGM_REQ_WIRETAPPING: bool = false;
|
||||||
|
const PCDU_REQ_WIRETAPPING: bool = false;
|
||||||
|
const MGT_REQ_WIRETAPPING: bool = false;
|
||||||
|
|
||||||
// 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 +40,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 {
|
||||||
@ -67,14 +74,14 @@ impl SimController {
|
|||||||
loop {
|
loop {
|
||||||
match self.request_receiver.try_recv() {
|
match self.request_receiver.try_recv() {
|
||||||
Ok(request) => {
|
Ok(request) => {
|
||||||
if request.timestamp < old_timestamp {
|
if request.timestamp < old_timestamp && WARNING_FOR_STALE_DATA {
|
||||||
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 +98,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 +130,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 +151,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 +176,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 +203,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);
|
||||||
|
@ -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 {
|
||||||
|
@ -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,19 +161,82 @@ 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,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
|
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, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub enum PcduRequest {
|
pub enum PcduRequest {
|
||||||
SwitchDevice {
|
SwitchDevice {
|
||||||
switch: PcduSwitch,
|
switch: PcduSwitch,
|
||||||
@ -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");
|
||||||
|
@ -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();
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
|
|
||||||
# [unreleased]
|
# [unreleased]
|
||||||
|
|
||||||
|
# [v0.2.0] 2024-11-04
|
||||||
|
|
||||||
|
Semver bump, due to added features in v0.1.4
|
||||||
|
|
||||||
# [v0.1.4] 2024-04-24
|
# [v0.1.4] 2024-04-24
|
||||||
|
|
||||||
## Added
|
## Added
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "satrs-shared"
|
name = "satrs-shared"
|
||||||
description = "Components shared by multiple sat-rs crates"
|
description = "Components shared by multiple sat-rs crates"
|
||||||
version = "0.1.4"
|
version = "0.2.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = ["Robin Mueller <muellerr@irs.uni-stuttgart.de>"]
|
authors = ["Robin Mueller <muellerr@irs.uni-stuttgart.de>"]
|
||||||
homepage = "https://absatsw.irs.uni-stuttgart.de/projects/sat-rs/"
|
homepage = "https://absatsw.irs.uni-stuttgart.de/projects/sat-rs/"
|
||||||
@ -22,12 +22,13 @@ version = "0.3"
|
|||||||
optional = true
|
optional = true
|
||||||
|
|
||||||
[dependencies.spacepackets]
|
[dependencies.spacepackets]
|
||||||
version = ">0.9, <=0.11"
|
version = ">0.9, <=0.13"
|
||||||
default-features = false
|
default-features = false
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
serde = ["dep:serde", "spacepackets/serde"]
|
serde = ["dep:serde", "spacepackets/serde"]
|
||||||
spacepackets = ["dep:defmt", "spacepackets/defmt"]
|
defmt = ["dep:defmt", "spacepackets/defmt"]
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
rustdoc-args = ["--cfg", "docs_rs", "--generate-link-to-definition"]
|
all-features = true
|
||||||
|
rustdoc-args = ["--generate-link-to-definition"]
|
||||||
|
3
satrs-shared/docs.sh
Executable file
3
satrs-shared/docs.sh
Executable file
@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
export RUSTDOCFLAGS="--cfg docsrs --generate-link-to-definition -Z unstable-options"
|
||||||
|
cargo +nightly doc --all-features --open
|
23
satrs-shared/release-checklist.md
Normal file
23
satrs-shared/release-checklist.md
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
Checklist for new releases
|
||||||
|
=======
|
||||||
|
|
||||||
|
# Pre-Release
|
||||||
|
|
||||||
|
1. Make sure any new modules are documented sufficiently enough and check docs by running
|
||||||
|
`docs.sh`.
|
||||||
|
2. Bump version specifier in `Cargo.toml`.
|
||||||
|
3. Update `CHANGELOG.md`: Convert `unreleased` section into version section with date and add new
|
||||||
|
`unreleased` section.
|
||||||
|
4. Run `cargo test --all-features` or `cargo nextest r --all-features` and `cargo test --doc`.
|
||||||
|
5. Run `cargo fmt` and `cargo clippy`. Check `cargo msrv` against MSRV in `Cargo.toml`.
|
||||||
|
6. Wait for CI/CD results for EGit and Github. These also check cross-compilation for bare-metal
|
||||||
|
targets.
|
||||||
|
|
||||||
|
# Release
|
||||||
|
|
||||||
|
1. `cargo publish`
|
||||||
|
|
||||||
|
# Post-Release
|
||||||
|
|
||||||
|
1. Create a new release on `EGit` with the name `satrs-<version>`.
|
||||||
|
|
@ -1,4 +1,4 @@
|
|||||||
//! This crates contains modules shared among other sat-rs framework crates.
|
//! This crates contains modules shared among other sat-rs framework crates.
|
||||||
#![no_std]
|
#![no_std]
|
||||||
#![cfg_attr(docs_rs, feature(doc_auto_cfg))]
|
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||||
pub mod res_code;
|
pub mod res_code;
|
||||||
|
@ -12,6 +12,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
|
|||||||
|
|
||||||
- Renamed `StaticPoolConfig::new` to `StaticPoolConfig::new_from_subpool_cfg_tuples`. The new
|
- Renamed `StaticPoolConfig::new` to `StaticPoolConfig::new_from_subpool_cfg_tuples`. The new
|
||||||
`new` implementation expects a type struct instead of tuples.
|
`new` implementation expects a type struct instead of tuples.
|
||||||
|
- Moved `cfdp` module to [dedicated crate](https://egit.irs.uni-stuttgart.de/rust/cfdp)
|
||||||
|
- Moved `seq_count` module to [spacepackets](https://egit.irs.uni-stuttgart.de/rust/spacepackets)
|
||||||
|
crate
|
||||||
|
|
||||||
## Added
|
## Added
|
||||||
|
|
||||||
|
@ -13,21 +13,22 @@ keywords = ["no-std", "space", "aerospace"]
|
|||||||
categories = ["aerospace", "aerospace::space-protocols", "no-std", "hardware-support", "embedded"]
|
categories = ["aerospace", "aerospace::space-protocols", "no-std", "hardware-support", "embedded"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
delegate = ">0.7, <=0.10"
|
delegate = ">0.7, <=0.13"
|
||||||
paste = "1"
|
paste = "1"
|
||||||
derive-new = "0.6"
|
derive-new = ">=0.6, <=0.7"
|
||||||
smallvec = "1"
|
smallvec = "1"
|
||||||
crc = "3"
|
crc = "3"
|
||||||
|
|
||||||
[dependencies.satrs-shared]
|
[dependencies.satrs-shared]
|
||||||
version = ">=0.1.3, <0.2"
|
version = ">=0.1.3, <=0.2"
|
||||||
|
path = "../satrs-shared"
|
||||||
|
|
||||||
[dependencies.num_enum]
|
[dependencies.num_enum]
|
||||||
version = ">0.5, <=0.7"
|
version = ">0.5, <=0.7"
|
||||||
default-features = false
|
default-features = false
|
||||||
|
|
||||||
[dependencies.spacepackets]
|
[dependencies.spacepackets]
|
||||||
version = "0.11"
|
version = "0.13"
|
||||||
default-features = false
|
default-features = false
|
||||||
|
|
||||||
[dependencies.cobs]
|
[dependencies.cobs]
|
||||||
@ -45,11 +46,11 @@ version = "1"
|
|||||||
optional = true
|
optional = true
|
||||||
|
|
||||||
[dependencies.hashbrown]
|
[dependencies.hashbrown]
|
||||||
version = "0.14"
|
version = ">=0.14, <=0.15"
|
||||||
optional = true
|
optional = true
|
||||||
|
|
||||||
[dependencies.heapless]
|
[dependencies.heapless]
|
||||||
version = "0.7"
|
version = "0.8"
|
||||||
optional = true
|
optional = true
|
||||||
|
|
||||||
[dependencies.downcast-rs]
|
[dependencies.downcast-rs]
|
||||||
@ -67,8 +68,8 @@ default-features = false
|
|||||||
optional = true
|
optional = true
|
||||||
|
|
||||||
[dependencies.thiserror]
|
[dependencies.thiserror]
|
||||||
version = "1"
|
version = "2"
|
||||||
optional = true
|
default-features = false
|
||||||
|
|
||||||
[dependencies.serde]
|
[dependencies.serde]
|
||||||
version = "1"
|
version = "1"
|
||||||
@ -81,7 +82,7 @@ features = ["all"]
|
|||||||
optional = true
|
optional = true
|
||||||
|
|
||||||
[dependencies.mio]
|
[dependencies.mio]
|
||||||
version = "0.8"
|
version = "1"
|
||||||
features = ["os-poll", "net"]
|
features = ["os-poll", "net"]
|
||||||
optional = true
|
optional = true
|
||||||
|
|
||||||
@ -91,7 +92,7 @@ optional = true
|
|||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
serde = "1"
|
serde = "1"
|
||||||
zerocopy = "0.7"
|
zerocopy = "0.8"
|
||||||
once_cell = "1"
|
once_cell = "1"
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
@ -111,7 +112,7 @@ std = [
|
|||||||
"serde/std",
|
"serde/std",
|
||||||
"spacepackets/std",
|
"spacepackets/std",
|
||||||
"num_enum/std",
|
"num_enum/std",
|
||||||
"thiserror",
|
"thiserror/std",
|
||||||
"socket2",
|
"socket2",
|
||||||
"mio"
|
"mio"
|
||||||
]
|
]
|
||||||
@ -127,8 +128,7 @@ crossbeam = ["crossbeam-channel"]
|
|||||||
heapless = ["dep:heapless"]
|
heapless = ["dep:heapless"]
|
||||||
defmt = ["dep:defmt", "spacepackets/defmt"]
|
defmt = ["dep:defmt", "spacepackets/defmt"]
|
||||||
test_util = []
|
test_util = []
|
||||||
doc-images = []
|
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
all-features = true
|
all-features = true
|
||||||
rustdoc-args = ["--cfg", "docs_rs", "--generate-link-to-definition"]
|
rustdoc-args = ["--generate-link-to-definition"]
|
||||||
|
3
satrs/docs.sh
Executable file
3
satrs/docs.sh
Executable file
@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
export RUSTDOCFLAGS="--cfg docsrs --generate-link-to-definition -Z unstable-options"
|
||||||
|
cargo +nightly doc --all-features --open
|
@ -3,8 +3,7 @@ Checklist for new releases
|
|||||||
|
|
||||||
# Pre-Release
|
# Pre-Release
|
||||||
|
|
||||||
1. Make sure any new modules are documented sufficiently enough and check docs with
|
1. Make sure any new modules are documented sufficiently enough and check docs by running `docs.sh`.
|
||||||
`cargo +nightly doc --all-features --config 'build.rustdocflags=["--cfg", "docs_rs"]' --open`.
|
|
||||||
2. Bump version specifier in `Cargo.toml`.
|
2. Bump version specifier in `Cargo.toml`.
|
||||||
3. Update `CHANGELOG.md`: Convert `unreleased` section into version section with date and add new
|
3. Update `CHANGELOG.md`: Convert `unreleased` section into version section with date and add new
|
||||||
`unreleased` section.
|
`unreleased` section.
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -1,769 +0,0 @@
|
|||||||
use alloc::string::{String, ToString};
|
|
||||||
use core::fmt::Display;
|
|
||||||
use crc::{Crc, CRC_32_CKSUM};
|
|
||||||
use spacepackets::cfdp::ChecksumType;
|
|
||||||
use spacepackets::ByteConversionError;
|
|
||||||
#[cfg(feature = "std")]
|
|
||||||
use std::error::Error;
|
|
||||||
use std::path::Path;
|
|
||||||
#[cfg(feature = "std")]
|
|
||||||
pub use std_mod::*;
|
|
||||||
|
|
||||||
pub const CRC_32: Crc<u32> = Crc::<u32>::new(&CRC_32_CKSUM);
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum FilestoreError {
|
|
||||||
FileDoesNotExist,
|
|
||||||
FileAlreadyExists,
|
|
||||||
DirDoesNotExist,
|
|
||||||
Permission,
|
|
||||||
IsNotFile,
|
|
||||||
IsNotDirectory,
|
|
||||||
ByteConversion(ByteConversionError),
|
|
||||||
Io {
|
|
||||||
raw_errno: Option<i32>,
|
|
||||||
string: String,
|
|
||||||
},
|
|
||||||
ChecksumTypeNotImplemented(ChecksumType),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<ByteConversionError> for FilestoreError {
|
|
||||||
fn from(value: ByteConversionError) -> Self {
|
|
||||||
Self::ByteConversion(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for FilestoreError {
|
|
||||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
|
||||||
match self {
|
|
||||||
FilestoreError::FileDoesNotExist => {
|
|
||||||
write!(f, "file does not exist")
|
|
||||||
}
|
|
||||||
FilestoreError::FileAlreadyExists => {
|
|
||||||
write!(f, "file already exists")
|
|
||||||
}
|
|
||||||
FilestoreError::DirDoesNotExist => {
|
|
||||||
write!(f, "directory does not exist")
|
|
||||||
}
|
|
||||||
FilestoreError::Permission => {
|
|
||||||
write!(f, "permission error")
|
|
||||||
}
|
|
||||||
FilestoreError::IsNotFile => {
|
|
||||||
write!(f, "is not a file")
|
|
||||||
}
|
|
||||||
FilestoreError::IsNotDirectory => {
|
|
||||||
write!(f, "is not a directory")
|
|
||||||
}
|
|
||||||
FilestoreError::ByteConversion(e) => {
|
|
||||||
write!(f, "filestore error: {e}")
|
|
||||||
}
|
|
||||||
FilestoreError::Io { raw_errno, string } => {
|
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
"filestore generic IO error with raw errno {:?}: {}",
|
|
||||||
raw_errno, string
|
|
||||||
)
|
|
||||||
}
|
|
||||||
FilestoreError::ChecksumTypeNotImplemented(checksum_type) => {
|
|
||||||
write!(f, "checksum {:?} not implemented", checksum_type)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Error for FilestoreError {
|
|
||||||
fn source(&self) -> Option<&(dyn Error + 'static)> {
|
|
||||||
match self {
|
|
||||||
FilestoreError::ByteConversion(e) => Some(e),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "std")]
|
|
||||||
impl From<std::io::Error> for FilestoreError {
|
|
||||||
fn from(value: std::io::Error) -> Self {
|
|
||||||
Self::Io {
|
|
||||||
raw_errno: value.raw_os_error(),
|
|
||||||
string: value.to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait VirtualFilestore {
|
|
||||||
fn create_file(&self, file_path: &str) -> Result<(), FilestoreError>;
|
|
||||||
|
|
||||||
fn remove_file(&self, file_path: &str) -> Result<(), FilestoreError>;
|
|
||||||
|
|
||||||
/// Truncating a file means deleting all its data so the resulting file is empty.
|
|
||||||
/// This can be more efficient than removing and re-creating a file.
|
|
||||||
fn truncate_file(&self, file_path: &str) -> Result<(), FilestoreError>;
|
|
||||||
|
|
||||||
fn remove_dir(&self, dir_path: &str, all: bool) -> Result<(), FilestoreError>;
|
|
||||||
fn create_dir(&self, dir_path: &str) -> Result<(), FilestoreError>;
|
|
||||||
|
|
||||||
fn read_data(
|
|
||||||
&self,
|
|
||||||
file_path: &str,
|
|
||||||
offset: u64,
|
|
||||||
read_len: u64,
|
|
||||||
buf: &mut [u8],
|
|
||||||
) -> Result<(), FilestoreError>;
|
|
||||||
|
|
||||||
fn write_data(&self, file: &str, offset: u64, buf: &[u8]) -> Result<(), FilestoreError>;
|
|
||||||
|
|
||||||
fn filename_from_full_path(path: &str) -> Option<&str>
|
|
||||||
where
|
|
||||||
Self: Sized,
|
|
||||||
{
|
|
||||||
// Convert the path string to a Path
|
|
||||||
let path = Path::new(path);
|
|
||||||
|
|
||||||
// Extract the file name using the file_name() method
|
|
||||||
path.file_name().and_then(|name| name.to_str())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_file(&self, path: &str) -> bool;
|
|
||||||
|
|
||||||
fn is_dir(&self, path: &str) -> bool {
|
|
||||||
!self.is_file(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn exists(&self, path: &str) -> bool;
|
|
||||||
|
|
||||||
/// This special function is the CFDP specific abstraction to verify the checksum of a file.
|
|
||||||
/// This allows to keep OS specific details like reading the whole file in the most efficient
|
|
||||||
/// manner inside the file system abstraction.
|
|
||||||
///
|
|
||||||
/// The passed verification buffer argument will be used by the specific implementation as
|
|
||||||
/// a buffer to read the file into. It is recommended to use common buffer sizes like
|
|
||||||
/// 4096 or 8192 bytes.
|
|
||||||
fn checksum_verify(
|
|
||||||
&self,
|
|
||||||
file_path: &str,
|
|
||||||
checksum_type: ChecksumType,
|
|
||||||
expected_checksum: u32,
|
|
||||||
verification_buf: &mut [u8],
|
|
||||||
) -> Result<bool, FilestoreError>;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "std")]
|
|
||||||
pub mod std_mod {
|
|
||||||
use super::*;
|
|
||||||
use std::{
|
|
||||||
fs::{self, File, OpenOptions},
|
|
||||||
io::{BufReader, Read, Seek, SeekFrom, Write},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct NativeFilestore {}
|
|
||||||
|
|
||||||
impl VirtualFilestore for NativeFilestore {
|
|
||||||
fn create_file(&self, file_path: &str) -> Result<(), FilestoreError> {
|
|
||||||
if self.exists(file_path) {
|
|
||||||
return Err(FilestoreError::FileAlreadyExists);
|
|
||||||
}
|
|
||||||
File::create(file_path)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn remove_file(&self, file_path: &str) -> Result<(), FilestoreError> {
|
|
||||||
if !self.exists(file_path) {
|
|
||||||
return Err(FilestoreError::FileDoesNotExist);
|
|
||||||
}
|
|
||||||
if !self.is_file(file_path) {
|
|
||||||
return Err(FilestoreError::IsNotFile);
|
|
||||||
}
|
|
||||||
fs::remove_file(file_path)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn truncate_file(&self, file_path: &str) -> Result<(), FilestoreError> {
|
|
||||||
if !self.exists(file_path) {
|
|
||||||
return Err(FilestoreError::FileDoesNotExist);
|
|
||||||
}
|
|
||||||
if !self.is_file(file_path) {
|
|
||||||
return Err(FilestoreError::IsNotFile);
|
|
||||||
}
|
|
||||||
OpenOptions::new()
|
|
||||||
.write(true)
|
|
||||||
.truncate(true)
|
|
||||||
.open(file_path)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_dir(&self, dir_path: &str) -> Result<(), FilestoreError> {
|
|
||||||
fs::create_dir(dir_path).map_err(|e| FilestoreError::Io {
|
|
||||||
raw_errno: e.raw_os_error(),
|
|
||||||
string: e.to_string(),
|
|
||||||
})?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn remove_dir(&self, dir_path: &str, all: bool) -> Result<(), FilestoreError> {
|
|
||||||
if !self.exists(dir_path) {
|
|
||||||
return Err(FilestoreError::DirDoesNotExist);
|
|
||||||
}
|
|
||||||
if !self.is_dir(dir_path) {
|
|
||||||
return Err(FilestoreError::IsNotDirectory);
|
|
||||||
}
|
|
||||||
if !all {
|
|
||||||
fs::remove_dir(dir_path)?;
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
fs::remove_dir_all(dir_path)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_data(
|
|
||||||
&self,
|
|
||||||
file_name: &str,
|
|
||||||
offset: u64,
|
|
||||||
read_len: u64,
|
|
||||||
buf: &mut [u8],
|
|
||||||
) -> Result<(), FilestoreError> {
|
|
||||||
if buf.len() < read_len as usize {
|
|
||||||
return Err(ByteConversionError::ToSliceTooSmall {
|
|
||||||
found: buf.len(),
|
|
||||||
expected: read_len as usize,
|
|
||||||
}
|
|
||||||
.into());
|
|
||||||
}
|
|
||||||
if !self.exists(file_name) {
|
|
||||||
return Err(FilestoreError::FileDoesNotExist);
|
|
||||||
}
|
|
||||||
if !self.is_file(file_name) {
|
|
||||||
return Err(FilestoreError::IsNotFile);
|
|
||||||
}
|
|
||||||
let mut file = File::open(file_name)?;
|
|
||||||
file.seek(SeekFrom::Start(offset))?;
|
|
||||||
file.read_exact(&mut buf[0..read_len as usize])?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write_data(&self, file: &str, offset: u64, buf: &[u8]) -> Result<(), FilestoreError> {
|
|
||||||
if !self.exists(file) {
|
|
||||||
return Err(FilestoreError::FileDoesNotExist);
|
|
||||||
}
|
|
||||||
if !self.is_file(file) {
|
|
||||||
return Err(FilestoreError::IsNotFile);
|
|
||||||
}
|
|
||||||
let mut file = OpenOptions::new().write(true).open(file)?;
|
|
||||||
file.seek(SeekFrom::Start(offset))?;
|
|
||||||
file.write_all(buf)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_file(&self, path: &str) -> bool {
|
|
||||||
let path = Path::new(path);
|
|
||||||
path.is_file()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn exists(&self, path: &str) -> bool {
|
|
||||||
let path = Path::new(path);
|
|
||||||
if !path.exists() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
fn checksum_verify(
|
|
||||||
&self,
|
|
||||||
file_path: &str,
|
|
||||||
checksum_type: ChecksumType,
|
|
||||||
expected_checksum: u32,
|
|
||||||
verification_buf: &mut [u8],
|
|
||||||
) -> Result<bool, FilestoreError> {
|
|
||||||
match checksum_type {
|
|
||||||
ChecksumType::Modular => {
|
|
||||||
if self.calc_modular_checksum(file_path)? == expected_checksum {
|
|
||||||
return Ok(true);
|
|
||||||
}
|
|
||||||
Ok(false)
|
|
||||||
}
|
|
||||||
ChecksumType::Crc32 => {
|
|
||||||
let mut digest = CRC_32.digest();
|
|
||||||
let file_to_check = File::open(file_path)?;
|
|
||||||
let mut buf_reader = BufReader::new(file_to_check);
|
|
||||||
loop {
|
|
||||||
let bytes_read = buf_reader.read(verification_buf)?;
|
|
||||||
if bytes_read == 0 {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
digest.update(&verification_buf[0..bytes_read]);
|
|
||||||
}
|
|
||||||
if digest.finalize() == expected_checksum {
|
|
||||||
return Ok(true);
|
|
||||||
}
|
|
||||||
Ok(false)
|
|
||||||
}
|
|
||||||
ChecksumType::NullChecksum => Ok(true),
|
|
||||||
_ => Err(FilestoreError::ChecksumTypeNotImplemented(checksum_type)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl NativeFilestore {
|
|
||||||
pub fn calc_modular_checksum(&self, file_path: &str) -> Result<u32, FilestoreError> {
|
|
||||||
let mut checksum: u32 = 0;
|
|
||||||
let file = File::open(file_path)?;
|
|
||||||
let mut buf_reader = BufReader::new(file);
|
|
||||||
let mut buffer = [0; 4];
|
|
||||||
|
|
||||||
loop {
|
|
||||||
let bytes_read = buf_reader.read(&mut buffer)?;
|
|
||||||
if bytes_read == 0 {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// Perform padding directly in the buffer
|
|
||||||
(bytes_read..4).for_each(|i| {
|
|
||||||
buffer[i] = 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
checksum = checksum.wrapping_add(u32::from_be_bytes(buffer));
|
|
||||||
}
|
|
||||||
Ok(checksum)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use std::{fs, path::Path, println};
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
use alloc::format;
|
|
||||||
use tempfile::tempdir;
|
|
||||||
|
|
||||||
const EXAMPLE_DATA_CFDP: [u8; 15] = [
|
|
||||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E,
|
|
||||||
];
|
|
||||||
|
|
||||||
const NATIVE_FS: NativeFilestore = NativeFilestore {};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_basic_native_filestore_create() {
|
|
||||||
let tmpdir = tempdir().expect("creating tmpdir failed");
|
|
||||||
let file_path = tmpdir.path().join("test.txt");
|
|
||||||
let result =
|
|
||||||
NATIVE_FS.create_file(file_path.to_str().expect("getting str for file failed"));
|
|
||||||
assert!(result.is_ok());
|
|
||||||
let path = Path::new(&file_path);
|
|
||||||
assert!(path.exists());
|
|
||||||
assert!(NATIVE_FS.exists(file_path.to_str().unwrap()));
|
|
||||||
assert!(NATIVE_FS.is_file(file_path.to_str().unwrap()));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_basic_native_fs_file_exists() {
|
|
||||||
let tmpdir = tempdir().expect("creating tmpdir failed");
|
|
||||||
let file_path = tmpdir.path().join("test.txt");
|
|
||||||
assert!(!NATIVE_FS.exists(file_path.to_str().unwrap()));
|
|
||||||
NATIVE_FS
|
|
||||||
.create_file(file_path.to_str().expect("getting str for file failed"))
|
|
||||||
.unwrap();
|
|
||||||
assert!(NATIVE_FS.exists(file_path.to_str().unwrap()));
|
|
||||||
assert!(NATIVE_FS.is_file(file_path.to_str().unwrap()));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_basic_native_fs_dir_exists() {
|
|
||||||
let tmpdir = tempdir().expect("creating tmpdir failed");
|
|
||||||
let dir_path = tmpdir.path().join("testdir");
|
|
||||||
assert!(!NATIVE_FS.exists(dir_path.to_str().unwrap()));
|
|
||||||
NATIVE_FS
|
|
||||||
.create_dir(dir_path.to_str().expect("getting str for file failed"))
|
|
||||||
.unwrap();
|
|
||||||
assert!(NATIVE_FS.exists(dir_path.to_str().unwrap()));
|
|
||||||
assert!(NATIVE_FS.is_dir(dir_path.as_path().to_str().unwrap()));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_basic_native_fs_remove_file() {
|
|
||||||
let tmpdir = tempdir().expect("creating tmpdir failed");
|
|
||||||
let file_path = tmpdir.path().join("test.txt");
|
|
||||||
NATIVE_FS
|
|
||||||
.create_file(file_path.to_str().expect("getting str for file failed"))
|
|
||||||
.expect("creating file failed");
|
|
||||||
assert!(NATIVE_FS.exists(file_path.to_str().unwrap()));
|
|
||||||
NATIVE_FS
|
|
||||||
.remove_file(file_path.to_str().unwrap())
|
|
||||||
.expect("removing file failed");
|
|
||||||
assert!(!NATIVE_FS.exists(file_path.to_str().unwrap()));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_basic_native_fs_write() {
|
|
||||||
let tmpdir = tempdir().expect("creating tmpdir failed");
|
|
||||||
let file_path = tmpdir.path().join("test.txt");
|
|
||||||
assert!(!NATIVE_FS.exists(file_path.to_str().unwrap()));
|
|
||||||
NATIVE_FS
|
|
||||||
.create_file(file_path.to_str().expect("getting str for file failed"))
|
|
||||||
.unwrap();
|
|
||||||
assert!(NATIVE_FS.exists(file_path.to_str().unwrap()));
|
|
||||||
assert!(NATIVE_FS.is_file(file_path.to_str().unwrap()));
|
|
||||||
println!("{}", file_path.to_str().unwrap());
|
|
||||||
let write_data = "hello world\n";
|
|
||||||
NATIVE_FS
|
|
||||||
.write_data(file_path.to_str().unwrap(), 0, write_data.as_bytes())
|
|
||||||
.expect("writing to file failed");
|
|
||||||
let read_back = fs::read_to_string(file_path).expect("reading back data failed");
|
|
||||||
assert_eq!(read_back, write_data);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_basic_native_fs_read() {
|
|
||||||
let tmpdir = tempdir().expect("creating tmpdir failed");
|
|
||||||
let file_path = tmpdir.path().join("test.txt");
|
|
||||||
assert!(!NATIVE_FS.exists(file_path.to_str().unwrap()));
|
|
||||||
NATIVE_FS
|
|
||||||
.create_file(file_path.to_str().expect("getting str for file failed"))
|
|
||||||
.unwrap();
|
|
||||||
assert!(NATIVE_FS.exists(file_path.to_str().unwrap()));
|
|
||||||
assert!(NATIVE_FS.is_file(file_path.to_str().unwrap()));
|
|
||||||
println!("{}", file_path.to_str().unwrap());
|
|
||||||
let write_data = "hello world\n";
|
|
||||||
NATIVE_FS
|
|
||||||
.write_data(file_path.to_str().unwrap(), 0, write_data.as_bytes())
|
|
||||||
.expect("writing to file failed");
|
|
||||||
let read_back = fs::read_to_string(file_path).expect("reading back data failed");
|
|
||||||
assert_eq!(read_back, write_data);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_truncate_file() {
|
|
||||||
let tmpdir = tempdir().expect("creating tmpdir failed");
|
|
||||||
let file_path = tmpdir.path().join("test.txt");
|
|
||||||
NATIVE_FS
|
|
||||||
.create_file(file_path.to_str().expect("getting str for file failed"))
|
|
||||||
.expect("creating file failed");
|
|
||||||
fs::write(file_path.clone(), [1, 2, 3, 4]).unwrap();
|
|
||||||
assert_eq!(fs::read(file_path.clone()).unwrap(), [1, 2, 3, 4]);
|
|
||||||
NATIVE_FS
|
|
||||||
.truncate_file(file_path.to_str().unwrap())
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(fs::read(file_path.clone()).unwrap(), []);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_remove_dir() {
|
|
||||||
let tmpdir = tempdir().expect("creating tmpdir failed");
|
|
||||||
let dir_path = tmpdir.path().join("testdir");
|
|
||||||
assert!(!NATIVE_FS.exists(dir_path.to_str().unwrap()));
|
|
||||||
NATIVE_FS
|
|
||||||
.create_dir(dir_path.to_str().expect("getting str for file failed"))
|
|
||||||
.unwrap();
|
|
||||||
assert!(NATIVE_FS.exists(dir_path.to_str().unwrap()));
|
|
||||||
NATIVE_FS
|
|
||||||
.remove_dir(dir_path.to_str().unwrap(), false)
|
|
||||||
.unwrap();
|
|
||||||
assert!(!NATIVE_FS.exists(dir_path.to_str().unwrap()));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_read_file() {
|
|
||||||
let tmpdir = tempdir().expect("creating tmpdir failed");
|
|
||||||
let file_path = tmpdir.path().join("test.txt");
|
|
||||||
NATIVE_FS
|
|
||||||
.create_file(file_path.to_str().expect("getting str for file failed"))
|
|
||||||
.expect("creating file failed");
|
|
||||||
fs::write(file_path.clone(), [1, 2, 3, 4]).unwrap();
|
|
||||||
let read_buf: &mut [u8] = &mut [0; 4];
|
|
||||||
NATIVE_FS
|
|
||||||
.read_data(file_path.to_str().unwrap(), 0, 4, read_buf)
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!([1, 2, 3, 4], read_buf);
|
|
||||||
NATIVE_FS
|
|
||||||
.write_data(file_path.to_str().unwrap(), 4, &[5, 6, 7, 8])
|
|
||||||
.expect("writing to file failed");
|
|
||||||
NATIVE_FS
|
|
||||||
.read_data(file_path.to_str().unwrap(), 2, 4, read_buf)
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!([3, 4, 5, 6], read_buf);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_remove_which_does_not_exist() {
|
|
||||||
let tmpdir = tempdir().expect("creating tmpdir failed");
|
|
||||||
let file_path = tmpdir.path().join("test.txt");
|
|
||||||
let result = NATIVE_FS.read_data(file_path.to_str().unwrap(), 0, 4, &mut [0; 4]);
|
|
||||||
assert!(result.is_err());
|
|
||||||
let error = result.unwrap_err();
|
|
||||||
if let FilestoreError::FileDoesNotExist = error {
|
|
||||||
assert_eq!(error.to_string(), "file does not exist");
|
|
||||||
} else {
|
|
||||||
panic!("unexpected error");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_file_already_exists() {
|
|
||||||
let tmpdir = tempdir().expect("creating tmpdir failed");
|
|
||||||
let file_path = tmpdir.path().join("test.txt");
|
|
||||||
let result =
|
|
||||||
NATIVE_FS.create_file(file_path.to_str().expect("getting str for file failed"));
|
|
||||||
assert!(result.is_ok());
|
|
||||||
let result =
|
|
||||||
NATIVE_FS.create_file(file_path.to_str().expect("getting str for file failed"));
|
|
||||||
assert!(result.is_err());
|
|
||||||
let error = result.unwrap_err();
|
|
||||||
if let FilestoreError::FileAlreadyExists = error {
|
|
||||||
assert_eq!(error.to_string(), "file already exists");
|
|
||||||
} else {
|
|
||||||
panic!("unexpected error");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_remove_file_with_dir_api() {
|
|
||||||
let tmpdir = tempdir().expect("creating tmpdir failed");
|
|
||||||
let file_path = tmpdir.path().join("test.txt");
|
|
||||||
NATIVE_FS
|
|
||||||
.create_file(file_path.to_str().expect("getting str for file failed"))
|
|
||||||
.unwrap();
|
|
||||||
let result = NATIVE_FS.remove_dir(file_path.to_str().unwrap(), true);
|
|
||||||
assert!(result.is_err());
|
|
||||||
let error = result.unwrap_err();
|
|
||||||
if let FilestoreError::IsNotDirectory = error {
|
|
||||||
assert_eq!(error.to_string(), "is not a directory");
|
|
||||||
} else {
|
|
||||||
panic!("unexpected error");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_remove_dir_remove_all() {
|
|
||||||
let tmpdir = tempdir().expect("creating tmpdir failed");
|
|
||||||
let dir_path = tmpdir.path().join("test");
|
|
||||||
NATIVE_FS
|
|
||||||
.create_dir(dir_path.to_str().expect("getting str for file failed"))
|
|
||||||
.unwrap();
|
|
||||||
let file_path = dir_path.as_path().join("test.txt");
|
|
||||||
NATIVE_FS
|
|
||||||
.create_file(file_path.to_str().expect("getting str for file failed"))
|
|
||||||
.unwrap();
|
|
||||||
let result = NATIVE_FS.remove_dir(dir_path.to_str().unwrap(), true);
|
|
||||||
assert!(result.is_ok());
|
|
||||||
assert!(!NATIVE_FS.exists(dir_path.to_str().unwrap()));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_remove_dir_with_file_api() {
|
|
||||||
let tmpdir = tempdir().expect("creating tmpdir failed");
|
|
||||||
let file_path = tmpdir.path().join("test");
|
|
||||||
NATIVE_FS
|
|
||||||
.create_dir(file_path.to_str().expect("getting str for file failed"))
|
|
||||||
.unwrap();
|
|
||||||
let result = NATIVE_FS.remove_file(file_path.to_str().unwrap());
|
|
||||||
assert!(result.is_err());
|
|
||||||
let error = result.unwrap_err();
|
|
||||||
if let FilestoreError::IsNotFile = error {
|
|
||||||
assert_eq!(error.to_string(), "is not a file");
|
|
||||||
} else {
|
|
||||||
panic!("unexpected error");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_remove_dir_which_does_not_exist() {
|
|
||||||
let tmpdir = tempdir().expect("creating tmpdir failed");
|
|
||||||
let file_path = tmpdir.path().join("test");
|
|
||||||
let result = NATIVE_FS.remove_dir(file_path.to_str().unwrap(), true);
|
|
||||||
assert!(result.is_err());
|
|
||||||
let error = result.unwrap_err();
|
|
||||||
if let FilestoreError::DirDoesNotExist = error {
|
|
||||||
assert_eq!(error.to_string(), "directory does not exist");
|
|
||||||
} else {
|
|
||||||
panic!("unexpected error");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_remove_file_which_does_not_exist() {
|
|
||||||
let tmpdir = tempdir().expect("creating tmpdir failed");
|
|
||||||
let file_path = tmpdir.path().join("test.txt");
|
|
||||||
let result = NATIVE_FS.remove_file(file_path.to_str().unwrap());
|
|
||||||
assert!(result.is_err());
|
|
||||||
let error = result.unwrap_err();
|
|
||||||
if let FilestoreError::FileDoesNotExist = error {
|
|
||||||
assert_eq!(error.to_string(), "file does not exist");
|
|
||||||
} else {
|
|
||||||
panic!("unexpected error");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_truncate_file_which_does_not_exist() {
|
|
||||||
let tmpdir = tempdir().expect("creating tmpdir failed");
|
|
||||||
let file_path = tmpdir.path().join("test.txt");
|
|
||||||
let result = NATIVE_FS.truncate_file(file_path.to_str().unwrap());
|
|
||||||
assert!(result.is_err());
|
|
||||||
let error = result.unwrap_err();
|
|
||||||
if let FilestoreError::FileDoesNotExist = error {
|
|
||||||
assert_eq!(error.to_string(), "file does not exist");
|
|
||||||
} else {
|
|
||||||
panic!("unexpected error");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_truncate_file_on_directory() {
|
|
||||||
let tmpdir = tempdir().expect("creating tmpdir failed");
|
|
||||||
let file_path = tmpdir.path().join("test");
|
|
||||||
NATIVE_FS.create_dir(file_path.to_str().unwrap()).unwrap();
|
|
||||||
let result = NATIVE_FS.truncate_file(file_path.to_str().unwrap());
|
|
||||||
assert!(result.is_err());
|
|
||||||
let error = result.unwrap_err();
|
|
||||||
if let FilestoreError::IsNotFile = error {
|
|
||||||
assert_eq!(error.to_string(), "is not a file");
|
|
||||||
} else {
|
|
||||||
panic!("unexpected error");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_byte_conversion_error_when_reading() {
|
|
||||||
let tmpdir = tempdir().expect("creating tmpdir failed");
|
|
||||||
let file_path = tmpdir.path().join("test.txt");
|
|
||||||
NATIVE_FS
|
|
||||||
.create_file(file_path.to_str().expect("getting str for file failed"))
|
|
||||||
.unwrap();
|
|
||||||
let result = NATIVE_FS.read_data(file_path.to_str().unwrap(), 0, 2, &mut []);
|
|
||||||
assert!(result.is_err());
|
|
||||||
let error = result.unwrap_err();
|
|
||||||
if let FilestoreError::ByteConversion(byte_conv_error) = error {
|
|
||||||
if let ByteConversionError::ToSliceTooSmall { found, expected } = byte_conv_error {
|
|
||||||
assert_eq!(found, 0);
|
|
||||||
assert_eq!(expected, 2);
|
|
||||||
} else {
|
|
||||||
panic!("unexpected error");
|
|
||||||
}
|
|
||||||
assert_eq!(
|
|
||||||
error.to_string(),
|
|
||||||
format!("filestore error: {}", byte_conv_error)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
panic!("unexpected error");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_read_file_on_dir() {
|
|
||||||
let tmpdir = tempdir().expect("creating tmpdir failed");
|
|
||||||
let dir_path = tmpdir.path().join("test");
|
|
||||||
NATIVE_FS
|
|
||||||
.create_dir(dir_path.to_str().expect("getting str for file failed"))
|
|
||||||
.unwrap();
|
|
||||||
let result = NATIVE_FS.read_data(dir_path.to_str().unwrap(), 0, 4, &mut [0; 4]);
|
|
||||||
assert!(result.is_err());
|
|
||||||
let error = result.unwrap_err();
|
|
||||||
if let FilestoreError::IsNotFile = error {
|
|
||||||
assert_eq!(error.to_string(), "is not a file");
|
|
||||||
} else {
|
|
||||||
panic!("unexpected error");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_write_file_non_existing() {
|
|
||||||
let tmpdir = tempdir().expect("creating tmpdir failed");
|
|
||||||
let file_path = tmpdir.path().join("test.txt");
|
|
||||||
let result = NATIVE_FS.write_data(file_path.to_str().unwrap(), 0, &[]);
|
|
||||||
assert!(result.is_err());
|
|
||||||
let error = result.unwrap_err();
|
|
||||||
if let FilestoreError::FileDoesNotExist = error {
|
|
||||||
} else {
|
|
||||||
panic!("unexpected error");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_write_file_on_dir() {
|
|
||||||
let tmpdir = tempdir().expect("creating tmpdir failed");
|
|
||||||
let file_path = tmpdir.path().join("test");
|
|
||||||
NATIVE_FS.create_dir(file_path.to_str().unwrap()).unwrap();
|
|
||||||
let result = NATIVE_FS.write_data(file_path.to_str().unwrap(), 0, &[]);
|
|
||||||
assert!(result.is_err());
|
|
||||||
let error = result.unwrap_err();
|
|
||||||
if let FilestoreError::IsNotFile = error {
|
|
||||||
} else {
|
|
||||||
panic!("unexpected error");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_filename_extraction() {
|
|
||||||
let tmpdir = tempdir().expect("creating tmpdir failed");
|
|
||||||
let file_path = tmpdir.path().join("test.txt");
|
|
||||||
NATIVE_FS
|
|
||||||
.create_file(file_path.to_str().expect("getting str for file failed"))
|
|
||||||
.unwrap();
|
|
||||||
NativeFilestore::filename_from_full_path(file_path.to_str().unwrap());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_modular_checksum() {
|
|
||||||
let tmpdir = tempdir().expect("creating tmpdir failed");
|
|
||||||
let file_path = tmpdir.path().join("mod-crc.bin");
|
|
||||||
fs::write(file_path.as_path(), EXAMPLE_DATA_CFDP).expect("writing test file failed");
|
|
||||||
// Kind of re-writing the modular checksum impl here which we are trying to test, but the
|
|
||||||
// numbers/correctness were verified manually using calculators, so this is okay.
|
|
||||||
let mut checksum: u32 = 0;
|
|
||||||
let mut buffer: [u8; 4] = [0; 4];
|
|
||||||
for i in 0..3 {
|
|
||||||
buffer = EXAMPLE_DATA_CFDP[i * 4..(i + 1) * 4].try_into().unwrap();
|
|
||||||
checksum = checksum.wrapping_add(u32::from_be_bytes(buffer));
|
|
||||||
}
|
|
||||||
buffer[0..3].copy_from_slice(&EXAMPLE_DATA_CFDP[12..15]);
|
|
||||||
buffer[3] = 0;
|
|
||||||
checksum = checksum.wrapping_add(u32::from_be_bytes(buffer));
|
|
||||||
let mut verif_buf: [u8; 32] = [0; 32];
|
|
||||||
let result = NATIVE_FS.checksum_verify(
|
|
||||||
file_path.to_str().unwrap(),
|
|
||||||
ChecksumType::Modular,
|
|
||||||
checksum,
|
|
||||||
&mut verif_buf,
|
|
||||||
);
|
|
||||||
assert!(result.is_ok());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_null_checksum_impl() {
|
|
||||||
let tmpdir = tempdir().expect("creating tmpdir failed");
|
|
||||||
let file_path = tmpdir.path().join("mod-crc.bin");
|
|
||||||
// The file to check does not even need to exist, and the verification buffer can be
|
|
||||||
// empty: the null checksum is always yields the same result.
|
|
||||||
let result = NATIVE_FS.checksum_verify(
|
|
||||||
file_path.to_str().unwrap(),
|
|
||||||
ChecksumType::NullChecksum,
|
|
||||||
0,
|
|
||||||
&mut [],
|
|
||||||
);
|
|
||||||
assert!(result.is_ok());
|
|
||||||
assert!(result.unwrap());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_checksum_not_implemented() {
|
|
||||||
let tmpdir = tempdir().expect("creating tmpdir failed");
|
|
||||||
let file_path = tmpdir.path().join("mod-crc.bin");
|
|
||||||
// The file to check does not even need to exist, and the verification buffer can be
|
|
||||||
// empty: the null checksum is always yields the same result.
|
|
||||||
let result = NATIVE_FS.checksum_verify(
|
|
||||||
file_path.to_str().unwrap(),
|
|
||||||
ChecksumType::Crc32Proximity1,
|
|
||||||
0,
|
|
||||||
&mut [],
|
|
||||||
);
|
|
||||||
assert!(result.is_err());
|
|
||||||
let error = result.unwrap_err();
|
|
||||||
if let FilestoreError::ChecksumTypeNotImplemented(cksum_type) = error {
|
|
||||||
assert_eq!(
|
|
||||||
error.to_string(),
|
|
||||||
format!("checksum {:?} not implemented", cksum_type)
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
panic!("unexpected error");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,668 +0,0 @@
|
|||||||
//! This module contains the implementation of the CFDP high level classes as specified in the
|
|
||||||
//! CCSDS 727.0-B-5.
|
|
||||||
use core::{cell::RefCell, fmt::Debug, hash::Hash};
|
|
||||||
|
|
||||||
use crc::{Crc, CRC_32_CKSUM};
|
|
||||||
use hashbrown::HashMap;
|
|
||||||
use spacepackets::{
|
|
||||||
cfdp::{
|
|
||||||
pdu::{FileDirectiveType, PduError, PduHeader},
|
|
||||||
ChecksumType, ConditionCode, FaultHandlerCode, PduType, TransmissionMode,
|
|
||||||
},
|
|
||||||
util::{UnsignedByteField, UnsignedEnum},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[cfg(feature = "alloc")]
|
|
||||||
use alloc::boxed::Box;
|
|
||||||
#[cfg(feature = "serde")]
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use crate::time::CountdownProvider;
|
|
||||||
|
|
||||||
#[cfg(feature = "std")]
|
|
||||||
pub mod dest;
|
|
||||||
#[cfg(feature = "alloc")]
|
|
||||||
pub mod filestore;
|
|
||||||
#[cfg(feature = "std")]
|
|
||||||
pub mod source;
|
|
||||||
pub mod user;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub enum EntityType {
|
|
||||||
Sending,
|
|
||||||
Receiving,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum TimerContext {
|
|
||||||
CheckLimit {
|
|
||||||
local_id: UnsignedByteField,
|
|
||||||
remote_id: UnsignedByteField,
|
|
||||||
entity_type: EntityType,
|
|
||||||
},
|
|
||||||
NakActivity {
|
|
||||||
expiry_time_seconds: f32,
|
|
||||||
},
|
|
||||||
PositiveAck {
|
|
||||||
expiry_time_seconds: f32,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A generic trait which allows CFDP entities to create check timers which are required to
|
|
||||||
/// implement special procedures in unacknowledged transmission mode, as specified in 4.6.3.2
|
|
||||||
/// and 4.6.3.3.
|
|
||||||
///
|
|
||||||
/// This trait also allows the creation of different check timers depending on context and purpose
|
|
||||||
/// of the timer, the runtime environment (e.g. standard clock timer vs. timer using a RTC) or
|
|
||||||
/// other factors.
|
|
||||||
///
|
|
||||||
/// The countdown timer is used by 3 mechanisms of the CFDP protocol.
|
|
||||||
///
|
|
||||||
/// ## 1. Check limit handling
|
|
||||||
///
|
|
||||||
/// The first mechanism is the check limit handling for unacknowledged transfers as specified
|
|
||||||
/// in 4.6.3.2 and 4.6.3.3 of the CFDP standard.
|
|
||||||
/// For this mechanism, the timer has different functionality depending on whether
|
|
||||||
/// the using entity is the sending entity or the receiving entity for the unacknowledged
|
|
||||||
/// transmission mode.
|
|
||||||
///
|
|
||||||
/// For the sending entity, this timer determines the expiry period for declaring a check limit
|
|
||||||
/// fault after sending an EOF PDU with requested closure. This allows a timeout of the transfer.
|
|
||||||
/// Also see 4.6.3.2 of the CFDP standard.
|
|
||||||
///
|
|
||||||
/// For the receiving entity, this timer determines the expiry period for incrementing a check
|
|
||||||
/// counter after an EOF PDU is received for an incomplete file transfer. This allows out-of-order
|
|
||||||
/// reception of file data PDUs and EOF PDUs. Also see 4.6.3.3 of the CFDP standard.
|
|
||||||
///
|
|
||||||
/// ## 2. NAK activity limit
|
|
||||||
///
|
|
||||||
/// The timer will be used to perform the NAK activity check as specified in 4.6.4.7 of the CFDP
|
|
||||||
/// standard. The expiration period will be provided by the NAK timer expiration limit of the
|
|
||||||
/// remote entity configuration.
|
|
||||||
///
|
|
||||||
/// ## 3. Positive ACK procedures
|
|
||||||
///
|
|
||||||
/// The timer will be used to perform the Positive Acknowledgement Procedures as specified in
|
|
||||||
/// 4.7. 1of the CFDP standard. The expiration period will be provided by the Positive ACK timer
|
|
||||||
/// interval of the remote entity configuration.
|
|
||||||
#[cfg(feature = "alloc")]
|
|
||||||
pub trait CheckTimerCreator {
|
|
||||||
fn get_check_timer_provider(&self, timer_context: TimerContext) -> Box<dyn CountdownProvider>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Simple implementation of the [CheckTimerCreator] trait assuming a standard runtime.
|
|
||||||
/// It also assumes that a second accuracy of the check timer period is sufficient.
|
|
||||||
#[cfg(feature = "std")]
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct StdCheckTimer {
|
|
||||||
expiry_time_seconds: u64,
|
|
||||||
start_time: std::time::Instant,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "std")]
|
|
||||||
impl StdCheckTimer {
|
|
||||||
pub fn new(expiry_time_seconds: u64) -> Self {
|
|
||||||
Self {
|
|
||||||
expiry_time_seconds,
|
|
||||||
start_time: std::time::Instant::now(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "std")]
|
|
||||||
impl CountdownProvider for StdCheckTimer {
|
|
||||||
fn has_expired(&self) -> bool {
|
|
||||||
let elapsed_time = self.start_time.elapsed();
|
|
||||||
if elapsed_time.as_secs() > self.expiry_time_seconds {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
fn reset(&mut self) {
|
|
||||||
self.start_time = std::time::Instant::now();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This structure models the remote entity configuration information as specified in chapter 8.3
|
|
||||||
/// of the CFDP standard.
|
|
||||||
|
|
||||||
/// Some of the fields which were not considered necessary for the Rust implementation
|
|
||||||
/// were omitted. Some other fields which are not contained inside the standard but are considered
|
|
||||||
/// necessary for the Rust implementation are included.
|
|
||||||
///
|
|
||||||
/// ## Notes on Positive Acknowledgment Procedures
|
|
||||||
///
|
|
||||||
/// The `positive_ack_timer_interval_seconds` and `positive_ack_timer_expiration_limit` will
|
|
||||||
/// be used for positive acknowledgement procedures as specified in CFDP chapter 4.7. The sending
|
|
||||||
/// entity will start the timer for any PDUs where an acknowledgment is required (e.g. EOF PDU).
|
|
||||||
/// Once the expected ACK response has not been received for that interval, as counter will be
|
|
||||||
/// incremented and the timer will be reset. Once the counter exceeds the
|
|
||||||
/// `positive_ack_timer_expiration_limit`, a Positive ACK Limit Reached fault will be declared.
|
|
||||||
///
|
|
||||||
/// ## Notes on Deferred Lost Segment Procedures
|
|
||||||
///
|
|
||||||
/// This procedure will be active if an EOF (No Error) PDU is received in acknowledged mode. After
|
|
||||||
/// issuing the NAK sequence which has the whole file scope, a timer will be started. The timer is
|
|
||||||
/// reset when missing segments or missing metadata is received. The timer will be deactivated if
|
|
||||||
/// all missing data is received. If the timer expires, a new NAK sequence will be issued and a
|
|
||||||
/// counter will be incremented, which can lead to a NAK Limit Reached fault being declared.
|
|
||||||
///
|
|
||||||
/// ## Fields
|
|
||||||
///
|
|
||||||
/// * `entity_id` - The ID of the remote entity.
|
|
||||||
/// * `max_packet_len` - This determines of all PDUs generated for that remote entity in addition
|
|
||||||
/// to the `max_file_segment_len` attribute which also determines the size of file data PDUs.
|
|
||||||
/// * `max_file_segment_len` The maximum file segment length which determines the maximum size
|
|
||||||
/// of file data PDUs in addition to the `max_packet_len` attribute. If this field is set
|
|
||||||
/// to None, the maximum file segment length will be derived from the maximum packet length.
|
|
||||||
/// If this has some value which is smaller than the segment value derived from
|
|
||||||
/// `max_packet_len`, this value will be picked.
|
|
||||||
/// * `closure_requested_by_default` - If the closure requested field is not supplied as part of
|
|
||||||
/// the Put Request, it will be determined from this field in the remote configuration.
|
|
||||||
/// * `crc_on_transmission_by_default` - If the CRC option is not supplied as part of the Put
|
|
||||||
/// Request, it will be determined from this field in the remote configuration.
|
|
||||||
/// * `default_transmission_mode` - If the transmission mode is not supplied as part of the
|
|
||||||
/// Put Request, it will be determined from this field in the remote configuration.
|
|
||||||
/// * `disposition_on_cancellation` - Determines whether an incomplete received file is discard on
|
|
||||||
/// transaction cancellation. Defaults to False.
|
|
||||||
/// * `default_crc_type` - Default checksum type used to calculate for all file transmissions to
|
|
||||||
/// this remote entity.
|
|
||||||
/// * `check_limit` - This timer determines the expiry period for incrementing a check counter
|
|
||||||
/// after an EOF PDU is received for an incomplete file transfer. This allows out-of-order
|
|
||||||
/// reception of file data PDUs and EOF PDUs. Also see 4.6.3.3 of the CFDP standard. Defaults to
|
|
||||||
/// 2, so the check limit timer may expire twice.
|
|
||||||
/// * `positive_ack_timer_interval_seconds`- See the notes on the Positive Acknowledgment
|
|
||||||
/// Procedures inside the class documentation. Expected as floating point seconds. Defaults to
|
|
||||||
/// 10 seconds.
|
|
||||||
/// * `positive_ack_timer_expiration_limit` - See the notes on the Positive Acknowledgment
|
|
||||||
/// Procedures inside the class documentation. Defaults to 2, so the timer may expire twice.
|
|
||||||
/// * `immediate_nak_mode` - Specifies whether a NAK sequence should be issued immediately when a
|
|
||||||
/// file data gap or lost metadata is detected in the acknowledged mode. Defaults to True.
|
|
||||||
/// * `nak_timer_interval_seconds` - See the notes on the Deferred Lost Segment Procedure inside
|
|
||||||
/// the class documentation. Expected as floating point seconds. Defaults to 10 seconds.
|
|
||||||
/// * `nak_timer_expiration_limit` - See the notes on the Deferred Lost Segment Procedure inside
|
|
||||||
/// the class documentation. Defaults to 2, so the timer may expire two times.
|
|
||||||
#[derive(Debug, Copy, Clone)]
|
|
||||||
pub struct RemoteEntityConfig {
|
|
||||||
pub entity_id: UnsignedByteField,
|
|
||||||
pub max_packet_len: usize,
|
|
||||||
pub max_file_segment_len: usize,
|
|
||||||
pub closure_requested_by_default: bool,
|
|
||||||
pub crc_on_transmission_by_default: bool,
|
|
||||||
pub default_transmission_mode: TransmissionMode,
|
|
||||||
pub default_crc_type: ChecksumType,
|
|
||||||
pub positive_ack_timer_interval_seconds: f32,
|
|
||||||
pub positive_ack_timer_expiration_limit: u32,
|
|
||||||
pub check_limit: u32,
|
|
||||||
pub disposition_on_cancellation: bool,
|
|
||||||
pub immediate_nak_mode: bool,
|
|
||||||
pub nak_timer_interval_seconds: f32,
|
|
||||||
pub nak_timer_expiration_limit: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RemoteEntityConfig {
|
|
||||||
pub fn new_with_default_values(
|
|
||||||
entity_id: UnsignedByteField,
|
|
||||||
max_file_segment_len: usize,
|
|
||||||
max_packet_len: usize,
|
|
||||||
closure_requested_by_default: bool,
|
|
||||||
crc_on_transmission_by_default: bool,
|
|
||||||
default_transmission_mode: TransmissionMode,
|
|
||||||
default_crc_type: ChecksumType,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
entity_id,
|
|
||||||
max_file_segment_len,
|
|
||||||
max_packet_len,
|
|
||||||
closure_requested_by_default,
|
|
||||||
crc_on_transmission_by_default,
|
|
||||||
default_transmission_mode,
|
|
||||||
default_crc_type,
|
|
||||||
check_limit: 2,
|
|
||||||
positive_ack_timer_interval_seconds: 10.0,
|
|
||||||
positive_ack_timer_expiration_limit: 2,
|
|
||||||
disposition_on_cancellation: false,
|
|
||||||
immediate_nak_mode: true,
|
|
||||||
nak_timer_interval_seconds: 10.0,
|
|
||||||
nak_timer_expiration_limit: 2,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait RemoteEntityConfigProvider {
|
|
||||||
/// Retrieve the remote entity configuration for the given remote ID.
|
|
||||||
fn get_remote_config(&self, remote_id: u64) -> Option<&RemoteEntityConfig>;
|
|
||||||
fn get_remote_config_mut(&mut self, remote_id: u64) -> Option<&mut RemoteEntityConfig>;
|
|
||||||
/// Add a new remote configuration. Return [true] if the configuration was
|
|
||||||
/// inserted successfully, and [false] if a configuration already exists.
|
|
||||||
fn add_config(&mut self, cfg: &RemoteEntityConfig) -> bool;
|
|
||||||
/// Remote a configuration. Returns [true] if the configuration was removed successfully,
|
|
||||||
/// and [false] if no configuration exists for the given remote ID.
|
|
||||||
fn remove_config(&mut self, remote_id: u64) -> bool;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "std")]
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct StdRemoteEntityConfigProvider {
|
|
||||||
remote_cfg_table: HashMap<u64, RemoteEntityConfig>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "std")]
|
|
||||||
impl RemoteEntityConfigProvider for StdRemoteEntityConfigProvider {
|
|
||||||
fn get_remote_config(&self, remote_id: u64) -> Option<&RemoteEntityConfig> {
|
|
||||||
self.remote_cfg_table.get(&remote_id)
|
|
||||||
}
|
|
||||||
fn get_remote_config_mut(&mut self, remote_id: u64) -> Option<&mut RemoteEntityConfig> {
|
|
||||||
self.remote_cfg_table.get_mut(&remote_id)
|
|
||||||
}
|
|
||||||
fn add_config(&mut self, cfg: &RemoteEntityConfig) -> bool {
|
|
||||||
self.remote_cfg_table
|
|
||||||
.insert(cfg.entity_id.value(), *cfg)
|
|
||||||
.is_some()
|
|
||||||
}
|
|
||||||
fn remove_config(&mut self, remote_id: u64) -> bool {
|
|
||||||
self.remote_cfg_table.remove(&remote_id).is_some()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This trait introduces some callbacks which will be called when a particular CFDP fault
|
|
||||||
/// handler is called.
|
|
||||||
///
|
|
||||||
/// It is passed into the CFDP handlers as part of the [DefaultFaultHandler] and the local entity
|
|
||||||
/// configuration and provides a way to specify custom user error handlers. This allows to
|
|
||||||
/// implement some CFDP features like fault handler logging, which would not be possible
|
|
||||||
/// generically otherwise.
|
|
||||||
///
|
|
||||||
/// For each error reported by the [DefaultFaultHandler], the appropriate fault handler callback
|
|
||||||
/// will be called depending on the [FaultHandlerCode].
|
|
||||||
pub trait UserFaultHandler {
|
|
||||||
fn notice_of_suspension_cb(
|
|
||||||
&mut self,
|
|
||||||
transaction_id: TransactionId,
|
|
||||||
cond: ConditionCode,
|
|
||||||
progress: u64,
|
|
||||||
);
|
|
||||||
|
|
||||||
fn notice_of_cancellation_cb(
|
|
||||||
&mut self,
|
|
||||||
transaction_id: TransactionId,
|
|
||||||
cond: ConditionCode,
|
|
||||||
progress: u64,
|
|
||||||
);
|
|
||||||
|
|
||||||
fn abandoned_cb(&mut self, transaction_id: TransactionId, cond: ConditionCode, progress: u64);
|
|
||||||
|
|
||||||
fn ignore_cb(&mut self, transaction_id: TransactionId, cond: ConditionCode, progress: u64);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This structure is used to implement the fault handling as specified in chapter 4.8 of the CFDP
|
|
||||||
/// standard.
|
|
||||||
///
|
|
||||||
/// It does so by mapping each applicable [spacepackets::cfdp::ConditionCode] to a fault handler
|
|
||||||
/// which is denoted by the four [spacepackets::cfdp::FaultHandlerCode]s. This code is used
|
|
||||||
/// to select the error handling inside the CFDP handler itself in addition to dispatching to a
|
|
||||||
/// user-provided callback function provided by the [UserFaultHandler].
|
|
||||||
///
|
|
||||||
/// Some note on the provided default settings:
|
|
||||||
///
|
|
||||||
/// - Checksum failures will be ignored by default. This is because for unacknowledged transfers,
|
|
||||||
/// cancelling the transfer immediately would interfere with the check limit mechanism specified
|
|
||||||
/// in chapter 4.6.3.3.
|
|
||||||
/// - Unsupported checksum types will also be ignored by default. Even if the checksum type is
|
|
||||||
/// not supported the file transfer might still have worked properly.
|
|
||||||
///
|
|
||||||
/// For all other faults, the default fault handling operation will be to cancel the transaction.
|
|
||||||
/// These defaults can be overriden by using the [Self::set_fault_handler] method.
|
|
||||||
/// Please note that in any case, fault handler overrides can be specified by the sending CFDP
|
|
||||||
/// entity.
|
|
||||||
pub struct DefaultFaultHandler {
|
|
||||||
handler_array: [FaultHandlerCode; 10],
|
|
||||||
// Could also change the user fault handler trait to have non mutable methods, but that limits
|
|
||||||
// flexbility on the user side..
|
|
||||||
user_fault_handler: RefCell<Box<dyn UserFaultHandler + Send>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DefaultFaultHandler {
|
|
||||||
fn condition_code_to_array_index(conditon_code: ConditionCode) -> Option<usize> {
|
|
||||||
Some(match conditon_code {
|
|
||||||
ConditionCode::PositiveAckLimitReached => 0,
|
|
||||||
ConditionCode::KeepAliveLimitReached => 1,
|
|
||||||
ConditionCode::InvalidTransmissionMode => 2,
|
|
||||||
ConditionCode::FilestoreRejection => 3,
|
|
||||||
ConditionCode::FileChecksumFailure => 4,
|
|
||||||
ConditionCode::FileSizeError => 5,
|
|
||||||
ConditionCode::NakLimitReached => 6,
|
|
||||||
ConditionCode::InactivityDetected => 7,
|
|
||||||
ConditionCode::CheckLimitReached => 8,
|
|
||||||
ConditionCode::UnsupportedChecksumType => 9,
|
|
||||||
_ => return None,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_fault_handler(
|
|
||||||
&mut self,
|
|
||||||
condition_code: ConditionCode,
|
|
||||||
fault_handler: FaultHandlerCode,
|
|
||||||
) {
|
|
||||||
let array_idx = Self::condition_code_to_array_index(condition_code);
|
|
||||||
if array_idx.is_none() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
self.handler_array[array_idx.unwrap()] = fault_handler;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new(user_fault_handler: Box<dyn UserFaultHandler + Send>) -> Self {
|
|
||||||
let mut init_array = [FaultHandlerCode::NoticeOfCancellation; 10];
|
|
||||||
init_array
|
|
||||||
[Self::condition_code_to_array_index(ConditionCode::FileChecksumFailure).unwrap()] =
|
|
||||||
FaultHandlerCode::IgnoreError;
|
|
||||||
init_array[Self::condition_code_to_array_index(ConditionCode::UnsupportedChecksumType)
|
|
||||||
.unwrap()] = FaultHandlerCode::IgnoreError;
|
|
||||||
Self {
|
|
||||||
handler_array: init_array,
|
|
||||||
user_fault_handler: RefCell::new(user_fault_handler),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_fault_handler(&self, condition_code: ConditionCode) -> FaultHandlerCode {
|
|
||||||
let array_idx = Self::condition_code_to_array_index(condition_code);
|
|
||||||
if array_idx.is_none() {
|
|
||||||
return FaultHandlerCode::IgnoreError;
|
|
||||||
}
|
|
||||||
self.handler_array[array_idx.unwrap()]
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn report_fault(
|
|
||||||
&self,
|
|
||||||
transaction_id: TransactionId,
|
|
||||||
condition: ConditionCode,
|
|
||||||
progress: u64,
|
|
||||||
) -> FaultHandlerCode {
|
|
||||||
let array_idx = Self::condition_code_to_array_index(condition);
|
|
||||||
if array_idx.is_none() {
|
|
||||||
return FaultHandlerCode::IgnoreError;
|
|
||||||
}
|
|
||||||
let fh_code = self.handler_array[array_idx.unwrap()];
|
|
||||||
let mut handler_mut = self.user_fault_handler.borrow_mut();
|
|
||||||
match fh_code {
|
|
||||||
FaultHandlerCode::NoticeOfCancellation => {
|
|
||||||
handler_mut.notice_of_cancellation_cb(transaction_id, condition, progress);
|
|
||||||
}
|
|
||||||
FaultHandlerCode::NoticeOfSuspension => {
|
|
||||||
handler_mut.notice_of_suspension_cb(transaction_id, condition, progress);
|
|
||||||
}
|
|
||||||
FaultHandlerCode::IgnoreError => {
|
|
||||||
handler_mut.ignore_cb(transaction_id, condition, progress);
|
|
||||||
}
|
|
||||||
FaultHandlerCode::AbandonTransaction => {
|
|
||||||
handler_mut.abandoned_cb(transaction_id, condition, progress);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fh_code
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct IndicationConfig {
|
|
||||||
pub eof_sent: bool,
|
|
||||||
pub eof_recv: bool,
|
|
||||||
pub file_segment_recv: bool,
|
|
||||||
pub transaction_finished: bool,
|
|
||||||
pub suspended: bool,
|
|
||||||
pub resumed: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for IndicationConfig {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
eof_sent: true,
|
|
||||||
eof_recv: true,
|
|
||||||
file_segment_recv: true,
|
|
||||||
transaction_finished: true,
|
|
||||||
suspended: true,
|
|
||||||
resumed: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct LocalEntityConfig {
|
|
||||||
pub id: UnsignedByteField,
|
|
||||||
pub indication_cfg: IndicationConfig,
|
|
||||||
pub default_fault_handler: DefaultFaultHandler,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The CFDP transaction ID of a CFDP transaction consists of the source entity ID and the sequence
|
|
||||||
/// number of that transfer which is also determined by the CFDP source entity.
|
|
||||||
#[derive(Debug, Eq, Copy, Clone)]
|
|
||||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
|
||||||
pub struct TransactionId {
|
|
||||||
source_id: UnsignedByteField,
|
|
||||||
seq_num: UnsignedByteField,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TransactionId {
|
|
||||||
pub fn new(source_id: UnsignedByteField, seq_num: UnsignedByteField) -> Self {
|
|
||||||
Self { source_id, seq_num }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn source_id(&self) -> &UnsignedByteField {
|
|
||||||
&self.source_id
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn seq_num(&self) -> &UnsignedByteField {
|
|
||||||
&self.seq_num
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Hash for TransactionId {
|
|
||||||
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
|
|
||||||
self.source_id.value().hash(state);
|
|
||||||
self.seq_num.value().hash(state);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PartialEq for TransactionId {
|
|
||||||
fn eq(&self, other: &Self) -> bool {
|
|
||||||
self.source_id.value() == other.source_id.value()
|
|
||||||
&& self.seq_num.value() == other.seq_num.value()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
|
||||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
|
||||||
pub enum TransactionStep {
|
|
||||||
Idle = 0,
|
|
||||||
TransactionStart = 1,
|
|
||||||
ReceivingFileDataPdus = 2,
|
|
||||||
ReceivingFileDataPdusWithCheckLimitHandling = 3,
|
|
||||||
SendingAckPdu = 4,
|
|
||||||
TransferCompletion = 5,
|
|
||||||
SendingFinishedPdu = 6,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
|
||||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
|
||||||
pub enum State {
|
|
||||||
Idle = 0,
|
|
||||||
Busy = 1,
|
|
||||||
Suspended = 2,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const CRC_32: Crc<u32> = Crc::<u32>::new(&CRC_32_CKSUM);
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
|
|
||||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
|
||||||
pub enum PacketTarget {
|
|
||||||
SourceEntity,
|
|
||||||
DestEntity,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This is a helper struct which contains base information about a particular PDU packet.
|
|
||||||
/// This is also necessary information for CFDP packet routing. For example, some packet types
|
|
||||||
/// like file data PDUs can only be used by CFDP source entities.
|
|
||||||
pub struct PacketInfo<'raw_packet> {
|
|
||||||
pdu_type: PduType,
|
|
||||||
pdu_directive: Option<FileDirectiveType>,
|
|
||||||
target: PacketTarget,
|
|
||||||
raw_packet: &'raw_packet [u8],
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'raw> PacketInfo<'raw> {
|
|
||||||
pub fn new(raw_packet: &'raw [u8]) -> Result<Self, PduError> {
|
|
||||||
let (pdu_header, header_len) = PduHeader::from_bytes(raw_packet)?;
|
|
||||||
if pdu_header.pdu_type() == PduType::FileData {
|
|
||||||
return Ok(Self {
|
|
||||||
pdu_type: pdu_header.pdu_type(),
|
|
||||||
pdu_directive: None,
|
|
||||||
target: PacketTarget::DestEntity,
|
|
||||||
raw_packet,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if pdu_header.pdu_datafield_len() < 1 {
|
|
||||||
return Err(PduError::FormatError);
|
|
||||||
}
|
|
||||||
// Route depending on PDU type and directive type if applicable. Retrieve directive type
|
|
||||||
// from the raw stream for better performance (with sanity and directive code check).
|
|
||||||
// The routing is based on section 4.5 of the CFDP standard which specifies the PDU forwarding
|
|
||||||
// procedure.
|
|
||||||
let directive = FileDirectiveType::try_from(raw_packet[header_len]).map_err(|_| {
|
|
||||||
PduError::InvalidDirectiveType {
|
|
||||||
found: raw_packet[header_len],
|
|
||||||
expected: None,
|
|
||||||
}
|
|
||||||
})?;
|
|
||||||
let packet_target = match directive {
|
|
||||||
// Section c) of 4.5.3: These PDUs should always be targeted towards the file sender a.k.a.
|
|
||||||
// the source handler
|
|
||||||
FileDirectiveType::NakPdu
|
|
||||||
| FileDirectiveType::FinishedPdu
|
|
||||||
| FileDirectiveType::KeepAlivePdu => PacketTarget::SourceEntity,
|
|
||||||
// Section b) of 4.5.3: These PDUs should always be targeted towards the file receiver a.k.a.
|
|
||||||
// the destination handler
|
|
||||||
FileDirectiveType::MetadataPdu
|
|
||||||
| FileDirectiveType::EofPdu
|
|
||||||
| FileDirectiveType::PromptPdu => PacketTarget::DestEntity,
|
|
||||||
// Section a): Recipient depends of the type of PDU that is being acknowledged. We can simply
|
|
||||||
// extract the PDU type from the raw stream. If it is an EOF PDU, this packet is passed to
|
|
||||||
// the source handler, for a Finished PDU, it is passed to the destination handler.
|
|
||||||
FileDirectiveType::AckPdu => {
|
|
||||||
let acked_directive = FileDirectiveType::try_from(raw_packet[header_len + 1])
|
|
||||||
.map_err(|_| PduError::InvalidDirectiveType {
|
|
||||||
found: raw_packet[header_len],
|
|
||||||
expected: None,
|
|
||||||
})?;
|
|
||||||
if acked_directive == FileDirectiveType::EofPdu {
|
|
||||||
PacketTarget::SourceEntity
|
|
||||||
} else if acked_directive == FileDirectiveType::FinishedPdu {
|
|
||||||
PacketTarget::DestEntity
|
|
||||||
} else {
|
|
||||||
// TODO: Maybe a better error? This might be confusing..
|
|
||||||
return Err(PduError::InvalidDirectiveType {
|
|
||||||
found: raw_packet[header_len + 1],
|
|
||||||
expected: None,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Ok(Self {
|
|
||||||
pdu_type: pdu_header.pdu_type(),
|
|
||||||
pdu_directive: Some(directive),
|
|
||||||
target: packet_target,
|
|
||||||
raw_packet,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn pdu_type(&self) -> PduType {
|
|
||||||
self.pdu_type
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn pdu_directive(&self) -> Option<FileDirectiveType> {
|
|
||||||
self.pdu_directive
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn target(&self) -> PacketTarget {
|
|
||||||
self.target
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn raw_packet(&self) -> &[u8] {
|
|
||||||
self.raw_packet
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use spacepackets::cfdp::{
|
|
||||||
lv::Lv,
|
|
||||||
pdu::{
|
|
||||||
eof::EofPdu,
|
|
||||||
file_data::FileDataPdu,
|
|
||||||
metadata::{MetadataGenericParams, MetadataPduCreator},
|
|
||||||
CommonPduConfig, FileDirectiveType, PduHeader, WritablePduPacket,
|
|
||||||
},
|
|
||||||
PduType,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::cfdp::PacketTarget;
|
|
||||||
|
|
||||||
use super::PacketInfo;
|
|
||||||
|
|
||||||
fn generic_pdu_header() -> PduHeader {
|
|
||||||
let pdu_conf = CommonPduConfig::default();
|
|
||||||
PduHeader::new_no_file_data(pdu_conf, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_metadata_pdu_info() {
|
|
||||||
let mut buf: [u8; 128] = [0; 128];
|
|
||||||
let pdu_header = generic_pdu_header();
|
|
||||||
let metadata_params = MetadataGenericParams::default();
|
|
||||||
let src_file_name = "hello.txt";
|
|
||||||
let dest_file_name = "hello-dest.txt";
|
|
||||||
let src_lv = Lv::new_from_str(src_file_name).unwrap();
|
|
||||||
let dest_lv = Lv::new_from_str(dest_file_name).unwrap();
|
|
||||||
let metadata_pdu =
|
|
||||||
MetadataPduCreator::new_no_opts(pdu_header, metadata_params, src_lv, dest_lv);
|
|
||||||
metadata_pdu
|
|
||||||
.write_to_bytes(&mut buf)
|
|
||||||
.expect("writing metadata PDU failed");
|
|
||||||
|
|
||||||
let packet_info = PacketInfo::new(&buf).expect("creating packet info failed");
|
|
||||||
assert_eq!(packet_info.pdu_type(), PduType::FileDirective);
|
|
||||||
assert!(packet_info.pdu_directive().is_some());
|
|
||||||
assert_eq!(
|
|
||||||
packet_info.pdu_directive().unwrap(),
|
|
||||||
FileDirectiveType::MetadataPdu
|
|
||||||
);
|
|
||||||
assert_eq!(packet_info.target(), PacketTarget::DestEntity);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_filedata_pdu_info() {
|
|
||||||
let mut buf: [u8; 128] = [0; 128];
|
|
||||||
let pdu_header = generic_pdu_header();
|
|
||||||
let file_data_pdu = FileDataPdu::new_no_seg_metadata(pdu_header, 0, &[]);
|
|
||||||
file_data_pdu
|
|
||||||
.write_to_bytes(&mut buf)
|
|
||||||
.expect("writing file data PDU failed");
|
|
||||||
let packet_info = PacketInfo::new(&buf).expect("creating packet info failed");
|
|
||||||
assert_eq!(packet_info.pdu_type(), PduType::FileData);
|
|
||||||
assert!(packet_info.pdu_directive().is_none());
|
|
||||||
assert_eq!(packet_info.target(), PacketTarget::DestEntity);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_eof_pdu_info() {
|
|
||||||
let mut buf: [u8; 128] = [0; 128];
|
|
||||||
let pdu_header = generic_pdu_header();
|
|
||||||
let eof_pdu = EofPdu::new_no_error(pdu_header, 0, 0);
|
|
||||||
eof_pdu
|
|
||||||
.write_to_bytes(&mut buf)
|
|
||||||
.expect("writing file data PDU failed");
|
|
||||||
let packet_info = PacketInfo::new(&buf).expect("creating packet info failed");
|
|
||||||
assert_eq!(packet_info.pdu_type(), PduType::FileDirective);
|
|
||||||
assert!(packet_info.pdu_directive().is_some());
|
|
||||||
assert_eq!(
|
|
||||||
packet_info.pdu_directive().unwrap(),
|
|
||||||
FileDirectiveType::EofPdu
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,15 +0,0 @@
|
|||||||
#![allow(dead_code)]
|
|
||||||
use spacepackets::util::UnsignedByteField;
|
|
||||||
|
|
||||||
pub struct SourceHandler {
|
|
||||||
id: UnsignedByteField,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SourceHandler {
|
|
||||||
pub fn new(id: impl Into<UnsignedByteField>) -> Self {
|
|
||||||
Self { id: id.into() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {}
|
|
@ -1,96 +0,0 @@
|
|||||||
use spacepackets::{
|
|
||||||
cfdp::{
|
|
||||||
pdu::{
|
|
||||||
file_data::SegmentMetadata,
|
|
||||||
finished::{DeliveryCode, FileStatus},
|
|
||||||
},
|
|
||||||
tlv::{msg_to_user::MsgToUserTlv, WritableTlv},
|
|
||||||
ConditionCode,
|
|
||||||
},
|
|
||||||
util::UnsignedByteField,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::TransactionId;
|
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone)]
|
|
||||||
pub struct TransactionFinishedParams {
|
|
||||||
pub id: TransactionId,
|
|
||||||
pub condition_code: ConditionCode,
|
|
||||||
pub delivery_code: DeliveryCode,
|
|
||||||
pub file_status: FileStatus,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct MetadataReceivedParams<'src_file, 'dest_file, 'msgs_to_user> {
|
|
||||||
pub id: TransactionId,
|
|
||||||
pub source_id: UnsignedByteField,
|
|
||||||
pub file_size: u64,
|
|
||||||
pub src_file_name: &'src_file str,
|
|
||||||
pub dest_file_name: &'dest_file str,
|
|
||||||
pub msgs_to_user: &'msgs_to_user [MsgToUserTlv<'msgs_to_user>],
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "alloc")]
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct OwnedMetadataRecvdParams {
|
|
||||||
pub id: TransactionId,
|
|
||||||
pub source_id: UnsignedByteField,
|
|
||||||
pub file_size: u64,
|
|
||||||
pub src_file_name: alloc::string::String,
|
|
||||||
pub dest_file_name: alloc::string::String,
|
|
||||||
pub msgs_to_user: alloc::vec::Vec<alloc::vec::Vec<u8>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "alloc")]
|
|
||||||
impl From<MetadataReceivedParams<'_, '_, '_>> for OwnedMetadataRecvdParams {
|
|
||||||
fn from(value: MetadataReceivedParams) -> Self {
|
|
||||||
Self::from(&value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "alloc")]
|
|
||||||
impl From<&MetadataReceivedParams<'_, '_, '_>> for OwnedMetadataRecvdParams {
|
|
||||||
fn from(value: &MetadataReceivedParams) -> Self {
|
|
||||||
Self {
|
|
||||||
id: value.id,
|
|
||||||
source_id: value.source_id,
|
|
||||||
file_size: value.file_size,
|
|
||||||
src_file_name: value.src_file_name.into(),
|
|
||||||
dest_file_name: value.dest_file_name.into(),
|
|
||||||
msgs_to_user: value.msgs_to_user.iter().map(|tlv| tlv.to_vec()).collect(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct FileSegmentRecvdParams<'seg_meta> {
|
|
||||||
pub id: TransactionId,
|
|
||||||
pub offset: u64,
|
|
||||||
pub length: usize,
|
|
||||||
pub segment_metadata: Option<&'seg_meta SegmentMetadata<'seg_meta>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait CfdpUser {
|
|
||||||
fn transaction_indication(&mut self, id: &TransactionId);
|
|
||||||
fn eof_sent_indication(&mut self, id: &TransactionId);
|
|
||||||
fn transaction_finished_indication(&mut self, finished_params: &TransactionFinishedParams);
|
|
||||||
fn metadata_recvd_indication(&mut self, md_recvd_params: &MetadataReceivedParams);
|
|
||||||
fn file_segment_recvd_indication(&mut self, segment_recvd_params: &FileSegmentRecvdParams);
|
|
||||||
// TODO: The standard does not strictly specify how the report information looks..
|
|
||||||
fn report_indication(&mut self, id: &TransactionId);
|
|
||||||
fn suspended_indication(&mut self, id: &TransactionId, condition_code: ConditionCode);
|
|
||||||
fn resumed_indication(&mut self, id: &TransactionId, progress: u64);
|
|
||||||
fn fault_indication(
|
|
||||||
&mut self,
|
|
||||||
id: &TransactionId,
|
|
||||||
condition_code: ConditionCode,
|
|
||||||
progress: u64,
|
|
||||||
);
|
|
||||||
fn abandoned_indication(
|
|
||||||
&mut self,
|
|
||||||
id: &TransactionId,
|
|
||||||
condition_code: ConditionCode,
|
|
||||||
progress: u64,
|
|
||||||
);
|
|
||||||
fn eof_recvd_indication(&mut self, id: &TransactionId);
|
|
||||||
}
|
|
@ -1,4 +1,4 @@
|
|||||||
//! # sat-rs: A framework to build on-board software for remote systems
|
//! # sat-rs: A library to build on-board software for remote systems
|
||||||
//!
|
//!
|
||||||
//! You can find more information about the sat-rs framework on the
|
//! You can find more information about the sat-rs framework on the
|
||||||
//! [homepage](https://absatsw.irs.uni-stuttgart.de/projects/sat-rs/).
|
//! [homepage](https://absatsw.irs.uni-stuttgart.de/projects/sat-rs/).
|
||||||
@ -14,7 +14,7 @@
|
|||||||
//! - The [pus] module which provides special support for projects using
|
//! - The [pus] module which provides special support for projects using
|
||||||
//! the [ECSS PUS C standard](https://ecss.nl/standard/ecss-e-st-70-41c-space-engineering-telemetry-and-telecommand-packet-utilization-15-april-2016/).
|
//! the [ECSS PUS C standard](https://ecss.nl/standard/ecss-e-st-70-41c-space-engineering-telemetry-and-telecommand-packet-utilization-15-april-2016/).
|
||||||
#![no_std]
|
#![no_std]
|
||||||
#![cfg_attr(docs_rs, feature(doc_auto_cfg))]
|
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||||
#[cfg(feature = "alloc")]
|
#[cfg(feature = "alloc")]
|
||||||
extern crate alloc;
|
extern crate alloc;
|
||||||
#[cfg(feature = "alloc")]
|
#[cfg(feature = "alloc")]
|
||||||
@ -22,8 +22,6 @@ extern crate downcast_rs;
|
|||||||
#[cfg(any(feature = "std", test))]
|
#[cfg(any(feature = "std", test))]
|
||||||
extern crate std;
|
extern crate std;
|
||||||
|
|
||||||
#[cfg(feature = "alloc")]
|
|
||||||
pub mod cfdp;
|
|
||||||
pub mod encoding;
|
pub mod encoding;
|
||||||
pub mod event_man;
|
pub mod event_man;
|
||||||
pub mod events;
|
pub mod events;
|
||||||
@ -38,7 +36,6 @@ pub mod pus;
|
|||||||
pub mod queue;
|
pub mod queue;
|
||||||
pub mod request;
|
pub mod request;
|
||||||
pub mod res_code;
|
pub mod res_code;
|
||||||
pub mod seq_count;
|
|
||||||
pub mod time;
|
pub mod time;
|
||||||
pub mod tmtc;
|
pub mod tmtc;
|
||||||
|
|
||||||
|
@ -1584,23 +1584,21 @@ mod tests {
|
|||||||
mod heapless_tests {
|
mod heapless_tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::static_subpool;
|
use crate::static_subpool;
|
||||||
use core::mem::MaybeUninit;
|
use core::ptr::addr_of_mut;
|
||||||
|
|
||||||
const SUBPOOL_1_BLOCK_SIZE: usize = 4;
|
const SUBPOOL_1_BLOCK_SIZE: usize = 4;
|
||||||
const SUBPOOL_1_NUM_ELEMENTS: u16 = 4;
|
const SUBPOOL_1_NUM_ELEMENTS: u16 = 4;
|
||||||
static mut SUBPOOL_1: MaybeUninit<
|
static mut SUBPOOL_1: [u8; SUBPOOL_1_NUM_ELEMENTS as usize * SUBPOOL_1_BLOCK_SIZE] =
|
||||||
[u8; SUBPOOL_1_NUM_ELEMENTS as usize * SUBPOOL_1_BLOCK_SIZE],
|
[0; SUBPOOL_1_NUM_ELEMENTS as usize * SUBPOOL_1_BLOCK_SIZE];
|
||||||
> = MaybeUninit::new([0; SUBPOOL_1_NUM_ELEMENTS as usize * SUBPOOL_1_BLOCK_SIZE]);
|
static mut SUBPOOL_1_SIZES: [usize; SUBPOOL_1_NUM_ELEMENTS as usize] =
|
||||||
static mut SUBPOOL_1_SIZES: MaybeUninit<[usize; SUBPOOL_1_NUM_ELEMENTS as usize]> =
|
[STORE_FREE; SUBPOOL_1_NUM_ELEMENTS as usize];
|
||||||
MaybeUninit::new([STORE_FREE; SUBPOOL_1_NUM_ELEMENTS as usize]);
|
|
||||||
|
|
||||||
const SUBPOOL_2_NUM_ELEMENTS: u16 = 2;
|
const SUBPOOL_2_NUM_ELEMENTS: u16 = 2;
|
||||||
const SUBPOOL_2_BLOCK_SIZE: usize = 8;
|
const SUBPOOL_2_BLOCK_SIZE: usize = 8;
|
||||||
static mut SUBPOOL_2: MaybeUninit<
|
static mut SUBPOOL_2: [u8; SUBPOOL_2_NUM_ELEMENTS as usize * SUBPOOL_2_BLOCK_SIZE] =
|
||||||
[u8; SUBPOOL_2_NUM_ELEMENTS as usize * SUBPOOL_2_BLOCK_SIZE],
|
[0; SUBPOOL_2_NUM_ELEMENTS as usize * SUBPOOL_2_BLOCK_SIZE];
|
||||||
> = MaybeUninit::new([0; SUBPOOL_2_NUM_ELEMENTS as usize * SUBPOOL_2_BLOCK_SIZE]);
|
static mut SUBPOOL_2_SIZES: [usize; SUBPOOL_2_NUM_ELEMENTS as usize] =
|
||||||
static mut SUBPOOL_2_SIZES: MaybeUninit<[usize; SUBPOOL_2_NUM_ELEMENTS as usize]> =
|
[STORE_FREE; SUBPOOL_2_NUM_ELEMENTS as usize];
|
||||||
MaybeUninit::new([STORE_FREE; SUBPOOL_2_NUM_ELEMENTS as usize]);
|
|
||||||
|
|
||||||
const SUBPOOL_3_NUM_ELEMENTS: u16 = 1;
|
const SUBPOOL_3_NUM_ELEMENTS: u16 = 1;
|
||||||
const SUBPOOL_3_BLOCK_SIZE: usize = 16;
|
const SUBPOOL_3_BLOCK_SIZE: usize = 16;
|
||||||
@ -1643,18 +1641,18 @@ mod tests {
|
|||||||
StaticHeaplessMemoryPool::new(false);
|
StaticHeaplessMemoryPool::new(false);
|
||||||
assert!(heapless_pool
|
assert!(heapless_pool
|
||||||
.grow(
|
.grow(
|
||||||
unsafe { SUBPOOL_1.assume_init_mut() },
|
unsafe { &mut *addr_of_mut!(SUBPOOL_1) },
|
||||||
unsafe { SUBPOOL_1_SIZES.assume_init_mut() },
|
unsafe { &mut *addr_of_mut!(SUBPOOL_1_SIZES) },
|
||||||
SUBPOOL_1_NUM_ELEMENTS,
|
SUBPOOL_1_NUM_ELEMENTS,
|
||||||
false
|
true
|
||||||
)
|
)
|
||||||
.is_ok());
|
.is_ok());
|
||||||
assert!(heapless_pool
|
assert!(heapless_pool
|
||||||
.grow(
|
.grow(
|
||||||
unsafe { SUBPOOL_2.assume_init_mut() },
|
unsafe { &mut *addr_of_mut!(SUBPOOL_2) },
|
||||||
unsafe { SUBPOOL_2_SIZES.assume_init_mut() },
|
unsafe { &mut *addr_of_mut!(SUBPOOL_2_SIZES) },
|
||||||
SUBPOOL_2_NUM_ELEMENTS,
|
SUBPOOL_2_NUM_ELEMENTS,
|
||||||
false
|
true
|
||||||
)
|
)
|
||||||
.is_ok());
|
.is_ok());
|
||||||
assert!(heapless_pool
|
assert!(heapless_pool
|
||||||
@ -1662,7 +1660,7 @@ mod tests {
|
|||||||
unsafe { SUBPOOL_3.assume_init_mut() },
|
unsafe { SUBPOOL_3.assume_init_mut() },
|
||||||
unsafe { SUBPOOL_3_SIZES.assume_init_mut() },
|
unsafe { SUBPOOL_3_SIZES.assume_init_mut() },
|
||||||
SUBPOOL_3_NUM_ELEMENTS,
|
SUBPOOL_3_NUM_ELEMENTS,
|
||||||
false
|
true
|
||||||
)
|
)
|
||||||
.is_ok());
|
.is_ok());
|
||||||
heapless_pool
|
heapless_pool
|
||||||
@ -1782,10 +1780,10 @@ mod tests {
|
|||||||
StaticHeaplessMemoryPool::new(true);
|
StaticHeaplessMemoryPool::new(true);
|
||||||
assert!(heapless_pool
|
assert!(heapless_pool
|
||||||
.grow(
|
.grow(
|
||||||
unsafe { SUBPOOL_2.assume_init_mut() },
|
unsafe { &mut *addr_of_mut!(SUBPOOL_2) },
|
||||||
unsafe { SUBPOOL_2_SIZES.assume_init_mut() },
|
unsafe { &mut *addr_of_mut!(SUBPOOL_2_SIZES) },
|
||||||
SUBPOOL_2_NUM_ELEMENTS,
|
SUBPOOL_2_NUM_ELEMENTS,
|
||||||
false
|
true
|
||||||
)
|
)
|
||||||
.is_ok());
|
.is_ok());
|
||||||
assert!(heapless_pool
|
assert!(heapless_pool
|
||||||
@ -1793,7 +1791,7 @@ mod tests {
|
|||||||
unsafe { SUBPOOL_4.assume_init_mut() },
|
unsafe { SUBPOOL_4.assume_init_mut() },
|
||||||
unsafe { SUBPOOL_4_SIZES.assume_init_mut() },
|
unsafe { SUBPOOL_4_SIZES.assume_init_mut() },
|
||||||
SUBPOOL_4_NUM_ELEMENTS,
|
SUBPOOL_4_NUM_ELEMENTS,
|
||||||
false
|
true
|
||||||
)
|
)
|
||||||
.is_ok());
|
.is_ok());
|
||||||
generic_test_spills_to_higher_subpools(&mut heapless_pool);
|
generic_test_spills_to_higher_subpools(&mut heapless_pool);
|
||||||
@ -1808,7 +1806,7 @@ mod tests {
|
|||||||
unsafe { SUBPOOL_5.assume_init_mut() },
|
unsafe { SUBPOOL_5.assume_init_mut() },
|
||||||
unsafe { SUBPOOL_5_SIZES.assume_init_mut() },
|
unsafe { SUBPOOL_5_SIZES.assume_init_mut() },
|
||||||
SUBPOOL_5_NUM_ELEMENTS,
|
SUBPOOL_5_NUM_ELEMENTS,
|
||||||
false
|
true
|
||||||
)
|
)
|
||||||
.is_ok());
|
.is_ok());
|
||||||
assert!(heapless_pool
|
assert!(heapless_pool
|
||||||
@ -1816,7 +1814,7 @@ mod tests {
|
|||||||
unsafe { SUBPOOL_3.assume_init_mut() },
|
unsafe { SUBPOOL_3.assume_init_mut() },
|
||||||
unsafe { SUBPOOL_3_SIZES.assume_init_mut() },
|
unsafe { SUBPOOL_3_SIZES.assume_init_mut() },
|
||||||
SUBPOOL_3_NUM_ELEMENTS,
|
SUBPOOL_3_NUM_ELEMENTS,
|
||||||
false
|
true
|
||||||
)
|
)
|
||||||
.is_ok());
|
.is_ok());
|
||||||
generic_test_spillage_fails_as_well(&mut heapless_pool);
|
generic_test_spillage_fails_as_well(&mut heapless_pool);
|
||||||
@ -1831,7 +1829,7 @@ mod tests {
|
|||||||
unsafe { SUBPOOL_5.assume_init_mut() },
|
unsafe { SUBPOOL_5.assume_init_mut() },
|
||||||
unsafe { SUBPOOL_5_SIZES.assume_init_mut() },
|
unsafe { SUBPOOL_5_SIZES.assume_init_mut() },
|
||||||
SUBPOOL_5_NUM_ELEMENTS,
|
SUBPOOL_5_NUM_ELEMENTS,
|
||||||
false
|
true
|
||||||
)
|
)
|
||||||
.is_ok());
|
.is_ok());
|
||||||
assert!(heapless_pool
|
assert!(heapless_pool
|
||||||
@ -1839,7 +1837,7 @@ mod tests {
|
|||||||
unsafe { SUBPOOL_6.assume_init_mut() },
|
unsafe { SUBPOOL_6.assume_init_mut() },
|
||||||
unsafe { SUBPOOL_6_SIZES.assume_init_mut() },
|
unsafe { SUBPOOL_6_SIZES.assume_init_mut() },
|
||||||
SUBPOOL_6_NUM_ELEMENTS,
|
SUBPOOL_6_NUM_ELEMENTS,
|
||||||
false
|
true
|
||||||
)
|
)
|
||||||
.is_ok());
|
.is_ok());
|
||||||
assert!(heapless_pool
|
assert!(heapless_pool
|
||||||
@ -1847,7 +1845,7 @@ mod tests {
|
|||||||
unsafe { SUBPOOL_3.assume_init_mut() },
|
unsafe { SUBPOOL_3.assume_init_mut() },
|
||||||
unsafe { SUBPOOL_3_SIZES.assume_init_mut() },
|
unsafe { SUBPOOL_3_SIZES.assume_init_mut() },
|
||||||
SUBPOOL_3_NUM_ELEMENTS,
|
SUBPOOL_3_NUM_ELEMENTS,
|
||||||
false
|
true
|
||||||
)
|
)
|
||||||
.is_ok());
|
.is_ok());
|
||||||
generic_test_spillage_works_across_multiple_subpools(&mut heapless_pool);
|
generic_test_spillage_works_across_multiple_subpools(&mut heapless_pool);
|
||||||
@ -1862,7 +1860,7 @@ mod tests {
|
|||||||
unsafe { SUBPOOL_5.assume_init_mut() },
|
unsafe { SUBPOOL_5.assume_init_mut() },
|
||||||
unsafe { SUBPOOL_5_SIZES.assume_init_mut() },
|
unsafe { SUBPOOL_5_SIZES.assume_init_mut() },
|
||||||
SUBPOOL_5_NUM_ELEMENTS,
|
SUBPOOL_5_NUM_ELEMENTS,
|
||||||
false
|
true
|
||||||
)
|
)
|
||||||
.is_ok());
|
.is_ok());
|
||||||
assert!(heapless_pool
|
assert!(heapless_pool
|
||||||
@ -1870,7 +1868,7 @@ mod tests {
|
|||||||
unsafe { SUBPOOL_6.assume_init_mut() },
|
unsafe { SUBPOOL_6.assume_init_mut() },
|
||||||
unsafe { SUBPOOL_6_SIZES.assume_init_mut() },
|
unsafe { SUBPOOL_6_SIZES.assume_init_mut() },
|
||||||
SUBPOOL_6_NUM_ELEMENTS,
|
SUBPOOL_6_NUM_ELEMENTS,
|
||||||
false
|
true
|
||||||
)
|
)
|
||||||
.is_ok());
|
.is_ok());
|
||||||
assert!(heapless_pool
|
assert!(heapless_pool
|
||||||
@ -1878,7 +1876,7 @@ mod tests {
|
|||||||
unsafe { SUBPOOL_3.assume_init_mut() },
|
unsafe { SUBPOOL_3.assume_init_mut() },
|
||||||
unsafe { SUBPOOL_3_SIZES.assume_init_mut() },
|
unsafe { SUBPOOL_3_SIZES.assume_init_mut() },
|
||||||
SUBPOOL_3_NUM_ELEMENTS,
|
SUBPOOL_3_NUM_ELEMENTS,
|
||||||
false
|
true
|
||||||
)
|
)
|
||||||
.is_ok());
|
.is_ok());
|
||||||
generic_test_spillage_fails_across_multiple_subpools(&mut heapless_pool);
|
generic_test_spillage_fails_across_multiple_subpools(&mut heapless_pool);
|
||||||
|
@ -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()
|
||||||
if self.dev_state == DeviceState::SwitchingPower {
|
.expect("receiving switch request failed");
|
||||||
if self.power_info.get_is_switch_on(0).unwrap() {
|
common_checks(&request);
|
||||||
self.dev_state = DeviceState::ON;
|
assert_eq!(request.message.target_state(), SwitchStateBinary::Off);
|
||||||
self.mode = 1;
|
}
|
||||||
}
|
|
||||||
}
|
#[test]
|
||||||
}
|
fn test_comand_switch_sending_mpsc_regular_rx_disconnected() {
|
||||||
}
|
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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
//! VerificationReportingProvider, VerificationReporterCfg, VerificationReporter
|
//! VerificationReportingProvider, VerificationReporterCfg, VerificationReporter
|
||||||
//! };
|
//! };
|
||||||
//! use satrs::tmtc::{SharedStaticMemoryPool, PacketSenderWithSharedPool};
|
//! use satrs::tmtc::{SharedStaticMemoryPool, PacketSenderWithSharedPool};
|
||||||
//! use satrs::seq_count::SeqCountProviderSimple;
|
//! use satrs::spacepackets::seq_count::SeqCountProviderSimple;
|
||||||
//! use satrs::request::UniqueApidTargetId;
|
//! use satrs::request::UniqueApidTargetId;
|
||||||
//! use spacepackets::ecss::PusPacket;
|
//! use spacepackets::ecss::PusPacket;
|
||||||
//! use spacepackets::SpHeader;
|
//! use spacepackets::SpHeader;
|
||||||
@ -97,8 +97,8 @@ use spacepackets::ecss::EcssEnumeration;
|
|||||||
use spacepackets::{ByteConversionError, CcsdsPacket, PacketId, PacketSequenceCtrl};
|
use spacepackets::{ByteConversionError, CcsdsPacket, PacketId, PacketSequenceCtrl};
|
||||||
use spacepackets::{SpHeader, MAX_APID};
|
use spacepackets::{SpHeader, MAX_APID};
|
||||||
|
|
||||||
pub use crate::seq_count::SeqCountProviderSimple;
|
|
||||||
pub use spacepackets::ecss::verification::*;
|
pub use spacepackets::ecss::verification::*;
|
||||||
|
pub use spacepackets::seq_count::SeqCountProviderSimple;
|
||||||
|
|
||||||
#[cfg(feature = "alloc")]
|
#[cfg(feature = "alloc")]
|
||||||
pub use alloc_mod::*;
|
pub use alloc_mod::*;
|
||||||
@ -1702,7 +1702,7 @@ pub mod tests {
|
|||||||
};
|
};
|
||||||
use crate::pus::{ChannelWithId, PusTmVariant};
|
use crate::pus::{ChannelWithId, PusTmVariant};
|
||||||
use crate::request::MessageMetadata;
|
use crate::request::MessageMetadata;
|
||||||
use crate::seq_count::{CcsdsSimpleSeqCountProvider, SequenceCountProviderCore};
|
use crate::spacepackets::seq_count::{CcsdsSimpleSeqCountProvider, SequenceCountProvider};
|
||||||
use crate::tmtc::{PacketSenderWithSharedPool, SharedPacketPool};
|
use crate::tmtc::{PacketSenderWithSharedPool, SharedPacketPool};
|
||||||
use crate::ComponentId;
|
use crate::ComponentId;
|
||||||
use alloc::format;
|
use alloc::format;
|
||||||
|
@ -1,250 +0,0 @@
|
|||||||
use core::cell::Cell;
|
|
||||||
#[cfg(feature = "alloc")]
|
|
||||||
use dyn_clone::DynClone;
|
|
||||||
use paste::paste;
|
|
||||||
use spacepackets::MAX_SEQ_COUNT;
|
|
||||||
#[cfg(feature = "std")]
|
|
||||||
pub use stdmod::*;
|
|
||||||
|
|
||||||
/// Core trait for objects which can provide a sequence count.
|
|
||||||
///
|
|
||||||
/// The core functions are not mutable on purpose to allow easier usage with
|
|
||||||
/// static structs when using the interior mutability pattern. This can be achieved by using
|
|
||||||
/// [Cell], [core::cell::RefCell] or atomic types.
|
|
||||||
pub trait SequenceCountProviderCore<Raw> {
|
|
||||||
fn get(&self) -> Raw;
|
|
||||||
|
|
||||||
fn increment(&self);
|
|
||||||
|
|
||||||
fn get_and_increment(&self) -> Raw {
|
|
||||||
let val = self.get();
|
|
||||||
self.increment();
|
|
||||||
val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Extension trait which allows cloning a sequence count provider after it was turned into
|
|
||||||
/// a trait object.
|
|
||||||
#[cfg(feature = "alloc")]
|
|
||||||
pub trait SequenceCountProvider<Raw>: SequenceCountProviderCore<Raw> + DynClone {}
|
|
||||||
#[cfg(feature = "alloc")]
|
|
||||||
dyn_clone::clone_trait_object!(SequenceCountProvider<u16>);
|
|
||||||
#[cfg(feature = "alloc")]
|
|
||||||
impl<T, Raw> SequenceCountProvider<Raw> for T where T: SequenceCountProviderCore<Raw> + Clone {}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct SeqCountProviderSimple<T: Copy> {
|
|
||||||
seq_count: Cell<T>,
|
|
||||||
max_val: T,
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! impl_for_primitives {
|
|
||||||
($($ty: ident,)+) => {
|
|
||||||
$(
|
|
||||||
paste! {
|
|
||||||
impl SeqCountProviderSimple<$ty> {
|
|
||||||
pub fn [<new_custom_max_val_ $ty>](max_val: $ty) -> Self {
|
|
||||||
Self {
|
|
||||||
seq_count: Cell::new(0),
|
|
||||||
max_val,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn [<new_ $ty>]() -> Self {
|
|
||||||
Self {
|
|
||||||
seq_count: Cell::new(0),
|
|
||||||
max_val: $ty::MAX
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for SeqCountProviderSimple<$ty> {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::[<new_ $ty>]()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SequenceCountProviderCore<$ty> for SeqCountProviderSimple<$ty> {
|
|
||||||
fn get(&self) -> $ty {
|
|
||||||
self.seq_count.get()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn increment(&self) {
|
|
||||||
self.get_and_increment();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_and_increment(&self) -> $ty {
|
|
||||||
let curr_count = self.seq_count.get();
|
|
||||||
|
|
||||||
if curr_count == self.max_val {
|
|
||||||
self.seq_count.set(0);
|
|
||||||
} else {
|
|
||||||
self.seq_count.set(curr_count + 1);
|
|
||||||
}
|
|
||||||
curr_count
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)+
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl_for_primitives!(u8, u16, u32, u64,);
|
|
||||||
|
|
||||||
/// This is a sequence count provider which wraps around at [MAX_SEQ_COUNT].
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct CcsdsSimpleSeqCountProvider {
|
|
||||||
provider: SeqCountProviderSimple<u16>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for CcsdsSimpleSeqCountProvider {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
provider: SeqCountProviderSimple::new_custom_max_val_u16(MAX_SEQ_COUNT),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SequenceCountProviderCore<u16> for CcsdsSimpleSeqCountProvider {
|
|
||||||
delegate::delegate! {
|
|
||||||
to self.provider {
|
|
||||||
fn get(&self) -> u16;
|
|
||||||
fn increment(&self);
|
|
||||||
fn get_and_increment(&self) -> u16;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "std")]
|
|
||||||
pub mod stdmod {
|
|
||||||
use super::*;
|
|
||||||
use std::sync::{Arc, Mutex};
|
|
||||||
|
|
||||||
macro_rules! sync_clonable_seq_counter_impl {
|
|
||||||
($($ty: ident,)+) => {
|
|
||||||
$(paste! {
|
|
||||||
/// These sequence counters can be shared between threads and can also be
|
|
||||||
/// configured to wrap around at specified maximum values. Please note that
|
|
||||||
/// that the API provided by this class will not panic und [Mutex] lock errors,
|
|
||||||
/// but it will yield 0 for the getter functions.
|
|
||||||
#[derive(Clone, Default)]
|
|
||||||
pub struct [<SeqCountProviderSync $ty:upper>] {
|
|
||||||
seq_count: Arc<Mutex<$ty>>,
|
|
||||||
max_val: $ty
|
|
||||||
}
|
|
||||||
|
|
||||||
impl [<SeqCountProviderSync $ty:upper>] {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self::new_with_max_val($ty::MAX)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new_with_max_val(max_val: $ty) -> Self {
|
|
||||||
Self {
|
|
||||||
seq_count: Arc::default(),
|
|
||||||
max_val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
impl SequenceCountProviderCore<$ty> for [<SeqCountProviderSync $ty:upper>] {
|
|
||||||
fn get(&self) -> $ty {
|
|
||||||
match self.seq_count.lock() {
|
|
||||||
Ok(counter) => *counter,
|
|
||||||
Err(_) => 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn increment(&self) {
|
|
||||||
self.get_and_increment();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_and_increment(&self) -> $ty {
|
|
||||||
match self.seq_count.lock() {
|
|
||||||
Ok(mut counter) => {
|
|
||||||
let val = *counter;
|
|
||||||
if val == self.max_val {
|
|
||||||
*counter = 0;
|
|
||||||
} else {
|
|
||||||
*counter += 1;
|
|
||||||
}
|
|
||||||
val
|
|
||||||
}
|
|
||||||
Err(_) => 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})+
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sync_clonable_seq_counter_impl!(u8, u16, u32, u64,);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use crate::seq_count::{
|
|
||||||
CcsdsSimpleSeqCountProvider, SeqCountProviderSimple, SeqCountProviderSyncU8,
|
|
||||||
SequenceCountProviderCore,
|
|
||||||
};
|
|
||||||
use spacepackets::MAX_SEQ_COUNT;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_u8_counter() {
|
|
||||||
let u8_counter = SeqCountProviderSimple::<u8>::default();
|
|
||||||
assert_eq!(u8_counter.get(), 0);
|
|
||||||
assert_eq!(u8_counter.get_and_increment(), 0);
|
|
||||||
assert_eq!(u8_counter.get_and_increment(), 1);
|
|
||||||
assert_eq!(u8_counter.get(), 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_u8_counter_overflow() {
|
|
||||||
let u8_counter = SeqCountProviderSimple::new_u8();
|
|
||||||
for _ in 0..256 {
|
|
||||||
u8_counter.increment();
|
|
||||||
}
|
|
||||||
assert_eq!(u8_counter.get(), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_ccsds_counter() {
|
|
||||||
let ccsds_counter = CcsdsSimpleSeqCountProvider::default();
|
|
||||||
assert_eq!(ccsds_counter.get(), 0);
|
|
||||||
assert_eq!(ccsds_counter.get_and_increment(), 0);
|
|
||||||
assert_eq!(ccsds_counter.get_and_increment(), 1);
|
|
||||||
assert_eq!(ccsds_counter.get(), 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_ccsds_counter_overflow() {
|
|
||||||
let ccsds_counter = CcsdsSimpleSeqCountProvider::default();
|
|
||||||
for _ in 0..MAX_SEQ_COUNT + 1 {
|
|
||||||
ccsds_counter.increment();
|
|
||||||
}
|
|
||||||
assert_eq!(ccsds_counter.get(), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_atomic_ref_counters() {
|
|
||||||
let sync_u8_counter = SeqCountProviderSyncU8::new();
|
|
||||||
assert_eq!(sync_u8_counter.get(), 0);
|
|
||||||
assert_eq!(sync_u8_counter.get_and_increment(), 0);
|
|
||||||
assert_eq!(sync_u8_counter.get_and_increment(), 1);
|
|
||||||
assert_eq!(sync_u8_counter.get(), 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_atomic_ref_counters_overflow() {
|
|
||||||
let sync_u8_counter = SeqCountProviderSyncU8::new();
|
|
||||||
for _ in 0..u8::MAX as u16 + 1 {
|
|
||||||
sync_u8_counter.increment();
|
|
||||||
}
|
|
||||||
assert_eq!(sync_u8_counter.get(), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_atomic_ref_counters_overflow_custom_max_val() {
|
|
||||||
let sync_u8_counter = SeqCountProviderSyncU8::new_with_max_val(128);
|
|
||||||
for _ in 0..129 {
|
|
||||||
sync_u8_counter.increment();
|
|
||||||
}
|
|
||||||
assert_eq!(sync_u8_counter.get(), 0);
|
|
||||||
}
|
|
||||||
}
|
|
Reference in New Issue
Block a user