openc3 7.4.1 → 7.5.0

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 (92) hide show
  1. checksums.yaml +4 -4
  2. data/bin/pipinstall +18 -5
  3. data/bin/pipuninstall +1 -1
  4. data/bin/uvinstall +36 -8
  5. data/data/config/default_plugins.yaml +43 -0
  6. data/data/config/interface_modifiers.yaml +22 -7
  7. data/data/config/item_modifiers.yaml +14 -4
  8. data/data/config/plugins.yaml +1 -1
  9. data/data/config/widgets.yaml +1 -1
  10. data/lib/openc3/api/cmd_api.rb +10 -13
  11. data/lib/openc3/api/interface_api.rb +1 -2
  12. data/lib/openc3/api/limits_api.rb +1 -1
  13. data/lib/openc3/api/metrics_api.rb +26 -1
  14. data/lib/openc3/api/router_api.rb +1 -2
  15. data/lib/openc3/api/tlm_api.rb +6 -10
  16. data/lib/openc3/config/config_parser.rb +3 -1
  17. data/lib/openc3/core_ext/tempfile.rb +2 -0
  18. data/lib/openc3/interfaces/file_interface.rb +6 -1
  19. data/lib/openc3/interfaces/http_client_interface.rb +14 -6
  20. data/lib/openc3/interfaces/interface.rb +27 -3
  21. data/lib/openc3/interfaces/read_queue_interface.rb +331 -0
  22. data/lib/openc3/interfaces/stream_interface.rb +22 -3
  23. data/lib/openc3/interfaces/tcpip_server_interface.rb +13 -0
  24. data/lib/openc3/interfaces/udp_interface.rb +16 -2
  25. data/lib/openc3/interfaces.rb +1 -0
  26. data/lib/openc3/logs/log_writer.rb +13 -4
  27. data/lib/openc3/microservices/decom_microservice.rb +7 -4
  28. data/lib/openc3/microservices/interface_microservice.rb +146 -50
  29. data/lib/openc3/microservices/log_microservice.rb +1 -3
  30. data/lib/openc3/microservices/microservice.rb +25 -1
  31. data/lib/openc3/microservices/plugin_microservice.rb +32 -0
  32. data/lib/openc3/microservices/text_log_microservice.rb +1 -3
  33. data/lib/openc3/models/interface_model.rb +1 -1
  34. data/lib/openc3/models/interface_status_model.rb +6 -0
  35. data/lib/openc3/models/metric_model.rb +52 -4
  36. data/lib/openc3/models/microservice_model.rb +7 -3
  37. data/lib/openc3/models/plugin_model.rb +12 -12
  38. data/lib/openc3/models/python_package_model.rb +3 -13
  39. data/lib/openc3/models/target_model.rb +83 -9
  40. data/lib/openc3/operators/microservice_operator.rb +5 -0
  41. data/lib/openc3/operators/operator.rb +69 -0
  42. data/lib/openc3/packets/packet.rb +10 -4
  43. data/lib/openc3/packets/packet_config.rb +12 -1
  44. data/lib/openc3/packets/packet_item.rb +27 -14
  45. data/lib/openc3/packets/parsers/state_parser.rb +1 -1
  46. data/lib/openc3/packets/structure_item.rb +3 -2
  47. data/lib/openc3/script/api_shared.rb +58 -84
  48. data/lib/openc3/script/extract.rb +215 -19
  49. data/lib/openc3/script/plugins.rb +4 -11
  50. data/lib/openc3/script/storage.rb +81 -10
  51. data/lib/openc3/system/system.rb +9 -6
  52. data/lib/openc3/topics/command_decom_topic.rb +1 -1
  53. data/lib/openc3/topics/command_topic.rb +2 -1
  54. data/lib/openc3/topics/router_topic.rb +2 -1
  55. data/lib/openc3/topics/telemetry_decom_topic.rb +1 -1
  56. data/lib/openc3/topics/telemetry_topic.rb +3 -2
  57. data/lib/openc3/topics/topic.rb +154 -2
  58. data/lib/openc3/utilities/cli_generator.rb +67 -15
  59. data/lib/openc3/utilities/config_overlay.rb +35 -17
  60. data/lib/openc3/utilities/default_plugins.rb +33 -0
  61. data/lib/openc3/utilities/local_mode.rb +82 -46
  62. data/lib/openc3/utilities/metric.rb +16 -0
  63. data/lib/openc3/utilities/process_stats.rb +160 -0
  64. data/lib/openc3/utilities/pypi_url.rb +65 -0
  65. data/lib/openc3/utilities/questdb_client.rb +14 -0
  66. data/lib/openc3/utilities/running_script.rb +22 -18
  67. data/lib/openc3/utilities/store_autoload.rb +20 -3
  68. data/lib/openc3/utilities/store_queued.rb +37 -9
  69. data/lib/openc3/utilities/target_file.rb +57 -12
  70. data/lib/openc3/utilities/topic_lag_monitor.rb +193 -0
  71. data/lib/openc3/version.rb +6 -6
  72. data/templates/conversion/conversion.py +6 -2
  73. data/templates/conversion/conversion.rb +5 -0
  74. data/templates/plugin/.github/workflows/playwright.yml +62 -0
  75. data/templates/plugin/.github/workflows/release.yml +45 -0
  76. data/templates/plugin/.github/workflows/unit-tests.yml +35 -0
  77. data/templates/plugin/.gitignore +6 -0
  78. data/templates/plugin/README.md +114 -2
  79. data/templates/plugin/lib/PACKAGE/__init__.py +4 -0
  80. data/templates/plugin/plugin.gemspec +10 -1
  81. data/templates/plugin/pyproject.toml +119 -0
  82. data/templates/tool_angular/package.json +2 -2
  83. data/templates/tool_angular/pnpm-workspace.yaml +16 -0
  84. data/templates/tool_react/package.json +1 -1
  85. data/templates/tool_react/pnpm-workspace.yaml +16 -0
  86. data/templates/tool_svelte/package.json +2 -1
  87. data/templates/tool_svelte/pnpm-workspace.yaml +20 -0
  88. data/templates/tool_vue/package.json +3 -3
  89. data/templates/tool_vue/pnpm-workspace.yaml +16 -0
  90. data/templates/widget/package.json +2 -2
  91. data/templates/widget/pnpm-workspace.yaml +16 -0
  92. metadata +16 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 38237f07ec722ff8f92d12fd8b1219f0f90fe58c7e37e30e8d50fd9107cc373e
