runtimepy 5.4.4__py3-none-any.whl → 5.6.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. runtimepy/__init__.py +2 -2
  2. runtimepy/channel/environment/__init__.py +0 -5
  3. runtimepy/channel/environment/array.py +15 -4
  4. runtimepy/channel/environment/base.py +0 -2
  5. runtimepy/channel/environment/sample.py +107 -43
  6. runtimepy/commands/all.py +7 -1
  7. runtimepy/commands/mtu.py +57 -0
  8. runtimepy/commands/server.py +35 -3
  9. runtimepy/commands/tftp.py +4 -2
  10. runtimepy/data/js/classes/TabInterface.js +37 -4
  11. runtimepy/data/js/util.js +12 -3
  12. runtimepy/data/sample_telemetry.yaml +29 -0
  13. runtimepy/net/arbiter/info.py +13 -3
  14. runtimepy/net/arbiter/struct/__init__.py +202 -0
  15. runtimepy/net/arbiter/tcp/__init__.py +1 -0
  16. runtimepy/net/mixin.py +26 -1
  17. runtimepy/net/mtu.py +141 -0
  18. runtimepy/net/server/app/__init__.py +21 -20
  19. runtimepy/net/server/app/env/tab/message.py +17 -5
  20. runtimepy/net/server/websocket/state.py +4 -2
  21. runtimepy/net/ssl.py +33 -0
  22. runtimepy/net/tcp/connection.py +38 -7
  23. runtimepy/net/tcp/create.py +2 -1
  24. runtimepy/net/tcp/http/__init__.py +6 -1
  25. runtimepy/net/udp/connection.py +2 -4
  26. runtimepy/net/udp/tftp/__init__.py +8 -7
  27. runtimepy/net/udp/tftp/base.py +14 -20
  28. runtimepy/net/util.py +69 -10
  29. runtimepy/net/websocket/connection.py +28 -6
  30. runtimepy/primitives/serializable/framer.py +73 -0
  31. runtimepy/struct/__init__.py +4 -1
  32. runtimepy/subprocess/interface.py +1 -1
  33. runtimepy/telemetry/sample.py +113 -0
  34. {runtimepy-5.4.4.dist-info → runtimepy-5.6.0.dist-info}/METADATA +28 -6
  35. {runtimepy-5.4.4.dist-info → runtimepy-5.6.0.dist-info}/RECORD +39 -32
  36. {runtimepy-5.4.4.dist-info → runtimepy-5.6.0.dist-info}/WHEEL +1 -1
  37. {runtimepy-5.4.4.dist-info → runtimepy-5.6.0.dist-info}/LICENSE +0 -0
  38. {runtimepy-5.4.4.dist-info → runtimepy-5.6.0.dist-info}/entry_points.txt +0 -0
  39. {runtimepy-5.4.4.dist-info → runtimepy-5.6.0.dist-info}/top_level.txt +0 -0
runtimepy/__init__.py CHANGED
@@ -1,7 +1,7 @@
1
1
  # =====================================
2
2
  # generator=datazen
3
3
  # version=3.1.4
4
- # hash=5a37b52aedb2af82fb174fbea8ce0c1f
4
+ # hash=6c21c41096ed57b6c6469232c97787cd
5
5
  # =====================================
6
6
 