4
- data.tar.gz: 5b2d61f7c5cfc095fc554099d4b4854d147b7c9335dc413a024afc14d09b6713
3
+ metadata.gz: 86e5a8920effe09dd66ebc0ca5f90e8df99948804786b07abce52711dbd03d37
4
+ data.tar.gz: 612c0bfe5198ecc591bb57722931174813ee343370db9ad1474b5f9e62a87a8f
5
5
  SHA512:
6
- metadata.gz: 5cf34d15ab53eee6dde3268c9735c51ebe8ed39ee910a34388549bde4e6c5abb9e60afd88a0698937e2c5f5ff16b106dd0633c268f62fdbf79be0f6b02e1fc22
7
- data.tar.gz: b9e56c46a90420c6a8c20e5dbcae3b49d30686c4453d640e5bfbca90d5391949eb3054a8d11380ef1735f1b05f830973b1dd79a70b3ce7b7178b17b0f892c673
6
+ metadata.gz: 36d1781323b82533f1af150fef184e63ec9b59c93006f4faaef8949f841d1983adbec5204ff5551f629cdd8d843cf843890edc3d5193249215514359c7cb437e
7
+ data.tar.gz: 3eafda9e5def7b46492a96627ce1e4b9a2c22d0f2e0ea3b51279051ebb2e658833ee5b18dacc119933cd3cafcb24219d587fc1afb8f11efab33daf9025ceabad
data/bin/pipinstall CHANGED
@@ -1,8 +1,21 @@
1
1
  #!/bin/sh
2
2
  VENV="${PIPINSTALL_VENV:-$PYTHONUSERBASE}"
3
+
4
+ # --no-sources finishes what --no-config starts. PypiUrl.build_args adds
5
+ # --no-config when an operator has designated an index, which stops uv searching
6
+ # a named index declared in [tool.uv].index - but a package pinned with
7
+ # [tool.uv].sources is project metadata that survives --no-config and still
8
+ # resolves against the index the pin names. --no-sources drops those pins so the
9
+ # operator's index is the only one consulted. Every uv command below resolves,
10
+ # so it applies to all of them.
11
+ case " $* " in
12
+ *" --no-config "*) NO_SOURCES="--no-sources" ;;
13
+ *) NO_SOURCES="" ;;
14
+ esac
15
+
3
16
  uv venv "$VENV" --allow-existing
4
- echo "uv pip install $@"
5
- uv pip install --python "$VENV" "$@"
17
+ echo "uv pip install $*"
18
+ uv pip install --python "$VENV" ${NO_SOURCES} "$@"
6
19
  if [ $? -eq 0 ]; then
7
20
  echo "Command succeeded"
8
21
  exit 0
@@ -26,9 +39,9 @@ LAST_ARG="$PREV"
26
39
  if [ -d "$LAST_ARG" ] && [ -f "$LAST_ARG/pyproject.toml" ]; then
27
40
  echo "Warning: Failed to build Python package, attempting to install declared dependencies from pyproject.toml"
28
41
  TMPFILE=$(mktemp)
29
- uv pip compile ${OPTS} "${LAST_ARG}/pyproject.toml" > "$TMPFILE"
42
+ uv pip compile ${NO_SOURCES} ${OPTS} "${LAST_ARG}/pyproject.toml" > "$TMPFILE"
30
43
  if [ $? -eq 0 ] && [ -s "$TMPFILE" ]; then
31
- uv pip install --python "$VENV" ${OPTS} -r "$TMPFILE"
44
+ uv pip install --python "$VENV" ${NO_SOURCES} ${OPTS} -r "$TMPFILE"
32
45
  if [ $? -eq 0 ]; then
33
46
  echo "Dependencies installed successfully"
34
47
  rm -f "$TMPFILE"
@@ -39,7 +52,7 @@ if [ -d "$LAST_ARG" ] && [ -f "$LAST_ARG/pyproject.toml" ]; then
39
52
  fi
40
53
 
41
54
  echo "Warning: Install failed - retrying with --no-index"
42
- uv pip install --python "$VENV" --no-index "$@"
55
+ uv pip install --python "$VENV" --no-index ${NO_SOURCES} "$@"
43
56
  if [ $? -ne 0 ]; then
44
57
  echo "ERROR: uv pip install failed"
45
58
  exit 1
data/bin/pipuninstall CHANGED
@@ -3,7 +3,7 @@
3
3
  # PythonPackageModel.destroy for per-plugin venvs), uninstall from that venv.
4
4
  # Otherwise fall back to the shared PYTHONUSERBASE.
5
5
  VENV="${PIPINSTALL_VENV:-$PYTHONUSERBASE}"
6
- echo "uv pip uninstall $@"
6
+ echo "uv pip uninstall $*"
7
7
  uv pip uninstall --python "$VENV" "$@"
8
8
  if [ $? -eq 0 ]; then
9
9
  echo "Command succeeded"
data/bin/uvinstall CHANGED
@@ -22,7 +22,28 @@
22
22
  PLUGIN_VENV_NAME="$1"
23
23
  GEM_PATH="$2"
24
24
  shift 2
25
- UV_ARGS="$@"
25
+ # Index arguments built by PypiUrl.build_args: --default-index (the PyPI URL
26
+ # from Admin Console -> Settings) and, when the deployment opts in,
27
+ # --allow-insecure-host. Only --allow-insecure-host is load bearing on the
28
+ # `uv sync --frozen` path below: a frozen sync never resolves, so uv installs
29
+ # from the registry and wheel URLs already recorded in uv.lock and the default
30
+ # index is unused. On the `uv pip` fallback paths both matter.
31
+ UV_ARGS="$*"
32
+
33
+ # --no-sources finishes what --no-config starts. PypiUrl.build_args adds
34
+ # --no-config when an operator has designated an index, which stops uv searching
35
+ # a named index the plugin declares in [tool.uv].index - but a package pinned
36
+ # with [tool.uv].sources is project metadata that survives --no-config and still
37
+ # resolves against the plugin author's index. --no-sources drops those pins so
38
+ # the operator's index is the only one consulted.
39
+ #
40
+ # It is added only for the commands that resolve: `uv sync --frozen` rejects
41
+ # --no-sources outright, and has no use for it, because a frozen sync installs
42
+ # the registry and wheel URLs already recorded in uv.lock without resolving.
43
+ RESOLVE_ARGS="$UV_ARGS"
44
+ case " ${UV_ARGS} " in
45
+ *" --no-config "*) RESOLVE_ARGS="${UV_ARGS} --no-sources" ;;
46
+ esac
26
47
 
27
48
  if [ -z "$PLUGIN_VENV_NAME" ] || [ -z "$GEM_PATH" ]; then
28
49
  echo "Usage: uvinstall <plugin_venv_name> <gem_path> [extra uv args...]"
@@ -57,7 +78,12 @@ uv_offline_then_online() {
57
78
  uv "$@"
58
79
  }
59
80
 
60
- VENV_BASE="/gems/plugin_venvs/${PLUGIN_VENV_NAME}"
81
+ # The venv root is overridable so the spec suite can drive this script against
82
+ # a temporary directory. Production never sets it; PluginModel computes the same
83
+ # /gems/plugin_venvs path independently when it records the .uv_managed marker,
84
+ # so pointing this elsewhere outside a test would put the two out of step.
85
+ VENV_ROOT="${UVINSTALL_VENV_ROOT:-/gems/plugin_venvs}"
86
+ VENV_BASE="${VENV_ROOT}/${PLUGIN_VENV_NAME}"
61
87
  VENV_DIR="${VENV_BASE}/.venv"
62
88
 
63
89
  echo "Creating per-plugin venv for '${PLUGIN_VENV_NAME}' at ${VENV_DIR}"
@@ -72,7 +98,9 @@ if [ -f "${GEM_PATH}/uv.lock" ] && [ -f "${GEM_PATH}/pyproject.toml" ]; then
72
98
  cp "${GEM_PATH}/uv.lock" "${VENV_BASE}/uv.lock"
73
99
 
74
100
  # Create venv and sync dependencies
75
- cd "${VENV_BASE}"
101
+ # Syncing in the wrong directory would resolve against whatever
102
+ # pyproject.toml happens to be there, so treat this as fatal.
103
+ cd "${VENV_BASE}" || exit 1
76
104
  uv venv "${VENV_DIR}" --allow-existing
77
105
 
78
106
  # Offline sync first, online only on a cache miss (bounded by UV_HTTP_*).
@@ -99,14 +127,14 @@ uv venv "${VENV_DIR}" --allow-existing
99
127
 
100
128
  if [ -f "${GEM_PATH}/requirements.txt" ]; then
101
129
  echo "Installing from requirements.txt via uv pip install"
102
- uv_offline_then_online pip install --python "${VENV_DIR}" ${UV_ARGS} -r "${GEM_PATH}/requirements.txt"
130
+ uv_offline_then_online pip install --python "${VENV_DIR}" ${RESOLVE_ARGS} -r "${GEM_PATH}/requirements.txt"
103
131
  RESULT=$?
104
132
  elif [ -f "${GEM_PATH}/pyproject.toml" ]; then
105
133
  echo "Installing from pyproject.toml via uv pip install"