7
7
  """
@@ -10,7 +10,7 @@ Useful defaults and other package metadata.
10
10
 
11
11
  DESCRIPTION = "A framework for implementing Python services."
12
12
  PKG_NAME = "runtimepy"
13
- VERSION = "5.4.4"
13
+ VERSION = "5.6.0"
14
14
 
15
15
  # runtimepy-specific content.
16
16
  METRICS_NAME = "metrics"
@@ -29,11 +29,6 @@ class ChannelEnvironment(
29
29
  ):
30
30
  """A class integrating channel and enumeration registries."""
31
31
 
32
- @property
33
- def names(self) -> _Iterator[str]:
34
- """Iterate over registered names in the environment."""
35
- yield from self.channels.names.names
36
-
37
32
  def search_names(
38
33
  self, pattern: str, exact: bool = False
39
34
  ) -> _Iterator[str]:
@@ -4,6 +4,7 @@ A channel-environment extension for creating arrays of primitives.
4
4
 
5
5
  # built-in
6
6
  from typing import Iterable as _Iterable
7
+ from typing import Iterator as _Iterator
7
8
  from typing import NamedTuple
8
9
  from typing import Optional as _Optional
9
10
 
@@ -23,9 +24,9 @@ class ChannelArray(NamedTuple):
23
24
  array: _PrimitiveArray
24
25
 
25
26
  @staticmethod
26
- def create() -> "ChannelArray":
27
+ def create(**kwargs) -> "ChannelArray":
27
28
  """Create a new, empty channel array."""
28
- return ChannelArray([], _PrimitiveArray())
29
+ return ChannelArray([], _PrimitiveArray(**kwargs))
29
30
 
30
31
 
31
32
  class ArrayChannelEnvironment(_BaseChannelEnvironment):
@@ -33,12 +34,19 @@ class ArrayChannelEnvironment(_BaseChannelEnvironment):
33
34
  A channel-environment extension for working with arrays of primitives.
34
35
  """
35
36
 
36
- def array(self, keys: _Iterable[_RegistryKey]) -> ChannelArray:
37
+ @property
38
+ def names(self) -> _Iterator[str]:
39
+ """Iterate over registered names in the environment."""
40
+ yield from self.channels.names.names
41
+
42
+ def array(
43
+ self, keys: _Iterable[_RegistryKey] = None, **kwargs
44
+ ) -> ChannelArray:
37
45
  """
38
46
  Create a primitive array from an in-order iterable of registry keys.
39
47
  """
40
48
 
41
- result = ChannelArray.create()
49
+ result = ChannelArray.create(**kwargs)
42
50
 
43
51
  curr_fields: _Optional[_BitFields] = None
44
52
  invalid_field_names: set[str] = set()
@@ -46,6 +54,9 @@ class ArrayChannelEnvironment(_BaseChannelEnvironment):
46
54
 
47
55
  names: set[str] = set()
48
56
 
57
+ if keys is None:
58
+ keys = self.names
59
+
49
60
  for key in keys:
50
61
  # All keys must be registered names.
51
62
  name = self.channels.names.name(key)
@@ -24,8 +24,6 @@ from runtimepy.enum import RuntimeEnum as _RuntimeEnum
24
24
  from runtimepy.enum.registry import EnumRegistry as _EnumRegistry
25
25
  from runtimepy.mixins.finalize import FinalizeMixin
26
26
  from runtimepy.primitives import BaseIntPrimitive, StrToBool
27
-
28
- # third-party
29
27
  from runtimepy.primitives.evaluation import EvalResult, Operator, sample_for
30
28
  from runtimepy.primitives.field import BitField as _BitField
31
29
  from runtimepy.primitives.field.fields import BitFields as _BitFields
@@ -7,19 +7,18 @@ from random import randint, random
7
7
 
8
8
  # internal
9
9
  from runtimepy.channel.environment import ChannelEnvironment
10
+ from runtimepy.enum import RuntimeEnum
10
11
  from runtimepy.primitives import Uint8
11
12
  from runtimepy.primitives.field import BitField, BitFlag
12
13
 
13
14
 
14
- def sample_env(env: ChannelEnvironment = None) -> ChannelEnvironment:
15
- """Register sample enumerations and channels to an environment."""
15
+ def sample_int_enum(
16
+ env: ChannelEnvironment, name: str = "SampleEnum"
17
+ ) -> RuntimeEnum:
18
+ """A sample integer enumeration."""
16
19
 