106
134
  # --python: The Python interpreter into which packages should be installed.
107
135
  # --project: Discover a project (i.e. pyproject.toml) in the given directory.
108
136
  # -r: Install the packages listed in the given files.
109
- uv_offline_then_online pip install --python "${VENV_DIR}" --project "${GEM_PATH}" ${UV_ARGS} -r "${GEM_PATH}/pyproject.toml"
137
+ uv_offline_then_online pip install --python "${VENV_DIR}" --project "${GEM_PATH}" ${RESOLVE_ARGS} -r "${GEM_PATH}/pyproject.toml"
110
138
  RESULT=$?
111
139
 
112
140
  # `uv pip install <path>` builds the plugin as a package before installing
@@ -121,11 +149,11 @@ elif [ -f "${GEM_PATH}/pyproject.toml" ]; then
121
149
  if [ $RESULT -ne 0 ]; then
122
150
  echo "Warning: Failed to build Python package, attempting to install declared dependencies from pyproject.toml"
123
151
  TMPFILE=$(mktemp)
124
- uv pip compile --offline ${UV_ARGS} "${GEM_PATH}/pyproject.toml" > "$TMPFILE"
152
+ uv pip compile --offline ${RESOLVE_ARGS} "${GEM_PATH}/pyproject.toml" > "$TMPFILE"
125
153
  COMPILED=$?
126
154
  if [ $COMPILED -ne 0 ] || [ ! -s "$TMPFILE" ]; then
127
155
  echo "Offline uv pip compile missed the cache; retrying online"
128
- uv pip compile ${UV_ARGS} "${GEM_PATH}/pyproject.toml" > "$TMPFILE"
156
+ uv pip compile ${RESOLVE_ARGS} "${GEM_PATH}/pyproject.toml" > "$TMPFILE"
129
157
  COMPILED=$?
130
158
  fi
131
159
  # Both the exit status AND a non-empty file are required: uv writes its
@@ -133,7 +161,7 @@ elif [ -f "${GEM_PATH}/pyproject.toml" ]; then
133
161
  # fails partway still leaves a non-empty file. Installing from that would
134
162
  # produce a venv missing dependencies and report success.
135
163
  if [ $COMPILED -eq 0 ] && [ -s "$TMPFILE" ]; then
136
- uv_offline_then_online pip install --python "${VENV_DIR}" ${UV_ARGS} -r "$TMPFILE"
164
+ uv_offline_then_online pip install --python "${VENV_DIR}" ${RESOLVE_ARGS} -r "$TMPFILE"
137
165
  RESULT=$?
138
166
  fi
139
167
  rm -f "$TMPFILE"
@@ -0,0 +1,43 @@
1
+ # Copyright 2026 OpenC3, Inc.
2
+ # All Rights Reserved.
3
+ #
4
+ # This program is distributed in the hope that it will be useful,
5
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
6
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
7
+ # See LICENSE.md for more details.
8
+ #
9
+ # This file may also be used under the terms of a commercial license
10
+ # if purchased from OpenC3, Inc.
11
+
12
+ # Plugins that ship with COSMOS. These are hidden by default in the Admin
13
+ # Plugins tab, skipped by plugin_list(default: false) and never written to
14
+ # the local mode plugins folder.
15
+ # This is the single source of truth: OpenC3::DefaultPlugins reads it and
16
+ # GET /openc3-api/plugins/all flags each plugin with 'default' for the
17
+ # frontend and script APIs.
18
+ - openc3-cosmos-tool-admin
19
+ - openc3-cosmos-tool-bucketexplorer
20
+ - openc3-cosmos-tool-cmdsender
21
+ - openc3-cosmos-tool-cmdqueue # Enterprise only
22
+ - openc3-cosmos-tool-cmdhistory # Enterprise only
23
+ - openc3-cosmos-tool-cmdtlmserver
24
+ - openc3-cosmos-tool-dataextractor
25
+ - openc3-cosmos-tool-dataviewer
26
+ - openc3-cosmos-tool-docs
27
+ - openc3-cosmos-tool-handbooks
28
+ - openc3-cosmos-tool-iframe
29
+ - openc3-cosmos-tool-limitsmonitor
30
+ - openc3-cosmos-tool-packetviewer
31
+ - openc3-cosmos-tool-scriptrunner
32
+ - openc3-cosmos-tool-tablemanager
33
+ - openc3-cosmos-tool-tlmgrapher
34
+ - openc3-cosmos-tool-tlmviewer
35
+ - openc3-cosmos-enterprise-tool-admin # Enterprise only
36
+ - openc3-cosmos-tool-autonomic # Enterprise only
37
+ - openc3-cosmos-tool-calendar # Enterprise only
38
+ - openc3-cosmos-tool-grafana # Enterprise only
39
+ - openc3-cosmos-tool-logexplorer # Enterprise only
40
+ - openc3-cosmos-tool-notebooks # Enterprise only
41
+ - openc3-cosmos-tool-systemhealth # Enterprise only
42
+ - openc3-enterprise-tool-base # Enterprise only
43
+ - openc3-tool-base
@@ -76,6 +76,10 @@ DISABLE_DISCONNECT:
76
76
  Use this keyword to prevent the user from disconnecting from the interface.
77
77
  This is typically used in a 'production' environment where you would not want
78
78
  the user to inadvertently disconnect from a target.
79
+ DONT_LOG:
80
+ summary: Deprecated keyword that has no effect
81
+ description: Accepted for backwards compatibility with older plugins. A warning is logged and the keyword is otherwise ignored.
82
+ deprecated: DONT_LOG does nothing and can be removed
79
83
  LOG_STREAM:
80
84
  summary: Log all data on the interface exactly as it is sent and received
81
85
  description:
@@ -154,15 +158,15 @@ OPTION:
154
158
  connections on (default 0.0.0.0).
155
159
  values: .*
156
160
  - name: Parameters
157
- required: false
158
- description: Parameters to pass to the option
161
+ required: true
162
+ description: One or more parameters to pass to the option
159
163
  values: .*
160
164
  example: |
161
165
  INTERFACE SERIAL_INT serial_interface.rb COM1 COM1 115200 NONE 1 10.0 nil
162
166
  OPTION FLOW_CONTROL RTSCTS
163
167
  OPTION DATA_BITS 8
164
168
  ROUTER SERIAL_ROUTER tcpip_server_interface.rb 2950 2950 10.0 nil BURST
165
- ROUTE SERIAL_INT
169
+ MAP_TARGET DATA
166
170
  OPTION LISTEN_ADDRESS 127.0.0.1
167
171
  SECRET:
168
172
  summary: Define a secret needed by this interface
@@ -261,7 +265,7 @@ CONTAINER:
261
265
  since: 5.7.0
262
266
  parameters:
263
267
  - name: Args
264
- required: false
268
+ required: true
265
269
  description: Name of the container
266
270
  values: .+
267
271
  ROUTE_PREFIX:
@@ -281,11 +285,22 @@ SHARD:
281
285
  since: 6.0.0
282
286
  parameters:
283
287
  - name: Shard
284
- required: false
288
+ required: true
285
289
  description: Shard number starting from 0
286
290
  values: \d+
287
291
  example: |
288
292
  SHARD 0
293
+ DB_SHARD:
294
+ summary: Database shard for this interface's command and directive streams
295
+ description: DB Shard. Only used if running multiple database shards typically in Kubernetes
296
+ since: 7.1.0
297
+ parameters:
298
+ - name: DB Shard
299
+ required: true
300
+ description: DB Shard number starting from 0
301
+ values: \d+
302
+ example: |
303
+ DB_SHARD 0
289
304
  BRIDGE:
290
305
  summary: Name of the bridge to run this interface on
291
306
  description: Run this interface on a bridge host rather than within COSMOS. Note only python is supported.
@@ -338,8 +353,8 @@ BRIDGE_OPTION:
338
353
  connections on (default 0.0.0.0).
339
354
  values: .*
340
355
  - name: Parameters
341
- required: false
342
- description: Parameters to pass to the option
356
+ required: true
357
+ description: One or more parameters to pass to the option
343
358
  values: .*
344
359
  example: |
345
360
  INTERFACE SERIAL_INT serial_interface.rb COM1 COM1 115200 NONE 1 10.0 nil
@@ -123,6 +123,12 @@ LIMITS:
123
123
  define additional named sets (e.g. TVAC), the DEFAULT set must be defined first.
124
124
  Attempting to define a named set before DEFAULT will raise an error of the form
125
125
  "DEFAULT limits set must be defined for TARGET PACKET ITEM before setting limits set NAME".
126
+
127
+ A value exactly equal to a limit is placed in the outer (more severe) state.
128
+ For example, with LIMITS DEFAULT 1 ENABLED 1 3 7 9 4 6 an integer item reports
129
+ RED_LOW at 1 and below, YELLOW_LOW at 2-3, GREEN_LOW at 4, BLUE at 5,
130
+ GREEN_HIGH at 6, YELLOW_HIGH at 7-8 and RED_HIGH at 9 and above. Without
131
+ the green limits, 4-6 would all be GREEN.
126
132
  example: |
127
133
  LIMITS DEFAULT 3 ENABLED -80.0 -70.0 60.0 80.0 -20.0 20.0
128
134
  LIMITS TVAC 3 ENABLED -80.0 -30.0 30.0 80.0
@@ -169,16 +175,20 @@ LIMITS:
169
175
  description: Setting the Green Low and Green High limits defines an
170
176
  "operational limit" which is colored blue by OpenC3. This allows for a
171
177
  distinct desired operational range which is narrower than the green safety limit.
172
- If the telemetry value is greater than or equal to this value, but less
173
- than the Green High Limit, a Blue operational condition will be detected.
178
+ If the telemetry value is greater than this value, but less than the
179
+ Green High Limit, a Blue operational condition will be detected. If the
180
+ telemetry value is less than or equal to this value, but greater than the
181
+ Yellow Low Limit, a Green Low condition will be detected.
174
182
  values: .+
175
183
  - name: Green High Limit
176
184
  required: false
177
185
  description: Setting the Green Low and Green High limits defines an
178
186
  "operational limit" which is colored blue by OpenC3. This allows for a
179
187
  distinct desired operational range which is narrower than the green safety limit.