17
- if env is None:
18
- env = ChannelEnvironment()
19
-
20
- # Register an enum.
21
- sample_enum = env.enum(
22
- "SampleEnum",
20
+ return env.enum(
21
+ name,
23
22
  "int",
24
23
  items={
25
24
  "zero": 0,
@@ -36,12 +35,16 @@ def sample_env(env: ChannelEnvironment = None) -> ChannelEnvironment:
36
35
  },
37
36
  )
38
37
 
39
- # Boolean enumeration.
40
- env.enum("OnOff", "bool", {"On": True, "Off": False})
41
- env.bool_channel("sample_state", commandable=True, enum="OnOff")
42
38
 
43
- env.enum(
44
- "InsanelyLongEnumNameForTesting",
39
+ def long_name_int_enum(
40
+ env: ChannelEnvironment,
41
+ name: str = "InsanelyLongEnumNameForTesting",
42
+ channel: str = "sample_enum",
43
+ ) -> RuntimeEnum:
44
+ """Add an integer enumeration with a long name."""
45
+
46
+ result = env.enum(
47
+ name,
45
48
  "int",
46
49
  {
47
50
  "very_long_member_name_0": 0,
@@ -50,40 +53,101 @@ def sample_env(env: ChannelEnvironment = None) -> ChannelEnvironment:
50
53
  "very_long_member_name_3": 3,
51
54
  },
52
55
  )
53
- env.int_channel(
54
- "sample_enum", commandable=True, enum="InsanelyLongEnumNameForTesting"
56
+
57
+ if channel:
58
+ env.int_channel(
59
+ channel,
60
+ commandable=True,
61
+ enum="InsanelyLongEnumNameForTesting",
62
+ description="A sample long-enum-name channel.",
63
+ )
64
+
65
+ return result
66
+
67
+
68
+ def sample_bool_enum(
69
+ env: ChannelEnvironment, name: str = "OnOff", channel: str = "sample_state"
70
+ ) -> RuntimeEnum:
71
+ """A sample boolean enumeration."""
72
+
73
+ result = env.enum(name, "bool", {"On": True, "Off": False})
74
+
75
+ if channel:
76
+ env.bool_channel(
77
+ channel,
78
+ commandable=True,
79
+ enum="OnOff",
80
+ description="A sample on-off state channel.",
81
+ )
82
+
83
+ return result
84
+
85
+
86
+ def sample_fields(env: ChannelEnvironment, enum: str = "SampleEnum") -> None:
87
+ """Add sample bit-fields to an environment."""
88
+
89
+ with env.names_pushed("fields"):
90
+ prim = Uint8()
91
+ env.int_channel("raw", prim)
92
+
93
+ # Add a bit field and flag.
94
+ env.add_field(BitFlag("flag1", prim, 0))
95
+ env.add_field(
96
+ BitFlag(
97
+ "flag2",
98
+ prim,
99
+ 1,
100
+ commandable=True,
101
+ description="Sample bit flag.",
102
+ )
103
+ )
104
+ env.add_field(
105
+ BitField(
106
+ "field1",
107
+ prim,
108
+ 2,
109
+ 2,
110
+ enum=enum if enum else None,
111
+ commandable=True,
112
+ description="Sample bit field.",
113
+ )
114
+ )
115
+ env.add_field(BitField("field2", prim, 4, 4, commandable=True))
116
+
117
+
118
+ def sample_float(
119
+ env: ChannelEnvironment,
120
+ channel: str = "sample_float",
121
+ kind: str = "double",
122
+ ) -> None:
123
+ """Add a sample floating-point channel."""
124
+
125
+ env.float_channel(
126
+ channel,
127
+ kind,
128
+ commandable=True,
129
+ description="A sample floating-point member.",
55
130
  )
56
- env.float_channel("sample_float", "double", commandable=True)
131
+
132
+
133
+ def sample_env(env: ChannelEnvironment = None) -> ChannelEnvironment:
134
+ """Register sample enumerations and channels to an environment."""
135
+
136
+ if env is None:
137
+ env = ChannelEnvironment()
138
+
139
+ # Register an enum.
140
+ sample_int_enum(env)
141
+
142
+ # Boolean enumeration.
143
+ sample_bool_enum(env)
144
+
145
+ long_name_int_enum(env)
146
+ sample_float(env)
57
147
 
58
148
  for name in ["a", "b", "c"]:
59
149
  with env.names_pushed(name):
60
- with env.names_pushed("fields"):
61
- prim = Uint8()
62
- env.int_channel("raw", prim)
63
-
64
- # Add a bit field and flag.
65
- env.add_field(BitFlag("flag1", prim, 0))
66
- env.add_field(
67
- BitFlag(
68
- "flag2",
69
- prim,
70
- 1,
71
- commandable=True,
72
- description="Sample bit flag.",
73
- )
74
- )
75
- env.add_field(
76
- BitField(
77
- "field1",
78
- prim,
79
- 2,
80
- 2,
81
- enum=sample_enum.id,
82
- commandable=True,
83
- description="Sample bit field.",
84
- )
85
- )
86
- env.add_field(BitField("field2", prim, 4, 4, commandable=True))
150
+ sample_fields(env)
87
151
 
88
152
  for i in range(10):
89
153
  with env.names_pushed(str(i)):
runtimepy/commands/all.py CHANGED
@@ -1,7 +1,7 @@
1
1
  # =====================================
2
2
  # generator=datazen
3
3
  # version=3.1.4
4
- # hash=5162b7ebed95a36719dad0f72cd0872f
4
+ # hash=1814b7f7fae3556a0dbeec37141e4182
5
5
  # =====================================
6
6
 
7
7
  """
@@ -17,6 +17,7 @@ from vcorelib.args import CommandRegister as _CommandRegister
17
17
 
18
18
  # internal
19
19
  from runtimepy.commands.arbiter import add_arbiter_cmd
20
+ from runtimepy.commands.mtu import add_mtu_cmd
20
21
  from runtimepy.commands.server import add_server_cmd
21
22
  from runtimepy.commands.task import add_task_cmd
22
23
  from runtimepy.commands.tftp import add_tftp_cmd
@@ -32,6 +33,11 @@ def commands() -> _List[_Tuple[str, str, _CommandRegister]]:
32
33
  "run a connection-arbiter application from a config",
33
34
  add_arbiter_cmd,
34
35
  ),
36
+ (
37
+ "mtu",
38
+ "probe for MTU size to some endpoint",
39
+ add_mtu_cmd,
40
+ ),
35
41
  (
36
42
  "server",
37
43
  "run a server for a specific connection factory",
@@ -0,0 +1,57 @@
1
+ """
2
+ An entry-point for the 'mtu' command.
3
+ """
4
+
5
+ # built-in
6
+ import argparse
7
+ import socket
8
+
9
+ # third-party
10
+ from vcorelib.args import CommandFunction
11
+
12
+ # internal
13
+ from runtimepy.net.mtu import ETHERNET_MTU, UDP_DEFAULT_MTU, discover_mtu
14
+
15
+
16
+ def mtu_cmd(args: argparse.Namespace) -> int:
17
+ """Execute the mtu command."""
18
+
19
+ discover_mtu(
20
+ args.destination[0],
21
+ *(int(x) for x in args.destination[1:]),
22
+ probe_size=args.probe_size,
23
+ fallback=args.fallback,
24
+ kind=socket.SOCK_STREAM if args.tcp else socket.SOCK_DGRAM,
25
+ )
26
+
27
+ return 0
28
+
29
+
30
+ def add_mtu_cmd(parser: argparse.ArgumentParser) -> CommandFunction:
31
+ """Add mtu-command arguments to its parser."""
32
+
33
+ parser.add_argument(
34
+ "--probe-size",
35
+ type=int,
36
+ default=UDP_DEFAULT_MTU,
37
+ help="data payload size to use for probe (default: %(default)d)",
38
+ )
39
+ parser.add_argument(
40
+ "--fallback",
41
+ type=int,
42
+ default=ETHERNET_MTU,
43
+ help="fallback MTU value if probing doesn't succeed "
44
+ "(i.e. not on Linux, default: %(default)d)",
45
+ )
46
+
47
+ parser.add_argument(
48
+ "-t", "--tcp", action="store_true", help="use TCP instead of UDP"
49
+ )
50
+
51
+ parser.add_argument(
52
+ "destination",
53
+ nargs="+",
54
+ help="endpoint parameters (host, port[, flowinfo, scope_id])",
55
+ )
56
+
57
+ return mtu_cmd
@@ -14,6 +14,9 @@ from vcorelib.args import CommandFunction as _CommandFunction
14
14
  from runtimepy.commands.arbiter import arbiter_cmd
15
15
  from runtimepy.commands.common import FACTORIES, arbiter_args, cmd_with_jit
16
16
 
17
+ SSL_PASSTHROUGH = ["cafile", "capath", "cadata", "certfile"]
18
+ PASSTHROUGH = SSL_PASSTHROUGH + ["keyfile"]
19
+
17
20
 
18
21
  def port_name(args: _Namespace, port: str = "port") -> str:
19
22
  """Get the name for a connection factory's port."""
@@ -25,7 +28,7 @@ def server_data(args: _Namespace) -> dict[str, Any]:
25
28
 
26
29
  return {
27
30
  "factory": args.factory,
28
- "kwargs": {"port": f"${port_name(args)}", "host": args.host},
31
+ "kwargs": get_kwargs(args, port=f"${port_name(args)}", host=args.host),
29
32
  }
30
33
 
31
34
 
@@ -34,6 +37,26 @@ def is_websocket(args: _Namespace) -> bool:
34
37
  return "websocket" in args.factory.lower()
35
38
 
36
39
 
40
+ def is_ssl(kwargs: dict[str, Any]) -> bool:
41
+ """Determine if server arugments indicate SSL use."""
42
+ return any(x in kwargs for x in SSL_PASSTHROUGH)
43
+
44
+
45
+ def get_kwargs(args: _Namespace, **kwargs) -> dict[str, Any]:
46
+ """Get boilerplate kwargs."""
47
+
48
+ new_kwargs: dict[str, Any] = {**kwargs}
49
+
50
+ # Pass additional arguments through.
51
+ print(args)
52
+ for opt in PASSTHROUGH:
53
+ value = getattr(args, opt, None)
54
+ if value is not None:
55
+ new_kwargs[opt] = value
56
+
57
+ return new_kwargs
58
+
59
+
37
60
  def client_data(args: _Namespace) -> dict[str, Any]:
38
61
  """Get client data based on command-line arguments."""
39
62
 
@@ -43,7 +66,9 @@ def client_data(args: _Namespace) -> dict[str, Any]:
43
66
  kwargs: dict[str, Any] = {}
44
67
 
45
68
  if is_websocket(args):
46
- arg_list.append(f"ws://localhost:{port}")
69
+ arg_list.append(
70
+ f"ws{'s' if is_ssl(get_kwargs(args)) else ''}://localhost:{port}"
71
+ )
47
72
  elif not args.udp:
48
73
  kwargs["host"] = "localhost"
49
74
  kwargs["port"] = port
@@ -76,7 +101,9 @@ def config_data(args: _Namespace) -> dict[str, Any]:
76
101
  {
77
102
  "name": port_name(args, port="server"),
78
103
  "factory": args.factory,
79
- "kwargs": {"local_addr": ["0.0.0.0", f"${port_name(args)}"]},
104
+ "kwargs": get_kwargs(
105
+ args, local_addr=["0.0.0.0", f"${port_name(args)}"]
106
+ ),
80
107
  }
81
108
  )
82
109
 
@@ -108,6 +135,11 @@ def add_server_cmd(parser: _ArgumentParser) -> _CommandFunction:
108
135
  """Add server-command arguments to its parser."""
109
136
 
110
137
  with arbiter_args(parser, nargs="*"):
138
+ for optional in PASSTHROUGH:
139
+ parser.add_argument(
140
+ f"--{optional}", help="passed directly to instantiation"
141
+ )
142
+
111
143
  parser.add_argument(
112
144
  "--host",
113
145
  default="0.0.0.0",
@@ -6,7 +6,6 @@ An entry-point for the 'tftp' command.
6
6
  import argparse
7
7
  import asyncio
8
8
  from pathlib import Path
9
- from socket import getaddrinfo
10
9
 
11
10
  # third-party
12
11
  from vcorelib.args import CommandFunction
@@ -16,13 +15,16 @@ from vcorelib.asyncio import run_handle_stop
16
15
  from runtimepy.net.udp.tftp import TFTP_PORT, tftp_read, tftp_write
17
16
  from runtimepy.net.udp.tftp.base import DEFAULT_TIMEOUT_S, REEMIT_PERIOD_S
18
17
  from runtimepy.net.udp.tftp.enums import DEFAULT_MODE
18
+ from runtimepy.net.util import normalize_host
19
19
 
20
20
 
21
21
  def tftp_cmd(args: argparse.Namespace) -> int:
22
22
  """Execute the tftp command."""
23
23
 
24
+ host = normalize_host(args.host, args.port)
25
+
24
26
  # Resolve hostname as early as possible.
25
- addr = (getaddrinfo(args.host, None)[0][4][0], args.port)
27
+ addr = host.address_str_tuple
26
28
 
27
29
  stop_sig = asyncio.Event()
28
30
  kwargs = {
@@ -43,6 +43,9 @@ class TabInterface {
43
43
  /* Update global reference. */
44
44
  shown_tab = this.name;
45
45
  hash.setTab(shown_tab);
46
+
47
+ /* Audit vertical bar position. */
48
+ this.correctVerticalBarPosition();
46
49
  }
47
50
  this.worker.send({kind : msg});
48
51
  } ];
@@ -51,6 +54,11 @@ class TabInterface {
51
54
  this.initCommand();
52
55
  this.initControls();
53
56
  this.initButton();
57
+
58
+ /* Ensure vertical divider is always visible. */
59
+ window.addEventListener(
60
+ "resize",
61
+ ((event) => { this.correctVerticalBarPosition(); }).bind(this));
54
62
  }
55
63
 
56
64
  initCommand() {
@@ -131,9 +139,9 @@ class TabInterface {
131
139
  }
132
140
 
133
141
  /* Initialize channel table and plot divider. */
134
- let divider = this.query(".vertical-divider");
135
- if (divider) {
136
- this.setupVerticalDivider(divider);
142
+ this.divider = this.query(".vertical-divider");
143
+ if (this.divider) {
144
+ this.setupVerticalDivider(this.divider);
137
145
  }
138
146
 
139
147
  /* Initialize sliders. */
@@ -185,12 +193,22 @@ class TabInterface {
185
193
 
186
194
  let origX = event.clientX || event.touches[0].clientX;
187
195
 
196
+ let barWidth = 0;
197
+ if (this.divider) {
198
+ barWidth = this.divider.clientWidth;
199
+ }
200
+
188
201
  /* Track mouse movement while the mouse is held down. */
189
202
  let handleMouse = (event) => {
190
203
  let eventX = event.clientX || event.touches[0].clientX;
191
204
 
192
205
  let deltaX = origX - eventX;
193
- elem.style.width = elem.getBoundingClientRect().width - deltaX + "px";
206
+
207
+ let rect = elem.getBoundingClientRect();
208
+
209
+ /* No use in allowing dragging out of view. */
210
+ let minWidth = window.innerWidth - (rect.left + barWidth);
211
+ elem.style.width = Math.min(minWidth, rect.width - deltaX) + "px";
194
212
  origX = eventX;
195
213
  };
196
214
  document.addEventListener(move, handleMouse);
@@ -206,6 +224,21 @@ class TabInterface {
206
224
  setupCursorContext(elem, this.setupVerticalDividerEvents.bind(this));
207
225
  }
208
226
 
227
+ correctVerticalBarPosition(elem) {
228
+ if (shown_tab == this.name && this.divider) {
229
+ let margin =
230
+ window.innerWidth - this.divider.getBoundingClientRect().right;
231
+
232
+ if (margin < 0) {
233
+ let column = this.query(".channel-column");
234
+ if (column) {
235
+ column.style.width =
236
+ column.getBoundingClientRect().width + margin + "px";
237
+ }
238
+ }
239
+ }
240
+ }
241
+
209
242
  initPlot() {
210
243
  let plot = this.query(`#${this.queryName}-plot`);
211
244
  if (plot) {
runtimepy/data/js/util.js CHANGED
@@ -1,6 +1,15 @@
1
1
  function worker_config(config) {
2
2
  let worker_cfg = {};
3
3
 
4
+ let port_name = "runtimepy_websocket";
5
+ let uri_prefix = "ws";
6
+
7
+ /* Ensured TLS is handled properly. */
8
+ if (location.protocol.includes("https")) {
9
+ port_name = "runtimepy_secure_websocket";
10
+ uri_prefix += "s";
11
+ }
12
+
4
13
  /* Look for connections to establish. */
5
14
  let ports = config["config"]["ports"];
6
15
  for (let port_idx in ports) {
@@ -10,11 +19,11 @@ function worker_config(config) {
10
19
  let hostname = window.location.hostname;
11
20
 
12
21
  /* This business logic could use some work. */
13
- if (port["name"].includes("runtimepy_websocket")) {
22
+ if (port["name"].includes(port_name)) {
14
23
  if (port["name"].includes("data")) {
15
- worker_cfg["data"] = "ws://" + hostname + ":" + port["port"];
24
+ worker_cfg["data"] = `${uri_prefix}://${hostname}:` + port["port"];
16
25
  } else {
17
- worker_cfg["json"] = "ws://" + hostname + ":" + port["port"];
26
+ worker_cfg["json"] = `${uri_prefix}://${hostname}:` + port["port"];
18
27
  }
19
28
  }
20
29
  }
@@ -0,0 +1,29 @@
1
+ ---
2
+ factories:
3
+ - {name: runtimepy.telemetry.sample.SampleTelemetry}
4
+ - {name: runtimepy.telemetry.sample.SampleTelemetryStruct}
5
+ - {name: runtimepy.telemetry.sample.SampleTelemetryPeriodic}
6
+
7
+ structs:
8
+ - name: tx
9
+ factory: SampleTelemetryStruct
10
+ - name: rx
11
+ factory: SampleTelemetryStruct
12
+
13
+ tasks:
14
+ - {name: tlm_periodic, factory: sample_telemetry_periodic, period_s: 0.01}
15
+
16
+ ports:
17
+ - {name: udp_sample_telemetry, type: udp}
18
+
19
+ clients:
20
+ - factory: SampleTelemetry
21
+ name: rx_conn
22
+ kwargs:
23
+ local_addr: ["0.0.0.0", "$udp_sample_telemetry"]
24
+
25
+ - factory: SampleTelemetry
26
+ name: tx_conn
27
+ defer: true
28
+ kwargs:
29
+ remote_addr: ["127.0.0.1", "$udp_sample_telemetry"]
@@ -31,6 +31,8 @@ from runtimepy.net.arbiter.result import OverallResult, results
31
31
  from runtimepy.net.connection import Connection as _Connection
32
32
  from runtimepy.net.manager import ConnectionManager
33
33
  from runtimepy.primitives import Uint32
34
+ from runtimepy.primitives.array import PrimitiveArray
35
+ from runtimepy.primitives.byte_order import DEFAULT_BYTE_ORDER, ByteOrder
34
36
  from runtimepy.struct import RuntimeStructBase
35
37
  from runtimepy.struct import StructMap as _StructMap
36
38
  from runtimepy.task import PeriodicTask, PeriodicTaskManager
@@ -51,15 +53,24 @@ class RuntimeStruct(RuntimeStructBase, _ABC):
51
53
  """A class implementing a base runtime structure."""
52
54
 
53
55
  app: "AppInfo"
56
+ array: PrimitiveArray
57
+
58
+ byte_order: ByteOrder = DEFAULT_BYTE_ORDER
54
59
 
55
60
  def init_env(self) -> None:
56
61
  """Initialize this sample environment."""
57
62
 
58
- async def build(self, app: "AppInfo") -> None:
63
+ async def build(self, app: "AppInfo", **kwargs) -> None:
59
64
  """Build a struct instance's channel environment."""
60
65
 
61
66
  self.app = app
62
67
  self.init_env()
68
+ self.update_byte_order(self.byte_order, **kwargs)
69
+
70
+ def update_byte_order(self, byte_order: ByteOrder, **kwargs) -> None:
71
+ """Update the over-the-wire byte order for this struct."""
72
+
73
+ self.array = self.env.array(byte_order=byte_order, **kwargs).array
63
74
 
64
75
 
65
76
  W = _TypeVar("W", bound=RuntimeStruct)
@@ -93,9 +104,8 @@ class SampleStruct(TrigStruct):
93
104
 
94
105
  def init_env(self) -> None:
95
106
  """Initialize this sample environment."""
96
-
97
- sample_env(self.env)
98
107
  super().init_env()
108
+ sample_env(self.env)
99
109
 
100
110
  def poll(self) -> None:
101
111
  """