180
- If the telemetry value is less than or equal to this value, but greater
181
- than the Green Low Limit, a Blue operational condition will be detected.
188
+ If the telemetry value is less than this value, but greater than the
189
+ Green Low Limit, a Blue operational condition will be detected. If the
190
+ telemetry value is greater than or equal to this value, but less than the
191
+ Yellow High Limit, a Green High condition will be detected.
182
192
  values: .+
183
193
  LIMITS_RESPONSE:
184
194
  summary: Defines a response class that is called when the limits state of the current item changes
@@ -79,7 +79,7 @@ INTERFACE:
79
79
  documentation for more details.
80
80
  ROUTER:
81
81
  modifiers:
82
- <%= MetaConfigParser.load('interface_modifiers.yaml').to_meta_config_yaml(4) %>
82
+ <%= MetaConfigParser.load('interface_modifiers.yaml').reject { |keyword, _| keyword.start_with?('BRIDGE') }.to_meta_config_yaml(4) %>
83
83
  summary: Create router to receive commands and output telemetry packets from one or more interfaces
84
84
  description: Creates an router which receives command packets from
85
85
  their remote clients and sends them to associated interfaces. They receive telemetry
@@ -1230,7 +1230,7 @@ Telemetry Widgets:
1230
1230
  parameters:
1231
1231
  - name: Icon name
1232
1232
  required: true
1233
- description: The astro UX icon to display. Valid choices are 'astro' icons taken from https://github.com/RocketCommunicationsInc/astro-components/blob/master/static/json/rux-icons.json.
1233
+ description: The astro UX icon to display. Valid choices are 'astro' icons taken from https://www.astrouxds.com/components/icon-library/.
1234
1234
  values: .+
1235
1235
  - name: Icon label
1236
1236
  required: false
@@ -211,18 +211,14 @@ module OpenC3
211
211
  # @param target_name [String] Name of the target
212
212
  # @return [Array<String>] Array of all command packet names
213
213
  def get_all_cmd_names(target_name, hidden: false, manual: false, scope: $openc3_scope, token: $openc3_token)
214
+ target_name = target_name.upcase
215
+ authorize(permission: 'cmd_info', target_name: target_name, scope: scope, token: token)
216
+ # Only the names are needed so avoid parsing every packet definition
214
217
  begin
215
- packets = get_all_cmds(target_name, scope: scope, token: token)
218
+ names = TargetModel.packet_names(target_name, type: :CMD, scope: scope)
219
+ names -= TargetModel.hidden_packet_names(target_name, type: :CMD, scope: scope) unless hidden
216
220
  rescue RuntimeError
217
- packets = []
218
- end
219
- names = []
220
- packets.each do |packet|
221
- if hidden
222
- names << packet['packet_name']
223
- else
224
- names << packet['packet_name'] unless packet['hidden']
225
- end
221
+ names = []
226
222
  end
227
223
  return names
228
224
  end
@@ -364,13 +360,14 @@ module OpenC3
364
360
  time = 0
365
361
  command_name = nil
366
362
  targets.each do |cur_target|
367
- TargetModel.packets(cur_target, type: :CMD, scope: scope).each do |packet|
368
- cur_time = CommandDecomTopic.get_cmd_item(cur_target, packet["packet_name"], 'RECEIVED_TIMESECONDS', type: :CONVERTED, scope: scope)
363
+ # Only the names are needed so avoid parsing every packet definition
364
+ TargetModel.packet_names(cur_target, type: :CMD, scope: scope).each do |packet_name|
365
+ cur_time = CommandDecomTopic.get_cmd_item(cur_target, packet_name, 'RECEIVED_TIMESECONDS', type: :CONVERTED, scope: scope)
369
366
  next unless cur_time
370
367
 
371
368
  if cur_time > time
372
369
  time = cur_time
373
- command_name = packet["packet_name"]
370
+ command_name = packet_name
374
371
  target_name = cur_target
375
372
  end
376
373
  end
@@ -48,8 +48,7 @@ module OpenC3
48
48
  authorize(permission: 'system', interface_name: interface_name, manual: manual, scope: scope, token: token)
49
49
  interface = InterfaceModel.get(name: interface_name, scope: scope)
50
50
  raise "Interface '#{interface_name}' does not exist (get_interface)" unless interface
51
-
52
- interface.merge(InterfaceStatusModel.get(name: interface_name, scope: scope))
51
+ interface.merge(InterfaceStatusModel.get_or_default(name: interface_name, scope: scope))
53
52
  end
54
53
 
55
54
  # @return [Array<String>] All the interface names
@@ -457,7 +457,7 @@ module OpenC3
457
457
  # @return Hash The requested item based on the packet name
458
458
  def _get_item(target_name, packet_name, item_name, cache_timeout: nil, scope:)
459
459
  # Determine if this item exists, it will raise appropriate errors if not
460
- packet_name = CvtModel.determine_latest_packet_for_item(target_name, item_name, cache_timeout: cache_timeout, scope: $openc3_scope) if packet_name == 'LATEST'
460
+ packet_name = CvtModel.determine_latest_packet_for_item(target_name, item_name, cache_timeout: cache_timeout, scope: scope) if packet_name == 'LATEST'
461
461
  return TargetModel.packet_item(target_name, packet_name, item_name, scope: scope)
462
462
  end
463
463
  end
@@ -14,6 +14,7 @@
14
14
  require 'openc3/models/metric_model'
15
15
  require 'openc3/utilities/authentication'
16
16
  require 'openc3/utilities/store'
17
+ require 'openc3/utilities/topic_lag_monitor'
17
18
 
18
19
  module OpenC3
19
20
  module Api
@@ -53,8 +54,12 @@ module OpenC3
53
54
  SUM_METRICS['tsdb_ingest_total'] = 0
54
55
  SUM_METRICS['tsdb_ingest_error_total'] = 0
55
56
 
56
- def get_metrics(manual: false, scope: $openc3_scope, token: $openc3_token)
57
+ # Metrics shown per service in Data Flows
58
+ FLOW_METRIC_SUFFIXES = %w(_topic_delta_seconds _duration_seconds _read_queue_bytes _total).freeze
59
+
60
+ def get_metrics(detailed: false, manual: false, scope: $openc3_scope, token: $openc3_token)
57
61
  authorize(permission: 'system', manual: manual, scope: scope, token: token)
62
+ return _flow_metrics(scope) if detailed
58
63
 
59
64
  sum_metrics = SUM_METRICS.dup
60
65
  duration_metrics = DURATION_METRICS.dup
@@ -93,5 +98,25 @@ module OpenC3
93
98
 
94
99
  return result
95
100
  end
101
+
102
+ # Per service metrics for Data Flows. Reads only what each microservice
103
+ # reported itself so updated_at shows when that service last published;
104
+ # the operator's process metrics would otherwise keep it fresh while the
105
+ # process is alive. Sends just the values Data Flows displays, plus the
106
+ # lag thresholds so its indicators match the falling behind notifications.
107
+ def _flow_metrics(scope)
108
+ services = {}
109
+ MetricModel.all_self_reported(scope: scope).each do |name, entry|
110
+ next unless entry and entry['values']
111
+ values = {}
112
+ entry['values'].each do |metric_name, data|
113
+ next unless data.is_a?(Hash) and metric_name.end_with?(*FLOW_METRIC_SUFFIXES)
114
+ values[metric_name] = { 'value' => data['value'] }
115
+ end
116
+ next if values.empty?
117
+ services[name] = { 'updated_at' => entry['updated_at'], 'values' => values }
118
+ end
119
+ { 'lag_thresholds' => TopicLagMonitor.thresholds, 'services' => services }
120
+ end
96
121
  end
97
122
  end
@@ -47,8 +47,7 @@ module OpenC3
47
47
  authorize(permission: 'system', router_name: router_name, manual: manual, scope: scope, token: token)
48
48
  router = RouterModel.get(name: router_name, scope: scope)
49
49
  raise "Router '#{router_name}' does not exist (get_router)" unless router
50
-
51
- router.merge(RouterStatusModel.get(name: router_name, scope: scope))
50
+ router.merge(RouterStatusModel.get_or_default(name: router_name, scope: scope))
52
51
  end
53
52
 
54
53
  # @return [Array<String>] All the router names
@@ -373,18 +373,14 @@ module OpenC3
373
373
  # @param target_name [String] Name of the target
374
374
  # @return [Array<String>] Array of all telemetry packet names
375
375
  def get_all_tlm_names(target_name, hidden: false, manual: false, scope: $openc3_scope, token: $openc3_token)
376
+ target_name = target_name.upcase
377
+ authorize(permission: 'tlm', target_name: target_name, manual: manual, scope: scope, token: token)
378
+ # Only the names are needed so avoid parsing every packet definition
376
379
  begin
377
- packets = get_all_tlm(target_name, manual: manual, scope: scope, token: token)
380
+ names = TargetModel.packet_names(target_name, type: :TLM, scope: scope)
381
+ names -= TargetModel.hidden_packet_names(target_name, type: :TLM, scope: scope) unless hidden
378
382
  rescue RuntimeError
379
- packets = []
380
- end
381
- names = []
382
- packets.each do |packet|
383
- if hidden
384
- names << packet['packet_name']
385
- else
386
- names << packet['packet_name'] unless packet['hidden']
387
- end
383
+ names = []
388
384
  end
389
385
  return names
390
386
  end
@@ -319,8 +319,10 @@ module OpenC3
319
319
  # primitive. All other values are simply returned.
320
320
  #
321
321
  # @param value [Object]
322
+ # @param default [Object] Returned if value is nil
322
323
  # @return [true|false|Object]
323
- def self.handle_true_false(value)
324
+ def self.handle_true_false(value, default: nil)
325
+ return default if value.nil?
324
326
  if String === value
325
327
  case value.upcase
326
328
  when 'TRUE'
@@ -15,4 +15,6 @@ require 'tempfile'
15
15
 
16
16
  class Tempfile
17
17
  attr_accessor :filename
18
+ # Set by open_bucket_dialog to identify where the file came from
19
+ attr_accessor :bucket, :bucket_path
18
20
  end
@@ -76,6 +76,9 @@ module OpenC3
76
76
 
77
77
  def connect
78
78
  super() # Reset the protocols
79
+ # Discard a disconnect marker left over from a previous connection. Entries only
80
+ # wake read_interface, which finds files by listing the folder, so none are lost.
81
+ @queue.clear
79
82
 
80
83
  if @telemetry_read_folder
81
84
  @listener = Listen.to(@telemetry_read_folder, force_polling: @polling) do |modified, added, removed|
@@ -96,7 +99,9 @@ module OpenC3
96
99
  @sleeper.cancel if @sleeper
97
100
  @listener.stop if @listener
98
101
  @listener = nil
99
- @queue << nil
102
+ # Only wake read_interface for the first disconnect of a connection, otherwise the
103
+ # unconsumed marker would immediately disconnect the next connection
104
+ @queue << nil if @connected
100
105
  super()
101
106
  @connected = false
102
107
  end
@@ -51,6 +51,10 @@ module OpenC3
51
51
 
52
52
  # Connects the interface to its target(s)
53
53
  def connect
54
+ # Discard anything still queued from a previous connection, such as a response to a
55
+ # request that completed after disconnect. A request still in flight across the
56
+ # reconnect can queue its response after this; that is a real response and is read normally.
57
+ @response_queue.clear
54
58
  # Per https://github.com/lostisland/faraday/blob/main/lib/faraday/options/env.rb
55
59
  # :timeout - time limit for the entire request (Integer in seconds)
56
60
  # :open_timeout - time limit for just the connection phase (e.g. handshake) (Integer in seconds)
@@ -79,15 +83,19 @@ module OpenC3
79
83
 
80
84
  # Disconnects the interface from its target(s)
81
85
  def disconnect
86
+ was_connected = !@http.nil?
82
87
  @http.close if @http
83
88
  @http = nil
84
- # Clear the response queue
85
- while @response_queue.length > 0
86
- @response_queue.pop
87
- end
88
89
  super()
89
- # Push nil to cause read_interface to return nil
90
- @response_queue.push(nil)
90
+ # disconnect is called more than once per connection loss (e.g. again after
91
+ # read_interface returns nil), so only the first call queues the marker. Another
92
+ # marker would be left unconsumed and immediately disconnect the next connection,
93
+ # and clearing again could discard a marker the reader hasn't consumed yet.
94
+ if was_connected
95
+ @response_queue.clear
96
+ # Push nil to cause read_interface to return nil
97
+ @response_queue.push(nil)
98
+ end
91
99
  end
92
100
 
93
101
  # Called to convert a packet into a data buffer. Write protocols then
@@ -70,6 +70,9 @@ module OpenC3
70
70
  # this interface
71
71
  attr_accessor :disable_disconnect
72
72
 
73
+ # @return [Boolean] Flag indicating if the interface is allowed to read
74
+ attr_writer :read_allowed
75
+
73
76
  # @return [StreamLogPair] StreamLogPair instance or nil
74
77
  attr_accessor :stream_log_pair
75
78
 
@@ -326,6 +329,12 @@ module OpenC3
326
329
  # Return packet
327
330
  @read_count += 1
328
331
  Logger.warn("#{@name}: Interface unexpectedly requested disconnect") unless packet
332
+ # Timestamp buffered reads with when the data arrived rather than when
333
+ # it was processed (otherwise received_time is set when it is handled)
334
+ if packet and !packet.received_time
335
+ data_time = read_queue_data_time
336
+ packet.received_time = data_time if data_time
337
+ end
329
338
  return packet
330
339
  end
331
340
  rescue Exception => e
@@ -349,7 +358,8 @@ module OpenC3
349
358
  if packet == :DISCONNECT
350
359
  Logger.info("#{@name}: Protocol #{protocol.class} write_packet requested disconnect")
351
360
  disconnect()
352
- return
361
+ # Nothing was written so reject rather than report success
362
+ raise WriteRejectError.new("Protocol #{protocol.class} write_packet requested disconnect")
353
363
  end
354
364
  return if packet == :STOP
355
365
  end
@@ -369,7 +379,8 @@ module OpenC3
369
379
  if data == :DISCONNECT
370
380
  Logger.info("#{@name}: Protocol #{protocol.class} write_data requested disconnect")
371
381
  disconnect()
372
- return
382
+ # Nothing was written so reject rather than report success
383
+ raise WriteRejectError.new("Protocol #{protocol.class} write_data requested disconnect")
373
384
  end
374
385
  return if data == :STOP
375
386
  end
@@ -443,6 +454,19 @@ module OpenC3
443
454
  config
444
455
  end
445
456
 
457
+ # @return [Integer] The number of bytes waiting on the read queue.
458
+ # Interfaces which buffer raw reads override this (see ReadQueueInterface).
459
+ def read_queue_bytes
460
+ 0
461
+ end
462
+
463
+ # @return [Time, nil] When the data most recently returned by
464
+ # read_interface was read from the underlying source, or nil if unknown.
465
+ # Interfaces which buffer raw reads override this (see ReadQueueInterface).
466
+ def read_queue_data_time
467
+ nil
468
+ end
469
+
446
470
  # @return [Boolean] Whether reading is allowed
447
471
  def read_allowed?
448
472
  @read_allowed
@@ -564,7 +588,7 @@ module OpenC3
564
588
  # @return [String] Raw packet data
565
589
  def read_interface_base(data, _extra = nil)
566
590
  if @save_raw_data
567
- @read_raw_data_time = Time.now
591
+ @read_raw_data_time = read_queue_data_time || Time.now
568
592
  @read_raw_data = data.clone
569
593
  end
570
594
  @bytes_read += data.length