diff --git a/docs.openc3.com/docs/development/host-install.md b/docs.openc3.com/docs/development/host-install.md deleted file mode 100644 index 282cbe4e1..000000000 --- a/docs.openc3.com/docs/development/host-install.md +++ /dev/null @@ -1,49 +0,0 @@ ---- -title: Host Install -sidebar_custom_props: - myEmoji: 🖥️ ---- - -## Installing COSMOS Directly onto a Host (No Containers) - -Note: THIS IS NOT A RECOMMENDED CONFIGURATION. - -COSMOS 5 is released as containers and intended to be run as containers. However, for various reasons someone might want to run COSMOS directly on a host. These instructions will walk through getting COSMOS 5 installed and running directly on RHEL 7 or Centos 7. This configuration will create a working install, but falls short of the ideal in that it does not setup the COSMOS processes as proper services on the host OS (that restart themselves on boot, and maintain themselves running in case of errors). Contributions that add that functionality are welcome. - -Let's get started. - -1. The starting assumption is that you have a fresh install of either RHEL 7 or Centos 7. You are running as a normal user that has sudo permissions, and has git installed. - -2. Start by downloading the latest working version of COSMOS from Github - - ```bash - cd ~ - git clone https://github.com/openc3/cosmos.git - ``` - -3. Run the COSMOS installation script - - If you are feeling brave, you can run the one large installer script that installs everything in one step: - - ```bash - cd cosmos/examples/hostinstall/centos7 - ./openc3_install.sh - ``` - - Or, you may want to break it down to the same steps that are in that script, and make sure each individual step is successful: - - ```bash - cd cosmos/examples/hostinstall/centos7 - ./openc3_install_packages.sh - ./openc3_install_ruby.sh - ./openc3_install_redis.sh - ./openc3_install_minio.sh - ./openc3_install_traefik.sh - ./openc3_install_openc3.sh - ./openc3_start_services.sh - ./openc3_first_init.sh - ``` - -4. If all was successful, you should be able to open Firefox, and goto: http://localhost:2900. Congrats you have COSMOS running directly on a host. - -5. As stated at the beginning, this is not currently a supported configuration. Contributions that help to improve it are welcome. diff --git a/docs.openc3.com/docusaurus-plugin.config.js b/docs.openc3.com/docusaurus-plugin.config.js index e6b5a6d4c..3611ddcd2 100644 --- a/docs.openc3.com/docusaurus-plugin.config.js +++ b/docs.openc3.com/docusaurus-plugin.config.js @@ -2,109 +2,137 @@ // Note: type annotations allow type checking and IDEs autocompletion // Workaround to always use SHA256 to support FIPS mode -const crypto = require('crypto') -const crypto_orig_createHash = crypto.createHash -crypto.createHash = algorithm => - crypto_orig_createHash('sha256') +const crypto = require("crypto"); +const crypto_orig_createHash = crypto.createHash; +crypto.createHash = (algorithm) => crypto_orig_createHash("sha256"); -const {themes} = require('prism-react-renderer'); +const { themes } = require("prism-react-renderer"); const lightCodeTheme = themes.nightOwlLight; const darkCodeTheme = themes.nightOwl; /** @type {import('@docusaurus/types').Config} */ const config = { - title: 'OpenC3 Docs', - tagline: 'OpenC3 COSMOS Documentation', - favicon: 'img/favicon.png', + title: "OpenC3 Docs", + tagline: "OpenC3 COSMOS Documentation", + favicon: "img/favicon.png", // Set the production url of your site here - url: 'https://docs.openc3.com', + url: "https://docs.openc3.com", // Set the // pathname under which your site is served // For GitHub pages deployment, it is often '//' - baseUrl: '/tools/staticdocs/', + baseUrl: "/tools/staticdocs/", trailingSlash: false, // GitHub pages deployment config. // If you aren't using GitHub pages, you don't need these. - organizationName: 'OpenC3', // Usually your GitHub org/user name. - projectName: 'cosmos', // Usually your repo name. + organizationName: "OpenC3", // Usually your GitHub org/user name. + projectName: "cosmos", // Usually your repo name. - onBrokenLinks: 'throw', - onBrokenMarkdownLinks: 'throw', + onBrokenLinks: "throw", + onBrokenMarkdownLinks: "throw", // Even if you don't use internalization, you can use this field to set useful // metadata like html lang. For example, if your site is Chinese, you may want // to replace "en" with "zh-Hans". i18n: { - defaultLocale: 'en', - locales: ['en'], + defaultLocale: "en", + locales: ["en"], }, plugins: [ [ - '@docusaurus/plugin-client-redirects', + "@docusaurus/plugin-client-redirects", { redirects: [ - { to: '/docs/', from: '/docs/v5/', }, - { to: '/docs/tools', from: '/docs/v5/tools', }, - { to: '/docs/getting-started/installation', from: '/docs/v5/installation', }, - { to: '/docs/getting-started/gettingstarted', from: '/docs/v5/gettingstarted', }, - { to: '/docs/getting-started/upgrading', from: '/docs/v5/upgrading', }, - { to: '/docs/getting-started/requirements', from: '/docs/v5/requirements', }, - { to: '/docs/getting-started/podman', from: '/docs/v5/podman', }, - { to: '/docs/configuration/format', from: '/docs/v5/format', }, - { to: '/docs/configuration/plugins', from: '/docs/v5/plugins', }, - { to: '/docs/configuration/target', from: '/docs/v5/target', }, - { to: '/docs/configuration/command', from: '/docs/v5/command', }, - { to: '/docs/configuration/telemetry', from: '/docs/v5/telemetry', }, - { to: '/docs/configuration/interfaces', from: '/docs/v5/interfaces', }, - { to: '/docs/configuration/protocols', from: '/docs/v5/protocols', }, - { to: '/docs/configuration/table', from: '/docs/v5/table', }, - { to: '/docs/configuration/telemetry-screens', from: '/docs/v5/telemetry-screens', }, - { to: '/docs/configuration/ssl-tls', from: '/docs/v5/ssl-tls', }, - { to: '/docs/tools/cmd-tlm-server', from: '/docs/v5/cmd-tlm-server', }, - { to: '/docs/tools/limits-monitor', from: '/docs/v5/limits-monitor', }, - { to: '/docs/tools/cmd-sender', from: '/docs/v5/cmd-sender', }, - { to: '/docs/tools/script-runner', from: '/docs/v5/script-runner', }, - { to: '/docs/tools/packet-viewer', from: '/docs/v5/packet-viewer', }, - { to: '/docs/tools/tlm-viewer', from: '/docs/v5/tlm-viewer', }, - { to: '/docs/tools/tlm-grapher', from: '/docs/v5/tlm-grapher', }, - { to: '/docs/tools/data-extractor', from: '/docs/v5/data-extractor', }, - { to: '/docs/tools/data-viewer', from: '/docs/v5/data-viewer', }, - { to: '/docs/tools/bucket-explorer', from: '/docs/v5/bucket-explorer', }, - { to: '/docs/tools/table-manager', from: '/docs/v5/table-manager', }, - { to: '/docs/tools/handbooks', from: '/docs/v5/handbooks', }, - { to: '/docs/tools/calendar', from: '/docs/v5/calendar', }, - { to: '/docs/guides/bridges', from: '/docs/v5/bridges', }, - { to: '/docs/guides/cfs', from: '/docs/v5/cfs', }, - { to: '/docs/guides/custom-widgets', from: '/docs/v5/custom-widgets', }, - { to: '/docs/guides/little-endian-bitfields', from: '/docs/v5/little-endian-bitfields', }, - { to: '/docs/guides/local-mode', from: '/docs/v5/local-mode', }, - { to: '/docs/guides/logging', from: '/docs/v5/logging', }, - { to: '/docs/guides/monitoring', from: '/docs/v5/monitoring', }, - { to: '/docs/guides/performance', from: '/docs/v5/performance', }, - { to: '/docs/guides/raspberrypi', from: '/docs/v5/raspberrypi', }, - { to: '/docs/guides/scripting-api', from: '/docs/v5/scripting-api', }, - { to: '/docs/guides/script-writing', from: '/docs/v5/script-writing', }, - { to: '/docs/development/roadmap', from: '/docs/v5/roadmap', }, - { to: '/docs/development/developing', from: '/docs/v5/development', }, - { to: '/docs/development/testing', from: '/docs/v5/testing', }, - { to: '/docs/development/json-api', from: '/docs/v5/json-api', }, - { to: '/docs/development/streaming-api', from: '/docs/v5/streaming-api', }, - { to: '/docs/development/log-structure', from: '/docs/v5/log-structure', }, - { to: '/docs/development/host-install', from: '/docs/v5/host-install', }, - { to: '/docs/meta/contributing', from: '/docs/v5/contributing', }, - { to: '/docs/meta/philosophy', from: '/docs/v5/philosophy', }, - { to: '/docs/meta/xtce', from: '/docs/v5/xtce', }, + { to: "/docs/", from: "/docs/v5/" }, + { to: "/docs/tools", from: "/docs/v5/tools" }, + { + to: "/docs/getting-started/installation", + from: "/docs/v5/installation", + }, + { + to: "/docs/getting-started/gettingstarted", + from: "/docs/v5/gettingstarted", + }, + { to: "/docs/getting-started/upgrading", from: "/docs/v5/upgrading" }, + { + to: "/docs/getting-started/requirements", + from: "/docs/v5/requirements", + }, + { to: "/docs/getting-started/podman", from: "/docs/v5/podman" }, + { to: "/docs/configuration/format", from: "/docs/v5/format" }, + { to: "/docs/configuration/plugins", from: "/docs/v5/plugins" }, + { to: "/docs/configuration/target", from: "/docs/v5/target" }, + { to: "/docs/configuration/command", from: "/docs/v5/command" }, + { to: "/docs/configuration/telemetry", from: "/docs/v5/telemetry" }, + { to: "/docs/configuration/interfaces", from: "/docs/v5/interfaces" }, + { to: "/docs/configuration/protocols", from: "/docs/v5/protocols" }, + { to: "/docs/configuration/table", from: "/docs/v5/table" }, + { + to: "/docs/configuration/telemetry-screens", + from: "/docs/v5/telemetry-screens", + }, + { to: "/docs/configuration/ssl-tls", from: "/docs/v5/ssl-tls" }, + { to: "/docs/tools/cmd-tlm-server", from: "/docs/v5/cmd-tlm-server" }, + { to: "/docs/tools/limits-monitor", from: "/docs/v5/limits-monitor" }, + { to: "/docs/tools/cmd-sender", from: "/docs/v5/cmd-sender" }, + { to: "/docs/tools/script-runner", from: "/docs/v5/script-runner" }, + { to: "/docs/tools/packet-viewer", from: "/docs/v5/packet-viewer" }, + { to: "/docs/tools/tlm-viewer", from: "/docs/v5/tlm-viewer" }, + { to: "/docs/tools/tlm-grapher", from: "/docs/v5/tlm-grapher" }, + { to: "/docs/tools/data-extractor", from: "/docs/v5/data-extractor" }, + { to: "/docs/tools/data-viewer", from: "/docs/v5/data-viewer" }, + { + to: "/docs/tools/bucket-explorer", + from: "/docs/v5/bucket-explorer", + }, + { to: "/docs/tools/table-manager", from: "/docs/v5/table-manager" }, + { to: "/docs/tools/handbooks", from: "/docs/v5/handbooks" }, + { to: "/docs/tools/calendar", from: "/docs/v5/calendar" }, + { to: "/docs/guides/bridges", from: "/docs/v5/bridges" }, + { to: "/docs/guides/cfs", from: "/docs/v5/cfs" }, + { + to: "/docs/guides/custom-widgets", + from: "/docs/v5/custom-widgets", + }, + { + to: "/docs/guides/little-endian-bitfields", + from: "/docs/v5/little-endian-bitfields", + }, + { to: "/docs/guides/local-mode", from: "/docs/v5/local-mode" }, + { to: "/docs/guides/logging", from: "/docs/v5/logging" }, + { to: "/docs/guides/monitoring", from: "/docs/v5/monitoring" }, + { to: "/docs/guides/performance", from: "/docs/v5/performance" }, + { to: "/docs/guides/raspberrypi", from: "/docs/v5/raspberrypi" }, + { to: "/docs/guides/scripting-api", from: "/docs/v5/scripting-api" }, + { + to: "/docs/guides/script-writing", + from: "/docs/v5/script-writing", + }, + { to: "/docs/development/roadmap", from: "/docs/v5/roadmap" }, + { to: "/docs/development/developing", from: "/docs/v5/development" }, + { to: "/docs/development/testing", from: "/docs/v5/testing" }, + { to: "/docs/development/json-api", from: "/docs/v5/json-api" }, + { + to: "/docs/development/streaming-api", + from: "/docs/v5/streaming-api", + }, + { + to: "/docs/development/log-structure", + from: "/docs/v5/log-structure", + }, + { to: "/docs/meta/contributing", from: "/docs/v5/contributing" }, + { to: "/docs/meta/philosophy", from: "/docs/v5/philosophy" }, + { to: "/docs/meta/xtce", from: "/docs/v5/xtce" }, ], }, ], - require.resolve('docusaurus-lunr-search'), + require.resolve("docusaurus-lunr-search"), ], presets: [ [ - 'classic', + "classic", /** @type {import('@docusaurus/preset-classic').Options} */ ({ docs: { @@ -112,10 +140,10 @@ const config = { // Please change this to your repo. // Remove this to remove the "edit this page" links. editUrl: - 'https://github.com/OpenC3/cosmos/tree/main/docs.openc3.com/', + "https://github.com/OpenC3/cosmos/tree/main/docs.openc3.com/", }, theme: { - customCss: require.resolve('./src/css/custom.css'), + customCss: require.resolve("./src/css/custom.css"), }, }), ], @@ -127,64 +155,64 @@ const config = { // Replace with your project's social card // image: 'img/docusaurus-social-card.jpg', navbar: { - title: 'OpenC3 Docs', + title: "OpenC3 Docs", logo: { - alt: 'OpenC3 Logo', - src: 'img/logo.svg', + alt: "OpenC3 Logo", + src: "img/logo.svg", }, items: [ { - type: 'docSidebar', - sidebarId: 'defaultSidebar', - position: 'left', - label: 'Documentation', + type: "docSidebar", + sidebarId: "defaultSidebar", + position: "left", + label: "Documentation", }, { - to: 'https://openc3.com/enterprise/', - label: 'Enterprise', + to: "https://openc3.com/enterprise/", + label: "Enterprise", }, ], }, footer: { - style: 'dark', + style: "dark", links: [ { - title: 'Homepage', + title: "Homepage", items: [ { - label: 'Home', - to: 'https://openc3.com', + label: "Home", + to: "https://openc3.com", }, ], }, { - title: 'Docs', + title: "Docs", items: [ { - label: 'Documentation', - to: '/docs', + label: "Documentation", + to: "/docs", }, ], }, { - title: 'Community', + title: "Community", items: [ { - label: 'LinkedIn', - href: 'https://www.linkedin.com/company/openc3', + label: "LinkedIn", + href: "https://www.linkedin.com/company/openc3", }, ], }, { - title: 'More', + title: "More", items: [ { - label: 'GitHub', - href: 'https://github.com/OpenC3/cosmos', + label: "GitHub", + href: "https://github.com/OpenC3/cosmos", }, { - label: 'Privacy', - to: '/docs/privacy', + label: "Privacy", + to: "/docs/privacy", }, ], }, @@ -194,10 +222,10 @@ const config = { prism: { theme: lightCodeTheme, darkTheme: darkCodeTheme, - additionalLanguages: ['ruby', 'python', 'bash', 'diff', 'json'], + additionalLanguages: ["ruby", "python", "bash", "diff", "json"], }, colorMode: { - defaultMode: 'dark', + defaultMode: "dark", disableSwitch: true, respectPrefersColorScheme: false, }, diff --git a/docs.openc3.com/docusaurus.config.js b/docs.openc3.com/docusaurus.config.js index 219a24dfa..8cff3d1b4 100644 --- a/docs.openc3.com/docusaurus.config.js +++ b/docs.openc3.com/docusaurus.config.js @@ -2,109 +2,137 @@ // Note: type annotations allow type checking and IDEs autocompletion // Workaround to always use SHA256 to support FIPS mode -const crypto = require('crypto') -const crypto_orig_createHash = crypto.createHash -crypto.createHash = algorithm => - crypto_orig_createHash('sha256') +const crypto = require("crypto"); +const crypto_orig_createHash = crypto.createHash; +crypto.createHash = (algorithm) => crypto_orig_createHash("sha256"); -const {themes} = require('prism-react-renderer'); +const { themes } = require("prism-react-renderer"); const lightCodeTheme = themes.nightOwlLight; const darkCodeTheme = themes.nightOwl; /** @type {import('@docusaurus/types').Config} */ const config = { - title: 'OpenC3 Docs', - tagline: 'OpenC3 COSMOS Documentation', - favicon: 'img/favicon.png', + title: "OpenC3 Docs", + tagline: "OpenC3 COSMOS Documentation", + favicon: "img/favicon.png", // Set the production url of your site here - url: 'https://docs.openc3.com', + url: "https://docs.openc3.com", // Set the // pathname under which your site is served // For GitHub pages deployment, it is often '//' - baseUrl: '/', + baseUrl: "/", trailingSlash: false, // GitHub pages deployment config. // If you aren't using GitHub pages, you don't need these. - organizationName: 'OpenC3', // Usually your GitHub org/user name. - projectName: 'cosmos', // Usually your repo name. + organizationName: "OpenC3", // Usually your GitHub org/user name. + projectName: "cosmos", // Usually your repo name. - onBrokenLinks: 'throw', - onBrokenMarkdownLinks: 'throw', + onBrokenLinks: "throw", + onBrokenMarkdownLinks: "throw", // Even if you don't use internalization, you can use this field to set useful // metadata like html lang. For example, if your site is Chinese, you may want // to replace "en" with "zh-Hans". i18n: { - defaultLocale: 'en', - locales: ['en'], + defaultLocale: "en", + locales: ["en"], }, plugins: [ [ - '@docusaurus/plugin-client-redirects', + "@docusaurus/plugin-client-redirects", { redirects: [ - { to: '/docs/', from: '/docs/v5/', }, - { to: '/docs/tools', from: '/docs/v5/tools', }, - { to: '/docs/getting-started/installation', from: '/docs/v5/installation', }, - { to: '/docs/getting-started/gettingstarted', from: '/docs/v5/gettingstarted', }, - { to: '/docs/getting-started/upgrading', from: '/docs/v5/upgrading', }, - { to: '/docs/getting-started/requirements', from: '/docs/v5/requirements', }, - { to: '/docs/getting-started/podman', from: '/docs/v5/podman', }, - { to: '/docs/configuration/format', from: '/docs/v5/format', }, - { to: '/docs/configuration/plugins', from: '/docs/v5/plugins', }, - { to: '/docs/configuration/target', from: '/docs/v5/target', }, - { to: '/docs/configuration/command', from: '/docs/v5/command', }, - { to: '/docs/configuration/telemetry', from: '/docs/v5/telemetry', }, - { to: '/docs/configuration/interfaces', from: '/docs/v5/interfaces', }, - { to: '/docs/configuration/protocols', from: '/docs/v5/protocols', }, - { to: '/docs/configuration/table', from: '/docs/v5/table', }, - { to: '/docs/configuration/telemetry-screens', from: '/docs/v5/telemetry-screens', }, - { to: '/docs/configuration/ssl-tls', from: '/docs/v5/ssl-tls', }, - { to: '/docs/tools/cmd-tlm-server', from: '/docs/v5/cmd-tlm-server', }, - { to: '/docs/tools/limits-monitor', from: '/docs/v5/limits-monitor', }, - { to: '/docs/tools/cmd-sender', from: '/docs/v5/cmd-sender', }, - { to: '/docs/tools/script-runner', from: '/docs/v5/script-runner', }, - { to: '/docs/tools/packet-viewer', from: '/docs/v5/packet-viewer', }, - { to: '/docs/tools/tlm-viewer', from: '/docs/v5/tlm-viewer', }, - { to: '/docs/tools/tlm-grapher', from: '/docs/v5/tlm-grapher', }, - { to: '/docs/tools/data-extractor', from: '/docs/v5/data-extractor', }, - { to: '/docs/tools/data-viewer', from: '/docs/v5/data-viewer', }, - { to: '/docs/tools/bucket-explorer', from: '/docs/v5/bucket-explorer', }, - { to: '/docs/tools/table-manager', from: '/docs/v5/table-manager', }, - { to: '/docs/tools/handbooks', from: '/docs/v5/handbooks', }, - { to: '/docs/tools/calendar', from: '/docs/v5/calendar', }, - { to: '/docs/guides/bridges', from: '/docs/v5/bridges', }, - { to: '/docs/guides/cfs', from: '/docs/v5/cfs', }, - { to: '/docs/guides/custom-widgets', from: '/docs/v5/custom-widgets', }, - { to: '/docs/guides/little-endian-bitfields', from: '/docs/v5/little-endian-bitfields', }, - { to: '/docs/guides/local-mode', from: '/docs/v5/local-mode', }, - { to: '/docs/guides/logging', from: '/docs/v5/logging', }, - { to: '/docs/guides/monitoring', from: '/docs/v5/monitoring', }, - { to: '/docs/guides/performance', from: '/docs/v5/performance', }, - { to: '/docs/guides/raspberrypi', from: '/docs/v5/raspberrypi', }, - { to: '/docs/guides/scripting-api', from: '/docs/v5/scripting-api', }, - { to: '/docs/guides/script-writing', from: '/docs/v5/script-writing', }, - { to: '/docs/development/roadmap', from: '/docs/v5/roadmap', }, - { to: '/docs/development/developing', from: '/docs/v5/development', }, - { to: '/docs/development/testing', from: '/docs/v5/testing', }, - { to: '/docs/development/json-api', from: '/docs/v5/json-api', }, - { to: '/docs/development/streaming-api', from: '/docs/v5/streaming-api', }, - { to: '/docs/development/log-structure', from: '/docs/v5/log-structure', }, - { to: '/docs/development/host-install', from: '/docs/v5/host-install', }, - { to: '/docs/meta/contributing', from: '/docs/v5/contributing', }, - { to: '/docs/meta/philosophy', from: '/docs/v5/philosophy', }, - { to: '/docs/meta/xtce', from: '/docs/v5/xtce', }, + { to: "/docs/", from: "/docs/v5/" }, + { to: "/docs/tools", from: "/docs/v5/tools" }, + { + to: "/docs/getting-started/installation", + from: "/docs/v5/installation", + }, + { + to: "/docs/getting-started/gettingstarted", + from: "/docs/v5/gettingstarted", + }, + { to: "/docs/getting-started/upgrading", from: "/docs/v5/upgrading" }, + { + to: "/docs/getting-started/requirements", + from: "/docs/v5/requirements", + }, + { to: "/docs/getting-started/podman", from: "/docs/v5/podman" }, + { to: "/docs/configuration/format", from: "/docs/v5/format" }, + { to: "/docs/configuration/plugins", from: "/docs/v5/plugins" }, + { to: "/docs/configuration/target", from: "/docs/v5/target" }, + { to: "/docs/configuration/command", from: "/docs/v5/command" }, + { to: "/docs/configuration/telemetry", from: "/docs/v5/telemetry" }, + { to: "/docs/configuration/interfaces", from: "/docs/v5/interfaces" }, + { to: "/docs/configuration/protocols", from: "/docs/v5/protocols" }, + { to: "/docs/configuration/table", from: "/docs/v5/table" }, + { + to: "/docs/configuration/telemetry-screens", + from: "/docs/v5/telemetry-screens", + }, + { to: "/docs/configuration/ssl-tls", from: "/docs/v5/ssl-tls" }, + { to: "/docs/tools/cmd-tlm-server", from: "/docs/v5/cmd-tlm-server" }, + { to: "/docs/tools/limits-monitor", from: "/docs/v5/limits-monitor" }, + { to: "/docs/tools/cmd-sender", from: "/docs/v5/cmd-sender" }, + { to: "/docs/tools/script-runner", from: "/docs/v5/script-runner" }, + { to: "/docs/tools/packet-viewer", from: "/docs/v5/packet-viewer" }, + { to: "/docs/tools/tlm-viewer", from: "/docs/v5/tlm-viewer" }, + { to: "/docs/tools/tlm-grapher", from: "/docs/v5/tlm-grapher" }, + { to: "/docs/tools/data-extractor", from: "/docs/v5/data-extractor" }, + { to: "/docs/tools/data-viewer", from: "/docs/v5/data-viewer" }, + { + to: "/docs/tools/bucket-explorer", + from: "/docs/v5/bucket-explorer", + }, + { to: "/docs/tools/table-manager", from: "/docs/v5/table-manager" }, + { to: "/docs/tools/handbooks", from: "/docs/v5/handbooks" }, + { to: "/docs/tools/calendar", from: "/docs/v5/calendar" }, + { to: "/docs/guides/bridges", from: "/docs/v5/bridges" }, + { to: "/docs/guides/cfs", from: "/docs/v5/cfs" }, + { + to: "/docs/guides/custom-widgets", + from: "/docs/v5/custom-widgets", + }, + { + to: "/docs/guides/little-endian-bitfields", + from: "/docs/v5/little-endian-bitfields", + }, + { to: "/docs/guides/local-mode", from: "/docs/v5/local-mode" }, + { to: "/docs/guides/logging", from: "/docs/v5/logging" }, + { to: "/docs/guides/monitoring", from: "/docs/v5/monitoring" }, + { to: "/docs/guides/performance", from: "/docs/v5/performance" }, + { to: "/docs/guides/raspberrypi", from: "/docs/v5/raspberrypi" }, + { to: "/docs/guides/scripting-api", from: "/docs/v5/scripting-api" }, + { + to: "/docs/guides/script-writing", + from: "/docs/v5/script-writing", + }, + { to: "/docs/development/roadmap", from: "/docs/v5/roadmap" }, + { to: "/docs/development/developing", from: "/docs/v5/development" }, + { to: "/docs/development/testing", from: "/docs/v5/testing" }, + { to: "/docs/development/json-api", from: "/docs/v5/json-api" }, + { + to: "/docs/development/streaming-api", + from: "/docs/v5/streaming-api", + }, + { + to: "/docs/development/log-structure", + from: "/docs/v5/log-structure", + }, + { to: "/docs/meta/contributing", from: "/docs/v5/contributing" }, + { to: "/docs/meta/philosophy", from: "/docs/v5/philosophy" }, + { to: "/docs/meta/xtce", from: "/docs/v5/xtce" }, ], }, ], - require.resolve('docusaurus-lunr-search'), + require.resolve("docusaurus-lunr-search"), ], presets: [ [ - 'classic', + "classic", /** @type {import('@docusaurus/preset-classic').Options} */ ({ docs: { @@ -112,10 +140,10 @@ const config = { // Please change this to your repo. // Remove this to remove the "edit this page" links. editUrl: - 'https://github.com/OpenC3/cosmos/tree/main/docs.openc3.com/', + "https://github.com/OpenC3/cosmos/tree/main/docs.openc3.com/", }, theme: { - customCss: require.resolve('./src/css/custom.css'), + customCss: require.resolve("./src/css/custom.css"), }, }), ], @@ -127,85 +155,85 @@ const config = { // Replace with your project's social card // image: 'img/docusaurus-social-card.jpg', navbar: { - title: 'OpenC3 Docs', + title: "OpenC3 Docs", logo: { - alt: 'OpenC3 Logo', - src: 'img/logo.svg', + alt: "OpenC3 Logo", + src: "img/logo.svg", }, items: [ { - to: 'https://openc3.com', - label: 'Home', + to: "https://openc3.com", + label: "Home", }, { - to: 'https://openc3.com/enterprise/', - label: 'Enterprise', + to: "https://openc3.com/enterprise/", + label: "Enterprise", }, { - type: 'docSidebar', - sidebarId: 'defaultSidebar', - position: 'left', - label: 'Documentation', + type: "docSidebar", + sidebarId: "defaultSidebar", + position: "left", + label: "Documentation", }, { - to: 'https://openc3.com/news/', - label: 'News', + to: "https://openc3.com/news/", + label: "News", }, { - to: 'https://openc3.com/contact/', - label: 'Contact', + to: "https://openc3.com/contact/", + label: "Contact", }, { - to: 'https://openc3.com/about/', - label: 'About', + to: "https://openc3.com/about/", + label: "About", }, { - to: 'https://github.com/OpenC3/cosmos', - label: 'GitHub', - position: 'right', + to: "https://github.com/OpenC3/cosmos", + label: "GitHub", + position: "right", }, ], }, footer: { - style: 'dark', + style: "dark", links: [ { - title: 'Homepage', + title: "Homepage", items: [ { - label: 'Home', - to: 'https://openc3.com', + label: "Home", + to: "https://openc3.com", }, ], }, { - title: 'Docs', + title: "Docs", items: [ { - label: 'Documentation', - to: '/docs', + label: "Documentation", + to: "/docs", }, ], }, { - title: 'Community', + title: "Community", items: [ { - label: 'LinkedIn', - href: 'https://www.linkedin.com/company/openc3', + label: "LinkedIn", + href: "https://www.linkedin.com/company/openc3", }, ], }, { - title: 'More', + title: "More", items: [ { - label: 'GitHub', - href: 'https://github.com/OpenC3/cosmos', + label: "GitHub", + href: "https://github.com/OpenC3/cosmos", }, { - label: 'Privacy', - to: '/docs/privacy', + label: "Privacy", + to: "/docs/privacy", }, ], }, @@ -215,10 +243,10 @@ const config = { prism: { theme: lightCodeTheme, darkTheme: darkCodeTheme, - additionalLanguages: ['ruby', 'python', 'bash', 'diff', 'json'], + additionalLanguages: ["ruby", "python", "bash", "diff", "json"], }, colorMode: { - defaultMode: 'dark', + defaultMode: "dark", disableSwitch: false, respectPrefersColorScheme: false, }, diff --git a/examples/hostinstall/centos7/.gemrc b/examples/hostinstall/centos7/.gemrc deleted file mode 100644 index 8d4bdf108..000000000 --- a/examples/hostinstall/centos7/.gemrc +++ /dev/null @@ -1,15 +0,0 @@ ---- -gem: --no-ri --no-rdoc --no-document --suggestions -install: --no-document -update: --no-document - -:benchmark: false - -:verbose: true - -:backtrace: true - -:update_sources: true -:sources: - # This is a variable that gets substituted by the Dockerfile - - RUBYGEMS_URL diff --git a/examples/hostinstall/centos7/Dockerfile b/examples/hostinstall/centos7/Dockerfile deleted file mode 100644 index 318182cb8..000000000 --- a/examples/hostinstall/centos7/Dockerfile +++ /dev/null @@ -1,67 +0,0 @@ -# WARNING: This Dockerfile is used as an easy way to develop running OPENC3 directly on a host -# To install on your host, use the openc3_install.sh script instead -# docker build -t openc3_centos7 . -# docker run -it --rm --name openc3_centos7 -p 2900:2900 openc3_centos7 - -FROM centos:7 - -# We require a local certificate file so set that up. -# You must place a valid cacert.pem file in your OPENC3 development folder for this work -# Comment out these lines if this is not required in your environment -COPY cacert.pem /devel/cacert.pem -ENV SSL_CERT_FILE=/devel/cacert.pem -ENV CURL_CA_BUNDLE=/devel/cacert.pem -ENV REQUESTS_CA_BUNDLE=/devel/cacert.pem - -# Base packages so we can create a sudo user -RUN yum update -y && yum install -y \ - git \ - shadow-utils \ - sudo - -# Set user and group -ENV IMAGE_USER=openc3 -ENV IMAGE_GROUP=openc3 -ENV USER_ID=1000 -ENV GROUP_ID=1000 -RUN /usr/sbin/groupadd -g ${GROUP_ID} ${IMAGE_GROUP} -RUN /usr/sbin/useradd -u ${USER_ID} -g ${IMAGE_GROUP} -g wheel -s /bin/ash ${IMAGE_USER} -RUN echo "openc3 ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/openc3 - -# Switch to user -USER ${USER_ID}:${GROUP_ID} -WORKDIR /home/openc3/ - -# Now do all the work you would do on a real host - -# Act like a user who starts with pulling OPENC3 COSMOS from git -RUN git clone https://github.com/OpenC3/cosmos.git - -# Install extra needed packages -COPY ./openc3_install_packages.sh /home/openc3/cosmos/examples/hostinstall/centos7/. -RUN ./cosmos/examples/hostinstall/centos7/openc3_install_packages.sh - -# Install Ruby -COPY ./openc3_install_ruby.sh /home/openc3/cosmos/examples/hostinstall/centos7/. -RUN ./cosmos/examples/hostinstall/centos7/openc3_install_ruby.sh - -# Install Redis -COPY ./openc3_install_redis.sh /home/openc3/cosmos/examples/hostinstall/centos7/. -RUN ./cosmos/examples/hostinstall/centos7/openc3_install_redis.sh - -# Install Minio -COPY ./openc3_install_minio.sh /home/openc3/cosmos/examples/hostinstall/centos7/. -RUN ./cosmos/examples/hostinstall/centos7/openc3_install_minio.sh - -# Install Traefik -COPY ./openc3_install_traefik.sh /home/openc3/cosmos/examples/hostinstall/centos7/. -RUN ./cosmos/examples/hostinstall/centos7/openc3_install_traefik.sh - -# Install OPENC3 -COPY ./openc3_install_openc3.sh /home/openc3/cosmos/examples/hostinstall/centos7/. -RUN ./cosmos/examples/hostinstall/centos7/openc3_install_openc3.sh - -COPY ./openc3_start_services.sh /home/openc3/cosmos/examples/hostinstall/centos7/. -COPY ./openc3_first_init.sh /home/openc3/cosmos/examples/hostinstall/centos7/. -COPY ./docker_init.sh /home/openc3/cosmos/examples/hostinstall/centos7/. -CMD [ "/home/openc3/cosmos/examples/hostinstall/centos7/docker_init.sh" ] diff --git a/examples/hostinstall/centos7/docker_init.sh b/examples/hostinstall/centos7/docker_init.sh deleted file mode 100755 index 5ab60f5da..000000000 --- a/examples/hostinstall/centos7/docker_init.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/sh -set -eux - -SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) -cd $SCRIPT_DIR - -./openc3_start_services.sh -./openc3_first_init.sh diff --git a/examples/hostinstall/centos7/openc3_env.sh b/examples/hostinstall/centos7/openc3_env.sh deleted file mode 100755 index eb3ae7f9d..000000000 --- a/examples/hostinstall/centos7/openc3_env.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/bin/sh -set -eux - -export NOKOGIRI_USE_SYSTEM_LIBRARIES=1 - -export RUBYGEMS_URL=https://rubygems.org -export NPM_URL=https://registry.npmjs.org - -export SECRET_KEY_BASE=bdb4300d46c9d4f116ce3dbbd54cac6b20802d8be1c2333cf5f6f90b1627799ac5d043e8460744077bc0bd6aacdd5c4bf53f499a68303c6752e7f327b874b96a -export OPENC3_REDIS_HOSTNAME=localhost -export OPENC3_REDIS_PORT=6379 -export OPENC3_REDIS_EPHEMERAL_HOSTNAME=localhost -export OPENC3_REDIS_EPHEMERAL_PORT=6380 -export OPENC3_BUCKET_URL=http://localhost:9000 - -export OPENC3_REDIS_USERNAME=openc3 -export OPENC3_REDIS_PASSWORD=openc3password - -export OPENC3_BUCKET_USERNAME=openc3minio -export OPENC3_BUCKET_PASSWORD=openc3miniopassword - -export OPENC3_SR_REDIS_USERNAME=scriptrunner -export OPENC3_SR_REDIS_PASSWORD=scriptrunnerpassword -export OPENC3_SR_BUCKET_USERNAME=scriptrunnerminio -export OPENC3_SR_BUCKET_PASSWORD=scriptrunnerminiopassword - -export OPENC3_TAG=latest - -export OPENC3_DEMO=1 - -export RUBYLIB=/openc3/lib -export OPENC3_PATH=/openc3 diff --git a/examples/hostinstall/centos7/openc3_first_init.sh b/examples/hostinstall/centos7/openc3_first_init.sh deleted file mode 100755 index 38eaeb307..000000000 --- a/examples/hostinstall/centos7/openc3_first_init.sh +++ /dev/null @@ -1,42 +0,0 @@ -#!/bin/sh -set -eux - -SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) -cd $SCRIPT_DIR -source ./openc3_env.sh - -# Configure Minio -mc alias set openc3minio "${OPENC3_BUCKET_URL}" ${OPENC3_BUCKET_USERNAME} ${OPENC3_BUCKET_PASSWORD} || exit 1 - -# Create new canned policy by name script using script-runner.json policy file. -mc admin policy create openc3minio script $SCRIPT_DIR/../../../openc3-cosmos-init/script-runner.json || exit 1 - -# Create a new user scriptrunner on MinIO use mc admin user. -mc admin user add openc3minio ${OPENC3_SR_BUCKET_USERNAME} ${OPENC3_SR_BUCKET_PASSWORD} || exit 1 - -# Once the user is successfully created you can now apply the getonly policy for this user. -mc admin policy attach openc3minio script --user=${OPENC3_SR_BUCKET_USERNAME} || exit 1 - -# Install Plugins -mkdir -p /tmp/openc3/tmp/tmp -sudo -E --preserve-env=RUBYLIB /openc3/bin/openc3cli load $SCRIPT_DIR/../../../openc3-cosmos-init/plugins/gems/openc3-tool-base-*.gem || exit 1 -sudo -E --preserve-env=RUBYLIB /openc3/bin/openc3cli load $SCRIPT_DIR/../../../openc3-cosmos-init/plugins/gems/openc3-cosmos-tool-cmdtlmserver-*.gem || exit 1 -sudo -E --preserve-env=RUBYLIB /openc3/bin/openc3cli load $SCRIPT_DIR/../../../openc3-cosmos-init/plugins/gems/openc3-cosmos-tool-limitsmonitor-*.gem || exit 1 -sudo -E --preserve-env=RUBYLIB /openc3/bin/openc3cli load $SCRIPT_DIR/../../../openc3-cosmos-init/plugins/gems/openc3-cosmos-tool-cmdsender-*.gem || exit 1 -sudo -E --preserve-env=RUBYLIB /openc3/bin/openc3cli load $SCRIPT_DIR/../../../openc3-cosmos-init/plugins/gems/openc3-cosmos-tool-scriptrunner-*.gem || exit 1 -sudo -E --preserve-env=RUBYLIB /openc3/bin/openc3cli load $SCRIPT_DIR/../../../openc3-cosmos-init/plugins/gems/openc3-cosmos-tool-packetviewer-*.gem || exit 1 -sudo -E --preserve-env=RUBYLIB /openc3/bin/openc3cli load $SCRIPT_DIR/../../../openc3-cosmos-init/plugins/gems/openc3-cosmos-tool-tlmviewer-*.gem || exit 1 -sudo -E --preserve-env=RUBYLIB /openc3/bin/openc3cli load $SCRIPT_DIR/../../../openc3-cosmos-init/plugins/gems/openc3-cosmos-tool-tlmgrapher-*.gem || exit 1 -sudo -E --preserve-env=RUBYLIB /openc3/bin/openc3cli load $SCRIPT_DIR/../../../openc3-cosmos-init/plugins/gems/openc3-cosmos-tool-dataextractor-*.gem || exit 1 -sudo -E --preserve-env=RUBYLIB /openc3/bin/openc3cli load $SCRIPT_DIR/../../../openc3-cosmos-init/plugins/gems/openc3-cosmos-tool-dataviewer-*.gem || exit 1 -sudo -E --preserve-env=RUBYLIB /openc3/bin/openc3cli load $SCRIPT_DIR/../../../openc3-cosmos-init/plugins/gems/openc3-cosmos-tool-handbooks-*.gem || exit 1 -sudo -E --preserve-env=RUBYLIB /openc3/bin/openc3cli load $SCRIPT_DIR/../../../openc3-cosmos-init/plugins/gems/openc3-cosmos-tool-tablemanager-*.gem || exit 1 -sudo -E --preserve-env=RUBYLIB /openc3/bin/openc3cli load $SCRIPT_DIR/../../../openc3-cosmos-init/plugins/gems/openc3-cosmos-tool-admin-*.gem || exit 1 -sudo -E --preserve-env=RUBYLIB /openc3/bin/openc3cli load $SCRIPT_DIR/../../../openc3-cosmos-init/plugins/gems/openc3-cosmos-tool-bucketexplorer-*.gem || exit 1 -sudo -E --preserve-env=RUBYLIB /openc3/bin/openc3cli load $SCRIPT_DIR/../../../openc3-cosmos-init/plugins/gems/openc3-cosmos-demo-*.gem || exit 1 - -# Sleep To Keep Process Alive - Ctrl-C when done -echo "Sleep until Ctrl-C to Keep Process Alive" -sleep 1000000000 - -cd ~/ diff --git a/examples/hostinstall/centos7/openc3_install.sh b/examples/hostinstall/centos7/openc3_install.sh deleted file mode 100755 index 3e90c0075..000000000 --- a/examples/hostinstall/centos7/openc3_install.sh +++ /dev/null @@ -1,26 +0,0 @@ -# This script install OpenC3 directly on a host instead of Docker -# Note: This is not a supported configuration. Official Releases are provided as Docker Containers - -# Install extra needed packages -./openc3_install_packages.sh - -# Install Ruby -./openc3_install_ruby.sh - -# Install Redis -./openc3_install_redis.sh - -# Install Minio -./openc3_install_minio.sh - -# Install Traefik -./openc3_install_traefik.sh - -# Install OpenC3 -./openc3_install_openc3.sh - -# Start all the OpenC3 Services -./openc3_start_services.sh - -# First Time Initialization -./openc3_first_init.sh diff --git a/examples/hostinstall/centos7/openc3_install_minio.sh b/examples/hostinstall/centos7/openc3_install_minio.sh deleted file mode 100755 index 97eb2ef9d..000000000 --- a/examples/hostinstall/centos7/openc3_install_minio.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/sh -set -eux - -cd /usr/bin - -sudo wget https://dl.min.io/server/minio/release/linux-amd64/minio -sudo chmod +x minio -sudo wget https://dl.min.io/client/mc/release/linux-amd64/mc -sudo chmod +x mc diff --git a/examples/hostinstall/centos7/openc3_install_openc3.sh b/examples/hostinstall/centos7/openc3_install_openc3.sh deleted file mode 100755 index 8a9fa7303..000000000 --- a/examples/hostinstall/centos7/openc3_install_openc3.sh +++ /dev/null @@ -1,69 +0,0 @@ -#!/bin/sh -set -eux - -SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) -cd $SCRIPT_DIR -source ./openc3_env.sh - -export USER=`whoami` - -# Create gems folder for OpenC3 to install gems to -sudo mkdir /gems && sudo chown $USER:$USER /gems - -# OpenC3 Containerized apps expect the openc3 libraries to be at /openc3 -sudo cp -r $SCRIPT_DIR/../../../openc3 /openc3 - -cd /openc3 - -sudo mkdir -p lib/openc3/ext -sudo -E bundle config set --local without 'development' -sudo -E bundle install --quiet -sudo -E bundle exec rake build - -cd $SCRIPT_DIR/../../../openc3-cosmos-cmd-tlm-api - -sudo -E bundle config set --local without 'development' -sudo -E bundle install --quiet - -cd $SCRIPT_DIR/../../../openc3-cosmos-script-runner-api - -sudo -E bundle config set --local without 'development' -sudo -E bundle install --quiet - -if [ -f "/etc/centos-release" ]; then - sudo yum install epel-release -y || true -else - sudo subscription-manager repos --enable rhel-*-optional-rpms \ - --enable rhel-*-extras-rpms \ - --enable rhel-ha-for-rhel-*-server-rpms - sudo subscription-manager repos --disable=rhel-7-server-e4s-optional-rpms --disable=rhel-7-server-eus-optional-rpms - sudo yum install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm || true -fi -sudo yum install nodejs npm -y -sudo npm install --global yarn - -cd $SCRIPT_DIR/../../../openc3-cosmos-init/plugins/ - -yarn config set registry $NPM_URL -yarn - -PLUGINS="$SCRIPT_DIR/../../../openc3-cosmos-init/plugins/" -GEMS="$SCRIPT_DIR/../../../openc3-cosmos-init/plugins/gems/" -OPENC3_RELEASE_VERSION=5.21.0-beta0 - -mkdir -p ${GEMS} -cd ${PLUGINS}openc3-tool-base && yarn install && yarn run build && rake build VERSION=${OPENC3_RELEASE_VERSION} && mv *.gem ${GEMS} -cd ${PLUGINS}packages/openc3-cosmos-tool-admin && yarn run build && rake build VERSION=${OPENC3_RELEASE_VERSION} && mv *.gem ${GEMS} -cd ${PLUGINS}packages/openc3-cosmos-tool-cmdsender && yarn run build && rake build VERSION=${OPENC3_RELEASE_VERSION} && mv *.gem ${GEMS} -cd ${PLUGINS}packages/openc3-cosmos-tool-cmdtlmserver && yarn run build && rake build VERSION=${OPENC3_RELEASE_VERSION} && mv *.gem ${GEMS} -cd ${PLUGINS}packages/openc3-cosmos-tool-dataextractor && yarn run build && rake build VERSION=${OPENC3_RELEASE_VERSION} && mv *.gem ${GEMS} -cd ${PLUGINS}packages/openc3-cosmos-tool-dataviewer && yarn run build && rake build VERSION=${OPENC3_RELEASE_VERSION} && mv *.gem ${GEMS} -cd ${PLUGINS}packages/openc3-cosmos-tool-handbooks && yarn run build && rake build VERSION=${OPENC3_RELEASE_VERSION} && mv *.gem ${GEMS} -cd ${PLUGINS}packages/openc3-cosmos-tool-limitsmonitor && yarn run build && rake build VERSION=${OPENC3_RELEASE_VERSION} && mv *.gem ${GEMS} -cd ${PLUGINS}packages/openc3-cosmos-tool-packetviewer && yarn run build && rake build VERSION=${OPENC3_RELEASE_VERSION} && mv *.gem ${GEMS} -cd ${PLUGINS}packages/openc3-cosmos-tool-scriptrunner && yarn run build && rake build VERSION=${OPENC3_RELEASE_VERSION} && mv *.gem ${GEMS} -cd ${PLUGINS}packages/openc3-cosmos-tool-tablemanager && yarn run build && rake build VERSION=${OPENC3_RELEASE_VERSION} && mv *.gem ${GEMS} -cd ${PLUGINS}packages/openc3-cosmos-tool-tlmgrapher && yarn run build && rake build VERSION=${OPENC3_RELEASE_VERSION} && mv *.gem ${GEMS} -cd ${PLUGINS}packages/openc3-cosmos-tool-tlmviewer && yarn run build && rake build VERSION=${OPENC3_RELEASE_VERSION} && mv *.gem ${GEMS} -cd ${PLUGINS}packages/openc3-cosmos-tool-bucketexplorer && yarn run build && rake build VERSION=${OPENC3_RELEASE_VERSION} && mv *.gem ${GEMS} -cd ${PLUGINS}packages/openc3-cosmos-demo && yarn run build && rake build VERSION=${OPENC3_RELEASE_VERSION} && mv *.gem ${GEMS} diff --git a/examples/hostinstall/centos7/openc3_install_packages.sh b/examples/hostinstall/centos7/openc3_install_packages.sh deleted file mode 100755 index 9d8755fa9..000000000 --- a/examples/hostinstall/centos7/openc3_install_packages.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/sh -set -eux - -sudo yum update -y && sudo yum install -y \ - gcc \ - gcc-c++ \ - gdbm-devel \ - iproute \ - libyaml-devel \ - libffi-devel \ - make \ - ncurses-devel \ - net-tools \ - nc \ - openssl-devel \ - readline-devel \ - wget \ - zlib-devel diff --git a/examples/hostinstall/centos7/openc3_install_redis.sh b/examples/hostinstall/centos7/openc3_install_redis.sh deleted file mode 100755 index 076a2daac..000000000 --- a/examples/hostinstall/centos7/openc3_install_redis.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/sh -set -eux - -SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) -cd $SCRIPT_DIR - -sudo mkdir -p /config -sudo cp $SCRIPT_DIR/../../../openc3-redis/config/* /config/. - -wget -O redis.tar.gz "https://download.redis.io/releases/redis-6.2.6.tar.gz" -echo "5b2b8b7a50111ef395bf1c1d5be11e6e167ac018125055daa8b5c2317ae131ab redis.tar.gz" | sha256sum --check --strict - -sudo mkdir -p /usr/src/redis -sudo tar -xzvf redis.tar.gz -C /usr/src/redis --strip-components=1 -rm redis.tar.gz - -cd /usr/src/redis - -sudo make -sudo make install - -cd ~/ -sudo rm -r /usr/src/redis - -sudo sysctl vm.overcommit_memory=1 diff --git a/examples/hostinstall/centos7/openc3_install_ruby.sh b/examples/hostinstall/centos7/openc3_install_ruby.sh deleted file mode 100755 index a8483766f..000000000 --- a/examples/hostinstall/centos7/openc3_install_ruby.sh +++ /dev/null @@ -1,60 +0,0 @@ -#!/bin/sh -set -eux - -SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) -cd $SCRIPT_DIR -source ./openc3_env.sh - -sed -i "s|RUBYGEMS_URL|${RUBYGEMS_URL}|g" .gemrc -cp .gemrc ~/. -sudo cp .gemrc /root/. - -# Only ruby 2.0 in Centos7 so build from source - -# START MODIFIED CONTENT FROM OFFICIAL RUBY DOCKERFILE - -export LANG=C.UTF-8 -export RUBY_MAJOR=3.0 -export RUBY_VERSION=3.0.3 -export RUBY_DOWNLOAD_SHA256=88cc7f0f021f15c4cd62b1f922e3a401697f7943551fe45b1fdf4f2417a17a9c - -# some of ruby's build scripts are written in ruby -# we purge system ruby later to make sure our final image uses what we just built - -wget -O ruby.tar.xz "https://cache.ruby-lang.org/pub/ruby/${RUBY_MAJOR%-rc}/ruby-$RUBY_VERSION.tar.xz" -echo "$RUBY_DOWNLOAD_SHA256 *ruby.tar.xz" | sha256sum --check --strict; -sudo mkdir -p /usr/src/ruby -sudo tar -xJf ruby.tar.xz -C /usr/src/ruby --strip-components=1 -rm ruby.tar.xz; - -# hack in "ENABLE_PATH_CHECK" disabling to suppress: -# warning: Insecure world writable dir -{ \ - echo '#define ENABLE_PATH_CHECK 0'; \ - echo; \ - cat /usr/src/ruby/file.c; \ -} > file.c.new; -sudo mv file.c.new /usr/src/ruby/file.c - -cd /usr/src/ruby; - -sudo ./configure --disable-install-doc --enable-shared --prefix=/usr - -sudo make -j "$(nproc)" -sudo make install - -cd / -sudo rm -r /usr/src/ruby - -# rough smoke test -ruby --version -gem --version -bundle --version - -sudo gem update --system 3.3.5 -sudo gem install bundler -sudo gem install rake -sudo bundle config build.nokogiri --use-system-libraries -sudo bundle config git.allow_insecure true - -# END CONTENT FROM OFFICIAL RUBY DOCKERFILE diff --git a/examples/hostinstall/centos7/openc3_install_traefik.sh b/examples/hostinstall/centos7/openc3_install_traefik.sh deleted file mode 100755 index d4fc79381..000000000 --- a/examples/hostinstall/centos7/openc3_install_traefik.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/sh -set -eux - -SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) -cd $SCRIPT_DIR - -export TRAEFIK_DOWNLOAD_SHA256=5bf0e79b131b5f893d93c1912681deb1a49badb06218c234e43d3b0f7e3b8588 - -wget -O traefik.tar.gz "https://github.com/traefik/traefik/releases/download/v2.4.13/traefik_v2.4.13_linux_amd64.tar.gz" -echo "$TRAEFIK_DOWNLOAD_SHA256 *traefik.tar.gz" | sha256sum --check --strict - -sudo mkdir -p /opt/traefik -sudo tar -zxvf traefik.tar.gz -C /opt/traefik -rm traefik.tar.gz - -# Configure Traefik for OpenC3 -sudo mkdir -p /etc/traefik -sudo cp $SCRIPT_DIR/traefik.yaml /etc/traefik/traefik.yaml diff --git a/examples/hostinstall/centos7/openc3_start_services.sh b/examples/hostinstall/centos7/openc3_start_services.sh deleted file mode 100755 index 326562b6c..000000000 --- a/examples/hostinstall/centos7/openc3_start_services.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/bin/sh -set -x - -SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) -cd $SCRIPT_DIR -source ./openc3_env.sh - -# Start Redis -redis-server /config/redis.conf & -redis-server /config/redis_ephemeral.conf & - -# Start Minio -export MINIO_ROOT_USER=${OPENC3_BUCKET_USERNAME} -export MINIO_ROOT_PASSWORD=${OPENC3_BUCKET_PASSWORD} -mkdir -p ~/minio -minio server --console-address ":9090" ~/minio & - -# Wait for Redis and Minio to be up -echo "30 Second Delay to Allow Startup" -sleep 30 - -# Start cmd-tlm-api -cd $SCRIPT_DIR/../../../openc3-cosmos-cmd-tlm-api && rails s -b 0.0.0.0 -p 2901 & - -# Start script-runner-api -cd $SCRIPT_DIR/../../../openc3-cosmos-script-runner-api && rails s -b 0.0.0.0 -p 2902 & - -# Start openc3-operator -cd /openc3/lib/openc3/operators/ && ruby microservice_operator.rb & - -# Start openc3-traefik -cd /opt/traefik && ./traefik & - -cd ~/ diff --git a/examples/hostinstall/centos7/traefik.yaml b/examples/hostinstall/centos7/traefik.yaml deleted file mode 100644 index 7a4eea146..000000000 --- a/examples/hostinstall/centos7/traefik.yaml +++ /dev/null @@ -1,110 +0,0 @@ ---- -# Listen for everything coming in on the standard HTTP port -entrypoints: - web: - address: ":2900" -http: - middlewares: - # Removes the first part of the url before passing onto the service - # ie. /openc3-api/api becomes /api - removeFirst: - replacePathRegex: - regex: "^/([^/]*)/(.*)" - replacement: "/$2" - # Serve /tools/base/index.html - gotoToolsBaseIndex: - replacePath: - path: "/tools/base/index.html" - # Adds /tools/base to the beginning of the given url - # ie. /index.html becomes /tools/base/index.html - addToolsBase: - replacePathRegex: - regex: "^/(.*)" - replacement: "/tools/base/$1" - routers: - # Note: Priorities control router check order with highest priority evaluated first - # Route to the openc3 cmd/tlm api - api-router: - rule: PathPrefix(`/openc3-api`) - service: service-api - priority: 7 - # Route to the script api - script-router: - rule: PathPrefix(`/script-api`) - service: service-script - priority: 6 - # Route to other tool plugins hosted statically in Minio - # Matches any path with a file extension which is assumed to be - # a static file - tools-router: - rule: PathRegexp(`/tools/.*/.*[.].*`) - service: service-minio - priority: 5 - # Route to minio user interface - minio-router: - rule: PathPrefix(`/minio`) - service: service-minio - priority: 4 - # Route to any path in minio - files-router: - rule: PathPrefix(`/files`) - middlewares: - # remove /files from the beginning - - "removeFirst" - service: service-minio - priority: 3 - # Route to base files hosted statically in Minio - # Matches any path with a file extension which is assumed to be - # a static file - base-router: - rule: PathRegexp(`/.*[.].*`) - middlewares: - # add /tools/base to the beginning - - "addToolsBase" - service: service-minio - priority: 2 - # This is the default route for everything that doesn't match a more specific route - # It gets us to the base openc3 application - web-router: - rule: HostRegexp(`.*`) - middlewares: - # Serve /tools/base/index.html from minio - - "gotoToolsBaseIndex" - service: service-minio - priority: 1 - services: - # The OpenC3 cmd/tlm api service - service-api: - loadBalancer: - passHostHeader: false - servers: - - url: "http://localhost:2901" - # The OpenC3 script api service - service-script: - loadBalancer: - passHostHeader: false - servers: - - url: "http://localhost:2902" - # The Minio S3 file server - service-minio: - loadBalancer: - passHostHeader: false - servers: - - url: "http://localhost:9000" -# Declare the routes are currently coming from this file, not dynamically -providers: - file: - filename: /etc/traefik/traefik.yaml -accessLog: {} -# api: -# dashboard: true -# insecure: true -# log: -# filePath: '/etc/traefik/traefik.log' -# level: 'DEBUG' -# accessLog: -# filePath: '/etc/traefik/access.log' -# fields: -# defaultMode: keep -# headers: -# defaultMode: keep diff --git a/openc3-cosmos-init/plugins/packages/openc3-cosmos-mqtt-test/.editorconfig b/examples/openc3-cosmos-mqtt-test/.editorconfig similarity index 100% rename from openc3-cosmos-init/plugins/packages/openc3-cosmos-mqtt-test/.editorconfig rename to examples/openc3-cosmos-mqtt-test/.editorconfig diff --git a/openc3-cosmos-init/plugins/packages/openc3-cosmos-mqtt-test/.gitignore b/examples/openc3-cosmos-mqtt-test/.gitignore similarity index 100% rename from openc3-cosmos-init/plugins/packages/openc3-cosmos-mqtt-test/.gitignore rename to examples/openc3-cosmos-mqtt-test/.gitignore diff --git a/examples/openc3-cosmos-mqtt-test/README.md b/examples/openc3-cosmos-mqtt-test/README.md new file mode 100644 index 000000000..f2ecd4aa6 --- /dev/null +++ b/examples/openc3-cosmos-mqtt-test/README.md @@ -0,0 +1,59 @@ +# Installation + +First build the plugin using the COSMOS CLI: + +```bash +openc3.sh cli rake build VERSION=1.0.0 +``` + +# Usage + +The easiest way to test the MQTT interface is to interact with the server at https://test.mosquitto.org/. It has an unencrypted unauthenticated port, an unencrypted authenticated port, an encrypted unauthenticated port, and an encrypted authenticated port to test against. + +## Unencrypted Unauthenticated + +To connect to the unencrypted unauthenticated port install the plugin and change the values to the following: + +![Install Unauthenticated](./img/install_unauthenticated.png) + +The interface should connect at which point you can send whatever data you want in Command Sender: + +![Command Sender](./img/command_sender.png) + +And verify the result in Packet Viewer: + +![Packet Viewer](./img/packet_viewer.png) + +## Unencrypted Authenticated + +To use the unencrypted authenticated port you need to first install a password secret. Go to the Admin / Secrets tab and create a secret name 'PASSWORD' with value 'readwrite'. + +![Secrets Password](./img/secrets_password.png) + +Then install the plugin using the following values. Note the port is 1884 and the username is 'rw': + +![Install Authenticated](./img/install_authenticated.png) + +THe interface should connect and you can send and receive data. + +## Encrypted Unauthenticated + +To use the encrypted unauthenticated port you need to first install the ca_cert file into the COSMOS Secrets. Go to the Admin / Secrets tab and create a secret name 'CA_FILE', click the paperclip and select the mosquitto.org.crt file. + +![Secrets CA Cert](./img/secrets_ca_cert.png) + +Then install the plugin using the following values. Note the port is 8883, the username and password are cleared, and the mqtt_ca_file_secret is 'CA_FILE'. This matches the secret name we just created and will load the ca_file file into the plugin when it starts. + +![Install Encrypted](./img/install_encrypted.png) + +The interface should connect and you can send and receive data. + +## Encrypted Authenticated + +To use the encrypted authenticated port you need to install both the cert and key files into the COSMOS Secrets. Go to the Admin / Secrets tab and create a secret name 'CERT', click the paperclip and select the client.crt file. Create another secret named 'KEY', click the paperclip and select the client.key file. + +Then install the plugin using the following values. Note the port is 8884, the username and password are cleared, the mqtt_cert_secret is 'CERT', the mqtt_key_secret is 'KEY', and the mqtt_ca_file_secret is 'CA_FILE'. This matches the secrets we just created and will load the cert file, key file, and ca_file into the plugin when it starts. + +![MQTT Cert](./img/install_cert.png) + +The interface should connect and you can send and receive data. diff --git a/openc3-cosmos-init/plugins/packages/openc3-cosmos-mqtt-test/Rakefile b/examples/openc3-cosmos-mqtt-test/Rakefile similarity index 100% rename from openc3-cosmos-init/plugins/packages/openc3-cosmos-mqtt-test/Rakefile rename to examples/openc3-cosmos-mqtt-test/Rakefile diff --git a/examples/openc3-cosmos-mqtt-test/client.crt b/examples/openc3-cosmos-mqtt-test/client.crt new file mode 100644 index 000000000..deb4864a8 --- /dev/null +++ b/examples/openc3-cosmos-mqtt-test/client.crt @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDnjCCAoagAwIBAgIBADANBgkqhkiG9w0BAQsFADCBkDELMAkGA1UEBhMCR0Ix +FzAVBgNVBAgMDlVuaXRlZCBLaW5nZG9tMQ4wDAYDVQQHDAVEZXJieTESMBAGA1UE +CgwJTW9zcXVpdHRvMQswCQYDVQQLDAJDQTEWMBQGA1UEAwwNbW9zcXVpdHRvLm9y +ZzEfMB0GCSqGSIb3DQEJARYQcm9nZXJAYXRjaG9vLm9yZzAeFw0yNDExMTMxNzA4 +MDZaFw0yNTAyMTExNzA4MDZaMHgxCzAJBgNVBAYTAlVTMREwDwYDVQQIDAhDb2xv +cmFkbzENMAsGA1UEBwwERXJpZTEPMA0GA1UECgwGT3BlbkMzMRMwEQYDVQQDDApv +cGVuYzMuY29tMSEwHwYJKoZIhvcNAQkBFhJzdXBwb3J0QG9wZW5jMy5jb20wggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCn/keflJj1+3fx1JPnCvYLw4qM +Z/OfEE3JB6igHZx9k3Nx+aFhuVYxl0SVRRvqmjmy3ompKpgQlRgpevTM7PIGIzOU +JM9fOXt32rVKkYan/gwTo8loBsCArwxWoI2gdwQWIdRlNkNmnG09UbUa15PsQkqI +zkJ1hCxXJRY7R9hoonSKoMYoaa8mZR4jyjY35GgkxyWA7IoBZCDmb97GhX+YAxKL +wJKkfeT0SaMNy5hWvWMDk0j/wlsk3IJ0tb/VGRJBKErBc9zzWe94O+GeO0QaB2GZ +KwWFnzaK2YOeJBiG5FYQ8XrKhRqyDXcMZSgiH/ZHSBOKq6ABCdVtTHF6c9PvAgMB +AAGjGjAYMAkGA1UdEwQCMAAwCwYDVR0PBAQDAgXgMA0GCSqGSIb3DQEBCwUAA4IB +AQCtzhnpsDp4CME+RWgj8EpqP4WRPQUEjx+4NTT3De7xLeEWni6ymXOvfodxSOyG +cQh540zlpHYSaanRnfaEbZXeGNdp2YL3saZIkgsQxCqDQPMv0Emy5qJ7jId9EvhS +TPZQXeEKZQpl+xxyDKqYmTHtjgi6OLoVoTZPr6skBnadRExCyViUazcZL0bcWj1u +23u3z2PtuuPnMnCf0Z/luaqZq+K0KpI6Vhs16r1qEOVEqiRnzMvta6EA+tDe5XXR +hcA8tBpuDEwmOGHbuXZgqxrU9UZFzZ01YApPFX5KK97Yd6uKgfhcgKdpQLZjoKRJ +1O2fil+LkhzntjehEXZoFvjw +-----END CERTIFICATE----- diff --git a/examples/openc3-cosmos-mqtt-test/client.key b/examples/openc3-cosmos-mqtt-test/client.key new file mode 100644 index 000000000..ddb0e5c95 --- /dev/null +++ b/examples/openc3-cosmos-mqtt-test/client.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEuwIBADANBgkqhkiG9w0BAQEFAASCBKUwggShAgEAAoIBAQCn/keflJj1+3fx +1JPnCvYLw4qMZ/OfEE3JB6igHZx9k3Nx+aFhuVYxl0SVRRvqmjmy3ompKpgQlRgp +evTM7PIGIzOUJM9fOXt32rVKkYan/gwTo8loBsCArwxWoI2gdwQWIdRlNkNmnG09 +UbUa15PsQkqIzkJ1hCxXJRY7R9hoonSKoMYoaa8mZR4jyjY35GgkxyWA7IoBZCDm +b97GhX+YAxKLwJKkfeT0SaMNy5hWvWMDk0j/wlsk3IJ0tb/VGRJBKErBc9zzWe94 +O+GeO0QaB2GZKwWFnzaK2YOeJBiG5FYQ8XrKhRqyDXcMZSgiH/ZHSBOKq6ABCdVt +THF6c9PvAgMBAAECgf8Py8bLuoinc+t35AUoxqg3cvSmY7IpMHGNrGGKGmNBImVd +G1K17+49KTLuQ+y3fe2BP5Af1/O9m2vhDOWKFis9+sg6OBdHpg2/JQhUnzXTTNke +/6MCyZ+955MvJFdRY99+fmaxHZ/vqqEtNSYtu2CtVmnQTrk6LvyOotvPtyiP0o+0 +CVXVsnum+Ym8fzCvXiZ2VVmshQIw7xMjF8KYNlSSMhmW7VbOi8VeSC5zJdBTGv1W +xD8N5gfHetjBQ7MDHD8in4kfiu6258F7yKkM6jQ+Q9S7b8f4OuneyTxdH6Z1p0/J +DJve0LRf1LmgkwEtnW/KNny1l5MTekYS5zjhZpECgYEA0pIyX8do/X8petDHB2Jj +QeOTebD5VXDKeFbEVc8mqvvIY/84o1XlTNt4Q80xyT0R0N9NUwrFnAiPqqx53BFZ +rRKWaHlwd/rQUfsJSVuhMMEuoGYlIz3onBWWj8ZgOT67xFO3lehdTGJTl5DFHDyM +JBiKIvHMAUpJKTyXLAKJVRECgYEAzDyEx3av1K3CWaxHkrEvVykRM7X8qNQPnSvk +137hhyb3iJJR/89wQ+23A+jHn4lwA+udLEcB7FFdvDd6quNmCzYIFx1mUMORhx9X +C/gd02ADQ+Nz5AlNfs2JOyzSKBUR7xC/MnZVWNRJJdCPc4/8MKFgmIvgZjsivqJK +z77GmP8CgYEAk2Pz0Kjy62WD8kyK07dhhLA3/RkMHWsavcr2GJ4sEci1hpER4vpv +yzFf6A2MCLEvdEWpiTPkCAjSDwQ1M/4NCCEXmL9QIxszj/6ojGmP1WGTMoDtA9ME +q6RMxAx2M/ueUJjMyyhfjeTRYCjcX5xd97IZlsYrJsgJl8yqgAqyeBECgYA7Yp15 +IhmeTaflSyLElKCfR2GpF5GPxZmEqe6wekQ5eCshmuoc58RM/CHrERR6XwwjF69r +4Hm+rSoEQF16swRI3j0b+4B0m2kFUSeOY2bIv3Izmz9nXw0ZgcoVWsMAxJ+iU8xE +cAQBADkEtdAAWi2KNmJP+NwW3bsMk0QNg/tbIQKBgClGq7WLdpKwNhmTfdlBteDY +WvkZKEAC9gcTzTflAwxuFY38zL7torbYX7Azx5ycU7Bk9z9bVIbgei5afnJxshzA +aJX2pp7P8Cf/R1a5a108H0b0NynthNHgr4VsndCwCdfnaTeziaFtNWAuP7T5r8an +8Lm1rCMYAr5S5pyCfua8 +-----END PRIVATE KEY----- diff --git a/examples/openc3-cosmos-mqtt-test/img/command_sender.png b/examples/openc3-cosmos-mqtt-test/img/command_sender.png new file mode 100644 index 000000000..377f6b059 Binary files /dev/null and b/examples/openc3-cosmos-mqtt-test/img/command_sender.png differ diff --git a/examples/openc3-cosmos-mqtt-test/img/install_authenticated.png b/examples/openc3-cosmos-mqtt-test/img/install_authenticated.png new file mode 100644 index 000000000..a839de18c Binary files /dev/null and b/examples/openc3-cosmos-mqtt-test/img/install_authenticated.png differ diff --git a/examples/openc3-cosmos-mqtt-test/img/install_cert.png b/examples/openc3-cosmos-mqtt-test/img/install_cert.png new file mode 100644 index 000000000..4e6c6944b Binary files /dev/null and b/examples/openc3-cosmos-mqtt-test/img/install_cert.png differ diff --git a/examples/openc3-cosmos-mqtt-test/img/install_encrypted.png b/examples/openc3-cosmos-mqtt-test/img/install_encrypted.png new file mode 100644 index 000000000..84d0a6ebd Binary files /dev/null and b/examples/openc3-cosmos-mqtt-test/img/install_encrypted.png differ diff --git a/examples/openc3-cosmos-mqtt-test/img/install_unauthenticated.png b/examples/openc3-cosmos-mqtt-test/img/install_unauthenticated.png new file mode 100644 index 000000000..9f7baa028 Binary files /dev/null and b/examples/openc3-cosmos-mqtt-test/img/install_unauthenticated.png differ diff --git a/examples/openc3-cosmos-mqtt-test/img/packet_viewer.png b/examples/openc3-cosmos-mqtt-test/img/packet_viewer.png new file mode 100644 index 000000000..dfcc91cb5 Binary files /dev/null and b/examples/openc3-cosmos-mqtt-test/img/packet_viewer.png differ diff --git a/examples/openc3-cosmos-mqtt-test/img/secrets_ca_cert.png b/examples/openc3-cosmos-mqtt-test/img/secrets_ca_cert.png new file mode 100644 index 000000000..2a3079bf0 Binary files /dev/null and b/examples/openc3-cosmos-mqtt-test/img/secrets_ca_cert.png differ diff --git a/examples/openc3-cosmos-mqtt-test/img/secrets_key.png b/examples/openc3-cosmos-mqtt-test/img/secrets_key.png new file mode 100644 index 000000000..96bb74e84 Binary files /dev/null and b/examples/openc3-cosmos-mqtt-test/img/secrets_key.png differ diff --git a/examples/openc3-cosmos-mqtt-test/img/secrets_password.png b/examples/openc3-cosmos-mqtt-test/img/secrets_password.png new file mode 100644 index 000000000..54d2786bf Binary files /dev/null and b/examples/openc3-cosmos-mqtt-test/img/secrets_password.png differ diff --git a/examples/openc3-cosmos-mqtt-test/mosquitto.org.crt b/examples/openc3-cosmos-mqtt-test/mosquitto.org.crt new file mode 100644 index 000000000..e76dbd855 --- /dev/null +++ b/examples/openc3-cosmos-mqtt-test/mosquitto.org.crt @@ -0,0 +1,24 @@ +-----BEGIN CERTIFICATE----- +MIIEAzCCAuugAwIBAgIUBY1hlCGvdj4NhBXkZ/uLUZNILAwwDQYJKoZIhvcNAQEL +BQAwgZAxCzAJBgNVBAYTAkdCMRcwFQYDVQQIDA5Vbml0ZWQgS2luZ2RvbTEOMAwG +A1UEBwwFRGVyYnkxEjAQBgNVBAoMCU1vc3F1aXR0bzELMAkGA1UECwwCQ0ExFjAU +BgNVBAMMDW1vc3F1aXR0by5vcmcxHzAdBgkqhkiG9w0BCQEWEHJvZ2VyQGF0Y2hv +by5vcmcwHhcNMjAwNjA5MTEwNjM5WhcNMzAwNjA3MTEwNjM5WjCBkDELMAkGA1UE +BhMCR0IxFzAVBgNVBAgMDlVuaXRlZCBLaW5nZG9tMQ4wDAYDVQQHDAVEZXJieTES +MBAGA1UECgwJTW9zcXVpdHRvMQswCQYDVQQLDAJDQTEWMBQGA1UEAwwNbW9zcXVp +dHRvLm9yZzEfMB0GCSqGSIb3DQEJARYQcm9nZXJAYXRjaG9vLm9yZzCCASIwDQYJ +KoZIhvcNAQEBBQADggEPADCCAQoCggEBAME0HKmIzfTOwkKLT3THHe+ObdizamPg +UZmD64Tf3zJdNeYGYn4CEXbyP6fy3tWc8S2boW6dzrH8SdFf9uo320GJA9B7U1FW +Te3xda/Lm3JFfaHjkWw7jBwcauQZjpGINHapHRlpiCZsquAthOgxW9SgDgYlGzEA +s06pkEFiMw+qDfLo/sxFKB6vQlFekMeCymjLCbNwPJyqyhFmPWwio/PDMruBTzPH +3cioBnrJWKXc3OjXdLGFJOfj7pP0j/dr2LH72eSvv3PQQFl90CZPFhrCUcRHSSxo +E6yjGOdnz7f6PveLIB574kQORwt8ePn0yidrTC1ictikED3nHYhMUOUCAwEAAaNT +MFEwHQYDVR0OBBYEFPVV6xBUFPiGKDyo5V3+Hbh4N9YSMB8GA1UdIwQYMBaAFPVV +6xBUFPiGKDyo5V3+Hbh4N9YSMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL +BQADggEBAGa9kS21N70ThM6/Hj9D7mbVxKLBjVWe2TPsGfbl3rEDfZ+OKRZ2j6AC +6r7jb4TZO3dzF2p6dgbrlU71Y/4K0TdzIjRj3cQ3KSm41JvUQ0hZ/c04iGDg/xWf ++pp58nfPAYwuerruPNWmlStWAXf0UTqRtg4hQDWBuUFDJTuWuuBvEXudz74eh/wK +sMwfu1HFvjy5Z0iMDU8PUDepjVolOCue9ashlS4EB5IECdSR2TItnAIiIwimx839 +LdUdRudafMu5T5Xma182OC0/u/xRlEm+tvKGGmfFcN0piqVl8OrSPBgIlb+1IKJE +m/XriWr/Cq4h/JfB7NTsezVslgkBaoU= +-----END CERTIFICATE----- diff --git a/openc3-cosmos-init/plugins/packages/openc3-cosmos-mqtt-test/openc3-cosmos-mqtt-test.gemspec b/examples/openc3-cosmos-mqtt-test/openc3-cosmos-mqtt-test.gemspec similarity index 100% rename from openc3-cosmos-init/plugins/packages/openc3-cosmos-mqtt-test/openc3-cosmos-mqtt-test.gemspec rename to examples/openc3-cosmos-mqtt-test/openc3-cosmos-mqtt-test.gemspec diff --git a/openc3-cosmos-init/plugins/packages/openc3-cosmos-mqtt-test/plugin.txt b/examples/openc3-cosmos-mqtt-test/plugin.txt similarity index 51% rename from openc3-cosmos-init/plugins/packages/openc3-cosmos-mqtt-test/plugin.txt rename to examples/openc3-cosmos-mqtt-test/plugin.txt index fb81db4c0..401043e88 100644 --- a/openc3-cosmos-init/plugins/packages/openc3-cosmos-mqtt-test/plugin.txt +++ b/examples/openc3-cosmos-mqtt-test/plugin.txt @@ -1,26 +1,39 @@ VARIABLE mqtt_target_name MQTT VARIABLE mqtt_host test.mosquitto.org VARIABLE mqtt_port 1883 -VARIABLE mqtt_ssl false VARIABLE mqtt_cmd_topic test VARIABLE mqtt_tlm_topic test -VARIABLE mqtt_username_secret "" +VARIABLE mqtt_username "" VARIABLE mqtt_password_secret "" VARIABLE mqtt_cert_secret "" VARIABLE mqtt_key_secret "" VARIABLE mqtt_ca_file_secret "" -<% include_mqtt_username_secret = (mqtt_username_secret.to_s.strip.length > 0) %> +<% include_mqtt_username = (mqtt_username.to_s.strip.length > 0) %> <% include_mqtt_password_secret = (mqtt_password_secret.to_s.strip.length > 0) %> <% include_mqtt_cert_secret = (mqtt_cert_secret.to_s.strip.length > 0) %> <% include_mqtt_key_secret = (mqtt_key_secret.to_s.strip.length > 0) %> <% include_mqtt_ca_file_secret = (mqtt_ca_file_secret.to_s.strip.length > 0) %> +# NOTE: SECRET only works if you update the Secrets tab with the expected secrets +# See the README for more information + TARGET MQTT <%= mqtt_target_name %> -INTERFACE MQTT_INT mqtt_interface.rb <%= mqtt_host %> <%= mqtt_port %> <%= mqtt_ssl %> + +# There are 4 possible MQTT interfaces: 2 each for Ruby and Python +# You can use the META TOPIC in the cmd/tlm definitions using the mqtt_interface +# Or read and write all telemetry from a fixed topic using the mqtt_stream_interface + +#=> Regular MQTT Interfaces (uses META TOPIC in the cmd/tlm definitions) +INTERFACE MQTT_INT openc3/interfaces/mqtt_interface.py <%= mqtt_host %> <%= mqtt_port %> +# INTERFACE MQTT_INT mqtt_interface.rb <%= mqtt_host %> <%= mqtt_port %> +#=> Streaming MQTT Interfaces (uses fixed cmd/tlm topics) +# INTERFACE MQTT_INT openc3/interfaces/mqtt_stream_interface.py <%= mqtt_host %> <%= mqtt_port %> <%= mqtt_cmd_topic %> <%= mqtt_tlm_topic %> +# INTERFACE MQTT_INT mqtt_stream_interface.rb <%= mqtt_host %> <%= mqtt_port %> <%= mqtt_cmd_topic %> <%= mqtt_tlm_topic %> MAP_TARGET <%= mqtt_target_name %> - <% if include_mqtt_username_secret %> - SECRET ENV <%= mqtt_username_secret %> "<%= mqtt_target_name %>_MQTT_USERNAME" USERNAME + <% if include_mqtt_username %> + # No need to hide the USERNAME so directly set the OPTION + OPTION USERNAME <%= mqtt_username %> <% end %> <% if include_mqtt_password_secret %> SECRET ENV <%= mqtt_password_secret %> "<%= mqtt_target_name %>_MQTT_PASSWORD" PASSWORD @@ -33,4 +46,4 @@ INTERFACE MQTT_INT mqtt_interface.rb <%= mqtt_host %> <%= mqtt_port %> <%= mqtt_ <% end %> <% if include_mqtt_ca_file_secret %> SECRET FILE <%= mqtt_ca_file_secret %> "/tmp/<%= mqtt_target_name %>/MQTT_CA_FILE" CA_FILE - <% end %> \ No newline at end of file + <% end %> diff --git a/openc3-cosmos-init/plugins/packages/openc3-cosmos-mqtt-test/targets/MQTT/cmd_tlm/mqtt_cmd.txt b/examples/openc3-cosmos-mqtt-test/targets/MQTT/cmd_tlm/mqtt_cmd.txt similarity index 100% rename from openc3-cosmos-init/plugins/packages/openc3-cosmos-mqtt-test/targets/MQTT/cmd_tlm/mqtt_cmd.txt rename to examples/openc3-cosmos-mqtt-test/targets/MQTT/cmd_tlm/mqtt_cmd.txt diff --git a/openc3-cosmos-init/plugins/packages/openc3-cosmos-mqtt-test/targets/MQTT/cmd_tlm/mqtt_tlm.txt b/examples/openc3-cosmos-mqtt-test/targets/MQTT/cmd_tlm/mqtt_tlm.txt similarity index 100% rename from openc3-cosmos-init/plugins/packages/openc3-cosmos-mqtt-test/targets/MQTT/cmd_tlm/mqtt_tlm.txt rename to examples/openc3-cosmos-mqtt-test/targets/MQTT/cmd_tlm/mqtt_tlm.txt diff --git a/openc3.code-workspace b/openc3.code-workspace index ab79c3808..4e858bd35 100644 --- a/openc3.code-workspace +++ b/openc3.code-workspace @@ -11,6 +11,9 @@ }, { "path": "../cosmos-enterprise-project" + }, + { + "path": "../cosmos-enterprise-plugins" } ], "settings": { diff --git a/openc3/lib/openc3/interfaces.rb b/openc3/lib/openc3/interfaces.rb index ace410686..2c05e9b5e 100644 --- a/openc3/lib/openc3/interfaces.rb +++ b/openc3/lib/openc3/interfaces.rb @@ -25,6 +25,7 @@ module OpenC3 autoload(:HttpClientInterface, 'openc3/interfaces/http_client_interface.rb') autoload(:HttpServerInterface, 'openc3/interfaces/http_server_interface.rb') autoload(:MqttInterface, 'openc3/interfaces/mqtt_interface.rb') + autoload(:MqttStreamInterface, 'openc3/interfaces/mqtt_stream_interface.rb') autoload(:StreamInterface, 'openc3/interfaces/stream_interface.rb') autoload(:SerialInterface, 'openc3/interfaces/serial_interface.rb') autoload(:SimulatedTargetInterface, 'openc3/interfaces/simulated_target_interface.rb') diff --git a/openc3/lib/openc3/interfaces/mqtt_interface.rb b/openc3/lib/openc3/interfaces/mqtt_interface.rb index f78e8999f..0e28fafbc 100644 --- a/openc3/lib/openc3/interfaces/mqtt_interface.rb +++ b/openc3/lib/openc3/interfaces/mqtt_interface.rb @@ -18,6 +18,7 @@ # You can quickly setup an unauthenticated MQTT server in Docker with # docker run -it -p 1883:1883 eclipse-mosquitto:2.0.15 mosquitto -c /mosquitto-no-auth.conf +# You can also test against encrypted and authenticated servers at https://test.mosquitto.org/ require 'openc3/interfaces/interface' require 'openc3/config/config_parser' @@ -91,12 +92,11 @@ module OpenC3 class MqttInterface < Interface # @param hostname [String] MQTT server to connect to # @param port [Integer] MQTT port - # @param ssl [Boolean] Use SSL true/false - def initialize(hostname, port = 1883, ssl = false) + def initialize(hostname, port = 1883) super() @hostname = hostname @port = Integer(port) - @ssl = ConfigParser.handle_true_false(ssl) + @ack_timeout = 5.0 @username = nil @password = nil @cert = nil @@ -122,7 +122,7 @@ def initialize(hostname, port = 1883, ssl = false) end def connection_string - return "#{@hostname}:#{@port} (ssl: #{@ssl})" + return "#{@hostname}:#{@port}" end # Connects the interface to its target(s) @@ -130,14 +130,20 @@ def connect @write_topics = [] @read_topics = [] @client = MQTT::Client.new + @client.ack_timeout = @ack_timeout @client.host = @hostname @client.port = @port - @client.ssl = @ssl @client.username = @username if @username @client.password = @password if @password - @client.cert = @cert if @cert - @client.key = @key if @key - @client.ca_file = @ca_file.path if @ca_file + if @cert and @key + @client.ssl = true + @client.cert_file = @cert.path + @client.key_file = @key.path + end + if @ca_file + @client.ssl = true + @client.ca_file = @ca_file.path + end @client.connect @read_packets_by_topic.each do |topic, _| Logger.info "#{@name}: Subscribing to #{topic}" @@ -146,9 +152,7 @@ def connect super() end - # @return [Boolean] Whether the active ports (read and/or write) have - # created sockets. Since UDP is connectionless, creation of the sockets - # is used to determine connection. + # @return [Boolean] Whether the MQTT client is connected def connected? if @client return @client.connected? @@ -159,8 +163,10 @@ def connected? # Disconnects the interface from its target(s) def disconnect - @client.disconnect - @client = nil + if @client + @client.disconnect + @client = nil + end super() end @@ -188,12 +194,12 @@ def write(packet) super(packet) end else - raise "Command packet #{packet.target_name} #{packet.packet_name} requires a META TOPIC or TOPICS" + raise "Command packet '#{packet.target_name} #{packet.packet_name}' requires a META TOPIC or TOPICS" end end end - # Reads from the socket if the read_port is defined + # Reads from the client def read_interface topic, data = @client.get if data.nil? or data.length <= 0 @@ -209,7 +215,7 @@ def read_interface return nil end - # Writes to the socket + # Writes to the client # @param data [String] Raw packet data def write_interface(data, extra = nil) write_interface_base(data, extra) @@ -228,14 +234,22 @@ def write_interface(data, extra = nil) def set_option(option_name, option_values) super(option_name, option_values) case option_name.upcase + when 'ACK_TIMEOUT' + @ack_timeout = Float(option_values[0]) when 'USERNAME' @username = option_values[0] when 'PASSWORD' @password = option_values[0] when 'CERT' - @cert = option_values[0] + # CERT must be given as a file + @cert = Tempfile.new('cert') + @cert.write(option_values[0]) + @cert.close when 'KEY' - @key = option_values[0] + # KEY must be given as a file + @key = Tempfile.new('key') + @key.write(option_values[0]) + @key.close when 'CA_FILE' # CA_FILE must be given as a file @ca_file = Tempfile.new('ca_file') diff --git a/openc3/lib/openc3/interfaces/mqtt_stream_interface.rb b/openc3/lib/openc3/interfaces/mqtt_stream_interface.rb index b4c239fea..86812a6a5 100644 --- a/openc3/lib/openc3/interfaces/mqtt_stream_interface.rb +++ b/openc3/lib/openc3/interfaces/mqtt_stream_interface.rb @@ -16,6 +16,10 @@ # This file may also be used under the terms of a commercial license # if purchased from OpenC3, Inc. +# You can quickly setup an unauthenticated MQTT server in Docker with +# docker run -it -p 1883:1883 eclipse-mosquitto:2.0.15 mosquitto -c /mosquitto-no-auth.conf +# You can also test against encrypted and authenticated servers at https://test.mosquitto.org/ + require 'openc3/interfaces/stream_interface' require 'openc3/streams/mqtt_stream' @@ -23,14 +27,15 @@ module OpenC3 class MqttStreamInterface < StreamInterface # @param hostname [String] MQTT server to connect to # @param port [Integer] MQTT port - # @param ssl [Boolean] Use SSL true/false - def initialize(hostname, port = 1883, ssl = false, write_topic = nil, read_topic = nil, protocol_type = nil, *protocol_args) + # @param write_topic [String] MQTT publish topic + # @param read_topic [String] MQTT receive topic + def initialize(hostname, port = 1883, write_topic = nil, read_topic = nil, protocol_type = nil, *protocol_args) super(protocol_type, protocol_args) @hostname = hostname @port = Integer(port) - @ssl = ConfigParser.handle_true_false(ssl) @write_topic = ConfigParser.handle_nil(write_topic) @read_topic = ConfigParser.handle_nil(read_topic) + @ack_timeout = 5.0 @username = nil @password = nil @cert = nil @@ -39,7 +44,7 @@ def initialize(hostname, port = 1883, ssl = false, write_topic = nil, read_topic end def connection_string - result = "#{@hostname}:#{@port} (ssl: #{@ssl})" + result = "#{@hostname}:#{@port}" result += " write topic: #{@write_topic}" if @write_topic result += " read topic: #{@read_topic}" if @read_topic return result @@ -47,7 +52,7 @@ def connection_string # Creates a new {SerialStream} using the parameters passed in the constructor def connect - @stream = MqttStream.new(@hostname, @port, @ssl, @write_topic, @read_topic) + @stream = MqttStream.new(@hostname, @port, @write_topic, @read_topic, @ack_timeout) @stream.username = @username if @username @stream.password = @password if @password @stream.cert = @cert if @cert @@ -66,14 +71,22 @@ def connect def set_option(option_name, option_values) super(option_name, option_values) case option_name.upcase + when 'ACK_TIMEOUT' + @ack_timeout = Float(option_values[0]) when 'USERNAME' @username = option_values[0] when 'PASSWORD' @password = option_values[0] when 'CERT' - @cert = option_values[0] + # CERT must be given as a file + @cert = Tempfile.new('cert') + @cert.write(option_values[0]) + @cert.close when 'KEY' - @key = option_values[0] + # KEY must be given as a file + @key = Tempfile.new('key') + @key.write(option_values[0]) + @key.close when 'CA_FILE' # CA_FILE must be given as a file @ca_file = Tempfile.new('ca_file') diff --git a/openc3/lib/openc3/interfaces/serial_interface.rb b/openc3/lib/openc3/interfaces/serial_interface.rb index 09529a0f9..2cacff67f 100644 --- a/openc3/lib/openc3/interfaces/serial_interface.rb +++ b/openc3/lib/openc3/interfaces/serial_interface.rb @@ -98,6 +98,7 @@ def connect # Supported Options # FLOW_CONTROL - Flow control method NONE or RTSCTS. Defaults to NONE + # DATA_BITS - Number of data bits 5, 6, 7, or 8. Defaults to 8 def set_option(option_name, option_values) super(option_name, option_values) case option_name.upcase diff --git a/openc3/lib/openc3/models/interface_model.rb b/openc3/lib/openc3/models/interface_model.rb index fc10ffe37..ddbc59600 100644 --- a/openc3/lib/openc3/models/interface_model.rb +++ b/openc3/lib/openc3/models/interface_model.rb @@ -297,9 +297,7 @@ def handle_config(parser, keyword, parameters) # Option Name, Secret Name @secret_options << [parameters[3], parameters[1]] end - if ConfigParser.handle_nil(parameters[4]) - @secrets[-1] << parameters[4] - end + @secrets[-1] << ConfigParser.handle_nil(parameters[4]) when 'ENV' parser.verify_num_parameters(2, 2, "#{keyword} ") diff --git a/openc3/lib/openc3/models/tool_model.rb b/openc3/lib/openc3/models/tool_model.rb index c84d37397..4c3cdb386 100644 --- a/openc3/lib/openc3/models/tool_model.rb +++ b/openc3/lib/openc3/models/tool_model.rb @@ -181,6 +181,10 @@ def create(update: false, force: false, queued: false) end end + if @url and !@url.start_with?('/') and !@url.start_with?('http') + raise "URL must be a full URL (http://domain.com/path) or a relative path (/path)" + end + super(update: update, force: force, queued: queued) end diff --git a/openc3/lib/openc3/packets/limits.rb b/openc3/lib/openc3/packets/limits.rb index dc9083848..9cfec72d6 100644 --- a/openc3/lib/openc3/packets/limits.rb +++ b/openc3/lib/openc3/packets/limits.rb @@ -50,18 +50,6 @@ def sets return @config.limits_sets end - # (see OpenC3::Packet#out_of_limits) - def out_of_limits - items = [] - @config.telemetry.each do |target_name, target_packets| - target_packets.each do |packet_name, packet| - new_items = packet.out_of_limits - items.concat(new_items) - end - end - return items - end - # @return [Hash(String, Array)] The defined limits groups def groups return @config.limits_groups diff --git a/openc3/lib/openc3/streams/mqtt_stream.rb b/openc3/lib/openc3/streams/mqtt_stream.rb index 936e379ea..37c1926f5 100644 --- a/openc3/lib/openc3/streams/mqtt_stream.rb +++ b/openc3/lib/openc3/streams/mqtt_stream.rb @@ -1,6 +1,6 @@ # encoding: ascii-8bit -# Copyright 2023 OpenC3, Inc. +# Copyright 2024 OpenC3, Inc. # All Rights Reserved. # # This program is free software; you can modify and/or redistribute it @@ -33,15 +33,14 @@ class MqttStream < Stream attr_accessor :key attr_accessor :ca_file - def initialize(hostname, port = 1883, ssl = false, write_topic = nil, read_topic = nil) + def initialize(hostname, port = 1883, write_topic = nil, read_topic = nil, ack_timeout = 5) super() @hostname = hostname @port = Integer(port) - @ssl = ConfigParser.handle_true_false(ssl) @write_topic = ConfigParser.handle_nil(write_topic) @read_topic = ConfigParser.handle_nil(read_topic) - @connected = false + @ack_timeout = Float(ack_timeout) @username = nil @password = nil @@ -49,11 +48,46 @@ def initialize(hostname, port = 1883, ssl = false, write_topic = nil, read_topic @key = nil @ca_file = nil - # Mutex on write is needed to protect from commands coming in from more - # than one tool + # Mutex on write is needed to protect from commands coming in from more than one tool @write_mutex = Mutex.new end + # Connect the stream + def connect + @client = MQTT::Client.new + @client.ack_timeout = @ack_timeout + @client.host = @hostname + @client.port = @port + @client.username = @username if @username + @client.password = @password if @password + if @cert and @key + @client.ssl = true + @client.cert_file = @cert.path + @client.key_file = @key.path + end + if @ca_file + @client.ssl = true + @client.ca_file = @ca_file.path + end + @client.connect + @client.subscribe(@read_topic) if @read_topic + end + + def connected? + if @client + return @client.connected? + else + return false + end + end + + def disconnect + if @client + @client.disconnect + @client = nil + end + end + # @return [String] Returns a binary string of data from the read_topic def read raise "Attempt to read from write only stream" unless @read_topic @@ -77,33 +111,5 @@ def write(data) @client.publish(@write_topic, data) end end - - # Connect the stream - def connect - @client = MQTT::Client.new - @client.host = @hostname - @client.port = @port - @client.ssl = @ssl - @client.username = @username if @username - @client.password = @password if @password - @client.cert = @cert if @cert - @client.key = @key if @key - @client.ca_file = @ca_file.path if @ca_file - @client.connect - @client.subscribe(@read_topic) if @read_topic - @connected = true - end - - def connected? - @connected - end - - def disconnect - if @connected - @client.disconnect - @client = nil - @connected = false - end - end end end diff --git a/openc3/lib/openc3/streams/serial_stream.rb b/openc3/lib/openc3/streams/serial_stream.rb index e52d9ed12..2163ac2dc 100644 --- a/openc3/lib/openc3/streams/serial_stream.rb +++ b/openc3/lib/openc3/streams/serial_stream.rb @@ -14,7 +14,7 @@ # GNU Affero General Public License for more details. # Modified by OpenC3, Inc. -# All changes Copyright 2022, OpenC3, Inc. +# All changes Copyright 2024, OpenC3, Inc. # All Rights Reserved # # This file may also be used under the terms of a commercial license @@ -115,31 +115,6 @@ def initialize(write_port_name, @write_mutex = Mutex.new end - # @return [String] Returns a binary string of data from the serial port - def read - raise "Attempt to read from write only stream" unless @read_serial_port - - # No read mutex is needed because reads happen serially - @read_serial_port.read - end - - # @return [String] Returns a binary string of data from the serial port without blocking - def read_nonblock - raise "Attempt to read from write only stream" unless @read_serial_port - - # No read mutex is needed because reads happen serially - @read_serial_port.read_nonblock - end - - # @param data [String] A binary string of data to write to the serial port - def write(data) - raise "Attempt to write to read only stream" unless @write_serial_port - - @write_mutex.synchronize do - @write_serial_port.write(data) - end - end - # Connect the stream def connect # N/A - Serial streams 'connect' on creation @@ -168,5 +143,30 @@ def disconnect @connected = false end end - end # class SerialStream + + # @return [String] Returns a binary string of data from the serial port + def read + raise "Attempt to read from write only stream" unless @read_serial_port + + # No read mutex is needed because reads happen serially + @read_serial_port.read + end + + # @return [String] Returns a binary string of data from the serial port without blocking + def read_nonblock + raise "Attempt to read from write only stream" unless @read_serial_port + + # No read mutex is needed because reads happen serially + @read_serial_port.read_nonblock + end + + # @param data [String] A binary string of data to write to the serial port + def write(data) + raise "Attempt to write to read only stream" unless @write_serial_port + + @write_mutex.synchronize do + @write_serial_port.write(data) + end + end + end end diff --git a/openc3/lib/openc3/streams/stream.rb b/openc3/lib/openc3/streams/stream.rb index 692a03aa3..86614a975 100644 --- a/openc3/lib/openc3/streams/stream.rb +++ b/openc3/lib/openc3/streams/stream.rb @@ -27,6 +27,22 @@ module OpenC3 # allows Streams to simply focus on getting and sending raw data while the # higher level processing occurs in {Protocol}. class Stream + # Connects the stream + def connect + raise "connect not defined by Stream" + end + + # @return [Boolean] true if connected or false otherwise + def connected? + raise "connected? not defined by Stream" + end + + # Disconnects the stream + # Note that streams are not designed to be reconnected and must be recreated + def disconnect + raise "disconnect not defined by Stream" + end + # Expected to return any amount of data on success, or a blank string on # closed/EOF, and may raise Timeout::Error, or other errors def read @@ -46,21 +62,5 @@ def read_nonblock def write(data) raise "write not defined by Stream" end - - # Connects the stream - def connect - raise "connect not defined by Stream" - end - - # @return [Boolean] true if connected or false otherwise - def connected? - raise "connected? not defined by Stream" - end - - # Disconnects the stream - # Note that streams are not designed to be reconnected and must be recreated - def disconnect - raise "disconnect not defined by Stream" - end - end # class Stream + end end diff --git a/openc3/lib/openc3/streams/tcpip_client_stream.rb b/openc3/lib/openc3/streams/tcpip_client_stream.rb index 110b4ad87..b45c6faef 100644 --- a/openc3/lib/openc3/streams/tcpip_client_stream.rb +++ b/openc3/lib/openc3/streams/tcpip_client_stream.rb @@ -118,5 +118,5 @@ def connect_nonblock(socket, addr) rescue IOError, Errno::ENOTSOCK raise "Connect canceled" end - end # class TcpipClientStream + end end diff --git a/openc3/lib/openc3/streams/tcpip_socket_stream.rb b/openc3/lib/openc3/streams/tcpip_socket_stream.rb index d4a3e9cf4..7bcc48481 100644 --- a/openc3/lib/openc3/streams/tcpip_socket_stream.rb +++ b/openc3/lib/openc3/streams/tcpip_socket_stream.rb @@ -58,6 +58,25 @@ def initialize(write_socket, read_socket, write_timeout, read_timeout) @connected = false end + # Connect the stream + def connect + # If called directly this class is acting as a server and does not need to connect the sockets + @connected = true + end + + # @return [Boolean] Whether the sockets are connected + def connected? + @connected + end + + # Disconnect by closing the sockets + def disconnect + OpenC3.close_socket(@write_socket) + OpenC3.close_socket(@read_socket) + @pipe_writer.write('.') + @connected = false + end + # @return [String] Returns a binary string of data from the socket def read raise "Attempt to read from write only stream" unless @read_socket @@ -142,25 +161,6 @@ def write(data) end end - # Connect the stream - def connect - # If called directly this class is acting as a server and does not need to connect the sockets - @connected = true - end - - # @return [Boolean] Whether the sockets are connected - def connected? - @connected - end - - # Disconnect by closing the sockets - def disconnect - OpenC3.close_socket(@write_socket) - OpenC3.close_socket(@read_socket) - @pipe_writer.write('.') - @connected = false - end - def set_option(option_name, option_values) option_name_upcase = option_name.upcase diff --git a/openc3/python/openc3/accessors/accessor.py b/openc3/python/openc3/accessors/accessor.py index 005f611e1..2a4ca640b 100644 --- a/openc3/python/openc3/accessors/accessor.py +++ b/openc3/python/openc3/accessors/accessor.py @@ -104,6 +104,6 @@ def convert_to_type(cls, value, item): else: value = float(value) case _: - raise AttributeError(f"data_type {item.data_type} is not recognized") + raise TypeError(f"data_type {item.data_type} is not recognized") return value diff --git a/openc3/python/openc3/accessors/binary_accessor.py b/openc3/python/openc3/accessors/binary_accessor.py index 5d4e667d9..1b764bb0c 100644 --- a/openc3/python/openc3/accessors/binary_accessor.py +++ b/openc3/python/openc3/accessors/binary_accessor.py @@ -95,7 +95,7 @@ def get_host_endianness(cls): @classmethod def raise_buffer_error(cls, read_write, buffer, data_type, given_bit_offset, given_bit_size): - raise AttributeError( + raise ValueError( f"{len(buffer)} byte buffer insufficient to {read_write} {data_type} at bit_offset {given_bit_offset} with bit_size {given_bit_size}" ) @@ -158,7 +158,7 @@ def handle_write_variable_bit_size(self, item, value, buffer): # Probably not possible to get this condition because we don't allow 0 sized floats # but check for it just to cover all the possible data_types elif item.data_type == "FLOAT": - raise AttributeError("Variable bit size not currently supported for FLOAT data type") + raise ValueError("Variable bit size not currently supported for FLOAT data type") else: # STRING, BLOCK, or array types adjustment = self._write_variable_other(item, value, buffer) @@ -188,7 +188,7 @@ def _write_variable_int(self, item, value, buffer): case 3: current_bit_size = 62 case _: - raise AttributeError( + raise ValueError( f"Value {item.variable_bit_size['length_item_name']} has unknown QUIC bit size encoding: {length_item_value}" ) @@ -364,7 +364,7 @@ def read(cls, bit_offset, bit_size, data_type, buffer, endianness): return buffer[lower_bound : upper_bound + 1] else: - raise AttributeError(f"bit_offset {given_bit_offset} is not byte aligned for data_type {data_type}") + raise ValueError(f"bit_offset {given_bit_offset} is not byte aligned for data_type {data_type}") elif data_type in ["INT", "UINT"]: ################################### @@ -397,7 +397,7 @@ def read(cls, bit_offset, bit_size, data_type, buffer, endianness): lower_bound = upper_bound - num_bytes + 1 if lower_bound < 0: - raise AttributeError( + raise ValueError( f"LITTLE_ENDIAN bitfield with bit_offset {given_bit_offset} and bit_size {given_bit_size} is invalid" ) temp_lower = lower_bound - 1 if lower_bound > 0 else lower_bound @@ -446,15 +446,15 @@ def read(cls, bit_offset, bit_size, data_type, buffer, endianness): buffer[lower_bound : upper_bound + 1], )[0] else: - raise AttributeError(f"bit_size is {given_bit_size} but must be 32 or 64 for data_type {data_type}") + raise ValueError(f"bit_size is {given_bit_size} but must be 32 or 64 for data_type {data_type}") else: - raise AttributeError(f"bit_offset {given_bit_offset} is not byte aligned for data_type {data_type}") + raise ValueError(f"bit_offset {given_bit_offset} is not byte aligned for data_type {data_type}") else: ############################ # Handle Unknown data types ############################ - raise AttributeError(f"data_type {data_type} is not recognized") + raise TypeError(f"data_type {data_type} is not recognized") # Writes binary data of any data type to a buffer # @@ -492,7 +492,7 @@ def write(cls, value, bit_offset, bit_size, data_type, buffer, endianness, overf and (overflow != "ERROR") and (overflow != "ERROR_ALLOW_HEX") ): - raise AttributeError(f"unknown overflow type {overflow}") + raise ValueError(f"unknown overflow type {overflow}") if (data_type == "STRING") or (data_type == "BLOCK"): ####################################### @@ -500,7 +500,7 @@ def write(cls, value, bit_offset, bit_size, data_type, buffer, endianness, overf ####################################### if cls.byte_aligned(bit_offset): - if data_type == "STRING" and isinstance(value, str): + if isinstance(value, str): temp = value.encode(encoding="utf-8") else: temp = value @@ -549,14 +549,14 @@ def write(cls, value, bit_offset, bit_size, data_type, buffer, endianness, overf # Resize the value to fit the field temp = value[0:byte_size] else: - raise AttributeError( + raise ValueError( f"value of {len(value)} bytes does not fit into {byte_size} bytes for data_type {data_type}" ) if bit_size != 0: buffer[lower_bound : lower_bound + len(temp)] = temp else: - raise AttributeError(f"bit_offset {given_bit_offset} is not byte aligned for data_type {data_type}") + raise ValueError(f"bit_offset {given_bit_offset} is not byte aligned for data_type {data_type}") elif (data_type == "INT") or (data_type == "UINT"): ################################### @@ -598,7 +598,7 @@ def write(cls, value, bit_offset, bit_size, data_type, buffer, endianness, overf upper_bound = math.floor(bit_offset / 8) lower_bound = upper_bound - num_bytes + 1 if lower_bound < 0: - raise AttributeError( + raise ValueError( f"LITTLE_ENDIAN bitfield with bit_offset {given_bit_offset} and bit_size {given_bit_size} is invalid" ) @@ -671,16 +671,16 @@ def write(cls, value, bit_offset, bit_size, data_type, buffer, endianness, overf value, ) else: - raise AttributeError(f"bit_size is {given_bit_size} but must be 32 or 64 for data_type {data_type}") + raise ValueError(f"bit_size is {given_bit_size} but must be 32 or 64 for data_type {data_type}") else: - raise AttributeError(f"bit_offset {given_bit_offset} is not byte aligned for data_type {data_type}") + raise ValueError(f"bit_offset {given_bit_offset} is not byte aligned for data_type {data_type}") else: ############################ # Handle Unknown data types ############################ - raise AttributeError(f"data_type {data_type} is not recognized") + raise TypeError(f"data_type {data_type} is not recognized") return value @@ -691,12 +691,12 @@ def check_bit_offset_and_size(cls, read_or_write, given_bit_offset, given_bit_si bit_offset = given_bit_offset if (given_bit_size <= 0) and (data_type != "STRING") and (data_type != "BLOCK"): - raise AttributeError( + raise ValueError( f"bit_size {given_bit_size} must be positive for data types other than 'STRING' and 'BLOCK'" ) if (given_bit_size <= 0) and (given_bit_offset < 0): - raise AttributeError( + raise ValueError( f"negative or zero bit_sizes ({given_bit_size}) cannot be given with negative bit_offsets ({given_bit_offset})" ) @@ -824,7 +824,7 @@ def read_array(cls, bit_offset, bit_size, data_type, array_size, buffer, endiann # Handle negative and zero bit sizes if bit_size <= 0: - raise AttributeError(f"bit_size {given_bit_size} must be positive for arrays") + raise ValueError(f"bit_size {given_bit_size} must be positive for arrays") # Handle negative bit offsets if bit_offset < 0: @@ -835,7 +835,7 @@ def read_array(cls, bit_offset, bit_size, data_type, array_size, buffer, endiann # Handle negative and zero array sizes if array_size <= 0: if given_bit_offset < 0: - raise AttributeError( + raise ValueError( f"negative or zero array_size ({given_array_size}) cannot be given with negative bit_offset ({given_bit_offset})" ) else: @@ -848,7 +848,7 @@ def read_array(cls, bit_offset, bit_size, data_type, array_size, buffer, endiann # Calculate number of items in the array # If there is a remainder then we have a problem if array_size % bit_size != 0: - raise AttributeError(f"array_size {given_array_size} not a multiple of bit_size {given_bit_size}") + raise ValueError(f"array_size {given_array_size} not a multiple of bit_size {given_bit_size}") num_items = math.floor(array_size / bit_size) @@ -871,7 +871,7 @@ def read_array(cls, bit_offset, bit_size, data_type, array_size, buffer, endiann value.append(cls.read(bit_offset, bit_size, data_type, buffer, endianness)) bit_offset += bit_size else: - raise AttributeError(f"bit_offset {given_bit_offset} is not byte aligned for data_type {data_type}") + raise ValueError(f"bit_offset {given_bit_offset} is not byte aligned for data_type {data_type}") case "INT" | "UINT": ################################### @@ -895,7 +895,7 @@ def read_array(cls, bit_offset, bit_size, data_type, array_size, buffer, endiann # Handle 'INT' and 'UINT' Bitfields ################################## if endianness == "LITTLE_ENDIAN" and bit_size > 1: - raise AttributeError( + raise ValueError( "read_array does not support little endian bit fields with bit_size greater than 1-bit" ) @@ -921,19 +921,19 @@ def read_array(cls, bit_offset, bit_size, data_type, array_size, buffer, endiann ) ) else: - raise AttributeError( + raise ValueError( f"bit_size is {given_bit_size} but must be 32 or 64 for data_type {data_type}" ) else: - raise AttributeError(f"bit_offset {given_bit_offset} is not byte aligned for data_type {data_type}") + raise ValueError(f"bit_offset {given_bit_offset} is not byte aligned for data_type {data_type}") case _: ############################ # Handle Unknown data types ############################ - raise AttributeError(f"data_type {data_type} is not recognized") + raise TypeError(f"data_type {data_type} is not recognized") return list(value) @@ -969,11 +969,11 @@ def write_array( # Verify a list was given if not isinstance(values, list): - raise AttributeError(f"values must be a list but is {values.__class__.__name__}") + raise TypeError(f"values must be a list but is {values.__class__.__name__}") # Handle negative and zero bit sizes if bit_size <= 0: - raise AttributeError(f"bit_size {given_bit_size} must be positive for arrays") + raise ValueError(f"bit_size {given_bit_size} must be positive for arrays") # Handle negative bit offsets if bit_offset < 0: @@ -984,7 +984,7 @@ def write_array( # Handle negative and zero array sizes if array_size <= 0: if given_bit_offset < 0: - raise AttributeError( + raise ValueError( f"negative or zero array_size ({given_array_size}) cannot be given with negative bit_offset ({given_bit_offset})" ) else: @@ -1026,15 +1026,15 @@ def write_array( # Ensure the given_array_size is an even multiple of bit_size if array_size % bit_size != 0: - raise AttributeError(f"array_size {given_array_size} not a multiple of bit_size {given_bit_size}") + raise ValueError(f"array_size {given_array_size} not a multiple of bit_size {given_bit_size}") if num_writes < len(values): - raise AttributeError( + raise ValueError( f"too many values {len(values)} for given array_size {given_array_size} and bit_size {given_bit_size}" ) # Check overflow type if overflow not in BinaryAccessor.OVERFLOW_TYPES: - raise AttributeError(f"unknown overflow type {overflow}") + raise TypeError(f"unknown overflow type {overflow}") # Expand the values by appending 0 if len(values) < num_writes: @@ -1063,7 +1063,7 @@ def write_array( ) bit_offset += bit_size else: - raise AttributeError(f"bit_offset {given_bit_offset} is not byte aligned for data_type {data_type}") + raise ValueError(f"bit_offset {given_bit_offset} is not byte aligned for data_type {data_type}") case "INT" | "UINT": ################################### @@ -1103,7 +1103,7 @@ def write_array( # Handle 'INT' and 'UINT' Bitfields ################################## if endianness == "LITTLE_ENDIAN" and bit_size > 1: - raise AttributeError( + raise ValueError( "write_array does not support little endian bit fields with bit_size greater than 1-bit" ) @@ -1134,17 +1134,17 @@ def write_array( *values, ) else: - raise AttributeError( + raise ValueError( f"bit_size is {given_bit_size} but must be 32 or 64 for data_type {data_type}" ) else: - raise AttributeError(f"bit_offset {given_bit_offset} is not byte aligned for data_type {data_type}") + raise ValueError(f"bit_offset {given_bit_offset} is not byte aligned for data_type {data_type}") case _: ############################ # Handle Unknown data types ############################ - raise AttributeError(f"data_type {data_type} is not recognized") + raise TypeError(f"data_type {data_type} is not recognized") # # Adjusts the packed array to be the given number of bytes # # @@ -1205,12 +1205,12 @@ def check_overflow(cls, value, min_value, max_value, hex_max_value, bit_size, da if overflow == "SATURATE": value = max_value elif overflow == "ERROR" or value > hex_max_value: - raise AttributeError(f"value of {value} invalid for {bit_size}-bit {data_type}") + raise ValueError(f"value of {value} invalid for {bit_size}-bit {data_type}") elif value < min_value: if overflow == "SATURATE": value = min_value else: - raise AttributeError(f"value of {value} invalid for {bit_size}-bit {data_type}") + raise ValueError(f"value of {value} invalid for {bit_size}-bit {data_type}") return value # Checks for overflow of an array of integer data types diff --git a/openc3/python/openc3/api/tlm_api.py b/openc3/python/openc3/api/tlm_api.py index cba7dd7b5..5603e831e 100644 --- a/openc3/python/openc3/api/tlm_api.py +++ b/openc3/python/openc3/api/tlm_api.py @@ -242,7 +242,7 @@ def get_tlm_packet(*args, stale_time: int = 30, type: str = "CONVERTED", scope: packet = TargetModel.packet(target_name, packet_name, scope=scope) t = _validate_tlm_type(type) if t is None: - raise AttributeError(f"Unknown type '{type}' for {target_name} {packet_name}") + raise TypeError(f"Unknown type '{type}' for {target_name} {packet_name}") cvt_items = [[target_name, packet_name, item["name"].upper(), type] for item in packet["items"]] # This returns an array of arrays containing the value and the limits state: # [[0, None], [0, 'RED_LOW'], ... ] @@ -261,14 +261,14 @@ def get_tlm_packet(*args, stale_time: int = 30, type: str = "CONVERTED", scope: # given as symbols such as :RED, :YELLOW, :STALE def get_tlm_values(items, stale_time=30, cache_timeout=0.1, scope=OPENC3_SCOPE): if not isinstance(items, list) or len(items) == 0 or not isinstance(items[0], str): - raise AttributeError("items must be array of strings: ['TGT__PKT__ITEM__TYPE', ...]") + raise TypeError("items must be array of strings: ['TGT__PKT__ITEM__TYPE', ...]") packets = [] cvt_items = [] for item in items: try: target_name, packet_name, item_name, value_type = item.upper().split("__") except ValueError: - raise AttributeError("items must be formatted as TGT__PKT__ITEM__TYPE") + raise ValueError("items must be formatted as TGT__PKT__ITEM__TYPE") if packet_name == "LATEST": packet_name = CvtModel.determine_latest_packet_for_item(target_name, item_name, cache_timeout, scope) # Change packet_name in case of LATEST and ensure upcase diff --git a/openc3/python/openc3/config/config_parser.py b/openc3/python/openc3/config/config_parser.py index 82697dde3..ed043e95b 100644 --- a/openc3/python/openc3/config/config_parser.py +++ b/openc3/python/openc3/config/config_parser.py @@ -292,7 +292,7 @@ def handle_defined_constants(cls, value, data_type=None, bit_size=None): case "NEG_INFINITY": return float("-inf") case _: - raise AttributeError(f"Could not convert constant: {value}") + raise ValueError(f"Could not convert constant: {value}") return value @@ -323,10 +323,10 @@ def calculate_range_value(cls, type, data_type, bit_size): if type == "MIN": value *= -1 case _: - raise AttributeError(f"Invalid bit size {bit_size} for FLOAT type.") + raise ValueError(f"Invalid bit size {bit_size} for FLOAT type.") case _: - raise AttributeError(f"Invalid data type {data_type} when calculating range.") + raise TypeError(f"Invalid data type {data_type} when calculating range.") return value diff --git a/openc3/python/openc3/conversions/object_read_conversion.py b/openc3/python/openc3/conversions/object_read_conversion.py index dcc836b12..980600bc6 100644 --- a/openc3/python/openc3/conversions/object_read_conversion.py +++ b/openc3/python/openc3/conversions/object_read_conversion.py @@ -26,7 +26,7 @@ def __init__(self, cmd_or_tlm, target_name, packet_name): if cmd_or_tlm: self.cmd_or_tlm = str(cmd_or_tlm).upper() if self.cmd_or_tlm not in ["CMD", "TLM", "COMMAND", "TELEMETRY"]: - raise AttributeError(f"Unknown type:{cmd_or_tlm}") + raise TypeError(f"Unknown type: {cmd_or_tlm}") else: # Unknown - Will need to search self.cmd_or_tlm = None diff --git a/openc3/python/openc3/conversions/processor_conversion.py b/openc3/python/openc3/conversions/processor_conversion.py index 0e9f59ff5..a7551d47f 100644 --- a/openc3/python/openc3/conversions/processor_conversion.py +++ b/openc3/python/openc3/conversions/processor_conversion.py @@ -41,7 +41,7 @@ def __init__( if ConfigParser.handle_none(converted_type): self.converted_type = str(converted_type).upper() if self.converted_type not in BinaryAccessor.DATA_TYPES: - raise AttributeError(f"Unknown converted type: {converted_type}") + raise TypeError(f"Unknown converted type: {converted_type}") self.params.append(self.converted_type) if ConfigParser.handle_none(converted_bit_size): self.converted_bit_size = int(converted_bit_size) diff --git a/openc3/python/openc3/interfaces/mqtt_interface.py b/openc3/python/openc3/interfaces/mqtt_interface.py new file mode 100644 index 000000000..6f9755f99 --- /dev/null +++ b/openc3/python/openc3/interfaces/mqtt_interface.py @@ -0,0 +1,212 @@ +# Copyright 2024 OpenC3, Inc. +# All Rights Reserved. +# +# This program is free software; you can modify and/or redistribute it +# under the terms of the GNU Affero General Public License +# as published by the Free Software Foundation; version 3 with +# attribution addendums as found in the LICENSE.txt +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# This file may also be used under the terms of a commercial license +# if purchased from OpenC3, Inc. + +# You can quickly setup an unauthenticated MQTT server in Docker with +# docker run -it -p 1883:1883 eclipse-mosquitto:2.0.15 mosquitto -c /mosquitto-no-auth.conf +# You can also test against encrypted and authenticated servers at https://test.mosquitto.org/ + +from time import sleep +import queue +import tempfile +from openc3.interfaces.interface import Interface +from openc3.system.system import System +from openc3.utilities.logger import Logger + +# See https://eclipse.dev/paho/files/paho.mqtt.python/html/client.html +import paho.mqtt.client as mqtt + + +# Base class for interfaces that send and receive messages over MQTT +class MqttInterface(Interface): + # @param hostname [String] MQTT server to connect to + # @param port [Integer] MQTT port + def __init__(self, hostname, port=1883): + super().__init__() + self.hostname = hostname + self.port = int(port) + self.ack_timeout = 5.0 + self.username = None + self.password = None + self.cert = None + self.key = None + self.ca_file = None + self.keyfile_password = None + + self.read_topics = [] + self.write_topics = [] + self.pkt_queue = queue.Queue() + + # Build list of packets by topic + self.read_packets_by_topic = {} + for _, target_packets in System.telemetry.all().items(): + for _, packet in target_packets.items(): + topics = packet.meta.get("TOPIC") + if not topics: + topics = packet.meta.get("TOPICS") + if topics: + for topic in topics: + self.read_packets_by_topic[topic] = packet + + def connection_string(self): + return f"{self.hostname}:{self.port}" + + # Connects the interface to its target(s) + def connect(self): + self.read_topics = [] + self.write_topics = [] + self.pkt_queue.empty() + + self.client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2) + self.client.on_connect = self.on_connect + self.client.on_message = self.on_message + self.client.user_data_set(self.pkt_queue) # passed to on_message + + if self.username and self.password: + self.client.username_pw_set(self.username, self.password) + # You still need the ca_file if you're using your own cert and key + if self.cert and self.key and self.ca_file: + if self.keyfile_password: + self.client.tls_set( + ca_certs=self.ca_file.name, + certfile=self.cert.name, + keyfile=self.key.name, + keyfile_password=self.keyfile_password, + ) + else: + self.client.tls_set(ca_certs=self.ca_file.name, certfile=self.cert.name, keyfile=self.key.name) + elif self.ca_file: + self.client.tls_set(ca_certs=self.ca_file.name) + + self.client.loop_start() + # Connect doesn't fully establish the connection, it just sends the CONNECT packet + # When the client loop receives an ONNACK packet from the broker in response to the CONNECT packet + # it calls the on_connect callback and updates the client state to connected (is_connected() returns True) + self.client.connect(self.hostname, self.port) + i = 0 + while not self.client.is_connected() and i < (self.ack_timeout * 100): + sleep(0.01) + i += 1 + super().connect() + + def on_connect(self, client, userdata, flags, reason_code, properties): + if reason_code.is_failure: + Logger.error(f"MQTT failed to connect: {reason_code}") + else: + # we should always subscribe from on_connect callback to be sure + # our subscribed is persisted across reconnections. + for topic, _ in self.read_packets_by_topic.items(): + client.subscribe(topic) + + # @return [Boolean] Whether the MQTT client is connected + def connected(self): + if self.client: + return self.client.is_connected() + else: + return False + + # Disconnects the interface from its target(s) + def disconnect(self): + self.pkt_queue.put(None) + if self.client: + self.client.disconnect() + self.client = None + super().disconnect() + + def read(self): + packet = super().read() + if packet is not None: + topic = self.read_topics.pop(0) + identified_packet = self.read_packets_by_topic.get(topic) + if identified_packet: + identified_packet = identified_packet.clone() + identified_packet.buffer = packet.buffer + packet = identified_packet + packet.received_time = None + return packet + + def write(self, packet): + self.write_mutex.acquire() + topics = packet.meta.get("TOPIC") + if not topics: + topics = packet.meta.get("TOPICS") + if topics: + for topic in topics: + self.write_topics.append(topic) + super().write(packet) + else: + raise RuntimeError( + f"Command packet '{packet.target_name} {packet.packet_name}' requires a META TOPIC or TOPICS" + ) + self.write_mutex.release() + + def on_message(self, client, userdata, message): + # userdata is set via user_data_set + self.read_topics.append(message.topic) + userdata.put(message.payload) + + def read_interface(self): + data = self.pkt_queue.get(block=True) + extra = None + if data is not None: + self.read_interface_base(data, extra) + return (data, extra) + + # Writes to the client + # @param data [String] Raw packet data + def write_interface(self, data, extra=None): + self.write_interface_base(data, extra) + try: + topic = self.write_topics.pop(0) + except IndexError: + raise RuntimeError(f"write_interface called with no topics: {self.write_topics}") + info = self.client.publish(topic, data) + # This more closely matches the ruby implementation + info.wait_for_publish(timeout=self.ack_timeout) + + # Supported Options + # USERNAME - Username for Mqtt Server + # PASSWORD - Password for Mqtt Server + # CERT - Public Key for Client Cert Auth + # KEY - Private Key for Client Cert Auth + # CA_FILE - Certificate Authority for Client Cert Auth + # KEYFILE_PASSWORD - Password for encrypted cert and key files + # (see Interface#set_option) + def set_option(self, option_name, option_values): + super().set_option(option_name, option_values) + match option_name.upper(): + case "ACK_TIMEOUT": + self.ack_timeout = float(option_values[0]) + case "USERNAME": + self.username = option_values[0] + case "PASSWORD": + self.password = option_values[0] + case "CERT": + # CERT must be given as a file + self.cert = tempfile.NamedTemporaryFile(mode="w+", delete=False) + self.cert.write(option_values[0]) + self.cert.close() + case "KEY": + # KEY must be given as a file + self.key = tempfile.NamedTemporaryFile(mode="w+", delete=False) + self.key.write(option_values[0]) + self.key.close() + case "CA_FILE": + # CA_FILE must be given as a file + self.ca_file = tempfile.NamedTemporaryFile(mode="w+", delete=False) + self.ca_file.write(option_values[0]) + self.ca_file.close() + case "KEYFILE_PASSWORD": + self.keyfile_password = option_values[0] diff --git a/openc3/python/openc3/interfaces/mqtt_stream_interface.py b/openc3/python/openc3/interfaces/mqtt_stream_interface.py new file mode 100644 index 000000000..ebf445624 --- /dev/null +++ b/openc3/python/openc3/interfaces/mqtt_stream_interface.py @@ -0,0 +1,103 @@ +# Copyright 2024 OpenC3, Inc. +# All Rights Reserved. +# +# This program is free software; you can modify and/or redistribute it +# under the terms of the GNU Affero General Public License +# as published by the Free Software Foundation; version 3 with +# attribution addendums as found in the LICENSE.txt +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# This file may also be used under the terms of a commercial license +# if purchased from OpenC3, Inc. + +# You can quickly setup an unauthenticated MQTT server in Docker with +# docker run -it -p 1883:1883 eclipse-mosquitto:2.0.15 mosquitto -c /mosquitto-no-auth.conf +# You can also test against encrypted and authenticated servers at https://test.mosquitto.org/ + +import tempfile +from openc3.interfaces.stream_interface import StreamInterface +from openc3.config.config_parser import ConfigParser +from openc3.streams.mqtt_stream import MqttStream + + +# Base class for interfaces that send and receive messages over MQTT +class MqttStreamInterface(StreamInterface): + # @param hostname [String] MQTT server to connect to + # @param port [Integer] MQTT port + def __init__(self, hostname, port=1883, write_topic=None, read_topic=None, protocol_type=None, protocol_args=[]): + super().__init__(protocol_type, protocol_args) + self.hostname = hostname + self.port = int(port) + self.write_topic = ConfigParser.handle_none(write_topic) + self.read_topic = ConfigParser.handle_none(read_topic) + self.ack_timeout = 5.0 + self.username = None + self.password = None + self.cert = None + self.key = None + self.ca_file = None + self.keyfile_password = None + + def connection_string(self): + result = f"{self.hostname}:{self.port}" + if self.write_topic is not None: + result += f" write topic: {self.write_topic}" + if self.read_topic is not None: + result += f" read topic: {self.read_topic}" + return result + + # Creates a new {SerialStream} using the parameters passed in the constructor + def connect(self): + self.stream = MqttStream(self.hostname, self.port, self.write_topic, self.read_topic, self.ack_timeout) + if self.username: + self.stream.username = self.username + if self.password: + self.stream.password = self.password + if self.cert: + self.stream.cert = self.cert + if self.key: + self.stream.key = self.key + if self.ca_file: + self.stream.ca_file = self.ca_file + if self.keyfile_password: + self.stream.keyfile_password = self.keyfile_password + super().connect() + + # Supported Options + # USERNAME - Username for Mqtt Server + # PASSWORD - Password for Mqtt Server + # CERT - Public Key for Client Cert Auth + # KEY - Private Key for Client Cert Auth + # CA_FILE - Certificate Authority for Client Cert Auth + # KEYFILE_PASSWORD - Password for encrypted cert and key files + # (see Interface#set_option) + def set_option(self, option_name, option_values): + super().set_option(option_name, option_values) + match option_name.upper(): + case "ACK_TIMEOUT": + self.ack_timeout = float(option_values[0]) + case "USERNAME": + self.username = option_values[0] + case "PASSWORD": + self.password = option_values[0] + case "CERT": + # CERT must be given as a file + self.cert = tempfile.NamedTemporaryFile(mode="w+", delete=False) + self.cert.write(option_values[0]) + self.cert.close() + case "KEY": + # KEY must be given as a file + self.key = tempfile.NamedTemporaryFile(mode="w+", delete=False) + self.key.write(option_values[0]) + self.key.close() + case "CA_FILE": + # CA_FILE must be given as a file + self.ca_file = tempfile.NamedTemporaryFile(mode="w+", delete=False) + self.ca_file.write(option_values[0]) + self.ca_file.close() + case "KEYFILE_PASSWORD": + self.keyfile_password = option_values[0] diff --git a/openc3/python/openc3/interfaces/protocols/length_protocol.py b/openc3/python/openc3/interfaces/protocols/length_protocol.py index 816c1704d..e42feea3f 100644 --- a/openc3/python/openc3/interfaces/protocols/length_protocol.py +++ b/openc3/python/openc3/interfaces/protocols/length_protocol.py @@ -135,7 +135,7 @@ def write_data(self, data, extra=None): def calculate_length(self, buffer_length): length = int(buffer_length / self.length_bytes_per_count) - self.length_value_offset if self.max_length and length > self.max_length: - raise AttributeError(f"Calculated length {length} larger than max_length {self.max_length}") + raise ValueError(f"Calculated length {length} larger than max_length {self.max_length}") return length def reduce_to_single_packet(self, extra=None): @@ -152,13 +152,13 @@ def reduce_to_single_packet(self, extra=None): self.length_endianness, ) if self.max_length and length > self.max_length: - raise AttributeError(f"Length value received larger than max_length= {length} > {self.max_length}") + raise ValueError(f"Length value received larger than max_length= {length} > {self.max_length}") packet_length = (length * self.length_bytes_per_count) + self.length_value_offset # Ensure the calculated packet length is long enough to support the location of the length field # without overlap into the next packet if (packet_length * 8) < (self.length_bit_offset + self.length_bit_size): - raise AttributeError( + raise ValueError( f"Calculated packet length of {packet_length * 8} bits < (offset={self.length_bit_offset} + size={self.length_bit_size})" ) diff --git a/openc3/python/openc3/interfaces/protocols/template_protocol.py b/openc3/python/openc3/interfaces/protocols/template_protocol.py index 1911b8059..fe4114376 100644 --- a/openc3/python/openc3/interfaces/protocols/template_protocol.py +++ b/openc3/python/openc3/interfaces/protocols/template_protocol.py @@ -207,7 +207,7 @@ def write_packet(self, packet): self.response_template = None self.response_packet = None self.response_target_name = None - except AttributeError: + except Exception: # If there is no response template we set to nil self.response_template = None self.response_packet = None diff --git a/openc3/python/openc3/interfaces/stream_interface.py b/openc3/python/openc3/interfaces/stream_interface.py index eb7e085d7..9b9845e5d 100644 --- a/openc3/python/openc3/interfaces/stream_interface.py +++ b/openc3/python/openc3/interfaces/stream_interface.py @@ -1,4 +1,4 @@ -# Copyright 2023 OpenC3, Inc. +# Copyright 2024 OpenC3, Inc. # All Rights Reserved. # # This program is free software; you can modify and/or redistribute it @@ -41,7 +41,7 @@ def connect(self): def connected(self): if self.stream: - return self.stream.connected + return self.stream.connected() else: return False diff --git a/openc3/python/openc3/interfaces/tcpip_server_interface.py b/openc3/python/openc3/interfaces/tcpip_server_interface.py index d58dc73d5..78a03b338 100644 --- a/openc3/python/openc3/interfaces/tcpip_server_interface.py +++ b/openc3/python/openc3/interfaces/tcpip_server_interface.py @@ -613,4 +613,4 @@ def _write_to_clients(self, method, packet_or_data): # Delete any dead sockets for index_to_delete in indexes_to_delete: - self.write_interface_infos.delete_at(index_to_delete) + del self.write_interface_infos[index_to_delete] diff --git a/openc3/python/openc3/microservices/interface_microservice.py b/openc3/python/openc3/microservices/interface_microservice.py index fe9f8bde4..32a45f6e9 100644 --- a/openc3/python/openc3/microservices/interface_microservice.py +++ b/openc3/python/openc3/microservices/interface_microservice.py @@ -233,7 +233,7 @@ def process_cmd(self, topic, msg_id, msg_hash, redis): else: raise RuntimeError(f"Invalid command received:\n{msg_hash}") command.received_time = datetime.now(timezone.utc) - except (AttributeError, RuntimeError) as error: + except Exception as error: self.logger.error(f"{self.interface.name}: {msg_hash}") self.logger.error(f"{self.interface.name}: {repr(error)}") return repr(error) diff --git a/openc3/python/openc3/models/cvt_model.py b/openc3/python/openc3/models/cvt_model.py index 4df3de138..65f32b9d4 100644 --- a/openc3/python/openc3/models/cvt_model.py +++ b/openc3/python/openc3/models/cvt_model.py @@ -398,7 +398,7 @@ def _parse_item(cls, now, lookups, overrides, item, cache_timeout, scope): item_name, ] case _: - raise RuntimeError(f"Unknown value type '{value_type}'") + raise ValueError(f"Unknown value type '{value_type}'") # Check the overrides cache for this target / packet tgt_pkt_key = f"{scope}__tlm__{target_name}__{packet_name}" diff --git a/openc3/python/openc3/packets/limits.py b/openc3/python/openc3/packets/limits.py index a4654c967..409798137 100644 --- a/openc3/python/openc3/packets/limits.py +++ b/openc3/python/openc3/packets/limits.py @@ -38,14 +38,6 @@ def warnings(self): def sets(self): return self.config.limits_sets - # (see OpenC3::Packet#out_of_limits) - def out_of_limits(self): - items = [] - for _, target_packets in self.config.telemetry: - for _, packet in target_packets: - items += packet.out_of_limits() - return items - # @return [Hash(String, Array)] The defined limits groups def groups(self): return self.config.limits_groups @@ -86,6 +78,11 @@ def get(self, target_name, packet_name, item_name, limits_set=None): limits_set = self.system.limits_set() limits_for_set = limits.values.get(limits_set) if limits_for_set is not None: + gl = None + gh = None + if len(limits_for_set) > 4: + gl = limits_for_set[4] + gh = limits_for_set[5] return [ limits_set, limits.persistence_setting, @@ -94,8 +91,7 @@ def get(self, target_name, packet_name, item_name, limits_set=None): limits_for_set[1], limits_for_set[2], limits_for_set[3], - limits_for_set[4], - limits_for_set[5], + gl, gh, ] else: return [None, None, None, None, None, None, None, None, None] @@ -138,7 +134,7 @@ def set( if limits_set: limits_set = str(limits_set).upper() else: - limits_set = self.system.limits_set + limits_set = self.system.limits_set() if limits.values is None: if limits_set == "DEFAULT": limits.values = {"DEFAULT": []} diff --git a/openc3/python/openc3/packets/packet.py b/openc3/python/openc3/packets/packet.py index ee8604361..94b1cd3d8 100644 --- a/openc3/python/openc3/packets/packet.py +++ b/openc3/python/openc3/packets/packet.py @@ -110,7 +110,7 @@ def target_name(self, target_name): will have target name set to None.""" if target_name is not None: if not isinstance(target_name, str): - raise AttributeError(f"target_name must be a str but is a {target_name.__class__.__name__}") + raise TypeError(f"target_name must be a str but is a {target_name.__class__.__name__}") self.__target_name = target_name.upper() else: @@ -125,7 +125,7 @@ def packet_name(self, packet_name): """Sets the packet name. Unidentified packets will have packet name set to None""" if packet_name is not None: if not isinstance(packet_name, str): - raise AttributeError(f"packet_name must be a str but is a {packet_name.__class__.__name__}") + raise TypeError(f"packet_name must be a str but is a {packet_name.__class__.__name__}") self.__packet_name = packet_name.upper() else: @@ -140,7 +140,7 @@ def description(self, description): """Sets the packet description""" if description is not None: if not isinstance(description, str): - raise AttributeError(f"description must be a str but is a {description.__class__.__name__}") + raise TypeError(f"description must be a str but is a {description.__class__.__name__}") self.__description = description else: @@ -172,7 +172,7 @@ def received_time(self, received_time): """Sets the received time of the packet""" if received_time is not None: if not isinstance(received_time, datetime.datetime): - raise AttributeError(f"received_time must be a datetime but is a {received_time.__class__.__name__}") + raise TypeError(f"received_time must be a datetime but is a {received_time.__class__.__name__}") self.__received_time = received_time self.read_conversion_cache = {} else: @@ -186,7 +186,7 @@ def received_count(self): def received_count(self, received_count): """Sets the packet name. Unidentified packets will have packet name set to None""" if not isinstance(received_count, int): - raise AttributeError(f"received_count must be an int but is a {received_count.__class__.__name__}") + raise TypeError(f"received_count must be an int but is a {received_count.__class__.__name__}") self.__received_count = received_count self.read_conversion_cache = {} @@ -225,7 +225,8 @@ def identify(self, buffer): for item in self.id_items: try: value = self.read_item(item, "RAW", buffer) - except AttributeError: + # If the item is not found in the buffer, return None + except Exception: value = None if item.id_value != value: return False @@ -246,7 +247,8 @@ def read_id_values(self, buffer): for item in self.id_items: try: values.append(self.read_item(item, "RAW", buffer)) - except AttributeError: + # If the item is not found in the buffer, append None + except Exception: values.append(None) return values @@ -275,7 +277,7 @@ def buffer(self, buffer): with self.synchronize(): try: self.internal_buffer_equals(buffer) - except AttributeError: + except Exception: Logger.error( f"{self.target_name} {self.packet_name} buffer ({type(buffer)}) received with actual packet length of {len(buffer)} but defined length of {self.defined_length}" ) @@ -298,7 +300,7 @@ def hazardous_description(self, hazardous_description): """Sets the packet hazardous_description""" if hazardous_description is not None: if not isinstance(hazardous_description, str): - raise AttributeError( + raise TypeError( f"hazardous_description must be a str but is a {hazardous_description.__class__.__name__}" ) @@ -315,7 +317,7 @@ def given_values(self, given_values): """Sets the packet given_values""" if given_values is not None: if not isinstance(given_values, dict): - raise AttributeError(f"given_values must be a dict but is a {given_values.__class__.__name__}") + raise TypeError(f"given_values must be a dict but is a {given_values.__class__.__name__}") self.__given_values = given_values else: @@ -338,7 +340,7 @@ def template(self, template): """Sets the packet template""" if template is not None: if not isinstance(template, (bytes, bytearray)): - raise AttributeError(f"template must be bytes but is a {template.__class__.__name__}") + raise TypeError(f"template must be bytes but is a {template.__class__.__name__}") self.__template = template else: @@ -500,8 +502,8 @@ def append_item( def get_item(self, name): try: return super().get_item(name) - except AttributeError: - raise AttributeError(f"Packet item '{self.target_name} {self.packet_name} {name.upper()}' does not exist") + except ValueError as error: + raise RuntimeError(f"Packet item '{self.target_name} {self.packet_name} {name.upper()}' does not exist") from error # Read an item in the packet # @@ -598,7 +600,7 @@ def read_item(self, item, value_type="CONVERTED", buffer=None, given_raw=None): if not value_type.isascii(): value_type = simple_formatted(value_type) value_type += "..." - raise AttributeError( + raise ValueError( f"Unknown value type '{value_type}', must be 'RAW', 'CONVERTED', 'FORMATTED', or 'WITH_UNITS'" ) return value @@ -657,7 +659,7 @@ def write_item(self, item, value, value_type="CONVERTED", buffer=None): else: raise error case "FORMATTED" | "WITH_UNITS": - raise AttributeError(f"Invalid value type on write= {value_type}") + raise ValueError(f"Invalid value type on write: {value_type}") case _: # Trim potentially long string (like if they accidentally pass buffer as value_type): if len(str(value_type)) > 10: @@ -666,7 +668,7 @@ def write_item(self, item, value, value_type="CONVERTED", buffer=None): if not value_type.isascii(): value_type = simple_formatted(value_type) value_type += "..." - raise AttributeError( + raise ValueError( f"Unknown value type '{value_type}', must be 'RAW', 'CONVERTED', 'FORMATTED', or 'WITH_UNITS'" ) with self.synchronize(): diff --git a/openc3/python/openc3/packets/packet_config.py b/openc3/python/openc3/packets/packet_config.py index 1171d6696..eabec1866 100644 --- a/openc3/python/openc3/packets/packet_config.py +++ b/openc3/python/openc3/packets/packet_config.py @@ -397,7 +397,7 @@ def process_current_packet(self, parser, keyword, params): self.current_item = self.current_packet.get_item(params[0]) else: # DELETE self.current_packet.delete_item(params[0]) - except AttributeError: # Rescue the default exception to provide a nicer error message + except Exception: # Rescue the default exception to provide a nicer error message raise parser.error( f"{params[0]} not found in {self.current_cmd_or_tlm.lower()} packet {self.current_packet.target_name} {self.current_packet.packet_name}", usage, diff --git a/openc3/python/openc3/packets/packet_item.py b/openc3/python/openc3/packets/packet_item.py index 71a3110fa..2ea578159 100644 --- a/openc3/python/openc3/packets/packet_item.py +++ b/openc3/python/openc3/packets/packet_item.py @@ -66,11 +66,11 @@ def format_string(self): def format_string(self, format_string): if format_string: if not isinstance(format_string, str): - raise AttributeError( + raise TypeError( f"{self.name}: format_string must be a str but is a {format_string.__class__.__name__}" ) if not re.search(r"%.*(b|B|d|i|o|u|x|X|e|E|f|g|G|a|A|c|p|s|%)", format_string): - raise AttributeError(f"{self.name}: format_string invalid '{format_string}'") + raise ValueError(f"{self.name}: format_string invalid '{format_string}'") self.__format_string = format_string else: self.__format_string = None @@ -83,7 +83,7 @@ def read_conversion(self): def read_conversion(self, read_conversion): if read_conversion: if not isinstance(read_conversion, Conversion): - raise AttributeError( + raise TypeError( f"{self.name}: read_conversion must be a Conversion but is a {read_conversion.__class__.__name__}" ) self.__read_conversion = read_conversion @@ -98,7 +98,7 @@ def write_conversion(self): def write_conversion(self, write_conversion): if write_conversion: if not isinstance(write_conversion, Conversion): - raise AttributeError( + raise TypeError( f"{self.name}: write_conversion must be a Conversion but is a {write_conversion.__class__.__name__}" ) self.__write_conversion = write_conversion @@ -125,7 +125,7 @@ def states(self): def states(self, states): if states is not None: if not isinstance(states, dict): - raise AttributeError(f"{self.name}: states must be a dict but is a {states.__class__.__name__}") + raise TypeError(f"{self.name}: states must be a dict but is a {states.__class__.__name__}") # Make sure all states are in upper case self.__states = {} @@ -151,7 +151,7 @@ def description(self): def description(self, description): if description: if not isinstance(description, str): - raise AttributeError( + raise TypeError( f"{self.name}: description must be a str but is a {description.__class__.__name__}" ) self.__description = description @@ -166,7 +166,7 @@ def units_full(self): def units_full(self, units_full): if units_full: if not isinstance(units_full, str): - raise AttributeError(f"{self.name}: units_full must be a str but is a {units_full.__class__.__name__}") + raise TypeError(f"{self.name}: units_full must be a str but is a {units_full.__class__.__name__}") self.__units_full = units_full else: self.__units_full = None @@ -179,7 +179,7 @@ def units(self): def units(self, units): if units: if not isinstance(units, str): - raise AttributeError(f"{self.name}: units must be a str but is a {units.__class__.__name__}") + raise TypeError(f"{self.name}: units must be a str but is a {units.__class__.__name__}") self.__units = units else: self.__units = None @@ -188,43 +188,43 @@ def check_default_and_range_data_types(self): if self.default and not self.write_conversion: if self.array_size is not None: if not isinstance(self.default, list): - raise AttributeError( + raise TypeError( f"{self.name}: default must be a list but is a {self.default.__class__.__name__}" ) else: match self.data_type: case "INT" | "UINT": if not isinstance(self.default, int): - raise AttributeError( + raise TypeError( f"{self.name}: default must be a int but is a {self.default.__class__.__name__}" ) if not isinstance(self.minimum, int): - raise AttributeError( + raise TypeError( f"{self.name}: minimum must be a int but is a {self.minimum.__class__.__name__}" ) if not isinstance(self.maximum, int): - raise AttributeError( + raise TypeError( f"{self.name}: maximum must be a int but is a {self.maximum.__class__.__name__}" ) case "FLOAT": if not isinstance(self.default, (float, int)): - raise AttributeError( + raise TypeError( f"{self.name}: default must be a float but is a {self.default.__class__.__name__}" ) self.default = float(self.default) if not isinstance(self.minimum, (int, float)): - raise AttributeError( + raise TypeError( f"{self.name}: minimum must be a float but is a {self.minimum.__class__.__name__}" ) if not isinstance(self.maximum, (int, float)): - raise AttributeError( + raise TypeError( f"{self.name}: maximum must be a float but is a {self.maximum.__class__.__name__}" ) case "BLOCK" | "STRING": if not isinstance(self.default, (str, bytes, bytearray)): - raise AttributeError( + raise TypeError( f"{self.name}: default must be a str but is a {self.default.__class__.__name__}" ) self.default = str(self.default) @@ -237,7 +237,7 @@ def hazardous(self): def hazardous(self, hazardous): if hazardous is not None: if not isinstance(hazardous, dict): - raise AttributeError(f"{self.name}: hazardous must be a dict but is a {hazardous.__class__.__name__}") + raise TypeError(f"{self.name}: hazardous must be a dict but is a {hazardous.__class__.__name__}") self.__hazardous = hazardous else: self.__hazardous = None @@ -250,7 +250,7 @@ def messages_disabled(self): def messages_disabled(self, messages_disabled): if messages_disabled is not None: if not isinstance(messages_disabled, dict): - raise AttributeError( + raise TypeError( f"{self.name}: messages_disabled must be a dict but is a {messages_disabled.__class__.__name__}" ) @@ -266,7 +266,7 @@ def state_colors(self): def state_colors(self, state_colors): if state_colors is not None: if not isinstance(state_colors, dict): - raise AttributeError( + raise TypeError( f"{self.name}: state_colors must be a dict but is a {state_colors.__class__.__name__}" ) @@ -282,7 +282,7 @@ def limits(self): def limits(self, limits): if limits is not None: if not isinstance(limits, PacketItemLimits): - raise AttributeError( + raise TypeError( f"{self.name}: limits must be a PacketItemLimits but is a {limits.__class__.__name__}" ) @@ -298,7 +298,7 @@ def meta(self): def meta(self, meta): if meta is not None: if not isinstance(meta, dict): - raise AttributeError(f"{self.name}: meta must be a dict but is a {meta.__class__.__name__}") + raise TypeError(f"{self.name}: meta must be a dict but is a {meta.__class__.__name__}") self.__meta = meta else: diff --git a/openc3/python/openc3/packets/packet_item_limits.py b/openc3/python/openc3/packets/packet_item_limits.py index c89ead244..9e92ec933 100644 --- a/openc3/python/openc3/packets/packet_item_limits.py +++ b/openc3/python/openc3/packets/packet_item_limits.py @@ -59,9 +59,9 @@ def values(self): def values(self, values): if values is not None: if not isinstance(values, dict): - raise AttributeError(f"values must be a Hash but is a {values.__class__.__name__}") + raise TypeError(f"values must be a Hash but is a {values.__class__.__name__}") if "DEFAULT" not in values: - raise AttributeError("values must be a Hash with a 'DEFAULT' key") + raise ValueError("values must be a Hash with a 'DEFAULT' key") self.__values = values else: self.__values = None @@ -73,7 +73,7 @@ def state(self): @state.setter def state(self, state): if state not in PacketItemLimits.LIMITS_STATES: - raise AttributeError(f"state must be one of {PacketItemLimits.LIMITS_STATES} but is {state}") + raise ValueError(f"state must be one of {PacketItemLimits.LIMITS_STATES} but is {state}") self.__state = state @property @@ -84,58 +84,7 @@ def response(self): def response(self, response): if response is not None: if "LimitsResponse" not in response.__class__.__name__: - raise AttributeError(f"response must be a LimitsResponse but is a {response.__class__.__name__}") + raise TypeError(f"response must be a LimitsResponse but is a {response.__class__.__name__}") self.__response = response else: self.__response = None - - # def persistence_setting=(persistence_setting): - # if 0.__class__.__name__ == Integer: - # # Ruby version >= 2.4.0 - # raise AttributeError(f"persistence_setting must be an Integer but is a {persistence_setting.__class__.__name__}" unless Integer === persistence_setting - # else: - # # Ruby version < 2.4.0 - # raise AttributeError(f"persistence_setting must be a Fixnum but is a {persistence_setting.__class__.__name__}" unless Fixnum === persistence_setting - # self.persistence_setting = persistence_setting - - # def persistence_count=(persistence_count): - # if 0.__class__.__name__ == Integer: - # # Ruby version >= 2.4.0 - # raise AttributeError(f"persistence_count must be an Integer but is a {persistence_count.__class__.__name__}" unless Integer === persistence_count - # else: - # # Ruby version < 2.4.0 - # raise AttributeError(f"persistence_count must be a Fixnum but is a {persistence_count.__class__.__name__}" unless Fixnum === persistence_count - # self.persistence_count = persistence_count - - # # Make a light weight clone of this limits - # def clone: - # limits = super() - # limits.values = self.values.clone if self.values: - # limits.response = self.response.clone if self.response: - # limits - # alias dup clone - - # def as_json(*a): - # hash = {} - # hash['values'] = self.values - # hash['enabled'] = self.enabled - # hash['state'] = self.state - # if self.response: - # hash['response'] = self.response.to_s - # else: - # hash['response'] = None - # hash['persistence_setting'] = self.persistence_setting - # hash['persistence_count'] = self.persistence_count - # hash - - # @classmethod - # def from_json(cls, hash): - # limits = PacketItemLimits() - # limits.values = hash['values'].transform_keys(&:to_sym) if hash['values']: - # limits.enabled = hash['enabled'] - # limits.state = hash['state'] ? hash['state'].to_sym : None - # # Can't recreate a LimitsResponse class - # # limits.response = hash['response'] - # limits.persistence_setting = hash['persistence_setting'] if hash['persistence_setting']: - # limits.persistence_count = hash['persistence_count'] if hash['persistence_count']: - # limits diff --git a/openc3/python/openc3/packets/parsers/packet_item_parser.py b/openc3/python/openc3/packets/parsers/packet_item_parser.py index 9f0a1ee31..a9266ea35 100644 --- a/openc3/python/openc3/packets/parsers/packet_item_parser.py +++ b/openc3/python/openc3/packets/parsers/packet_item_parser.py @@ -87,7 +87,7 @@ def create_packet_item(self, packet, cmd_or_tlm): else: item = packet.define(item) return item - except AttributeError as error: + except Exception as error: raise self.parser.error(error, self.usage) def _append(self): diff --git a/openc3/python/openc3/packets/parsers/packet_parser.py b/openc3/python/openc3/packets/parsers/packet_parser.py index 91b3b474d..7b785a6e6 100644 --- a/openc3/python/openc3/packets/parsers/packet_parser.py +++ b/openc3/python/openc3/packets/parsers/packet_parser.py @@ -37,10 +37,10 @@ def check_item_data_types(cls, packet): for item in packet.sorted_items: item.check_default_and_range_data_types() - except AttributeError as error: + except TypeError as error: # Add the target name and packet name to the error message so the user # can debug where the error occurred - raise AttributeError(f"{packet.target_name} {packet.packet_name} {error}") from error + raise TypeError(f"{packet.target_name} {packet.packet_name} {error}") from error @classmethod def _check_for_duplicate(cls, type, list, packet): diff --git a/openc3/python/openc3/packets/parsers/processor_parser.py b/openc3/python/openc3/packets/parsers/processor_parser.py index 90b00d85c..cfa78cb1e 100644 --- a/openc3/python/openc3/packets/parsers/processor_parser.py +++ b/openc3/python/openc3/packets/parsers/processor_parser.py @@ -54,7 +54,7 @@ def create_processor(self, packet): else: processor = klass() if not isinstance(processor, Processor): - raise AttributeError(f"processor must be a Processor but is a {processor.__class__.__name__}") + raise TypeError(f"processor must be a Processor but is a {processor.__class__.__name__}") processor.name = self._get_processor_name() packet.processors[processor.name] = processor diff --git a/openc3/python/openc3/packets/structure.py b/openc3/python/openc3/packets/structure.py index c211ea616..2d2fe37e0 100644 --- a/openc3/python/openc3/packets/structure.py +++ b/openc3/python/openc3/packets/structure.py @@ -56,7 +56,7 @@ def __init__( self.mutex = None self.accessor = BinaryAccessor(self) else: - raise AttributeError(f"Unknown endianness '{default_endianness}', must be 'BIG_ENDIAN' or 'LITTLE_ENDIAN'") + raise ValueError(f"Unknown endianness '{default_endianness}', must be 'BIG_ENDIAN' or 'LITTLE_ENDIAN'") # Read an item in the structure # @@ -303,7 +303,7 @@ def append(self, item): def get_item(self, name): item = self.items.get(name.upper()) if not item: - raise AttributeError(f"Unknown item: {name}") + raise ValueError(f"Unknown item: {name}") return item # self.param item [#name] Instance of StructureItem or one of its subclasses. @@ -327,13 +327,13 @@ def set_item(self, item): if minimum_data_bits > 0 and item.bit_offset >= 0 and self.defined_length_bits == item.bit_offset: self.defined_length_bits += minimum_data_bits else: - raise AttributeError(f"Unknown item: {item.name} - Ensure item name is uppercase") + raise ValueError(f"Unknown item: {item.name} - Ensure item name is uppercase") # self.param name [String] Name of the item to delete in the items Hash def delete_item(self, name): item = self.items[name.upper()] if not item: - raise AttributeError(f"Unknown item: {name}") + raise RuntimeError(f"Unknown item: {name}") # Find the item to delete in the sorted_items array item_index = None @@ -547,7 +547,7 @@ def calculate_total_bit_size(self, item): # Bit size is full packet length - bits before item + negative bits saved at end return (len(self._buffer) * 8) - item.bit_offset + item.original_array_size else: - raise AttributeError("Unexpected use of calculate_total_bit_size for non-variable-sized item") + raise RuntimeError("Unexpected use of calculate_total_bit_size for non-variable-sized item") def recalculate_bit_offsets(self): adjustment = 0 @@ -566,7 +566,7 @@ def recalculate_bit_offsets(self): def internal_buffer_equals(self, buffer): if not isinstance(buffer, (bytes, bytearray)): - raise AttributeError(f"Buffer class is {buffer.__class__.__name__} but must be bytearray") + raise TypeError(f"Buffer class is {buffer.__class__.__name__} but must be bytearray") self._buffer = bytearray(buffer[:]) if not self.fixed_size: @@ -578,6 +578,6 @@ def internal_buffer_equals(self, buffer): if len(self._buffer) < self.defined_length: self.resize_buffer() if not self.short_buffer_allowed: - raise AttributeError("Buffer length less than defined length") + raise ValueError("Buffer length less than defined length") elif self.fixed_size and self.defined_length != 0: - raise AttributeError("Buffer length greater than defined length") + raise ValueError("Buffer length greater than defined length") diff --git a/openc3/python/openc3/packets/structure_item.py b/openc3/python/openc3/packets/structure_item.py index a94df89da..9e5754a1d 100644 --- a/openc3/python/openc3/packets/structure_item.py +++ b/openc3/python/openc3/packets/structure_item.py @@ -75,9 +75,9 @@ def name(self): @name.setter def name(self, name): if not isinstance(name, str): - raise AttributeError(f"name must be a String but is a {name.__class__.__name__}") + raise TypeError(f"name must be a String but is a {name.__class__.__name__}") if len(name) == 0: - raise AttributeError("name must contain at least one character") + raise ValueError("name must contain at least one character") self.__name = name.upper() if self.structure_item_constructed: @@ -90,9 +90,9 @@ def key(self): @key.setter def key(self, key): if not isinstance(key, str): - raise AttributeError(f"key must be a String but is a {key.__class__.__name__}") + raise TypeError(f"key must be a String but is a {key.__class__.__name__}") if len(key) == 0: - raise AttributeError("key must contain at least one character") + raise ValueError("key must contain at least one character") self.__key = key @property @@ -102,9 +102,9 @@ def endianness(self): @endianness.setter def endianness(self, endianness): if not isinstance(endianness, str): - raise AttributeError(f"{self.name}: endianness must be a String but is a {endianness.__class__.__name__}") + raise TypeError(f"{self.name}: endianness must be a String but is a {endianness.__class__.__name__}") if endianness not in BinaryAccessor.ENDIANNESS: - raise AttributeError( + raise ValueError( f"{self.name}: unknown endianness: {endianness} - Must be 'BIG_ENDIAN' or 'LITTLE_ENDIAN'" ) self.__endianness = endianness @@ -118,16 +118,16 @@ def bit_offset(self): @bit_offset.setter def bit_offset(self, bit_offset): if not isinstance(bit_offset, int): - raise AttributeError(f"{self.name}: bit_offset must be an Integer") + raise TypeError(f"{self.name}: bit_offset must be an Integer") byte_aligned = (bit_offset % 8) == 0 if (self.data_type == "FLOAT" or self.data_type == "STRING" or self.data_type == "BLOCK") and not byte_aligned: - raise AttributeError( + raise ValueError( f"{self.name}: bit_offset for 'FLOAT', 'STRING', and 'BLOCK' items must be byte aligned" ) if self.data_type == "DERIVED" and bit_offset != 0: - raise AttributeError(f"{self.name}: DERIVED items must have bit_offset of zero") + raise ValueError(f"{self.name}: DERIVED items must have bit_offset of zero") self.__bit_offset = bit_offset if self.structure_item_constructed: @@ -140,17 +140,17 @@ def bit_size(self): @bit_size.setter def bit_size(self, bit_size): if not isinstance(bit_size, int): - raise AttributeError(f"{self.name}: bit_size must be an Integer") + raise TypeError(f"{self.name}: bit_size must be an Integer") byte_multiple = (bit_size % 8) == 0 if bit_size <= 0 and self.data_type == "FLOAT": - raise AttributeError(f"{self.name}: bit_size cannot be negative or zero for 'FLOAT' items: {bit_size}") + raise ValueError(f"{self.name}: bit_size cannot be negative or zero for 'FLOAT' items: {bit_size}") if (self.data_type == "STRING" or self.data_type == "BLOCK") and not byte_multiple: - raise AttributeError(f"{self.name}: bit_size for STRING and BLOCK items must be byte multiples") + raise ValueError(f"{self.name}: bit_size for STRING and BLOCK items must be byte multiples") if self.data_type == "FLOAT" and bit_size != 32 and bit_size != 64: - raise AttributeError(f"{self.name}: bit_size for FLOAT items must be 32 or 64. Given: {bit_size}") + raise ValueError(f"{self.name}: bit_size for FLOAT items must be 32 or 64. Given: {bit_size}") if self.data_type == "DERIVED" and bit_size != 0: - raise AttributeError(f"{self.name}: DERIVED items must have bit_size of zero") + raise ValueError(f"{self.name}: DERIVED items must have bit_size of zero") self.__bit_size = bit_size if self.structure_item_constructed: @@ -163,11 +163,11 @@ def data_type(self): @data_type.setter def data_type(self, data_type): if not isinstance(data_type, str): - raise AttributeError( + raise TypeError( f"{self.name}: data_type must be a str but {data_type} is a {type(data_type).__name__}" ) if data_type not in StructureItem.DATA_TYPES: - raise AttributeError( + raise ValueError( f"{self.name}: unknown data_type: {data_type} - Must be 'INT', 'UINT', 'FLOAT', 'STRING', 'BLOCK', or 'DERIVED'" ) @@ -183,11 +183,11 @@ def array_size(self): def array_size(self, array_size): if array_size is not None: if not isinstance(array_size, int): - raise AttributeError(f"{self.name}: array_size must be an Integer") + raise TypeError(f"{self.name}: array_size must be an Integer") if not (self.bit_size == 0 or (array_size % self.bit_size == 0) or array_size < 0): - raise AttributeError(f"{self.name}: array_size must be a multiple of bit_size") + raise ValueError(f"{self.name}: array_size must be a multiple of bit_size") if self.bit_size <= 0: - raise AttributeError(f"{self.name}: bit_size cannot be negative or zero for array items") + raise ValueError(f"{self.name}: bit_size cannot be negative or zero for array items") self.__array_size = array_size if self.structure_item_constructed: @@ -200,10 +200,10 @@ def overflow(self): @overflow.setter def overflow(self, overflow): if not isinstance(overflow, str): - raise AttributeError(f"{self.name}: overflow type must be a String") + raise TypeError(f"{self.name}: overflow type must be a String") if overflow not in BinaryAccessor.OVERFLOW_TYPES: - raise AttributeError( + raise ValueError( f"{self.name}: unknown overflow type: {overflow} - Must be 'ERROR', 'ERROR_ALLOW_HEX', 'TRUNCATE', or 'SATURATE'" ) @@ -219,13 +219,13 @@ def variable_bit_size(self): def variable_bit_size(self, variable_bit_size): if variable_bit_size: if not isinstance(variable_bit_size, dict): - raise AttributeError(f"{self.name}: variable_bit_size must be a dict") + raise TypeError(f"{self.name}: variable_bit_size must be a dict") if not isinstance(variable_bit_size["length_item_name"], str): - raise AttributeError(f"{self.name}: variable_bit_size['length_item_name'] must be a String") + raise TypeError(f"{self.name}: variable_bit_size['length_item_name'] must be a String") if not isinstance(variable_bit_size["length_value_bit_offset"], int): - raise AttributeError(f"{self.name}: variable_bit_size['length_value_bit_offset'] must be an Integer") + raise ValueError(f"{self.name}: variable_bit_size['length_value_bit_offset'] must be an Integer") if not isinstance(variable_bit_size["length_bits_per_count"], int): - raise AttributeError(f"{self.name}: variable_bit_size['length_bits_per_count'] must be an Integer") + raise ValueError(f"{self.name}: variable_bit_size['length_bits_per_count'] must be an Integer") self.__variable_bit_size = variable_bit_size if self.structure_item_constructed: self.verify_overall() @@ -317,19 +317,19 @@ def verify_overall(self): # Verify negative bit_offset conditions if self.bit_offset < 0: if self.bit_size < 0: - raise AttributeError( + raise ValueError( f"{self.name}: Can't define an item with negative bit_size {self.bit_size} and negative bit_offset {self.bit_offset}" ) if self.array_size and self.array_size < 0: - raise AttributeError( + raise ValueError( f"{self.name}: Can't define an item with negative array_size {self.array_size} and negative bit_offset {self.bit_offset}" ) if self.array_size and self.array_size > abs(self.bit_offset): - raise AttributeError( + raise ValueError( f"{self.name}: Can't define an item with array_size {self.array_size} greater than negative bit_offset {self.bit_offset}" ) elif self.bit_size > abs(self.bit_offset): - raise AttributeError( + raise ValueError( f"{self.name}: Can't define an item with bit_size {self.bit_size} greater than negative bit_offset {self.bit_offset}" ) else: @@ -341,7 +341,7 @@ def verify_overall(self): lower_bound = upper_bound - num_bytes + 1 if lower_bound < 0: - raise AttributeError( + raise ValueError( f"{self.name}: LITTLE_ENDIAN bitfield with bit_offset {self.bit_offset} and bit_size {self.bit_size} is invalid" ) diff --git a/openc3/python/openc3/packets/telemetry.py b/openc3/python/openc3/packets/telemetry.py index fcd839b3f..c9d74542e 100644 --- a/openc3/python/openc3/packets/telemetry.py +++ b/openc3/python/openc3/packets/telemetry.py @@ -117,14 +117,14 @@ def packet(self, target_name, packet_name): # items = [] # # Verify item_array is a nested array - # raise AttributeError(f"item_array must be a nested array consisting of [[tgt,pkt,item],[tgt,pkt,item],...]") if not Array === item_array[0] + # raise ValueError(f"item_array must be a nested array consisting of [[tgt,pkt,item],[tgt,pkt,item],...]") if not Array === item_array[0] # states = [] # settings = [] # limits_set = System.limits_set() # if (Array === value_types) and len(item_array) != len(value_types): - # raise AttributeError(f"Passed {len(item_array)} items but only {len(value_types)} value types") + # raise ValueError(f"Passed {len(item_array)} items but only {len(value_types)} value types") # value_type = value_types if not Array === value_types # len(item_array).times do |index| diff --git a/openc3/python/openc3/processors/processor.py b/openc3/python/openc3/processors/processor.py index c38edbec0..ac1ceb0e6 100644 --- a/openc3/python/openc3/processors/processor.py +++ b/openc3/python/openc3/processors/processor.py @@ -26,7 +26,7 @@ def __init__(self, value_type="CONVERTED"): value_type = value_type.upper() self.value_type = value_type if self.value_type not in Packet.VALUE_TYPES: - raise AttributeError(f"value_type must be RAW, CONVERTED, FORMATTED, or WITH_UNITS. Is {self.value_type}") + raise ValueError(f"value_type must be RAW, CONVERTED, FORMATTED, or WITH_UNITS. Is {self.value_type}") self.results = {} diff --git a/openc3/python/openc3/script/metadata.py b/openc3/python/openc3/script/metadata.py index f463e4471..d39bf6353 100644 --- a/openc3/python/openc3/script/metadata.py +++ b/openc3/python/openc3/script/metadata.py @@ -74,7 +74,7 @@ def metadata_set( The json result of the method call """ if not isinstance(metadata, dict): - raise RuntimeError(f"metadata must be a dict: {metadata} is a {metadata.__class__.__name__}") + raise TypeError(f"metadata must be a dict: {metadata} is a {metadata.__class__.__name__}") data = {"color": color if color else "#003784", "metadata": metadata} if start: @@ -104,7 +104,7 @@ def metadata_update( The json result of the method call """ if not isinstance(metadata, dict): - raise RuntimeError(f"metadata must be a Hash: {metadata} is a {metadata.__class__.__name__}") + raise TypeError(f"metadata must be a Hash: {metadata} is a {metadata.__class__.__name__}") if start is None: # No start so grab latest existing = metadata_get() diff --git a/openc3/python/openc3/streams/mqtt_stream.py b/openc3/python/openc3/streams/mqtt_stream.py new file mode 100644 index 000000000..997dd5fba --- /dev/null +++ b/openc3/python/openc3/streams/mqtt_stream.py @@ -0,0 +1,129 @@ +# Copyright 2024 OpenC3, Inc. +# All Rights Reserved. +# +# This program is free software; you can modify and/or redistribute it +# under the terms of the GNU Affero General Public License +# as published by the Free Software Foundation; version 3 with +# attribution addendums as found in the LICENSE.txt +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# This file may also be used under the terms of a commercial license +# if purchased from OpenC3, Inc. + +import threading +import queue +from time import sleep +from openc3.config.config_parser import ConfigParser +from openc3.utilities.logger import Logger + +# See https://eclipse.dev/paho/files/paho.mqtt.python/html/client.html +import paho.mqtt.client as mqtt + +class MqttStream: + def __init__(self, hostname, port = 1883, write_topic = None, read_topic = None, ack_timeout = 5): + super().__init__() + + self.hostname = hostname + self.port = int(port) + self.write_topic = ConfigParser.handle_none(write_topic) + self.read_topic = ConfigParser.handle_none(read_topic) + self.ack_timeout = float(ack_timeout) + self.pkt_queue = queue.Queue() + + self.username = None + self.password = None + self.cert = None + self.key = None + self.ca_file = None + self.keyfile_password = None + + # Mutex on write is needed to protect from commands coming in from more than one tool + self.write_mutex = threading.RLock() + + # Connect the stream + def connect(self): + self.pkt_queue.empty() + + self.client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2) + self.client.on_connect = self.on_connect + self.client.on_message = self.on_message + self.client.user_data_set(self.pkt_queue) # passed to on_message + + if self.username and self.password: + self.client.username_pw_set(self.username, self.password) + # You still need the ca_file if you're using your own cert and key + if self.cert and self.key and self.ca_file: + if self.keyfile_password: + self.client.tls_set( + ca_certs=self.ca_file.name, + certfile=self.cert.name, + keyfile=self.key.name, + keyfile_password=self.keyfile_password, + ) + else: + self.client.tls_set(ca_certs=self.ca_file.name, certfile=self.cert.name, keyfile=self.key.name) + elif self.ca_file: + self.client.tls_set(ca_certs=self.ca_file.name) + + self.client.loop_start() + # Connect doesn't fully establish the connection, it just sends the CONNECT packet + # When the client loop receives an ONNACK packet from the broker in response to the CONNECT packet + # it calls the on_connect callback and updates the client state to connected (is_connected() returns True) + self.client.connect(self.hostname, self.port) + i = 0 + while not self.client.is_connected() and i < (self.ack_timeout * 100): + sleep(0.01) + i += 1 + + def on_connect(self, client, userdata, flags, reason_code, properties): + if reason_code.is_failure: + Logger.error(f"MQTT failed to connect: {reason_code}") + else: + # we should always subscribe from on_connect callback to be sure + # our subscribed is persisted across reconnections. + if self.read_topic: + client.subscribe(self.read_topic) + + def connected(self): + if self.client: + return self.client.is_connected() + else: + return False + + def disconnect(self): + if self.client: + self.client.disconnect() + self.client = None + + def on_message(self, client, userdata, message): + # userdata is set via user_data_set + userdata.put(message.payload) + + # @return [String] Returns a binary string of data from the read_topic + def read(self): + if not self.read_topic: + raise RuntimeError("Attempt to read from write only stream") + + # No read mutex is needed because reads happen serially + data = self.pkt_queue.get(block=True) + if data is None or len(data) <= 0: + if data is None: + Logger.info("MqttStream: read returned None") + if data is not None and len(data) <= 0: + Logger.info("MqttStream: read returned 0 bytes") + return None + + return data + + # @param data [String] A binary string of data to write to the write_topic + def write(self, data): + if not self.write_topic: + raise RuntimeError("Attempt to write to read only stream") + + self.write_mutex.acquire() + self.client.publish(self.write_topic, data) + self.write_mutex.release() diff --git a/openc3/python/openc3/streams/stream.py b/openc3/python/openc3/streams/stream.py index dcc32a793..98fad441c 100644 --- a/openc3/python/openc3/streams/stream.py +++ b/openc3/python/openc3/streams/stream.py @@ -21,6 +21,19 @@ # allows Streams to simply focus on getting and sending raw data while the # higher level processing occurs in {Protocol}. class Stream: + # Connects the stream + def connect(self): + raise RuntimeError("connect not defined by Stream") + + # Whether the stream is connected + def connected(self): + raise RuntimeError("connected not defined by Stream") + + # Disconnects the stream + # Note that streams are not designed to be reconnected and must be recreated + def disconnect(self): + raise RuntimeError("disconnect not defined by Stream") + # Expected to return any amount of data on success, or a blank string on # closed/EOF, and may raise Timeout='E'rror, or other errors def read(self): @@ -37,12 +50,3 @@ def read_nonblock(self): # self.param data [String] Binary data to write to the stream def write(self, data): raise RuntimeError("write not defined by Stream") - - # Connects the stream - def connect(self): - raise RuntimeError("connect not defined by Stream") - - # Disconnects the stream - # Note that streams are not designed to be reconnected and must be recreated - def disconnect(self): - raise RuntimeError("disconnect not defined by Stream") diff --git a/openc3/python/openc3/streams/tcpip_socket_stream.py b/openc3/python/openc3/streams/tcpip_socket_stream.py index 4bf6a347f..373a0e32a 100644 --- a/openc3/python/openc3/streams/tcpip_socket_stream.py +++ b/openc3/python/openc3/streams/tcpip_socket_stream.py @@ -1,4 +1,4 @@ -# Copyright 2023 OpenC3, Inc. +# Copyright 2024 OpenC3, Inc. # All Rights Reserved. # # This program is free software; you can modify and/or redistribute it @@ -48,7 +48,7 @@ def __init__(self, write_socket, read_socket, write_timeout, read_timeout): # than one tool self.write_mutex = threading.Lock() self.pipe_reader, self.pipe_writer = multiprocessing.Pipe() - self.connected = False + self._connected = False # self.return [String] Returns a binary string of data from the socket def read(self): @@ -115,13 +115,16 @@ def write(self, data): # Connect the stream def connect(self): # If called directly this class is acting as a server and does not need to connect the sockets - self.connected = True + self._connected = True + + def connected(self): + return self._connected # Disconnect by closing the sockets def disconnect(self): - if not self.connected: + if not self._connected: return close_socket(self.write_socket) close_socket(self.read_socket) self.pipe_writer.send(".") - self.connected = False + self._connected = False diff --git a/openc3/python/requirements.txt b/openc3/python/requirements.txt index a7e4fe6eb..a95bfb778 100644 --- a/openc3/python/requirements.txt +++ b/openc3/python/requirements.txt @@ -11,3 +11,4 @@ requests==2.32.3 redis[hiredis]==4.6.0 schedule==1.2.0 websockets==11.0.3 +paho-mqtt==2.1.0 \ No newline at end of file diff --git a/openc3/python/test/accessors/test_binary_accessor_read.py b/openc3/python/test/accessors/test_binary_accessor_read.py index c8dfd5528..b145aba46 100644 --- a/openc3/python/test/accessors/test_binary_accessor_read.py +++ b/openc3/python/test/accessors/test_binary_accessor_read.py @@ -25,25 +25,31 @@ def setUp(self): self.data = b"\x80\x81\x82\x83\x84\x85\x86\x87\x00\x09\x0A\x0B\x0C\x0D\x0E\x0F" def test_complains_about_unknown_data_types(self): - with self.assertRaisesRegex(AttributeError, "data_type BLOB is not recognized"): + with self.assertRaisesRegex(TypeError, "data_type BLOB is not recognized"): BinaryAccessor.read(0, 32, "BLOB", self.data, "BIG_ENDIAN") def test_complains_about_bit_offsets_before_the_beginning_of_the_buffer(self): with self.assertRaisesRegex( - AttributeError, + ValueError, f"{len(self.data)} byte buffer insufficient to read STRING at bit_offset {-((len(self.data) * 8) + 8)} with bit_size 32", ): BinaryAccessor.read(-(len(self.data) * 8 + 8), 32, "STRING", self.data, "BIG_ENDIAN") def test_complains_about_a_negative_bit_offset_and_zero_bit_size(self): - with self.assertRaisesRegex( - AttributeError, r"negative or zero bit_sizes \(0\) cannot be given with negative bit_offsets \(-8\)" - ): - BinaryAccessor.read(-8, 0, "STRING", self.data, "BIG_ENDIAN") + self.assertRaisesRegex( + ValueError, + r"negative or zero bit_sizes \(0\) cannot be given with negative bit_offsets \(-8\)", + BinaryAccessor.read, + -8, + 0, + "STRING", + self.data, + "BIG_ENDIAN", + ) def test_complains_about_a_negative_bit_offset_and_negative_bit_size(self): with self.assertRaisesRegex( - AttributeError, r"negative or zero bit_sizes \(-8\) cannot be given with negative bit_offsets \(-8\)" + ValueError, r"negative or zero bit_sizes \(-8\) cannot be given with negative bit_offsets \(-8\)" ): BinaryAccessor.read(-8, -8, "STRING", self.data, "BIG_ENDIAN") @@ -51,7 +57,7 @@ def test_complains_about_negative_bit_sizes_larger_than_the_size_of_the_buffer( self, ): with self.assertRaisesRegex( - AttributeError, + ValueError, f"{len(self.data)} byte buffer insufficient to read STRING at bit_offset 0 with bit_size {-((len(self.data) * 8) + 8)}", ): BinaryAccessor.read(0, -((len(self.data) * 8) + 8), "STRING", self.data, "BIG_ENDIAN") @@ -60,15 +66,15 @@ def test_complains_about_negative_or_zero_bit_sizes_with_data_types_other_than_s self, ): with self.assertRaisesRegex( - AttributeError, "bit_size -8 must be positive for data types other than 'STRING' and 'BLOCK'" + ValueError, "bit_size -8 must be positive for data types other than 'STRING' and 'BLOCK'" ): BinaryAccessor.read(0, -8, "INT", self.data, "BIG_ENDIAN") with self.assertRaisesRegex( - AttributeError, "bit_size -8 must be positive for data types other than 'STRING' and 'BLOCK'" + ValueError, "bit_size -8 must be positive for data types other than 'STRING' and 'BLOCK'" ): BinaryAccessor.read(0, -8, "UINT", self.data, "BIG_ENDIAN") with self.assertRaisesRegex( - AttributeError, "bit_size -8 must be positive for data types other than 'STRING' and 'BLOCK'" + ValueError, "bit_size -8 must be positive for data types other than 'STRING' and 'BLOCK'" ): BinaryAccessor.read(0, -8, "FLOAT", self.data, "BIG_ENDIAN") @@ -144,7 +150,7 @@ def test_reads_strings_with_negative_bit_offsets(self): ) def test_complains_about_unaligned_strings(self): - with self.assertRaisesRegex(AttributeError, "bit_offset 1 is not byte aligned for data_type STRING"): + with self.assertRaisesRegex(ValueError, "bit_offset 1 is not byte aligned for data_type STRING"): BinaryAccessor.read(1, 32, "STRING", self.data, "BIG_ENDIAN") def test_reads_aligned_blocks(self): @@ -174,14 +180,28 @@ def test_reads_blocks_with_negative_bit_offsets(self): ) def test_complains_about_unaligned_blocks(self): - with self.assertRaisesRegex(AttributeError, "bit_offset 7 is not byte aligned for data_type BLOCK"): - BinaryAccessor.read(7, 16, "BLOCK", self.data, "BIG_ENDIAN") + self.assertRaisesRegex( + ValueError, + "bit_offset 7 is not byte aligned for data_type BLOCK", + BinaryAccessor.read, + 7, + 16, + "BLOCK", + self.data, + "BIG_ENDIAN", + ) def test_complains_if_read_exceeds_the_size_of_the_buffer(self): - with self.assertRaisesRegex( - AttributeError, "16 byte buffer insufficient to read STRING at bit_offset 8 with bit_size 800" - ): - BinaryAccessor.read(8, 800, "STRING", self.data, "BIG_ENDIAN") + self.assertRaisesRegex( + ValueError, + "16 byte buffer insufficient to read STRING at bit_offset 8 with bit_size 800", + BinaryAccessor.read, + 8, + 800, + "STRING", + self.data, + "BIG_ENDIAN", + ) def test_reads_aligned_8_bit_unsigned_integers(self): for bit_offset in range(0, (len(self.data) - 1) * 8, 8): @@ -482,12 +502,28 @@ def test_reads_aligned_64_bit_floats(self): ) def test_complains_about_unaligned_floats(self): - with self.assertRaisesRegex(AttributeError, "bit_offset 17 is not byte aligned for data_type FLOAT"): - BinaryAccessor.read(17, 32, "FLOAT", self.data, "BIG_ENDIAN") + self.assertRaisesRegex( + ValueError, + "bit_offset 17 is not byte aligned for data_type FLOAT", + BinaryAccessor.read, + 17, + 32, + "FLOAT", + self.data, + "BIG_ENDIAN", + ) def test_complains_about_mis_sized_floats(self): - with self.assertRaisesRegex(AttributeError, "bit_size is 33 but must be 32 or 64 for data_type FLOAT"): - BinaryAccessor.read(0, 33, "FLOAT", self.data, "BIG_ENDIAN") + self.assertRaisesRegex( + ValueError, + "bit_size is 33 but must be 32 or 64 for data_type FLOAT", + BinaryAccessor.read, + 0, + 33, + "FLOAT", + self.data, + "BIG_ENDIAN", + ) class TestBinaryAccessorReadLittleEndian(unittest.TestCase): @@ -495,10 +531,16 @@ def setUp(self): self.data = b"\x80\x81\x82\x83\x84\x85\x86\x87\x00\x09\x0A\x0B\x0C\x0D\x0E\x0F" def test_complains_about_ill_defined_little_endian_bitfields(self): - with self.assertRaisesRegex( - AttributeError, "LITTLE_ENDIAN bitfield with bit_offset 3 and bit_size 7 is invalid" - ): - BinaryAccessor.read(3, 7, "UINT", self.data, "LITTLE_ENDIAN") + self.assertRaisesRegex( + ValueError, + "LITTLE_ENDIAN bitfield with bit_offset 3 and bit_size 7 is invalid", + BinaryAccessor.read, + 3, + 7, + "UINT", + self.data, + "LITTLE_ENDIAN", + ) def test_reads_1_bit_unsigned_integers(self): expected = [0x1, 0x0] @@ -769,12 +811,28 @@ def test_reads_aligned_64_bit_floats(self): ) def test_complains_about_unaligned_floats(self): - with self.assertRaisesRegex(AttributeError, "bit_offset 1 is not byte aligned for data_type FLOAT"): - BinaryAccessor.read(1, 32, "FLOAT", self.data, "LITTLE_ENDIAN") + self.assertRaisesRegex( + ValueError, + "bit_offset 1 is not byte aligned for data_type FLOAT", + BinaryAccessor.read, + 1, + 32, + "FLOAT", + self.data, + "LITTLE_ENDIAN", + ) def test_complains_about_mis_sized_floats(self): - with self.assertRaisesRegex(AttributeError, "bit_size is 65 but must be 32 or 64 for data_type FLOAT"): - BinaryAccessor.read(0, 65, "FLOAT", self.data, "LITTLE_ENDIAN") + self.assertRaisesRegex( + ValueError, + "bit_size is 65 but must be 32 or 64 for data_type FLOAT", + BinaryAccessor.read, + 0, + 65, + "FLOAT", + self.data, + "LITTLE_ENDIAN", + ) class TestBinaryAccessorReadArrayLE(unittest.TestCase): @@ -782,12 +840,16 @@ def setUp(self): self.data = b"\x80\x81\x82\x83\x84\x85\x86\x87\x00\x09\x0A\x0B\x0C\x0D\x0E\x0F" def test_complains_with_unknown_data_type(self): - with self.assertRaisesRegex(AttributeError, "data_type BLAH is not recognized"): + with self.assertRaisesRegex(TypeError, "data_type BLAH is not recognized"): BinaryAccessor.read_array(0, 8, "BLAH", 0, self.data, "LITTLE_ENDIAN") def test_complains_about_negative_bit_sizes(self): - with self.assertRaisesRegex(AttributeError, "bit_size -8 must be positive for arrays"): - BinaryAccessor.read_array(0, -8, "UINT", len(self.data) * 8, self.data, "LITTLE_ENDIAN") + with self.assertRaisesRegex( + ValueError, "bit_size -8 must be positive for arrays" + ): + BinaryAccessor.read_array( + 0, -8, "UINT", len(self.data) * 8, self.data, "LITTLE_ENDIAN" + ) def test_reads_the_given_array_size_amount_of_items(self): self.assertEqual( @@ -809,7 +871,9 @@ def test_reads_the_total_buffer_given_array_size_eql_buffer_size(self): ) def test_complains_with_an_array_size_not_a_multiple_of_bit_size(self): - with self.assertRaisesRegex(AttributeError, "array_size 10 not a multiple of bit_size 8"): + with self.assertRaisesRegex( + ValueError, "array_size 10 not a multiple of bit_size 8" + ): BinaryAccessor.read_array(0, 8, "UINT", 10, self.data, "LITTLE_ENDIAN") def test_reads_as_many_items_as_possible_with_a_zero_array_size(self): @@ -833,7 +897,7 @@ def test_returns_an_empty_array_if_the_offset_equals_the_negative_array_size(sel def test_complains_if_the_offset_is_greater_than_the_negative_array_size(self): offset = len(self.data) * 8 - 16 with self.assertRaisesRegex( - AttributeError, + ValueError, f"16 byte buffer insufficient to read UINT at bit_offset {offset} with bit_size 8", ): BinaryAccessor.read_array(offset, 8, "UINT", -32, self.data, "LITTLE_ENDIAN") @@ -859,7 +923,7 @@ def test_reads_an_array_if_the_negative_offset_is_the_size_of_the_array(self): def test_complains_if_the_offset_is_larger_than_the_buffer(self): with self.assertRaisesRegex( - AttributeError, + ValueError, f"{len(self.data)} byte buffer insufficient to read UINT at bit_offset -{len(self.data) * 8 + 1} with bit_size 8", ): BinaryAccessor.read_array( @@ -873,21 +937,21 @@ def test_complains_if_the_offset_is_larger_than_the_buffer(self): def test_complains_with_zero_array_size(self): with self.assertRaisesRegex( - AttributeError, + ValueError, r"negative or zero array_size \(0\) cannot be given with negative bit_offset \(-32\)", ): BinaryAccessor.read_array(-32, 8, "UINT", 0, self.data, "LITTLE_ENDIAN") def test_complains_with_negative_array_size(self): with self.assertRaisesRegex( - AttributeError, + ValueError, r"negative or zero array_size \(-8\) cannot be given with negative bit_offset \(-32\)", ): BinaryAccessor.read_array(-32, 8, "UINT", -8, self.data, "LITTLE_ENDIAN") def test_complains_about_accessing_data_from_a_buffer_which_is_too_small(self): with self.assertRaisesRegex( - AttributeError, + ValueError, "16 byte buffer insufficient to read STRING at bit_offset 0 with bit_size 256", ): BinaryAccessor.read_array(0, 256, "STRING", 256, self.data, "LITTLE_ENDIAN") @@ -896,7 +960,9 @@ def test_returns_an_empty_array_when_passed_a_zero_length_buffer(self): self.assertEqual(BinaryAccessor.read_array(0, 8, "UINT", 32, b"", "LITTLE_ENDIAN"), []) def test_complains_about_unaligned_strings(self): - with self.assertRaisesRegex(AttributeError, "bit_offset 1 is not byte aligned for data_type STRING"): + with self.assertRaisesRegex( + ValueError, "bit_offset 1 is not byte aligned for data_type STRING" + ): BinaryAccessor.read_array(1, 32, "STRING", 32, self.data, "LITTLE_ENDIAN") def test_reads_a_single_string_item(self): @@ -931,7 +997,7 @@ def test_reads_1_bit_integers(self): def test_complains_about_little_endian_bit_fields_greater_than_1_bit(self): with self.assertRaisesRegex( - AttributeError, + ValueError, "read_array does not support little endian bit fields with bit_size greater than 1-bit", ): BinaryAccessor.read_array(8, 7, "UINT", 21, self.data, "LITTLE_ENDIAN") @@ -985,11 +1051,15 @@ def test_reads_aligned_64_bit_floats(self): self.assertAlmostEqual(val, expected_array[index]) def test_complains_about_unaligned_floats(self): - with self.assertRaisesRegex(AttributeError, "bit_offset 1 is not byte aligned for data_type FLOAT"): + with self.assertRaisesRegex( + ValueError, "bit_offset 1 is not byte aligned for data_type FLOAT" + ): BinaryAccessor.read_array(1, 32, "FLOAT", 32, self.data, "LITTLE_ENDIAN") def test_complains_about_mis_sized_floats(self): - with self.assertRaisesRegex(AttributeError, "bit_size is 65 but must be 32 or 64 for data_type FLOAT"): + with self.assertRaisesRegex( + ValueError, "bit_size is 65 but must be 32 or 64 for data_type FLOAT" + ): BinaryAccessor.read_array(0, 65, "FLOAT", 65, self.data, "LITTLE_ENDIAN") diff --git a/openc3/python/test/accessors/test_binary_accessor_write.py b/openc3/python/test/accessors/test_binary_accessor_write.py index 50a490767..690291c12 100644 --- a/openc3/python/test/accessors/test_binary_accessor_write.py +++ b/openc3/python/test/accessors/test_binary_accessor_write.py @@ -265,7 +265,7 @@ def test_can_define_multiple_variable_sized_items_with_nonzero_value_offsets(sel def test_complains_about_unknown_data_types(self): self.assertRaisesRegex( - AttributeError, + TypeError, "data_type BLOB is not recognized", BinaryAccessor.write, 0, @@ -279,7 +279,7 @@ def test_complains_about_unknown_data_types(self): def test_complains_about_bit_offsets_before_the_beginning_the_buffer(self): self.assertRaisesRegex( - AttributeError, + ValueError, f"{len(self.data)} byte buffer insufficient to write STRING at bit_offset {-((len(self.data) * 8) + 8)} with bit_size 32", BinaryAccessor.write, "", @@ -293,7 +293,7 @@ def test_complains_about_bit_offsets_before_the_beginning_the_buffer(self): def test_complains_about_a_negative_bit_offset_and_zero_bit_size(self): self.assertRaisesRegex( - AttributeError, + ValueError, r"negative or zero bit_sizes \(0\) cannot be given with negative bit_offsets \(-8\)", BinaryAccessor.write, "", @@ -307,7 +307,7 @@ def test_complains_about_a_negative_bit_offset_and_zero_bit_size(self): def test_complains_about_a_negative_bit_offset_and_negative_bit_size(self): self.assertRaisesRegex( - AttributeError, + ValueError, r"negative or zero bit_sizes \(-8\) cannot be given with negative bit_offsets \(-8\)", BinaryAccessor.write, "", @@ -323,7 +323,7 @@ def test_complains_about_negative_or_zero_bit_sizes_with_data_types_other_than_s self, ): self.assertRaisesRegex( - AttributeError, + ValueError, "bit_size -8 must be positive for data types other than 'STRING' and 'BLOCK'", BinaryAccessor.write, 0, @@ -335,7 +335,7 @@ def test_complains_about_negative_or_zero_bit_sizes_with_data_types_other_than_s "ERROR", ) self.assertRaisesRegex( - AttributeError, + ValueError, "bit_size -8 must be positive for data types other than 'STRING' and 'BLOCK'", BinaryAccessor.write, 0, @@ -347,7 +347,7 @@ def test_complains_about_negative_or_zero_bit_sizes_with_data_types_other_than_s "ERROR", ) self.assertRaisesRegex( - AttributeError, + ValueError, "bit_size -8 must be positive for data types other than 'STRING' and 'BLOCK'", BinaryAccessor.write, 0, @@ -407,7 +407,7 @@ def test_writes_strings_with_bit_offsets(self): def test_complains_about_unaligned_strings(self): self.assertRaisesRegex( - AttributeError, + ValueError, "bit_offset 1 is not byte aligned for data_type STRING", BinaryAccessor.write, "", @@ -459,7 +459,7 @@ def test_writes_a_block_to_an_empty_buffer(self): data += struct.pack(">H", index) buffer = bytearray() self.assertRaisesRegex( - AttributeError, + ValueError, "0 byte buffer insufficient to write BLOCK at bit_offset 0 with bit_size -16", BinaryAccessor.write, data, @@ -477,7 +477,7 @@ def test_handles_a_huge_bit_offset_with_small_buffer(self): data += struct.pack(">H", index) buffer = bytearray() self.assertRaisesRegex( - AttributeError, + ValueError, "0 byte buffer insufficient to write BLOCK at bit_offset 1024 with bit_size 0", BinaryAccessor.write, data, @@ -495,7 +495,7 @@ def test_handles_an_edge_case_bit_offset(self): data += struct.pack(">H", index) buffer = bytearray(b"\x00" * 127) self.assertRaisesRegex( - AttributeError, + ValueError, "127 byte buffer insufficient to write BLOCK at bit_offset 1024 with bit_size 0", BinaryAccessor.write, data, @@ -507,6 +507,13 @@ def test_handles_an_edge_case_bit_offset(self): "ERROR", ) + def test_writes_a_string_to_a_block(self): + preserve = struct.pack(">I", 0xBEEF0123) + buffer = bytearray(b"\x00\x01") + bytearray(preserve[:]) # Should preserve this + print(buffer) + BinaryAccessor.write("HELLOWORLD", 0, 0, "BLOCK", buffer, "BIG_ENDIAN", "ERROR") + self.assertEqual(buffer, b"HELLOWORLD") + def test_writes_a_block_to_a_small_buffer_preserving_the_end(self): data = bytearray() for index in range(512): @@ -580,7 +587,7 @@ def test_complains_when_the_negative_index_exceeds_the_buffer_length(self): buffer += struct.pack(">H", 0xDEAD) self.assertRaisesRegex( - AttributeError, + ValueError, "32 byte buffer insufficient to write BLOCK at bit_offset 0 with bit_size -16192", BinaryAccessor.write, data, @@ -674,7 +681,7 @@ def test_writes_a_shorter_block_and_zero_fill_to_the_given_bit_size(self): def test_complains_about_unaligned_blocks(self): self.assertRaisesRegex( - AttributeError, + ValueError, "bit_offset 7 is not byte aligned for data_type BLOCK", BinaryAccessor.write, self.baseline_data, @@ -688,7 +695,7 @@ def test_complains_about_unaligned_blocks(self): def test_complains_if_write_exceeds_the_size_of_the_buffer(self): self.assertRaisesRegex( - AttributeError, + ValueError, "16 byte buffer insufficient to write STRING at bit_offset 8 with bit_size 800", BinaryAccessor.write, self.baseline_data, @@ -1161,7 +1168,7 @@ def test_complains_about_non_float_strings_when_writing_floats(self): def test_complains_about_unaligned_floats(self): self.assertRaisesRegex( - AttributeError, + ValueError, "bit_offset 17 is not byte aligned for data_type FLOAT", BinaryAccessor.write, 0.0, @@ -1175,7 +1182,7 @@ def test_complains_about_unaligned_floats(self): def test_complains_about_mis_sized_floats(self): self.assertRaisesRegex( - AttributeError, + ValueError, "bit_size is 33 but must be 32 or 64 for data_type FLOAT", BinaryAccessor.write, 0.0, @@ -1195,7 +1202,7 @@ def setUp(self): def test_complains_about_ill_defined_little_endian_bitfields(self): self.assertRaisesRegex( - AttributeError, + ValueError, "LITTLE_ENDIAN bitfield with bit_offset 3 and bit_size 7 is invalid", BinaryAccessor.write, 0x1, @@ -1516,7 +1523,7 @@ def test_writes_aligned_64_bit_floats(self): def test_le_complains_about_unaligned_floats(self): self.assertRaisesRegex( - AttributeError, + ValueError, "bit_offset 1 is not byte aligned for data_type FLOAT", BinaryAccessor.write, 0.0, @@ -1530,7 +1537,7 @@ def test_le_complains_about_unaligned_floats(self): def test_complains_about_mis_sized_floats(self): self.assertRaisesRegex( - AttributeError, + ValueError, "bit_size is 65 but must be 32 or 64 for data_type FLOAT", BinaryAccessor.write, 0.0, @@ -1549,7 +1556,7 @@ def setUp(self): def test_handles_invalid_overflow_types(self): self.assertRaisesRegex( - AttributeError, + ValueError, "unknown overflow type OTHER", BinaryAccessor.write, b"abcde", @@ -1564,7 +1571,7 @@ def test_handles_invalid_overflow_types(self): def test_prevents_overflow_of_string_and_block(self): for type in ["BLOCK", "STRING"]: self.assertRaisesRegex( - AttributeError, + ValueError, f"value of 5 bytes does not fit into 4 bytes for data_type {type}", BinaryAccessor.write, "abcde", @@ -1584,7 +1591,7 @@ def test_prevents_overflow_of_ints(self): else: value = 2**bit_size self.assertRaisesRegex( - AttributeError, + ValueError, f"value of {value} invalid for {bit_size}-bit {data_type}", BinaryAccessor.write, value, @@ -1597,7 +1604,7 @@ def test_prevents_overflow_of_ints(self): ) value = -(value + 1) self.assertRaisesRegex( - AttributeError, + ValueError, f"value of {value} invalid for {bit_size}-bit {data_type}", BinaryAccessor.write, value, @@ -1697,16 +1704,16 @@ def setUp(self): self.baseline_data_array.append(self.baseline_data[i]) def test_complains_about_value_other_than_array(self): - with self.assertRaisesRegex(AttributeError, "values must be a list but is str"): + with self.assertRaisesRegex(TypeError, "values must be a list but is str"): BinaryAccessor.write_array("", 0, 32, "STRING", 0, self.data, "BIG_ENDIAN", "ERROR") def test_complains_about_unknown_data_types(self): - with self.assertRaisesRegex(AttributeError, "data_type BLOB is not recognized"): + with self.assertRaisesRegex(TypeError, "data_type BLOB is not recognized"): BinaryAccessor.write_array([0], 0, 32, "BLOB", 0, self.data, "BIG_ENDIAN", "ERROR") def test_complains_about_bit_offsets_before_the_beginning_of_the_buffer(self): with self.assertRaisesRegex( - AttributeError, + ValueError, f"{len(self.data)} byte buffer insufficient to write STRING at bit_offset {-((len(self.data) * 8) + 8)} with bit_size 32", ): BinaryAccessor.write_array( @@ -1734,9 +1741,9 @@ def test_writes_if_a_negative_bit_offset_is_equal_to_length_of_buffer(self): self.assertEqual(self.data, self.baseline_data) def test_complains_about_a_negative_or_zero_bit_size(self): - with self.assertRaisesRegex(AttributeError, "bit_size 0 must be positive for arrays"): + with self.assertRaisesRegex(ValueError, "bit_size 0 must be positive for arrays"): BinaryAccessor.write_array([""], 0, 0, "STRING", 0, self.data, "BIG_ENDIAN", "ERROR") - with self.assertRaisesRegex(AttributeError, "bit_size -8 must be positive for arrays"): + with self.assertRaisesRegex(ValueError, "bit_size -8 must be positive for arrays"): BinaryAccessor.write_array([""], 0, -8, "STRING", 0, self.data, "BIG_ENDIAN", "ERROR") def test_writes_aligned_strings_with_fixed_array_size(self): @@ -1780,12 +1787,12 @@ def test_writes_strings_with_negative_bit_offsets(self): self.assertEqual(self.data, (b"\x00" * 14) + self.baseline_data[14:16]) def test_complains_about_unaligned_strings(self): - with self.assertRaisesRegex(AttributeError, "bit_offset 1 is not byte aligned for data_type STRING"): + with self.assertRaisesRegex(ValueError, "bit_offset 1 is not byte aligned for data_type STRING"): BinaryAccessor.write_array([], 1, 32, "STRING", 32, self.data, "BIG_ENDIAN", "ERROR") def test_complains_if_pass_more_values_than_the_given_array_size_can_hold(self): with self.assertRaisesRegex( - AttributeError, + ValueError, f"too many values {len(self.baseline_data_array)} for given array_size 32 and bit_size 8", ): BinaryAccessor.write_array( @@ -1859,11 +1866,11 @@ def test_writes_blocks_with_negative_bit_offsets(self): self.assertEqual(self.data, (b"\x00" * 12) + self.baseline_data[0:4]) def test_complains_with_a_pos_array_size_not_a_multiple_of_bit_size(self): - with self.assertRaisesRegex(AttributeError, "array_size 10 not a multiple of bit_size 8"): + with self.assertRaisesRegex(ValueError, "array_size 10 not a multiple of bit_size 8"): BinaryAccessor.write_array([1, 2], 0, 8, "UINT", 10, self.data, "BIG_ENDIAN", "ERROR") def test_complains_with_a_neg_array_size_not_a_multiple_of_bit_size(self): - with self.assertRaisesRegex(AttributeError, "array_size -10 not a multiple of bit_size 8"): + with self.assertRaisesRegex(ValueError, "array_size -10 not a multiple of bit_size 8"): BinaryAccessor.write_array([1, 2], 0, 8, "UINT", -10, self.data, "BIG_ENDIAN", "ERROR") def test_excludes_the_remaining_bits_if_array_size_is_negative(self): @@ -1922,7 +1929,7 @@ def test_shrinks_the_buffer_when_handling_negative_array_size(self): self.assertEqual(self.data, b"\x00\x00\x00\x01\x03") def test_complain_when_passed_a_zero_length_buffer(self): - with self.assertRaises(AttributeError): + with self.assertRaises(ValueError): BinaryAccessor.write_array([1, 2, 3], 0, 8, "UINT", 32, b"", "LITTLE_ENDIAN", "ERROR") def test_expands_the_buffer_if_the_offset_is_greater_than_the_negative_array_size( @@ -1935,14 +1942,14 @@ def test_expands_the_buffer_if_the_offset_is_greater_than_the_negative_array_siz def test_complains_with_negative_bit_offset_and_zero_array_size(self): with self.assertRaisesRegex( - AttributeError, + ValueError, r"negative or zero array_size \(0\) cannot be given with negative bit_offset \(-32\)", ): BinaryAccessor.write_array([1, 2], -32, 8, "UINT", 0, self.data, "LITTLE_ENDIAN", "ERROR") def test_complains_with_negative_array_size(self): with self.assertRaisesRegex( - AttributeError, + ValueError, r"negative or zero array_size \(-8\) cannot be given with negative bit_offset \(-32\)", ): BinaryAccessor.write_array([1, 2], -32, 8, "UINT", -8, self.data, "LITTLE_ENDIAN", "ERROR") @@ -1982,7 +1989,7 @@ def test_writes_a_shorter_block_and_zero_fill_to_the_given_bit_size(self): ) def test_complains_about_unaligned_blocks(self): - with self.assertRaisesRegex(AttributeError, "bit_offset 7 is not byte aligned for data_type BLOCK"): + with self.assertRaisesRegex(ValueError, "bit_offset 7 is not byte aligned for data_type BLOCK"): BinaryAccessor.write_array( self.baseline_data_array[0:2], 7, @@ -1996,7 +2003,7 @@ def test_complains_about_unaligned_blocks(self): def test_complains_if_write_exceeds_the_size_of_the_buffer(self): with self.assertRaisesRegex( - AttributeError, + ValueError, "16 byte buffer insufficient to write STRING at bit_offset 8 with bit_size 800", ): BinaryAccessor.write_array([], 8, 800, "STRING", 800, self.data, "BIG_ENDIAN", "ERROR") @@ -2028,7 +2035,7 @@ def test_writes_aligned_8_bit_signed_integers(self): self.assertEqual(self.data, b"\x00\x01\x02\x03\x04\x05\xFF\x7F") def test_complains_about_unaligned_strings_bin(self): - with self.assertRaisesRegex(AttributeError, "bit_offset 1 is not byte aligned for data_type STRING"): + with self.assertRaisesRegex(ValueError, "bit_offset 1 is not byte aligned for data_type STRING"): BinaryAccessor.write_array([b"X"], 1, 32, "STRING", 32, self.data, "BIG_ENDIAN", "ERROR") def test_writes_string_items(self): @@ -2220,11 +2227,11 @@ def test_writes_normal_aligned_64_bit_floats(self): self.assertAlmostEqual(BinaryAccessor.read(64, 64, "FLOAT", self.data, "BIG_ENDIAN"), data[1]) def test_complains_about_unaligned_floats(self): - with self.assertRaisesRegex(AttributeError, "bit_offset 17 is not byte aligned for data_type FLOAT"): + with self.assertRaisesRegex(ValueError, "bit_offset 17 is not byte aligned for data_type FLOAT"): BinaryAccessor.write_array([0.0], 17, 32, "FLOAT", 32, self.data, "BIG_ENDIAN", "ERROR") def test_complains_about_mis_sized_floats(self): - with self.assertRaisesRegex(AttributeError, "bit_size is 33 but must be 32 or 64 for data_type FLOAT"): + with self.assertRaisesRegex(ValueError, "bit_size is 33 but must be 32 or 64 for data_type FLOAT"): BinaryAccessor.write_array([0.0], 0, 33, "FLOAT", 33, self.data, "BIG_ENDIAN", "ERROR") @@ -2252,7 +2259,7 @@ def test_writes_1_bit_signed_integers(self): def test_complains_about_little_endian_bit_fields_greater_than_1_bit(self): with self.assertRaisesRegex( - AttributeError, + ValueError, "write_array does not support little endian bit fields with bit_size greater than 1-bit", ): BinaryAccessor.write_array( @@ -2334,11 +2341,11 @@ def test_writes_aligned_64_bit_floats(self): ) def test_complains_about_unaligned_floats(self): - with self.assertRaisesRegex(AttributeError, "bit_offset 1 is not byte aligned for data_type FLOAT"): + with self.assertRaisesRegex(ValueError, "bit_offset 1 is not byte aligned for data_type FLOAT"): BinaryAccessor.write_array([0.0], 1, 32, "FLOAT", 32, self.data, "LITTLE_ENDIAN", "ERROR") def test_complains_about_mis_sized_floats(self): - with self.assertRaisesRegex(AttributeError, "bit_size is 65 but must be 32 or 64 for data_type FLOAT"): + with self.assertRaisesRegex(ValueError, "bit_size is 65 but must be 32 or 64 for data_type FLOAT"): BinaryAccessor.write_array([0.0], 0, 65, "FLOAT", 65, self.data, "LITTLE_ENDIAN", "ERROR") @@ -2348,14 +2355,14 @@ def setUp(self): def test_prevents_overflow_of_string(self): with self.assertRaisesRegex( - AttributeError, + ValueError, "value of 5 bytes does not fit into 4 bytes for data_type STRING", ): BinaryAccessor.write_array(["abcde"], 0, 32, "STRING", 32, self.data, "BIG_ENDIAN", "ERROR") def test_prevents_overflow_of_block(self): with self.assertRaisesRegex( - AttributeError, + ValueError, "value of 5 bytes does not fit into 4 bytes for data_type BLOCK", ): BinaryAccessor.write_array(["abcde"], 0, 32, "BLOCK", 32, self.data, "BIG_ENDIAN", "ERROR") @@ -2364,7 +2371,7 @@ def test_prevents_overflow_of_8_bit_int(self): bit_size = 8 data_type = "INT" value = 2 ** (bit_size - 1) - with self.assertRaisesRegex(AttributeError, f"value of {value} invalid for {bit_size}-bit {data_type}"): + with self.assertRaisesRegex(ValueError, f"value of {value} invalid for {bit_size}-bit {data_type}"): BinaryAccessor.write_array( [value], 0, @@ -2380,7 +2387,7 @@ def test_prevents_overflow_of_16_bit_int(self): bit_size = 16 data_type = "INT" value = 2 ** (bit_size - 1) - with self.assertRaisesRegex(AttributeError, f"value of {value} invalid for {bit_size}-bit {data_type}"): + with self.assertRaisesRegex(ValueError, f"value of {value} invalid for {bit_size}-bit {data_type}"): BinaryAccessor.write_array( [value], 0, @@ -2396,7 +2403,7 @@ def test_prevents_overflow_of_32_bit_int(self): bit_size = 32 data_type = "INT" value = 2 ** (bit_size - 1) - with self.assertRaisesRegex(AttributeError, f"value of {value} invalid for {bit_size}-bit {data_type}"): + with self.assertRaisesRegex(ValueError, f"value of {value} invalid for {bit_size}-bit {data_type}"): BinaryAccessor.write_array( [value], 0, @@ -2412,7 +2419,7 @@ def test_prevents_overflow_of_64_bit_int(self): bit_size = 64 data_type = "INT" value = 2 ** (bit_size - 1) - with self.assertRaisesRegex(AttributeError, f"value of {value} invalid for {bit_size}-bit {data_type}"): + with self.assertRaisesRegex(ValueError, f"value of {value} invalid for {bit_size}-bit {data_type}"): BinaryAccessor.write_array( [value], 0, @@ -2428,7 +2435,7 @@ def test_prevents_overflow_of_3_bit_int(self): bit_size = 3 data_type = "INT" value = 2 ** (bit_size - 1) - with self.assertRaisesRegex(AttributeError, f"value of {value} invalid for {bit_size}-bit {data_type}"): + with self.assertRaisesRegex(ValueError, f"value of {value} invalid for {bit_size}-bit {data_type}"): BinaryAccessor.write_array( [value], 0, @@ -2444,7 +2451,7 @@ def test_prevents_overflow_of_8_bit_uint(self): bit_size = 8 data_type = "UINT" value = 2**bit_size - with self.assertRaisesRegex(AttributeError, f"value of {value} invalid for {bit_size}-bit {data_type}"): + with self.assertRaisesRegex(ValueError, f"value of {value} invalid for {bit_size}-bit {data_type}"): BinaryAccessor.write_array( [value], 0, @@ -2460,7 +2467,7 @@ def test_prevents_overflow_of_16_bit_uint(self): bit_size = 16 data_type = "UINT" value = 2**bit_size - with self.assertRaisesRegex(AttributeError, f"value of {value} invalid for {bit_size}-bit {data_type}"): + with self.assertRaisesRegex(ValueError, f"value of {value} invalid for {bit_size}-bit {data_type}"): BinaryAccessor.write_array( [value], 0, @@ -2476,7 +2483,7 @@ def test_prevents_overflow_of_32_bit_uint(self): bit_size = 32 data_type = "UINT" value = 2**bit_size - with self.assertRaisesRegex(AttributeError, f"value of {value} invalid for {bit_size}-bit {data_type}"): + with self.assertRaisesRegex(ValueError, f"value of {value} invalid for {bit_size}-bit {data_type}"): BinaryAccessor.write_array( [value], 0, @@ -2492,7 +2499,7 @@ def test_prevents_overflow_of_64_bit_uint(self): bit_size = 64 data_type = "UINT" value = 2**bit_size - with self.assertRaisesRegex(AttributeError, f"value of {value} invalid for {bit_size}-bit {data_type}"): + with self.assertRaisesRegex(ValueError, f"value of {value} invalid for {bit_size}-bit {data_type}"): BinaryAccessor.write_array( [value], 0, @@ -2508,7 +2515,7 @@ def test_prevents_overflow_of_3_bit_uint(self): bit_size = 3 data_type = "UINT" value = 2**bit_size - with self.assertRaisesRegex(AttributeError, f"value of {value} invalid for {bit_size}-bit {data_type}"): + with self.assertRaisesRegex(ValueError, f"value of {value} invalid for {bit_size}-bit {data_type}"): BinaryAccessor.write_array( [value], 0, diff --git a/openc3/python/test/api/test_tlm_api.py b/openc3/python/test/api/test_tlm_api.py index e8ff8bb78..9c145d36e 100644 --- a/openc3/python/test/api/test_tlm_api.py +++ b/openc3/python/test/api/test_tlm_api.py @@ -685,7 +685,7 @@ def test_complains_using_latest(self): def test_complains_about_non_existant_value_types(self): with self.assertRaisesRegex( - AttributeError, "Unknown type 'MINE' for INST HEALTH_STATUS" + TypeError, "Unknown type 'MINE' for INST HEALTH_STATUS" ): get_tlm_packet("INST HEALTH_STATUS", type="MINE") @@ -833,15 +833,15 @@ def test_get_tlm_values_complains_about_non_existant_items(self): get_tlm_values(["INST__LATEST__BLAH__CONVERTED"]) def test_get_tlm_values_complains_about_non_existant_value_types(self): - with self.assertRaisesRegex(RuntimeError, "Unknown value type 'MINE'"): + with self.assertRaisesRegex(ValueError, "Unknown value type 'MINE'"): get_tlm_values(["INST__HEALTH_STATUS__TEMP1__MINE"]) def test_get_tlm_values_complains_about_bad_arguments(self): - with self.assertRaisesRegex(AttributeError, "items must be array of strings"): + with self.assertRaisesRegex(TypeError, "items must be array of strings"): get_tlm_values([]) - with self.assertRaisesRegex(AttributeError, "items must be array of strings"): + with self.assertRaisesRegex(TypeError, "items must be array of strings"): get_tlm_values([["INST", "HEALTH_STATUS", "TEMP1"]]) - with self.assertRaisesRegex(AttributeError, "items must be formatted"): + with self.assertRaisesRegex(ValueError, "items must be formatted"): get_tlm_values(["INST", "HEALTH_STATUS", "TEMP1"]) def test_get_tlm_values_reads_all_the_specified_items(self): diff --git a/openc3/python/test/config/test_config_parser.py b/openc3/python/test/config/test_config_parser.py index 991164957..cde276e2c 100644 --- a/openc3/python/test/config/test_config_parser.py +++ b/openc3/python/test/config/test_config_parser.py @@ -574,7 +574,7 @@ def test_converts_string_constants_to_numbers(self): self.assertEqual(ConfigParser.handle_defined_constants("POS_INFINITY"), float("inf")) self.assertEqual(ConfigParser.handle_defined_constants("NEG_INFINITY"), float("-inf")) self.assertRaisesRegex( - AttributeError, + ValueError, "Invalid bit size 16 for FLOAT type.", ConfigParser.handle_defined_constants, "MIN", @@ -584,13 +584,13 @@ def test_converts_string_constants_to_numbers(self): def test_complains_about_undefined_strings(self): self.assertRaisesRegex( - AttributeError, + ValueError, "Could not convert constant: TRUE", ConfigParser.handle_defined_constants, "TRUE", ) self.assertRaisesRegex( - AttributeError, + TypeError, "Invalid data type BLAH when calculating range.", ConfigParser.handle_defined_constants, "MIN", diff --git a/openc3/python/test/conversions/test_object_read_conversion.py b/openc3/python/test/conversions/test_object_read_conversion.py index dea3610b5..faaf0cce7 100644 --- a/openc3/python/test/conversions/test_object_read_conversion.py +++ b/openc3/python/test/conversions/test_object_read_conversion.py @@ -34,7 +34,7 @@ def test_takes_cmd_tlm_target_name_packet_name(self): self.assertEqual(orc.converted_bit_size, 0) def test_complains_about_invalid_cmd_tlm(self): - with self.assertRaisesRegex(AttributeError, "Unknown type:OTHER"): + with self.assertRaisesRegex(TypeError, "Unknown type: OTHER"): ObjectReadConversion("OTHER", "TGT", "PKT") def test_fills_the_cmd_packet_and_returns_a_hash_of_the_converted_values(self): diff --git a/openc3/python/test/conversions/test_object_write_conversion.py b/openc3/python/test/conversions/test_object_write_conversion.py index 5569c8eed..038a80864 100644 --- a/openc3/python/test/conversions/test_object_write_conversion.py +++ b/openc3/python/test/conversions/test_object_write_conversion.py @@ -34,7 +34,7 @@ def test_takes_cmd_tlm_target_name_packet_name(self): self.assertEqual(owc.converted_bit_size, 0) def test_complains_about_invalid_cmd_tlm(self): - with self.assertRaisesRegex(AttributeError, "Unknown type:OTHER"): + with self.assertRaisesRegex(TypeError, "Unknown type: OTHER"): ObjectWriteConversion("OTHER", "TGT", "PKT") def test_writes_the_cmd_packet_and_returns_a_raw_block(self): diff --git a/openc3/python/test/conversions/test_unix_time_formatted_conversion.py b/openc3/python/test/conversions/test_unix_time_formatted_conversion.py index 79b824fe9..b9d75b25f 100644 --- a/openc3/python/test/conversions/test_unix_time_formatted_conversion.py +++ b/openc3/python/test/conversions/test_unix_time_formatted_conversion.py @@ -56,7 +56,7 @@ def test_complains_if_the_seconds_item_doesnt_exist(self): utfc = UnixTimeFormattedConversion("TIME") packet = Packet("TGT", "PKT") with self.assertRaisesRegex( - AttributeError, "Packet item 'TGT PKT TIME' does not exist" + RuntimeError, "Packet item 'TGT PKT TIME' does not exist" ): utfc.call(None, packet, packet.buffer) @@ -65,7 +65,7 @@ def test_complains_if_the_microseconds_item_doesnt_exist(self): packet = Packet("TGT", "PKT") packet.append_item("TIME", 32, "UINT") with self.assertRaisesRegex( - AttributeError, "Packet item 'TGT PKT TIME_US' does not exist" + RuntimeError, "Packet item 'TGT PKT TIME_US' does not exist" ): utfc.call(None, packet, packet.buffer) diff --git a/openc3/python/test/conversions/test_unix_time_seconds_conversion.py b/openc3/python/test/conversions/test_unix_time_seconds_conversion.py index 7c2203cdb..c29dc7d97 100644 --- a/openc3/python/test/conversions/test_unix_time_seconds_conversion.py +++ b/openc3/python/test/conversions/test_unix_time_seconds_conversion.py @@ -50,7 +50,7 @@ def test_complains_if_the_seconds_item_doesnt_exist(self): utsc = UnixTimeSecondsConversion("TIME") packet = Packet("TGT", "PKT") with self.assertRaisesRegex( - AttributeError, "Packet item 'TGT PKT TIME' does not exist" + RuntimeError, "Packet item 'TGT PKT TIME' does not exist" ): utsc.call(None, packet, packet.buffer) @@ -59,7 +59,7 @@ def test_complains_if_the_microseconds_item_doesnt_exist(self): packet = Packet("TGT", "PKT") packet.append_item("TIME", 32, "UINT") with self.assertRaisesRegex( - AttributeError, "Packet item 'TGT PKT TIME_US' does not exist" + RuntimeError, "Packet item 'TGT PKT TIME_US' does not exist" ): utsc.call(None, packet, packet.buffer) diff --git a/openc3/python/test/install/config/targets/INST/cmd_tlm/inst_cmds.txt b/openc3/python/test/install/config/targets/INST/cmd_tlm/inst_cmds.txt index 3c56bb9f5..bf246c483 100644 --- a/openc3/python/test/install/config/targets/INST/cmd_tlm/inst_cmds.txt +++ b/openc3/python/test/install/config/targets/INST/cmd_tlm/inst_cmds.txt @@ -1,4 +1,5 @@ COMMAND INST COLLECT BIG_ENDIAN "Starts a collect on the instrument" + META TOPIC COLLECT PARAMETER CCSDSVER 0 3 UINT 0 0 0 "CCSDS primary header version number" PARAMETER CCSDSTYPE 3 1 UINT 1 1 1 "CCSDS primary header packet type" PARAMETER CCSDSSHF 4 1 UINT 0 0 0 "CCSDS primary header secondary header flag" diff --git a/openc3/python/test/install/config/targets/INST/cmd_tlm/inst_tlm.txt b/openc3/python/test/install/config/targets/INST/cmd_tlm/inst_tlm.txt index c8980b16e..a2eed550c 100644 --- a/openc3/python/test/install/config/targets/INST/cmd_tlm/inst_tlm.txt +++ b/openc3/python/test/install/config/targets/INST/cmd_tlm/inst_tlm.txt @@ -1,4 +1,5 @@ TELEMETRY INST HEALTH_STATUS BIG_ENDIAN "Health and status from the instrument" + META TOPIC HEALTH_STATUS ITEM CCSDSVER 0 3 UINT "CCSDS packet version number (See CCSDS 133.0-B-1)" ITEM CCSDSTYPE 3 1 UINT "CCSDS packet type (command or telemetry)" STATE TLM 0 @@ -80,6 +81,7 @@ TELEMETRY INST HEALTH_STATUS BIG_ENDIAN "Health and status from the instrument" # PROCESSOR TEMP1WATER watermark_processor.rb TEMP1 TELEMETRY INST ADCS BIG_ENDIAN "Position and attitude data" + META TOPIC ADCS ITEM CCSDSVER 0 3 UINT "CCSDS packet version number (See CCSDS 133.0-B-1)" ITEM CCSDSTYPE 3 1 UINT "CCSDS packet type (command or telemetry)" STATE TLM 0 diff --git a/openc3/python/test/interfaces/protocols/test_burst_protocol.py b/openc3/python/test/interfaces/protocols/test_burst_protocol.py index 7c46c206e..f8f311285 100644 --- a/openc3/python/test/interfaces/protocols/test_burst_protocol.py +++ b/openc3/python/test/interfaces/protocols/test_burst_protocol.py @@ -52,28 +52,20 @@ def setUp(self): self.interface = TestBurstProtocol.MyInterface() def test_initializes_attributes(self): - self.interface.add_protocol( - BurstProtocol, [1, "0xDEADBEEF", True], "READ_WRITE" - ) + self.interface.add_protocol(BurstProtocol, [1, "0xDEADBEEF", True], "READ_WRITE") self.assertEqual(self.interface.read_protocols[0].data, b"") self.assertEqual(self.interface.read_protocols[0].discard_leading_bytes, 1) - self.assertEqual( - self.interface.read_protocols[0].sync_pattern, b"\xDE\xAD\xBE\xEF" - ) + self.assertEqual(self.interface.read_protocols[0].sync_pattern, b"\xDE\xAD\xBE\xEF") self.assertTrue(self.interface.read_protocols[0].fill_fields) def test_connect_clears_the_data(self): - self.interface.add_protocol( - BurstProtocol, [1, "0xDEADBEEF", True], "READ_WRITE" - ) + self.interface.add_protocol(BurstProtocol, [1, "0xDEADBEEF", True], "READ_WRITE") self.interface.read_protocols[0].data = b"\x00\x01\x02\x03" self.interface.connect() self.assertEqual(self.interface.read_protocols[0].data, b"") def test_disconnect_clears_the_data(self): - self.interface.add_protocol( - BurstProtocol, [1, "0xDEADBEEF", True], "READ_WRITE" - ) + self.interface.add_protocol(BurstProtocol, [1, "0xDEADBEEF", True], "READ_WRITE") self.interface.read_protocols[0].data = b"\x00\x01\x02\x03" self.interface.disconnect() self.assertEqual(self.interface.read_protocols[0].data, b"") @@ -177,23 +169,17 @@ def read(self): def test_handle_auto_allow_empty_data_correctly(self): self.interface.add_protocol(BurstProtocol, [0, None, False, None], "READ_WRITE") - self.assertEqual( - self.interface.read_protocols[0].read_data(b""), ("STOP", None) - ) + self.assertEqual(self.interface.read_protocols[0].read_data(b""), ("STOP", None)) self.assertEqual(self.interface.read_protocols[0].read_data(b"A"), (b"A", None)) self.interface.add_protocol(BurstProtocol, [0, None, False, None], "READ_WRITE") self.assertEqual(self.interface.read_protocols[0].read_data(b""), (b"", None)) - self.assertEqual( - self.interface.read_protocols[1].read_data(b""), ("STOP", None) - ) + self.assertEqual(self.interface.read_protocols[1].read_data(b""), ("STOP", None)) self.assertEqual(self.interface.read_protocols[0].read_data(b"A"), (b"A", None)) self.assertEqual(self.interface.read_protocols[1].read_data(b"A"), (b"A", None)) self.interface.add_protocol(BurstProtocol, [0, None, False, None], "READ_WRITE") self.assertEqual(self.interface.read_protocols[0].read_data(b""), (b"", None)) self.assertEqual(self.interface.read_protocols[1].read_data(b""), (b"", None)) - self.assertEqual( - self.interface.read_protocols[2].read_data(b""), ("STOP", None) - ) + self.assertEqual(self.interface.read_protocols[2].read_data(b""), ("STOP", None)) self.assertEqual(self.interface.read_protocols[0].read_data(b"A"), (b"A", None)) self.assertEqual(self.interface.read_protocols[1].read_data(b"A"), (b"A", None)) self.assertEqual(self.interface.read_protocols[2].read_data(b"A"), (b"A", None)) @@ -211,11 +197,9 @@ def test_complains_if_the_data_isnt_big_enough_to_hold_the_sync_pattern(self): data = Packet(None, None, "BIG_ENDIAN", None, b"\x00\x00") # Don't discard bytes, include and fill the sync pattern self.interface.stream = TestBurstProtocol.StreamStub() - self.interface.add_protocol( - BurstProtocol, [0, "0x12345678", True], "READ_WRITE" - ) + self.interface.add_protocol(BurstProtocol, [0, "0x12345678", True], "READ_WRITE") # 2 bytes are not enough to hold the 4 byte sync - with self.assertRaisesRegex(AttributeError, "buffer insufficient"): + with self.assertRaisesRegex(ValueError, "buffer insufficient"): self.interface.write(data) def test_fills_the_sync_pattern_in_the_data(self): @@ -232,9 +216,7 @@ def test_adds_the_sync_pattern_to_the_data_stream(self): data = Packet(None, None, "BIG_ENDIAN", None, b"\x00\x01\x02\x03") # Discard first 2 bytes (the sync pattern), include and fill the sync pattern self.interface.stream = TestBurstProtocol.StreamStub() - self.interface.add_protocol( - BurstProtocol, [2, "0x12345678", True], "READ_WRITE" - ) + self.interface.add_protocol(BurstProtocol, [2, "0x12345678", True], "READ_WRITE") self.interface.write(data) self.assertEqual(TestBurstProtocol.data, b"\x12\x34\x56\x78\x02\x03") diff --git a/openc3/python/test/interfaces/protocols/test_crc_protocol.py b/openc3/python/test/interfaces/protocols/test_crc_protocol.py index 1d21d9333..dc78726c5 100644 --- a/openc3/python/test/interfaces/protocols/test_crc_protocol.py +++ b/openc3/python/test/interfaces/protocols/test_crc_protocol.py @@ -763,9 +763,7 @@ def test_complains_if_the_item_does_not_exist(self): packet.append_item("CRC", 32, "UINT") packet.append_item("TRAILER", 32, "UINT") packet.buffer = b"\x00\x01\x02\x03\x00\x00\x00\x00\x04\x05\x06\x07" - with self.assertRaisesRegex( - AttributeError, "Packet item 'TGT PKT MYCRC' does not exist" - ): + with self.assertRaisesRegex(RuntimeError, "Packet item 'TGT PKT MYCRC' does not exist"): self.interface.write(packet) def test_calculates_and_writes_the_16_bit_crc_item(self): @@ -839,9 +837,7 @@ def test_calculates_and_writes_the_64_bit_crc_item(self): packet.append_item("DATA", 32, "UINT") packet.append_item("CRC", 64, "UINT") packet.append_item("TRAILER", 32, "UINT") - packet.buffer = ( - b"\x00\x01\x02\x03\x00\x00\x00\x00\x00\x00\x00\x00\x04\x05\x06\x07" - ) + packet.buffer = b"\x00\x01\x02\x03\x00\x00\x00\x00\x00\x00\x00\x00\x04\x05\x06\x07" self.interface.write(packet) buffer = b"\x00\x01\x02\x03" crc = Crc64().calc(buffer) diff --git a/openc3/python/test/interfaces/protocols/test_fixed_protocol.py b/openc3/python/test/interfaces/protocols/test_fixed_protocol.py index a0827d5bf..3ab00b533 100644 --- a/openc3/python/test/interfaces/protocols/test_fixed_protocol.py +++ b/openc3/python/test/interfaces/protocols/test_fixed_protocol.py @@ -55,15 +55,11 @@ def setUp(self): self.interface = TestFixedProtocol.MyInterface() def test_initializes_attributes(self): - self.interface.add_protocol( - FixedProtocol, [2, 1, "0xDEADBEEF", False, True], "READ_WRITE" - ) + self.interface.add_protocol(FixedProtocol, [2, 1, "0xDEADBEEF", False, True], "READ_WRITE") self.assertEqual(self.interface.read_protocols[0].data, b"") self.assertEqual(self.interface.read_protocols[0].min_id_size, 2) self.assertEqual(self.interface.read_protocols[0].discard_leading_bytes, 1) - self.assertEqual( - self.interface.read_protocols[0].sync_pattern, b"\xDE\xAD\xBE\xEF" - ) + self.assertEqual(self.interface.read_protocols[0].sync_pattern, b"\xDE\xAD\xBE\xEF") self.assertFalse(self.interface.read_protocols[0].telemetry) self.assertTrue(self.interface.read_protocols[0].fill_fields) @@ -122,35 +118,23 @@ def test_reads_telemetry_data_from_the_stream(self): self.interface.tlm_target_names = ["SYSTEM"] TestFixedProtocol.index = 1 packet = self.interface.read() - self.assertTrue( - datetime.now(timezone.utc).timestamp() - packet.received_time.timestamp() - < 0.1 - ) + self.assertLess(datetime.now(timezone.utc).timestamp() - packet.received_time.timestamp(), 0.1) self.assertEqual(packet.target_name, "SYSTEM") self.assertEqual(packet.packet_name, "META") TestFixedProtocol.index = 2 packet = self.interface.read() - self.assertTrue( - datetime.now(timezone.utc).timestamp() - packet.received_time.timestamp() - < 0.1 - ) + self.assertLess(datetime.now(timezone.utc).timestamp() - packet.received_time.timestamp(), 0.1) self.assertEqual(packet.target_name, "SYSTEM") self.assertEqual(packet.packet_name, "LIMITS_CHANGE") target.tlm_unique_id_mode = True TestFixedProtocol.index = 1 packet = self.interface.read() - self.assertTrue( - datetime.now(timezone.utc).timestamp() - packet.received_time.timestamp() - < 0.1 - ) + self.assertLess(datetime.now(timezone.utc).timestamp() - packet.received_time.timestamp(), 0.1) self.assertEqual(packet.target_name, "SYSTEM") self.assertEqual(packet.packet_name, "META") TestFixedProtocol.index = 2 packet = self.interface.read() - self.assertTrue( - datetime.now(timezone.utc).timestamp() - packet.received_time.timestamp() - < 0.1 - ) + self.assertLess(datetime.now(timezone.utc).timestamp() - packet.received_time.timestamp(), 0.1) self.assertEqual(packet.target_name, "SYSTEM") self.assertEqual(packet.packet_name, "LIMITS_CHANGE") target.tlm_unique_id_mode = False @@ -173,28 +157,20 @@ def read(self): return b"\x1A\xCF\xFC\x1D\x55\x55" + buffer # Require 8 bytes, discard 6 leading bytes, use 0x1ACFFC1D sync, telemetry = False (command) - self.interface.add_protocol( - FixedProtocol, [8, 6, "0x1ACFFC1D", False], "READ_WRITE" - ) + self.interface.add_protocol(FixedProtocol, [8, 6, "0x1ACFFC1D", False], "READ_WRITE") self.interface.stream = FixedStream2() self.interface.target_names = ["SYSTEM"] self.interface.cmd_target_names = ["SYSTEM"] self.interface.tlm_target_names = ["SYSTEM"] target.cmd_unique_id_mode = False packet = self.interface.read() - self.assertTrue( - datetime.now(timezone.utc).timestamp() - packet.received_time.timestamp() - < 0.01 - ) + self.assertLess(datetime.now(timezone.utc).timestamp() - packet.received_time.timestamp(), 0.1) self.assertEqual(packet.target_name, "SYSTEM") self.assertEqual(packet.packet_name, "STARTLOGGING") self.assertEqual(packet.buffer, buffer) target.cmd_unique_id_mode = True packet = self.interface.read() - self.assertTrue( - datetime.now(timezone.utc).timestamp() - packet.received_time.timestamp() - < 0.01 - ) + self.assertLess(datetime.now(timezone.utc).timestamp() - packet.received_time.timestamp(), 0.1) self.assertEqual(packet.target_name, "SYSTEM") self.assertEqual(packet.packet_name, "STARTLOGGING") self.assertEqual(packet.buffer, buffer) diff --git a/openc3/python/test/interfaces/protocols/test_ignore_packet_protocol.py b/openc3/python/test/interfaces/protocols/test_ignore_packet_protocol.py index cbe36e0fa..3de3f0a65 100644 --- a/openc3/python/test/interfaces/protocols/test_ignore_packet_protocol.py +++ b/openc3/python/test/interfaces/protocols/test_ignore_packet_protocol.py @@ -76,17 +76,11 @@ def test_complains_if_packet_is_not_given(self): def test_complains_if_the_target_is_not_found(self): with self.assertRaisesRegex(RuntimeError, "target 'BLAH' does not exist"): - self.interface.add_protocol( - IgnorePacketProtocol, ["BLAH", "META"], "READ_WRITE" - ) + self.interface.add_protocol(IgnorePacketProtocol, ["BLAH", "META"], "READ_WRITE") def test_complains_if_the_packet_is_not_found(self): - with self.assertRaisesRegex( - RuntimeError, "packet 'SYSTEM BLAH' does not exist" - ): - self.interface.add_protocol( - IgnorePacketProtocol, ["SYSTEM", "BLAH"], "READ_WRITE" - ) + with self.assertRaisesRegex(RuntimeError, "packet 'SYSTEM BLAH' does not exist"): + self.interface.add_protocol(IgnorePacketProtocol, ["SYSTEM", "BLAH"], "READ_WRITE") def test_read_ignores_the_packet_specified(self): self.interface.stream = TestIgnorePacketProtocol.IgnorePreStream() @@ -140,9 +134,7 @@ def test_read_can_be_added_multiple_times_to_ignore_different_packets(self): self.assertEqual(packet.buffer, TestIgnorePacketProtocol.buffer) # Now add the protocol to ignore the packet - self.interface.add_protocol( - IgnorePacketProtocol, ["INST", "HEALTH_STATUS"], "READ" - ) + self.interface.add_protocol(IgnorePacketProtocol, ["INST", "HEALTH_STATUS"], "READ") TestIgnorePacketProtocol.buffer = None self.interface.write(pkt) self.assertEqual(TestIgnorePacketProtocol.buffer, pkt.buffer) @@ -226,9 +218,7 @@ def test_write_ignores_the_packet_specified(self): def test_write_can_be_added_multiple_times_to_ignore_different_packets(self): self.interface.stream = TestIgnorePacketProtocol.IgnorePreStream() - self.interface.add_protocol( - IgnorePacketProtocol, ["INST", "HEALTH_STATUS"], "WRITE" - ) + self.interface.add_protocol(IgnorePacketProtocol, ["INST", "HEALTH_STATUS"], "WRITE") self.interface.add_protocol(IgnorePacketProtocol, ["INST", "ADCS"], "WRITE") pkt = System.telemetry.packet("INST", "HEALTH_STATUS") @@ -254,9 +244,7 @@ def test_write_can_be_added_multiple_times_to_ignore_different_packets(self): def test_read_write_ignores_the_packet_specified(self): self.interface.stream = TestIgnorePacketProtocol.IgnorePreStream() - self.interface.add_protocol( - IgnorePacketProtocol, ["SYSTEM", "META"], "READ_WRITE" - ) + self.interface.add_protocol(IgnorePacketProtocol, ["SYSTEM", "META"], "READ_WRITE") pkt = System.telemetry.packet("SYSTEM", "META") pkt.write("OPENC3_VERSION", "TEST") pkt.received_time = datetime.now(timezone.utc) @@ -281,9 +269,7 @@ def my_read(): def test_reads_and_writes_unknown_packets(self): self.interface.stream = TestIgnorePacketProtocol.IgnorePreStream() - self.interface.add_protocol( - IgnorePacketProtocol, ["SYSTEM", "META"], "READ_WRITE" - ) + self.interface.add_protocol(IgnorePacketProtocol, ["SYSTEM", "META"], "READ_WRITE") TestIgnorePacketProtocol.buffer = None pkt = Packet("TGT", "PTK") pkt.append_item("ITEM", 8, "INT") diff --git a/openc3/python/test/interfaces/protocols/test_length_protocol.py b/openc3/python/test/interfaces/protocols/test_length_protocol.py index 1c16325a3..9646e79de 100644 --- a/openc3/python/test/interfaces/protocols/test_length_protocol.py +++ b/openc3/python/test/interfaces/protocols/test_length_protocol.py @@ -61,13 +61,9 @@ def test_initializes_attributes(self): self.assertEqual(self.interface.read_protocols[0].length_bit_size, 32) self.assertEqual(self.interface.read_protocols[0].length_value_offset, 16) self.assertEqual(self.interface.read_protocols[0].length_bytes_per_count, 2) - self.assertEqual( - self.interface.read_protocols[0].length_endianness, "LITTLE_ENDIAN" - ) + self.assertEqual(self.interface.read_protocols[0].length_endianness, "LITTLE_ENDIAN") self.assertEqual(self.interface.read_protocols[0].discard_leading_bytes, 2) - self.assertEqual( - self.interface.read_protocols[0].sync_pattern, b"\xDE\xAD\xBE\xEF" - ) + self.assertEqual(self.interface.read_protocols[0].sync_pattern, b"\xDE\xAD\xBE\xEF") self.assertEqual(self.interface.read_protocols[0].max_length, 100) self.assertTrue(self.interface.read_protocols[0].fill_fields) @@ -95,12 +91,8 @@ def test_caches_data_for_reads_correctly(self): self.interface.read_protocols[0].read_data(b"\x03\x01\x02\x03\x04\x05"), (b"\x03\x01\x02", None), ) - self.assertEqual( - self.interface.read_protocols[0].read_data(b""), (b"\x03\x04\x05", None) - ) - self.assertEqual( - self.interface.read_protocols[0].read_data(b""), ("STOP", None) - ) + self.assertEqual(self.interface.read_protocols[0].read_data(b""), (b"\x03\x04\x05", None)) + self.assertEqual(self.interface.read_protocols[0].read_data(b""), ("STOP", None)) # This test match uses two length protocols to verify that data flows correctly between the two protocols and that earlier data # is removed correctly using discard leading bytes. In general it is not typical to use two different length protocols, but it could @@ -239,9 +231,7 @@ def test_raises_an_error_with_a_packet_length_of_0(self): "READ_WRITE", ) TestLengthProtocol.buffer = b"\x00\x01\x00\x00\x03\x04\x05\x06\x07\x08\x09" - with self.assertRaisesRegex( - AttributeError, "Calculated packet length of 0 bits" - ): + with self.assertRaisesRegex(ValueError, "Calculated packet length of 0 bits"): self.interface.read() def test_raises_an_error_if_packet_length_not_enough_to_support_offset_and_size( @@ -260,9 +250,7 @@ def test_raises_an_error_if_packet_length_not_enough_to_support_offset_and_size( "READ_WRITE", ) TestLengthProtocol.buffer = b"\x00\x01\x00\x00\x03\x04\x05\x06\x07\x08\x09" - with self.assertRaisesRegex( - AttributeError, "Calculated packet length of 24 bits" - ): + with self.assertRaisesRegex(ValueError, "Calculated packet length of 24 bits"): self.interface.read() def test_processes_a_0_length_with_a_non_zero_length_offset(self): @@ -301,9 +289,7 @@ def test_validates_length_against_the_maximum_length(self): "READ_WRITE", ) # max_length TestLengthProtocol.buffer = b"\x00\x01\xFF\xFF\x03\x04" - with self.assertRaisesRegex( - AttributeError, "Length value received larger than max_length= 65535 > 50" - ): + with self.assertRaisesRegex(ValueError, "Length value received larger than max_length= 65535 > 50"): self.interface.read() def test_handles_a_sync_value_in_the_packet(self): @@ -340,9 +326,7 @@ def test_handles_a_sync_value_that_is_discarded(self): ], "READ_WRITE", ) # sync - TestLengthProtocol.buffer = ( - b"\x00\xDE\xAD\x00\x08\x01\x02\x03\x04\x05\x06\x07\x08" - ) + TestLengthProtocol.buffer = b"\x00\xDE\xAD\x00\x08\x01\x02\x03\x04\x05\x06\x07\x08" packet = self.interface.read() self.assertEqual(packet.buffer, b"\x00\x08\x01\x02\x03\x04") @@ -380,9 +364,7 @@ def test_handles_a_sync_and_length_value_that_are_discarded(self): ], "READ_WRITE", ) # sync - TestLengthProtocol.buffer = ( - b"\x00\xDE\xAD\x0A\x00\x01\x02\x03\x04\x05\x06\x07\x08" - ) + TestLengthProtocol.buffer = b"\x00\xDE\xAD\x0A\x00\x01\x02\x03\x04\x05\x06\x07\x08" packet = self.interface.read() self.assertEqual(packet.buffer, b"\x01\x02\x03\x04\x05\x06") @@ -428,7 +410,7 @@ def test_complains_if_not_enough_data_to_write_the_sync_and_length_fields(self): packet = Packet(None, None) packet.buffer = b"\x01\x02\x03\x04" # 4 bytes are not enough since we expect the length field at offset 32 - with self.assertRaisesRegex(AttributeError, "buffer insufficient"): + with self.assertRaisesRegex(ValueError, "buffer insufficient"): self.interface.write(packet) def test_write_adjusts_length_by_offset(self): @@ -522,9 +504,7 @@ def test_validates_length_against_the_maximum_length_1(self): ) # fill fields packet = Packet(None, None) packet.buffer = b"\x01\x02\x03\x04\x05\x06" - with self.assertRaisesRegex( - AttributeError, "Calculated length 6 larger than max_length 4" - ): + with self.assertRaisesRegex(ValueError, "Calculated length 6 larger than max_length 4"): packet = self.interface.write(packet) def test_validates_length_against_the_maximum_length_2(self): @@ -547,9 +527,7 @@ def test_validates_length_against_the_maximum_length_2(self): ) # fill fields packet = Packet(None, None) packet.buffer = b"\x01\x02\x03\x04\x05\x06" - with self.assertRaisesRegex( - AttributeError, "Calculated length 8 larger than max_length 4" - ): + with self.assertRaisesRegex(ValueError, "Calculated length 8 larger than max_length 4"): packet = self.interface.write(packet) def test_inserts_the_sync_and_length_fields_into_the_packet_1(self): @@ -594,9 +572,7 @@ def test_inserts_the_sync_and_length_fields_into_the_packet_2(self): ) packet = Packet(None, None) # The packet buffer contains the sync and length fields which are overwritten by the write call - packet.buffer = ( - b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x02\x03\x04" - ) + packet.buffer = b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x02\x03\x04" self.interface.write(packet) # Since we discarded 0 leading bytes, they are simply written over by the write call self.assertEqual( @@ -652,9 +628,7 @@ def test_inserts_the_length_field_into_the_packet_and_sync_into_data_stream_2(se packet.buffer = b"\x01\x02\x03\x04\x05\x06" self.interface.write(packet) self.assertEqual(packet.buffer, b"\x00\x0A\x03\x04\x05\x06") - self.assertEqual( - TestLengthProtocol.buffer, b"\xBA\x5E\xBA\x11\x00\x0A\x03\x04\x05\x06" - ) + self.assertEqual(TestLengthProtocol.buffer, b"\xBA\x5E\xBA\x11\x00\x0A\x03\x04\x05\x06") def test_inserts_the_length_field_into_the_packet_and_sync_into_data_stream_3(self): self.interface.stream = TestLengthProtocol.LengthStream() @@ -754,9 +728,7 @@ def test_inserts_the_sync_and_length_fields_into_the_data_stream_1(self): packet.buffer = b"\x01\x02\x03\x04\x05\x06" self.interface.write(packet) self.assertEqual(packet.buffer, b"\x01\x02\x03\x04\x05\x06") - self.assertEqual( - TestLengthProtocol.buffer, b"\xDE\xAD\x0A\x00\x01\x02\x03\x04\x05\x06" - ) + self.assertEqual(TestLengthProtocol.buffer, b"\xDE\xAD\x0A\x00\x01\x02\x03\x04\x05\x06") def test_inserts_the_sync_and_length_fields_into_the_data_stream_2(self): TestLengthProtocol.buffer = "" @@ -780,6 +752,4 @@ def test_inserts_the_sync_and_length_fields_into_the_data_stream_2(self): packet.buffer = b"\x01\x02\x03\x04" self.interface.write(packet) self.assertEqual(packet.buffer, b"\x01\x02\x03\x04") - self.assertEqual( - TestLengthProtocol.buffer, b"\xBA\x5E\xBA\x11\x09\x01\x02\x03\x04" - ) + self.assertEqual(TestLengthProtocol.buffer, b"\xBA\x5E\xBA\x11\x09\x01\x02\x03\x04") diff --git a/openc3/python/test/interfaces/protocols/test_preidentified_protocol.py b/openc3/python/test/interfaces/protocols/test_preidentified_protocol.py index 12d74f374..d667db360 100644 --- a/openc3/python/test/interfaces/protocols/test_preidentified_protocol.py +++ b/openc3/python/test/interfaces/protocols/test_preidentified_protocol.py @@ -66,15 +66,11 @@ def setup_stream_pkt(self, args=[]): def verify_time_tgt_pkt_buffer(self, offset, time, pkt): self.assertEqual( - struct.unpack( - ">I", TestPreidentifiedProtocol.buffer[offset : (offset + 4)] - )[0], + struct.unpack(">I", TestPreidentifiedProtocol.buffer[offset : (offset + 4)])[0], int(time.timestamp()), ) self.assertEqual( - struct.unpack( - ">I", TestPreidentifiedProtocol.buffer[(offset + 4) : (offset + 8)] - )[0], + struct.unpack(">I", TestPreidentifiedProtocol.buffer[(offset + 4) : (offset + 8)])[0], 500000, ) offset += 8 # time fields @@ -93,9 +89,7 @@ def verify_time_tgt_pkt_buffer(self, offset, time, pkt): ) offset += pkt_name_length self.assertEqual( - struct.unpack( - ">I", TestPreidentifiedProtocol.buffer[offset : (offset + 4)] - )[0], + struct.unpack(">I", TestPreidentifiedProtocol.buffer[offset : (offset + 4)])[0], len(pkt.buffer), ) offset += 4 @@ -111,13 +105,9 @@ def test_handles_receiving_a_bad_packet_length(self): self.interface.read() def test_initializes_attributes(self): - self.interface.add_protocol( - PreidentifiedProtocol, ["0xDEADBEEF", 100], "READ_WRITE" - ) + self.interface.add_protocol(PreidentifiedProtocol, ["0xDEADBEEF", 100], "READ_WRITE") self.assertEqual(self.interface.read_protocols[0].data, b"") - self.assertEqual( - self.interface.read_protocols[0].sync_pattern, b"\xDE\xAD\xBE\xEF" - ) + self.assertEqual(self.interface.read_protocols[0].sync_pattern, b"\xDE\xAD\xBE\xEF") self.assertEqual(self.interface.read_protocols[0].max_length, 100) def test_write_creates_a_packet_header(self): @@ -149,9 +139,7 @@ def test_write_creates_a_packet_header_with_extra(self): json_extra = json.dumps(extra_data) offset += 1 self.assertEqual( - struct.unpack( - ">I", TestPreidentifiedProtocol.buffer[offset : (offset + 4)] - )[0], + struct.unpack(">I", TestPreidentifiedProtocol.buffer[offset : (offset + 4)])[0], len(json_extra), ) offset += 4 @@ -172,9 +160,7 @@ def test_write_creates_a_packet_header_with_stored_and_extra(self): json_extra = json.dumps(extra_data) offset = 1 self.assertEqual( - struct.unpack( - ">I", TestPreidentifiedProtocol.buffer[offset : (offset + 4)] - )[0], + struct.unpack(">I", TestPreidentifiedProtocol.buffer[offset : (offset + 4)])[0], len(json_extra), ) offset += 4 @@ -209,9 +195,7 @@ def test_write_handles_a_sync_pattern_with_stored_and_extra(self): json_extra = json.dumps(extra_data) offset = 3 self.assertEqual( - struct.unpack( - ">I", TestPreidentifiedProtocol.buffer[offset : (offset + 4)] - )[0], + struct.unpack(">I", TestPreidentifiedProtocol.buffer[offset : (offset + 4)])[0], len(json_extra), ) offset += 4 diff --git a/openc3/python/test/interfaces/protocols/test_slip_protocol.py b/openc3/python/test/interfaces/protocols/test_slip_protocol.py index e8609c0c8..1502a4706 100644 --- a/openc3/python/test/interfaces/protocols/test_slip_protocol.py +++ b/openc3/python/test/interfaces/protocols/test_slip_protocol.py @@ -54,28 +54,16 @@ def setUp(self): def test_complains_if_given_invalid_params(self): with self.assertRaisesRegex(ValueError, "invalid value 5.1234 for start_char"): self.interface.add_protocol(SlipProtocol, ["5.1234"], "READ_WRITE") - with self.assertRaisesRegex( - RuntimeError, "read_strip_characters must be True or False" - ): + with self.assertRaisesRegex(RuntimeError, "read_strip_characters must be True or False"): self.interface.add_protocol(SlipProtocol, [None, None], "READ_WRITE") - with self.assertRaisesRegex( - RuntimeError, "read_enable_escaping must be True or False" - ): + with self.assertRaisesRegex(RuntimeError, "read_enable_escaping must be True or False"): self.interface.add_protocol(SlipProtocol, [None, True, None], "READ_WRITE") - with self.assertRaisesRegex( - RuntimeError, "write_enable_escaping must be True or False" - ): - self.interface.add_protocol( - SlipProtocol, [None, True, True, None], "READ_WRITE" - ) + with self.assertRaisesRegex(RuntimeError, "write_enable_escaping must be True or False"): + self.interface.add_protocol(SlipProtocol, [None, True, True, None], "READ_WRITE") with self.assertRaisesRegex(ValueError, "invalid literal for int"): - self.interface.add_protocol( - SlipProtocol, [None, True, True, True, "5.1234"], "READ_WRITE" - ) + self.interface.add_protocol(SlipProtocol, [None, True, True, True, "5.1234"], "READ_WRITE") with self.assertRaisesRegex(ValueError, "invalid literal for int"): - self.interface.add_protocol( - SlipProtocol, [None, True, True, None, "0xC0", "5.1234"], "READ_WRITE" - ) + self.interface.add_protocol(SlipProtocol, [None, True, True, None, "0xC0", "5.1234"], "READ_WRITE") with self.assertRaisesRegex(ValueError, "invalid literal for int"): self.interface.add_protocol( SlipProtocol, @@ -187,22 +175,16 @@ def test_handles_bad_data_before_the_packet(self): def test_handles_escape_sequences(self): self.interface.stream = TestSlipProtocol.SlipStream() self.interface.add_protocol(SlipProtocol, [], "READ_WRITE") - TestSlipProtocol.buffer = ( - b"\x00\xDB\xDC\x44\xDB\xDD\x02\xDB\xDC\x03\xDB\xDD\xC0" - ) + TestSlipProtocol.buffer = b"\x00\xDB\xDC\x44\xDB\xDD\x02\xDB\xDC\x03\xDB\xDD\xC0" packet = self.interface.read() self.assertEqual(packet.buffer, b"\x00\xC0\x44\xDB\x02\xC0\x03\xDB") def test_leaves_escape_sequences(self): self.interface.stream = TestSlipProtocol.SlipStream() self.interface.add_protocol(SlipProtocol, [None, True, False], "READ_WRITE") - TestSlipProtocol.buffer = ( - b"\x00\xDB\xDC\x44\xDB\xDD\x02\xDB\xDC\x03\xDB\xDD\xC0" - ) + TestSlipProtocol.buffer = b"\x00\xDB\xDC\x44\xDB\xDD\x02\xDB\xDC\x03\xDB\xDD\xC0" packet = self.interface.read() - self.assertEqual( - packet.buffer, b"\x00\xDB\xDC\x44\xDB\xDD\x02\xDB\xDC\x03\xDB\xDD" - ) + self.assertEqual(packet.buffer, b"\x00\xDB\xDC\x44\xDB\xDD\x02\xDB\xDC\x03\xDB\xDD") def test_appends_end_char_to_the_packet(self): self.interface.stream = TestSlipProtocol.SlipStream() @@ -214,9 +196,7 @@ def test_appends_end_char_to_the_packet(self): def test_appends_a_different_end_char_to_the_packet(self): self.interface.stream = TestSlipProtocol.SlipStream() - self.interface.add_protocol( - SlipProtocol, [None, True, True, True, "0xEE"], "READ_WRITE" - ) + self.interface.add_protocol(SlipProtocol, [None, True, True, True, "0xEE"], "READ_WRITE") pkt = Packet("tgt", "pkt") pkt.buffer = b"\x00\x01\x02\x03" self.interface.write(pkt) @@ -252,15 +232,11 @@ def test_handles_writing_the_end_char_and_the_esc_char_inside_the_packet(self): pkt = Packet("tgt", "pkt") pkt.buffer = b"\x00\xC0\xDB\xDB\xC0\x02\x03" self.interface.write(pkt) - self.assertEqual( - TestSlipProtocol.buffer, b"\x00\xDB\xDC\xDB\xDD\xDB\xDD\xDB\xDC\x02\x03\xC0" - ) + self.assertEqual(TestSlipProtocol.buffer, b"\x00\xDB\xDC\xDB\xDD\xDB\xDD\xDB\xDC\x02\x03\xC0") def test_handles_not_writing_escape_sequences(self): self.interface.stream = TestSlipProtocol.SlipStream() - self.interface.add_protocol( - SlipProtocol, [None, True, True, False], "READ_WRITE" - ) + self.interface.add_protocol(SlipProtocol, [None, True, True, False], "READ_WRITE") pkt = Packet("tgt", "pkt") pkt.buffer = b"\x00\xC0\xDB\xDB\xC0\x02\x03" self.interface.write(pkt) @@ -276,6 +252,4 @@ def test_handles_different_escape_sequences(self): pkt = Packet("tgt", "pkt") pkt.buffer = b"\x00\xE0\xE1\xE1\xE0\x02\x03" self.interface.write(pkt) - self.assertEqual( - TestSlipProtocol.buffer, b"\x00\xE1\xE2\xE1\xE3\xE1\xE3\xE1\xE2\x02\x03\xE0" - ) + self.assertEqual(TestSlipProtocol.buffer, b"\x00\xE1\xE2\xE1\xE3\xE1\xE3\xE1\xE2\x02\x03\xE0") diff --git a/openc3/python/test/interfaces/protocols/test_template_protocol.py b/openc3/python/test/interfaces/protocols/test_template_protocol.py index a51fba82a..cc3387c69 100644 --- a/openc3/python/test/interfaces/protocols/test_template_protocol.py +++ b/openc3/python/test/interfaces/protocols/test_template_protocol.py @@ -59,21 +59,15 @@ def setUp(self): self.interface = TestTemplateProtocol.MyInterface() def test_initializes_attributes(self): - self.interface.add_protocol( - TemplateProtocol, ["0xABCD", "0xABCD"], "READ_WRITE" - ) + self.interface.add_protocol(TemplateProtocol, ["0xABCD", "0xABCD"], "READ_WRITE") self.assertEqual(self.interface.read_protocols[0].data, b"") def test_supports_an_initial_read_delay(self): self.interface.stream = TestTemplateProtocol.TemplateStream() - self.interface.add_protocol( - TemplateProtocol, ["0xABCD", "0xABCD", 0, 2], "READ_WRITE" - ) + self.interface.add_protocol(TemplateProtocol, ["0xABCD", "0xABCD", 0, 2], "READ_WRITE") start = time.time() self.interface.connect() - self.assertTrue( - self.interface.read_protocols[0].connect_complete_time >= (start + 2.0) - ) + self.assertTrue(self.interface.read_protocols[0].connect_complete_time >= (start + 2.0)) # def test_unblocks_writes_waiting_for_responses(self): # self.interface.stream = TestTemplateProtocol.TemplateStream() @@ -98,9 +92,7 @@ def test_supports_an_initial_read_delay(self): def test_ignores_all_data_during_the_connect_period(self): self.interface.stream = TestTemplateProtocol.TemplateStream() - self.interface.add_protocol( - TemplateProtocol, ["0xABCD", "0xABCD", 0, 1.5], "READ_WRITE" - ) + self.interface.add_protocol(TemplateProtocol, ["0xABCD", "0xABCD", 0, 1.5], "READ_WRITE") start = time.time() self.interface.connect() TestTemplateProtocol.read_buffer = b"\x31\x30\xAB\xCD" @@ -110,18 +102,14 @@ def test_ignores_all_data_during_the_connect_period(self): def test_waits_before_writing_during_the_initial_delay_period(self): self.interface.stream = TestTemplateProtocol.TemplateStream() - self.interface.add_protocol( - TemplateProtocol, ["0xABCD", "0xABCD", 0, 1.5], "READ_WRITE" - ) + self.interface.add_protocol(TemplateProtocol, ["0xABCD", "0xABCD", 0, 1.5], "READ_WRITE") packet = Packet("TGT", "CMD") packet.append_item("VOLTAGE", 16, "UINT") packet.get_item("VOLTAGE").default = 1 packet.append_item("CHANNEL", 16, "UINT") packet.get_item("CHANNEL").default = 2 packet.append_item("CMD_TEMPLATE", 1024, "STRING") - packet.get_item("CMD_TEMPLATE").default = ( - "SOUR'VOLT' , (self.)" - ) + packet.get_item("CMD_TEMPLATE").default = "SOUR'VOLT' , (self.)" packet.restore_defaults() self.interface.connect() write = time.time() @@ -130,23 +118,17 @@ def test_waits_before_writing_during_the_initial_delay_period(self): def test_works_without_a_response(self): self.interface.stream = TestTemplateProtocol.TemplateStream() - self.interface.add_protocol( - TemplateProtocol, ["0xABCD", "0xABCD"], "READ_WRITE" - ) + self.interface.add_protocol(TemplateProtocol, ["0xABCD", "0xABCD"], "READ_WRITE") packet = Packet("TGT", "CMD") packet.append_item("VOLTAGE", 16, "UINT") packet.get_item("VOLTAGE").default = 1 packet.append_item("CHANNEL", 16, "UINT") packet.get_item("CHANNEL").default = 2 packet.append_item("CMD_TEMPLATE", 1024, "STRING") - packet.get_item("CMD_TEMPLATE").default = ( - "SOUR'VOLT' , (self.)" - ) + packet.get_item("CMD_TEMPLATE").default = "SOUR'VOLT' , (self.)" packet.restore_defaults() self.interface.write(packet) - self.assertEqual( - TestTemplateProtocol.write_buffer, b"SOUR'VOLT' 1, (self.2)\xAB\xCD" - ) + self.assertEqual(TestTemplateProtocol.write_buffer, b"SOUR'VOLT' 1, (self.2)\xAB\xCD") def test_logs_an_error_if_it_doesnt_receive_a_response(self): self.interface.stream = TestTemplateProtocol.TemplateStream() @@ -240,9 +222,7 @@ def test_processes_responses_with_no_id_fields(self, mock_system): packet.append_item("CHANNEL", 16, "UINT") packet.get_item("CHANNEL").default = 1 packet.append_item("CMD_TEMPLATE", 1024, "STRING") - packet.get_item("CMD_TEMPLATE").default = ( - "SOUR'VOLT' , (self.)" - ) + packet.get_item("CMD_TEMPLATE").default = "SOUR'VOLT' , (self.)" packet.append_item("RSP_TEMPLATE", 1024, "STRING") packet.get_item("RSP_TEMPLATE").default = "" packet.append_item("RSP_PACKET", 1024, "STRING") @@ -260,9 +240,7 @@ def do_read(self): thread.start() self.interface.write(packet) time.sleep(0.55) - self.assertEqual( - TestTemplateProtocol.write_buffer, b"SOUR'VOLT' 11, (self.1)\xAB\xCD" - ) + self.assertEqual(TestTemplateProtocol.write_buffer, b"SOUR'VOLT' 11, (self.1)\xAB\xCD") self.assertEqual(self.read_result.read("VOLTAGE"), (10)) @patch("openc3.interfaces.protocols.template_protocol.System") @@ -285,18 +263,14 @@ def test_sets_the_response_id_to_the_defined_id_value(self, mock_system): ) self.interface.target_names = ["TGT"] packet = Packet("TGT", "CMD") - packet.append_item( - "CMD_ID", 16, "UINT", None, "BIG_ENDIAN", "ERROR", None, None, None, 1 - ) # ID == 1 + packet.append_item("CMD_ID", 16, "UINT", None, "BIG_ENDIAN", "ERROR", None, None, None, 1) # ID == 1 packet.get_item("CMD_ID").default = 1 packet.append_item("VOLTAGE", 16, "UINT") packet.get_item("VOLTAGE").default = 11 packet.append_item("CHANNEL", 16, "UINT") packet.get_item("CHANNEL").default = 1 packet.append_item("CMD_TEMPLATE", 1024, "STRING") - packet.get_item("CMD_TEMPLATE").default = ( - "SOUR'VOLT' , (self.)" - ) + packet.get_item("CMD_TEMPLATE").default = "SOUR'VOLT' , (self.)" packet.append_item("RSP_TEMPLATE", 1024, "STRING") packet.get_item("RSP_TEMPLATE").default = "" packet.append_item("RSP_PACKET", 1024, "STRING") @@ -314,12 +288,8 @@ def do_read(self): thread.start() self.interface.write(packet) time.sleep(0.55) - self.assertEqual( - TestTemplateProtocol.write_buffer, b"SOUR'VOLT' 11, (self.1)\xAB\xCD" - ) - self.assertEqual( - self.read_result.read("PKT_ID"), (1) - ) # Result ID set to the defined value) + self.assertEqual(TestTemplateProtocol.write_buffer, b"SOUR'VOLT' 11, (self.1)\xAB\xCD") + self.assertEqual(self.read_result.read("PKT_ID"), (1)) # Result ID set to the defined value) self.assertEqual(self.read_result.read("VOLTAGE"), (10)) @patch("openc3.interfaces.protocols.template_protocol.System") @@ -343,22 +313,16 @@ def test_handles_multiple_response_ids(self, mock_system): ) self.interface.target_names = ["TGT"] packet = Packet("TGT", "CMD") - packet.append_item( - "APID", 16, "UINT", None, "BIG_ENDIAN", "ERROR", None, None, None, 1 - ) # ID == 1 + packet.append_item("APID", 16, "UINT", None, "BIG_ENDIAN", "ERROR", None, None, None, 1) # ID == 1 packet.get_item("APID").default = 1 - packet.append_item( - "PKTID", 16, "UINT", None, "BIG_ENDIAN", "ERROR", None, None, None, 2 - ) # ID == 2 + packet.append_item("PKTID", 16, "UINT", None, "BIG_ENDIAN", "ERROR", None, None, None, 2) # ID == 2 packet.get_item("PKTID").default = 2 packet.append_item("VOLTAGE", 16, "UINT") packet.get_item("VOLTAGE").default = 11 packet.append_item("CHANNEL", 16, "UINT") packet.get_item("CHANNEL").default = 1 packet.append_item("CMD_TEMPLATE", 1024, "STRING") - packet.get_item("CMD_TEMPLATE").default = ( - "SOUR'VOLT' , (self.)" - ) + packet.get_item("CMD_TEMPLATE").default = "SOUR'VOLT' , (self.)" packet.append_item("RSP_TEMPLATE", 1024, "STRING") packet.get_item("RSP_TEMPLATE").default = "" packet.append_item("RSP_PACKET", 1024, "STRING") @@ -380,15 +344,9 @@ def do_read(self): self.interface.write(packet) time.sleep(0.55) - self.assertEqual( - TestTemplateProtocol.write_buffer, b"SOUR'VOLT' 11, (self.1)\xAB\xCD" - ) - self.assertEqual( - self.read_result.read("APID"), (10) - ) # ID item set to the defined value) - self.assertEqual( - self.read_result.read("PKTID"), (20) - ) # ID item set to the defined value) + self.assertEqual(TestTemplateProtocol.write_buffer, b"SOUR'VOLT' 11, (self.1)\xAB\xCD") + self.assertEqual(self.read_result.read("APID"), (10)) # ID item set to the defined value) + self.assertEqual(self.read_result.read("PKTID"), (20)) # ID item set to the defined value) @patch("openc3.interfaces.protocols.template_protocol.System") def test_handles_templates_with_more_values_than_the_response(self, mock_system): @@ -414,9 +372,7 @@ def test_handles_templates_with_more_values_than_the_response(self, mock_system) packet.append_item("CHANNEL", 16, "UINT") packet.get_item("CHANNEL").default = 2 packet.append_item("CMD_TEMPLATE", 1024, "STRING") - packet.get_item("CMD_TEMPLATE").default = ( - "SOUR'VOLT' , (self.)" - ) + packet.get_item("CMD_TEMPLATE").default = "SOUR'VOLT' , (self.)" packet.append_item("RSP_TEMPLATE", 1024, "STRING") packet.get_item("RSP_TEMPLATE").default = ";" packet.append_item("RSP_PACKET", 1024, "STRING") @@ -439,9 +395,7 @@ def do_read(self): stdout.getvalue(), ) - self.assertEqual( - TestTemplateProtocol.write_buffer, b"SOUR'VOLT' 12, (self.2)\xAB\xCD" - ) + self.assertEqual(TestTemplateProtocol.write_buffer, b"SOUR'VOLT' 12, (self.2)\xAB\xCD") @patch("openc3.interfaces.protocols.template_protocol.System") def test_handles_responses_with_more_values_than_the_template(self, mock_system): @@ -467,18 +421,14 @@ def test_handles_responses_with_more_values_than_the_template(self, mock_system) packet.append_item("CHANNEL", 16, "UINT") packet.get_item("CHANNEL").default = 2 packet.append_item("CMD_TEMPLATE", 1024, "STRING") - packet.get_item("CMD_TEMPLATE").default = ( - "SOUR'VOLT' , (self.)" - ) + packet.get_item("CMD_TEMPLATE").default = "SOUR'VOLT' , (self.)" packet.append_item("RSP_TEMPLATE", 1024, "STRING") packet.get_item("RSP_TEMPLATE").default = "" packet.append_item("RSP_PACKET", 1024, "STRING") packet.get_item("RSP_PACKET").default = "READ_VOLTAGE" packet.restore_defaults() self.interface.connect() - TestTemplateProtocol.read_buffer = ( - b"\x31\x30\x3B\x31\x31\xAB\xCD" # ASCII is '10;11' - ) + TestTemplateProtocol.read_buffer = b"\x31\x30\x3B\x31\x31\xAB\xCD" # ASCII is '10;11' def do_read(self): time.sleep(0.5) @@ -495,9 +445,7 @@ def do_read(self): stdout.getvalue(), ) - self.assertEqual( - TestTemplateProtocol.write_buffer, b"SOUR'VOLT' 12, (self.2)\xAB\xCD" - ) + self.assertEqual(TestTemplateProtocol.write_buffer, b"SOUR'VOLT' 12, (self.2)\xAB\xCD") @patch("openc3.interfaces.protocols.template_protocol.System") def test_ignores_response_lines(self, mock_system): @@ -519,9 +467,7 @@ def test_ignores_response_lines(self, mock_system): packet.append_item("CHANNEL", 16, "UINT") packet.get_item("CHANNEL").default = 20 packet.append_item("CMD_TEMPLATE", 1024, "STRING") - packet.get_item("CMD_TEMPLATE").default = ( - "SOUR'VOLT' , (self.)" - ) + packet.get_item("CMD_TEMPLATE").default = "SOUR'VOLT' , (self.)" packet.append_item("RSP_TEMPLATE", 1024, "STRING") packet.get_item("RSP_TEMPLATE").default = "" packet.append_item("RSP_PACKET", 1024, "STRING") @@ -529,9 +475,7 @@ def test_ignores_response_lines(self, mock_system): packet.restore_defaults() self.interface.connect() self.read_result = None - TestTemplateProtocol.read_buffer = ( - b"\x31\x30\x0A\x31\x32\x0A" # ASCII: 30:'0', 31:'1', etc - ) + TestTemplateProtocol.read_buffer = b"\x31\x30\x0A\x31\x32\x0A" # ASCII: 30:'0', 31:'1', etc def do_read(self): time.sleep(0.5) @@ -540,9 +484,7 @@ def do_read(self): thread = threading.Thread(target=do_read, args=[self]) thread.start() self.interface.write(packet) - self.assertEqual( - TestTemplateProtocol.write_buffer, b"SOUR'VOLT' 11, (self.20)\xAD" - ) + self.assertEqual(TestTemplateProtocol.write_buffer, b"SOUR'VOLT' 11, (self.20)\xAD") self.assertEqual(self.read_result.read("VOLTAGE"), 12) @patch("openc3.interfaces.protocols.template_protocol.System") @@ -557,9 +499,7 @@ def test_allows_multiple_response_lines(self, mock_system): mock_system.telemetry = Telemetry(pc, mock_system) self.interface.stream = TestTemplateProtocol.TemplateStream() - self.interface.add_protocol( - TemplateProtocol, ["0xAD", "0xA", 0, None, 2], "READ_WRITE" - ) + self.interface.add_protocol(TemplateProtocol, ["0xAD", "0xA", 0, None, 2], "READ_WRITE") self.interface.target_names = ["TGT"] packet = Packet("TGT", "CMD") packet.append_item("CMD_TEMPLATE", 1024, "STRING") diff --git a/openc3/python/test/interfaces/protocols/test_terminated_protocol.py b/openc3/python/test/interfaces/protocols/test_terminated_protocol.py index 4ec97d190..408359067 100644 --- a/openc3/python/test/interfaces/protocols/test_terminated_protocol.py +++ b/openc3/python/test/interfaces/protocols/test_terminated_protocol.py @@ -51,9 +51,7 @@ def setUp(self): self.interface = TestTerminatedProtocol.MyInterface() def test_initializes_attributes(self): - self.interface.add_protocol( - TerminatedProtocol, ["0xABCD", "0xABCD"], "READ_WRITE" - ) + self.interface.add_protocol(TerminatedProtocol, ["0xABCD", "0xABCD"], "READ_WRITE") self.assertEqual(self.interface.read_protocols[0].data, b"") def test_handles_multiple_reads(self): @@ -73,17 +71,13 @@ def read(self): return b"\xCD" self.interface.stream = MultiTerminatedStream() - self.interface.add_protocol( - TerminatedProtocol, ["", "0xABCD", True], "READ_WRITE" - ) + self.interface.add_protocol(TerminatedProtocol, ["", "0xABCD", True], "READ_WRITE") packet = self.interface.read() self.assertEqual(packet.buffer, b"\x01\x02") def test_strip_handles_empty_packets(self): self.interface.stream = TestTerminatedProtocol.TerminatedStream() - self.interface.add_protocol( - TerminatedProtocol, ["", "0xABCD", True], "READ_WRITE" - ) + self.interface.add_protocol(TerminatedProtocol, ["", "0xABCD", True], "READ_WRITE") TestTerminatedProtocol.buffer = b"\xAB\xCD\x01\x02\xAB\xCD" packet = self.interface.read() self.assertEqual(len(packet.buffer), 0) @@ -92,36 +86,28 @@ def test_strip_handles_empty_packets(self): def test_strip_handles_no_sync_pattern(self): self.interface.stream = TestTerminatedProtocol.TerminatedStream() - self.interface.add_protocol( - TerminatedProtocol, ["", "0xABCD", True], "READ_WRITE" - ) + self.interface.add_protocol(TerminatedProtocol, ["", "0xABCD", True], "READ_WRITE") TestTerminatedProtocol.buffer = b"\x00\x01\x02\xAB\xCD\x44\x02\x03" packet = self.interface.read() self.assertEqual(packet.buffer, b"\x00\x01\x02") def test_strip_handles_a_sync_pattern_inside_the_packet(self): self.interface.stream = TestTerminatedProtocol.TerminatedStream() - self.interface.add_protocol( - TerminatedProtocol, ["", "0xABCD", True, 0, "DEAD"], "READ_WRITE" - ) + self.interface.add_protocol(TerminatedProtocol, ["", "0xABCD", True, 0, "DEAD"], "READ_WRITE") TestTerminatedProtocol.buffer = b"\xDE\xAD\x00\x01\x02\xAB\xCD\x44\x02\x03" packet = self.interface.read() self.assertEqual(packet.buffer, b"\xDE\xAD\x00\x01\x02") def test_strip_handles_a_sync_pattern_outside_the_packet(self): self.interface.stream = TestTerminatedProtocol.TerminatedStream() - self.interface.add_protocol( - TerminatedProtocol, ["", "0xABCD", True, 2, "DEAD"], "READ_WRITE" - ) + self.interface.add_protocol(TerminatedProtocol, ["", "0xABCD", True, 2, "DEAD"], "READ_WRITE") TestTerminatedProtocol.buffer = b"\xDE\xAD\x00\x01\x02\xAB\xCD\x44\x02\x03" packet = self.interface.read() self.assertEqual(packet.buffer, b"\x00\x01\x02") def test_keep_handles_empty_packets(self): self.interface.stream = TestTerminatedProtocol.TerminatedStream() - self.interface.add_protocol( - TerminatedProtocol, ["", "0xABCD", False], "READ_WRITE" - ) + self.interface.add_protocol(TerminatedProtocol, ["", "0xABCD", False], "READ_WRITE") TestTerminatedProtocol.buffer = b"\xAB\xCD\x01\x02\xAB\xCD" packet = self.interface.read() self.assertEqual(packet.buffer, b"\xAB\xCD") @@ -130,27 +116,21 @@ def test_keep_handles_empty_packets(self): def test_keep_handles_no_sync_pattern(self): self.interface.stream = TestTerminatedProtocol.TerminatedStream() - self.interface.add_protocol( - TerminatedProtocol, ["", "0xABCD", False], "READ_WRITE" - ) + self.interface.add_protocol(TerminatedProtocol, ["", "0xABCD", False], "READ_WRITE") TestTerminatedProtocol.buffer = b"\x00\x01\x02\xAB\xCD\x44\x02\x03" packet = self.interface.read() self.assertEqual(packet.buffer, b"\x00\x01\x02\xAB\xCD") def test_keep_handles_a_sync_pattern_inside_the_packet(self): self.interface.stream = TestTerminatedProtocol.TerminatedStream() - self.interface.add_protocol( - TerminatedProtocol, ["", "0xABCD", False, 0, "DEAD"], "READ_WRITE" - ) + self.interface.add_protocol(TerminatedProtocol, ["", "0xABCD", False, 0, "DEAD"], "READ_WRITE") TestTerminatedProtocol.buffer = b"\xDE\xAD\x00\x01\x02\xAB\xCD\x44\x02\x03" packet = self.interface.read() self.assertEqual(packet.buffer, b"\xDE\xAD\x00\x01\x02\xAB\xCD") def test_keep_handles_a_sync_pattern_outside_the_packet(self): self.interface.stream = TestTerminatedProtocol.TerminatedStream() - self.interface.add_protocol( - TerminatedProtocol, ["", "0xABCD", False, 2, "DEAD"], "READ_WRITE" - ) + self.interface.add_protocol(TerminatedProtocol, ["", "0xABCD", False, 2, "DEAD"], "READ_WRITE") TestTerminatedProtocol.buffer = b"\xDE\xAD\x00\x01\x02\xAB\xCD\x44\x02\x03" packet = self.interface.read() self.assertEqual(packet.buffer, b"\x00\x01\x02\xAB\xCD") @@ -168,16 +148,12 @@ def test_complains_if_the_packet_buffer_contains_the_termination_characters(self self.interface.add_protocol(TerminatedProtocol, ["0xCDEF", ""], "READ_WRITE") pkt = Packet("tgt", "pkt") pkt.buffer = b"\x00\xCD\xEF\x03" - with self.assertRaisesRegex( - RuntimeError, "Packet contains termination characters!" - ): + with self.assertRaisesRegex(RuntimeError, "Packet contains termination characters!"): self.interface.write(pkt) def test_handles_writing_the_sync_field_inside_the_packet(self): self.interface.stream = TestTerminatedProtocol.TerminatedStream() - self.interface.add_protocol( - TerminatedProtocol, ["0xCDEF", "", True, 0, "DEAD", True], "READ_WRITE" - ) + self.interface.add_protocol(TerminatedProtocol, ["0xCDEF", "", True, 0, "DEAD", True], "READ_WRITE") pkt = Packet("tgt", "pkt") pkt.buffer = b"\x00\x01\x02\x03" self.interface.write(pkt) @@ -185,12 +161,8 @@ def test_handles_writing_the_sync_field_inside_the_packet(self): def test_handles_writing_the_sync_field_outside_the_packet(self): self.interface.stream = TestTerminatedProtocol.TerminatedStream() - self.interface.add_protocol( - TerminatedProtocol, ["0xCDEF", "", True, 2, "DEAD", True], "READ_WRITE" - ) + self.interface.add_protocol(TerminatedProtocol, ["0xCDEF", "", True, 2, "DEAD", True], "READ_WRITE") pkt = Packet("tgt", "pkt") pkt.buffer = b"\x00\x01\x02\x03" self.interface.write(pkt) - self.assertEqual( - TestTerminatedProtocol.buffer, b"\xDE\xAD\x00\x01\x02\x03\xCD\xEF" - ) + self.assertEqual(TestTerminatedProtocol.buffer, b"\xDE\xAD\x00\x01\x02\x03\xCD\xEF") diff --git a/openc3/python/test/interfaces/test_interface.py b/openc3/python/test/interfaces/test_interface.py index f02a404a2..c52f478d1 100644 --- a/openc3/python/test/interfaces/test_interface.py +++ b/openc3/python/test/interfaces/test_interface.py @@ -17,8 +17,6 @@ import time import unittest import threading -from unittest.mock import * -from test.test_helper import * from openc3.interfaces.interface import Interface from openc3.interfaces.protocols.protocol import Protocol from openc3.packets.packet import Packet @@ -28,9 +26,7 @@ class InterfaceTestProtocol(Protocol): - def __init__( - self, added_data, stop_count=0, packet_added_data=None, packet_stop_count=0 - ): + def __init__(self, added_data, stop_count=0, packet_added_data=None, packet_stop_count=0): self.added_data = added_data self.packet_added_data = packet_added_data self.stop_count = int(stop_count) @@ -464,9 +460,7 @@ def write_interface(self, data, extra=None): self.write_interface_base(data) interface = MyInterface() - interface.add_protocol( - InterfaceTestProtocol, [None, 0, "DISCONNECT", 0], "WRITE" - ) + interface.add_protocol(InterfaceTestProtocol, [None, 0, "DISCONNECT", 0], "WRITE") interface.write(self.packet) self.assertEqual(interface.write_count, 1) self.assertEqual(interface.bytes_written, 0) @@ -517,9 +511,7 @@ def write_interface(self, data, extra=None): self.write_interface_base(data) interface = MyInterface() - interface.add_protocol( - InterfaceTestProtocol, ["DISCONNECT", 0, None, 0], "WRITE" - ) + interface.add_protocol(InterfaceTestProtocol, ["DISCONNECT", 0, None, 0], "WRITE") interface.write(self.packet) self.assertEqual(interface.write_count, 1) self.assertEqual(interface.bytes_written, 0) @@ -672,17 +664,11 @@ def protocol_cmd(self, cmd_name, *cmd_args): def setUp(self): self.i = Interface() - self.i.add_protocol( - ProtocolCmd.InterfaceCmdProtocol, [None, 0, None, 0], "WRITE" - ) + self.i.add_protocol(ProtocolCmd.InterfaceCmdProtocol, [None, 0, None, 0], "WRITE") self.write_protocol = self.i.write_protocols[-1] - self.i.add_protocol( - ProtocolCmd.InterfaceCmdProtocol, [None, 0, None, 0], "READ" - ) + self.i.add_protocol(ProtocolCmd.InterfaceCmdProtocol, [None, 0, None, 0], "READ") self.read_protocol = self.i.read_protocols[-1] - self.i.add_protocol( - ProtocolCmd.InterfaceCmdProtocol, [None, 0, None, 0], "READ_WRITE" - ) + self.i.add_protocol(ProtocolCmd.InterfaceCmdProtocol, [None, 0, None, 0], "READ_WRITE") self.read_write_protocol = self.i.read_protocols[-1] def test_handles_unknown_protocol_descriptors(self): diff --git a/openc3/python/test/interfaces/test_mqtt_interface.py b/openc3/python/test/interfaces/test_mqtt_interface.py new file mode 100644 index 000000000..847994a72 --- /dev/null +++ b/openc3/python/test/interfaces/test_mqtt_interface.py @@ -0,0 +1,105 @@ +# Copyright 2024 OpenC3, Inc. +# All Rights Reserved. +# +# This program is free software; you can modify and/or redistribute it +# under the terms of the GNU Affero General Public License +# as published by the Free Software Foundation; version 3 with +# attribution addendums as found in the LICENSE.txt +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# This file may also be used under the terms of a commercial license +# if purchased from OpenC3, Inc. + +import unittest +from unittest.mock import patch, Mock, ANY +from test.test_helper import mock_redis, setup_system +from openc3.interfaces.mqtt_interface import MqttInterface +from openc3.system.system import System + + +class TestMqttInterface(unittest.TestCase): + def setUp(self): + mock_redis(self) + setup_system() + + def test_sets_all_the_instance_variables(self): + i = MqttInterface("localhost", "1883") + self.assertEqual(i.name, "MqttInterface") + self.assertEqual(i.hostname, "localhost") + self.assertEqual(i.port, 1883) + self.assertEqual(i.ack_timeout, 5.0) + + def test_builds_a_human_readable_connection_string(self): + i = MqttInterface("localhost", "1883") + self.assertEqual(i.connection_string(), "localhost:1883") + + @patch("openc3.interfaces.mqtt_interface.mqtt.Client") + def test_connects_to_mqtt_broker(self, mock_client): + mock_client_instance = mock_client.return_value + mock_client_instance.is_connected.return_value = True + i = MqttInterface("localhost", "1883") + i.set_option("ACK_TIMEOUT", ["10.0"]) + i.set_option("USERNAME", ["test_user"]) + i.set_option("PASSWORD", ["test_pass"]) + i.set_option("CERT", ["cert_content"]) + i.set_option("KEY", ["key_content"]) + i.set_option("CA_FILE", ["ca_file_content"]) + i.connect() + self.assertTrue(i.connected()) + self.assertEqual(i.ack_timeout, 10.0) + mock_client_instance.username_pw_set.assert_called_with("test_user", "test_pass") + mock_client_instance.tls_set.assert_called_with(ca_certs=ANY, certfile=ANY, keyfile=ANY) + mock_client_instance.loop_start.assert_called_once() + mock_client_instance.connect.assert_called_with("localhost", 1883) + mock_client_instance.is_connected.assert_called() + # Manually call the callback + reason_code = Mock() + reason_code.is_failure = False + mock_client_instance.on_connect(mock_client_instance, None, None, reason_code, None) + mock_client_instance.subscribe.assert_any_call("HEALTH_STATUS") + mock_client_instance.subscribe.assert_any_call("ADCS") + + @patch("openc3.interfaces.mqtt_interface.mqtt.Client") + def test_disconnects_the_mqtt_client(self, mock_client): + mock_client_instance = mock_client.return_value + i = MqttInterface("localhost", "1883") + i.connect() + i.disconnect() + self.assertFalse(i.connected()) + i.disconnect() # Safe to call twice + mock_client_instance.disconnect.assert_called() + + @patch("openc3.interfaces.mqtt_interface.mqtt.Client") + def test_reads_a_message_from_the_mqtt_client(self, mock_client): + mock_client_instance = mock_client.return_value + i = MqttInterface("localhost", "1883") + i.connect() + message = Mock() + message.topic = "HEALTH_STATUS" + message.payload = b"\x00\x00\x00\x00\x00\x00" + mock_client_instance.on_message(mock_client_instance, i.pkt_queue, message) + packet = i.read() + self.assertEqual(packet.target_name, "INST") + self.assertEqual(packet.packet_name, "HEALTH_STATUS") + + @patch("openc3.interfaces.mqtt_interface.mqtt.Client") + def test_writes_a_message_to_the_mqtt_client(self, mock_client): + mock_client_instance = mock_client.return_value + i = MqttInterface("localhost", "1883") + i.connect() + pkt = System.commands.packet("INST", "COLLECT") + pkt.restore_defaults() + i.write(pkt) + mock_client_instance.publish.assert_called_with("COLLECT", pkt.buffer) + + @patch("openc3.interfaces.mqtt_interface.mqtt.Client") + def test_raises_on_packets_without_meta_topic(self, _): + i = MqttInterface("localhost", "1883") + i.connect() + pkt = System.commands.packet("INST", "CLEAR") + with self.assertRaisesRegex(RuntimeError, "Command packet 'INST CLEAR' requires a META TOPIC or TOPICS"): + i.write(pkt) diff --git a/openc3/python/test/interfaces/test_mqtt_stream_interface.py b/openc3/python/test/interfaces/test_mqtt_stream_interface.py new file mode 100644 index 000000000..8e2e0e1a7 --- /dev/null +++ b/openc3/python/test/interfaces/test_mqtt_stream_interface.py @@ -0,0 +1,98 @@ +# Copyright 2024 OpenC3, Inc. +# All Rights Reserved. +# +# This program is free software; you can modify and/or redistribute it +# under the terms of the GNU Affero General Public License +# as published by the Free Software Foundation; version 3 with +# attribution addendums as found in the LICENSE.txt +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# This file may also be used under the terms of a commercial license +# if purchased from OpenC3, Inc. + +import unittest +from unittest.mock import patch, Mock, ANY +from test.test_helper import mock_redis, setup_system +from openc3.interfaces.mqtt_stream_interface import MqttStreamInterface +from openc3.system.system import System + + +class TestMqttStreamInterface(unittest.TestCase): + def setUp(self): + mock_redis(self) + setup_system() + + def test_sets_all_the_instance_variables(self): + i = MqttStreamInterface("localhost", "1883", "write_topic", "read_topic") + self.assertEqual(i.name, "MqttStreamInterface") + self.assertEqual(i.hostname, "localhost") + self.assertEqual(i.port, 1883) + self.assertEqual(i.write_topic, "write_topic") + self.assertEqual(i.read_topic, "read_topic") + + def test_builds_a_human_readable_connection_string(self): + i = MqttStreamInterface("localhost", "1883", "write_topic", "read_topic") + self.assertEqual(i.connection_string(), "localhost:1883 write topic: write_topic read topic: read_topic") + + @patch("openc3.streams.mqtt_stream.mqtt.Client") + def test_connects_to_mqtt_broker(self, mock_client): + mock_client_instance = mock_client.return_value + mock_client_instance.is_connected.return_value = True + i = MqttStreamInterface("localhost", "1883", "write_topic", "read_topic") + i.set_option("ACK_TIMEOUT", ["10.0"]) + i.set_option("USERNAME", ["test_user"]) + i.set_option("PASSWORD", ["test_pass"]) + i.set_option("CERT", ["cert_content"]) + i.set_option("KEY", ["key_content"]) + i.set_option("CA_FILE", ["ca_file_content"]) + i.connect() + self.assertTrue(i.connected()) + self.assertEqual(i.ack_timeout, 10.0) + mock_client_instance.username_pw_set.assert_called_with("test_user", "test_pass") + mock_client_instance.tls_set.assert_called_with(ca_certs=ANY, certfile=ANY, keyfile=ANY) + mock_client_instance.loop_start.assert_called_once() + mock_client_instance.connect.assert_called_with("localhost", 1883) + mock_client_instance.is_connected.assert_called() + # Manually call the callback + reason_code = Mock() + reason_code.is_failure = False + mock_client_instance.on_connect(mock_client_instance, None, None, reason_code, None) + mock_client_instance.subscribe.assert_called_with("read_topic") + + @patch("openc3.streams.mqtt_stream.mqtt.Client") + def test_disconnects_the_mqtt_client(self, mock_client): + mock_client_instance = mock_client.return_value + i = MqttStreamInterface("localhost", "1883", "write_topic", "read_topic") + i.connect() + i.disconnect() + self.assertFalse(i.connected()) + i.disconnect() # Safe to call twice + mock_client_instance.disconnect.assert_called() + + @patch("openc3.streams.mqtt_stream.mqtt.Client") + def test_reads_a_message_from_the_mqtt_client(self, mock_client): + mock_client_instance = mock_client.return_value + i = MqttStreamInterface("localhost", "1883", "write_topic", "read_topic") + i.connect() + message = Mock() + message.topic = "read_topic" + message.payload = b"\x00\x01\x02\x03\x04\x05" + mock_client_instance.on_message(mock_client_instance, i.stream.pkt_queue, message) + packet = i.read() + self.assertIsNone(packet.target_name) + self.assertIsNone(packet.packet_name) + self.assertEqual(packet.buffer, b"\x00\x01\x02\x03\x04\x05") + + @patch("openc3.streams.mqtt_stream.mqtt.Client") + def test_writes_a_message_to_the_mqtt_client(self, mock_client): + mock_client_instance = mock_client.return_value + i = MqttStreamInterface("localhost", "1883", "write_topic", "read_topic") + i.connect() + pkt = System.commands.packet("INST", "COLLECT") + pkt.restore_defaults() + i.write(pkt) + mock_client_instance.publish.assert_called_with("write_topic", pkt.buffer) diff --git a/openc3/python/test/interfaces/test_simulated_target_interface.py b/openc3/python/test/interfaces/test_simulated_target_interface.py index eeea8a0e6..e29187d8a 100644 --- a/openc3/python/test/interfaces/test_simulated_target_interface.py +++ b/openc3/python/test/interfaces/test_simulated_target_interface.py @@ -15,11 +15,11 @@ # if purchased from OpenC3, Inc. import unittest -from unittest.mock import * -from test.test_helper import * +from test.test_helper import mock_redis, setup_system from openc3.interfaces.simulated_target_interface import SimulatedTargetInterface from openc3.packets.packet import Packet from openc3.interfaces.protocols.protocol import Protocol +from openc3.system.system import System class MyProtocol(Protocol): @@ -41,9 +41,7 @@ def setUp(self): setup_system() def test_complains_if_the_simulated_target_file_doesnt_exist(self): - with self.assertRaisesRegex( - ModuleNotFoundError, "No module named 'doesnt_exist'" - ): + with self.assertRaisesRegex(ModuleNotFoundError, "No module named 'doesnt_exist'"): SimulatedTargetInterface("doesnt_exist.py") def test_creates_the_simulated_target_class(self): diff --git a/openc3/python/test/interfaces/test_stream_interface.py b/openc3/python/test/interfaces/test_stream_interface.py index 0ea7a471b..1fe5791c1 100644 --- a/openc3/python/test/interfaces/test_stream_interface.py +++ b/openc3/python/test/interfaces/test_stream_interface.py @@ -15,8 +15,6 @@ # if purchased from OpenC3, Inc. import unittest -from unittest.mock import * -from test.test_helper import * from openc3.interfaces.stream_interface import StreamInterface from openc3.interfaces.protocols.burst_protocol import BurstProtocol from openc3.interfaces.protocols.length_protocol import LengthProtocol diff --git a/openc3/python/test/interfaces/test_tcpip_client_interface.py b/openc3/python/test/interfaces/test_tcpip_client_interface.py index 461d302d0..1218c87ef 100644 --- a/openc3/python/test/interfaces/test_tcpip_client_interface.py +++ b/openc3/python/test/interfaces/test_tcpip_client_interface.py @@ -15,8 +15,6 @@ # if purchased from OpenC3, Inc. import unittest -from unittest.mock import * -from test.test_helper import * from openc3.interfaces.tcpip_client_interface import TcpipClientInterface @@ -53,9 +51,7 @@ def test_connection_string(self): self.assertEqual(i.connection_string(), "localhost:8889 (R/W)") i = TcpipClientInterface("localhost", "8889", "8890", "None", "5", "burst") - self.assertEqual( - i.connection_string(), "localhost:8889 (write) localhost:8890 (read)" - ) + self.assertEqual(i.connection_string(), "localhost:8889 (write) localhost:8890 (read)") i = TcpipClientInterface("localhost", "8889", "None", "None", "5", "burst") self.assertEqual(i.connection_string(), "localhost:8889 (write)") diff --git a/openc3/python/test/interfaces/test_tcpip_server_interface.py b/openc3/python/test/interfaces/test_tcpip_server_interface.py index 8beba25c0..1ee992d0d 100644 --- a/openc3/python/test/interfaces/test_tcpip_server_interface.py +++ b/openc3/python/test/interfaces/test_tcpip_server_interface.py @@ -18,8 +18,6 @@ import socket import threading import unittest -from unittest.mock import * -from test.test_helper import * from openc3.interfaces.tcpip_server_interface import TcpipServerInterface from openc3.packets.packet import Packet diff --git a/openc3/python/test/interfaces/test_udp_interface.py b/openc3/python/test/interfaces/test_udp_interface.py index 83f184066..52640a701 100644 --- a/openc3/python/test/interfaces/test_udp_interface.py +++ b/openc3/python/test/interfaces/test_udp_interface.py @@ -18,8 +18,7 @@ import socket import threading import unittest -from unittest.mock import * -from test.test_helper import * +from unittest.mock import patch from openc3.interfaces.udp_interface import UdpInterface from openc3.io.udp_sockets import UdpReadSocket, UdpWriteSocket from openc3.packets.packet import Packet @@ -73,9 +72,7 @@ def test_is_not_readable_if_no_read_port_given(self): self.assertFalse(i.read_allowed) def test_connection_string(self): - i = UdpInterface( - "123.4.5.6", "8888", "8889", "8890", "456.7.8.9", "64", "5", "5", "1.2.3.4" - ) + i = UdpInterface("123.4.5.6", "8888", "8889", "8890", "456.7.8.9", "64", "5", "5", "1.2.3.4") self.assertEqual( i.connection_string(), "123.4.5.6:8888 (write dest port) 8890 (write src port) 123.4.5.6:8889 (read) 456.7.8.9 (interface addr) 1.2.3.4 (bind addr)", diff --git a/openc3/python/test/models/test_cvt_model.py b/openc3/python/test/models/test_cvt_model.py index 616539144..b090aeaf0 100644 --- a/openc3/python/test/models/test_cvt_model.py +++ b/openc3/python/test/models/test_cvt_model.py @@ -269,7 +269,7 @@ def test_gettlm_raises_on_invalid_items(self): def test_gettlm_raises_on_invalid_types(self): self.update_temp1() - with self.assertRaisesRegex(RuntimeError, "Unknown value type 'NOPE'"): + with self.assertRaisesRegex(ValueError, "Unknown value type 'NOPE'"): CvtModel.get_tlm_values([["INST", "HEALTH_STATUS", "TEMP1", "NOPE"]]) def test_gets_different_value_types_from_the_cvt(self): diff --git a/openc3/python/test/packets/parsers/test_packet_item_parser.py b/openc3/python/test/packets/parsers/test_packet_item_parser.py index eaa314cb9..155e738fc 100644 --- a/openc3/python/test/packets/parsers/test_packet_item_parser.py +++ b/openc3/python/test/packets/parsers/test_packet_item_parser.py @@ -419,7 +419,7 @@ def test_requires_the_default_type_matches_the_data_type(self): tf.write(' PARAMETER ITEM1 0 32 UINT 4.5 5.5 6.5 "" LITTLE_ENDIAN\n') tf.seek(0) with self.assertRaisesRegex( - AttributeError, + TypeError, "TGT1 PKT1 ITEM1: default must be a int but is a float", ): self.pc.process_file(tf.name, "TGT1") diff --git a/openc3/python/test/packets/test_commands.py b/openc3/python/test/packets/test_commands.py index 5cccbe687..66cf66a7a 100644 --- a/openc3/python/test/packets/test_commands.py +++ b/openc3/python/test/packets/test_commands.py @@ -237,7 +237,7 @@ def test_build_cmd_complains_about_non_existant_packets(self): def test_build_cmd_complains_about_non_existant_items(self): for range_checking in [True, False]: for raw in [True, False]: - with self.assertRaisesRegex(AttributeError, "Packet item 'TGT1 PKT1 ITEMX' does not exist"): + with self.assertRaisesRegex(RuntimeError, "Packet item 'TGT1 PKT1 ITEMX' does not exist"): self.cmd.build_cmd("tgt1", "pkt1", {"itemX": 1}, range_checking, raw) def test_build_cmd_complains_about_missing_required_parameters(self): @@ -439,7 +439,7 @@ def test_cmd_hazardous_complains_about_non_existant_packets(self): self.cmd.cmd_hazardous("tgt1", "pktX") def test_cmd_hazardous_complains_about_non_existant_items(self): - with self.assertRaisesRegex(AttributeError, "Packet item 'TGT1 PKT1 ITEMX' does not exist"): + with self.assertRaisesRegex(RuntimeError, "Packet item 'TGT1 PKT1 ITEMX' does not exist"): self.cmd.cmd_hazardous("tgt1", "pkt1", {"itemX": 1}) def test_cmd_hazardous_returns_true_if_the_command_overall_is_hazardous(self): diff --git a/openc3/python/test/packets/test_limits.py b/openc3/python/test/packets/test_limits.py new file mode 100644 index 000000000..a60de0123 --- /dev/null +++ b/openc3/python/test/packets/test_limits.py @@ -0,0 +1,173 @@ +# Copyright 2024 OpenC3, Inc. +# All Rights Reserved. +# +# This program is free software; you can modify and/or redistribute it +# under the terms of the GNU Affero General Public License +# as published by the Free Software Foundation; version 3 with +# attribution addendums as found in the LICENSE.txt +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# This file may also be used under the terms of a commercial license +# if purchased from OpenC3, Inc. + +import tempfile +import unittest +from test.test_helper import mock_redis, setup_system +from openc3.system.system import System +from openc3.packets.limits import Limits +from openc3.packets.packet_config import PacketConfig +from openc3.packets.telemetry import Telemetry + +class TestLimits(unittest.TestCase): + def setUp(self): + mock_redis(self) + setup_system() + + tf = tempfile.NamedTemporaryFile(mode="w") + tf.write('# This is a comment\n') + tf.write('#\n') + tf.write('TELEMETRY tgt1 pkt1 LITTLE_ENDIAN "TGT1 PKT1 Description"\n') + tf.write(' APPEND_ID_ITEM item1 8 UINT 1 "Item1"\n') + tf.write(' LIMITS DEFAULT 1 ENABLED 1 2 4 5\n') + tf.write(' LIMITS TVAC 1 ENABLED 6 7 12 13 9 10\n') + tf.write(' APPEND_ITEM item2 8 UINT "Item2"\n') + tf.write(' LIMITS DEFAULT 1 ENABLED 1 2 4 5\n') + tf.write(' LIMITS TVAC 1 ENABLED 6 7 12 13 9 10\n') + tf.write(' APPEND_ITEM item3 8 UINT "Item3"\n') + tf.write(' LIMITS DEFAULT 1 ENABLED 1 2 4 5\n') + tf.write(' LIMITS TVAC 1 ENABLED 6 7 12 13 9 10\n') + tf.write(' APPEND_ITEM item4 8 UINT "Item4"\n') + tf.write(' LIMITS DEFAULT 1 ENABLED 1 2 4 5\n') + tf.write(' LIMITS TVAC 1 ENABLED 6 7 12 13 9 10\n') + tf.write(' APPEND_ITEM item5 8 UINT "Item5"\n') + tf.write('TELEMETRY tgt1 pkt2 LITTLE_ENDIAN "TGT1 PKT2 Description"\n') + tf.write(' APPEND_ID_ITEM item1 8 UINT 2 "Item1"\n') + tf.write(' LIMITS DEFAULT 1 ENABLED 1 2 4 5\n') + tf.write(' APPEND_ITEM item2 8 UINT "Item2"\n') + tf.write('TELEMETRY tgt2 pkt1 LITTLE_ENDIAN "TGT2 PKT1 Description"\n') + tf.write(' APPEND_ID_ITEM item1 8 UINT 3 "Item1"\n') + tf.write(' APPEND_ITEM item2 8 UINT "Item2"\n') + tf.write('LIMITS_GROUP GROUP1\n') + tf.write(' LIMITS_GROUP_ITEM TGT1 PKT1 ITEM1\n') + tf.write(' LIMITS_GROUP_ITEM TGT1 PKT1 ITEM2\n') + tf.write('LIMITS_GROUP GROUP2\n') + tf.write(' LIMITS_GROUP_ITEM TGT1 PKT1 ITEM1\n') + tf.write(' LIMITS_GROUP_ITEM TGT1 PKT1 ITEM2\n') + tf.seek(0) + + # Verify initially that everything is empty + pc = PacketConfig() + pc.process_file(tf.name, "SYSTEM") + self.tlm = Telemetry(pc, System) + self.limits = Limits(pc, System) + tf.close() + + def test_has_no_warnings(self): + self.assertEqual(Limits(PacketConfig(), System).warnings(), []) + + def test_returns_the_defined_limits_set(self): + sets = self.limits.sets() + sets.sort() + self.assertEqual(sets, ['DEFAULT', 'TVAC']) + + def test_returns_the_limits_groups(self): + self.assertEqual(list(self.limits.groups().keys()), ['GROUP1', 'GROUP2']) + + def test_sets_the_underlying_configuration(self): + tf = tempfile.NamedTemporaryFile(mode="w") + tf.write('\n') + tf.seek(0) + pc = PacketConfig() + pc.process_file(tf.name, "SYSTEM") + self.assertIn('TVAC', self.limits.sets()) + self.assertEqual(list(self.limits.groups().keys()), ['GROUP1', 'GROUP2']) + self.limits.config = pc + self.assertEqual(self.limits.sets(), ['DEFAULT']) + self.assertEqual(self.limits.groups(), ({})) + tf.close() + + def test_enabled_complains_about_non_existent_targets(self): + with self.assertRaisesRegex(RuntimeError, "Telemetry target 'TGTX' does not exist"): + self.limits.enabled("TGTX", "PKT1", "ITEM1") + + def test_enabled_complains_about_non_existent_packets(self): + with self.assertRaisesRegex(RuntimeError, "Telemetry packet 'TGT1 PKTX' does not exist"): + self.limits.enabled("TGT1", "PKTX", "ITEM1") + + def test_enabled_complains_about_non_existent_items(self): + with self.assertRaisesRegex(RuntimeError, "Packet item 'TGT1 PKT1 ITEMX' does not exist"): + self.limits.enabled("TGT1", "PKT1", "ITEMX") + + def test_returns_whether_limits_are_enable_for_an_item(self): + pkt = self.tlm.packet("TGT1", "PKT1") + self.assertFalse(self.limits.enabled("TGT1", "PKT1", "ITEM5")) + pkt.enable_limits("ITEM5") + self.assertTrue(self.limits.enabled("TGT1", "PKT1", "ITEM5")) + + def test_enable_complains_about_non_existent_targets(self): + with self.assertRaisesRegex(RuntimeError, "Telemetry target 'TGTX' does not exist"): + self.limits.enable("TGTX", "PKT1", "ITEM1") + + def test_enable_complains_about_non_existent_packets(self): + with self.assertRaisesRegex(RuntimeError, "Telemetry packet 'TGT1 PKTX' does not exist"): + self.limits.enable("TGT1", "PKTX", "ITEM1") + + def test_enable_complains_about_non_existent_items(self): + with self.assertRaisesRegex(RuntimeError, "Packet item 'TGT1 PKT1 ITEMX' does not exist"): + self.limits.enable("TGT1", "PKT1", "ITEMX") + + def test_enables_limits_for_an_item(self): + self.tlm.packet("TGT1", "PKT1") + self.assertFalse(self.limits.enabled("TGT1", "PKT1", "ITEM5")) + self.limits.enable("TGT1", "PKT1", "ITEM5") + self.assertTrue(self.limits.enabled("TGT1", "PKT1", "ITEM5")) + + def test_disable_complains_about_non_existent_targets(self): + with self.assertRaisesRegex(RuntimeError, "Telemetry target 'TGTX' does not exist"): + self.limits.disable("TGTX", "PKT1", "ITEM1") + + def test_disable_complains_about_non_existent_packets(self): + with self.assertRaisesRegex(RuntimeError, "Telemetry packet 'TGT1 PKTX' does not exist"): + self.limits.disable("TGT1", "PKTX", "ITEM1") + + def test_disable_complains_about_non_existent_items(self): + with self.assertRaisesRegex(RuntimeError, "Packet item 'TGT1 PKT1 ITEMX' does not exist"): + self.limits.disable("TGT1", "PKT1", "ITEMX") + + def test_disables_limits_for_an_item(self): + self.tlm.packet("TGT1", "PKT1") + self.limits.enable("TGT1", "PKT1", "ITEM1") + self.assertTrue(self.limits.enabled("TGT1", "PKT1", "ITEM1")) + self.limits.disable("TGT1", "PKT1", "ITEM1") + self.assertFalse(self.limits.enabled("TGT1", "PKT1", "ITEM1")) + + def test_gets_the_limits_for_an_item_with_limits(self): + self.assertEqual(self.limits.get("TGT1", "PKT1", "ITEM1"), ['DEFAULT', 1, True, 1.0, 2.0, 4.0, 5.0, None, None]) + + def test_handles_an_item_without_limits(self): + self.assertEqual(self.limits.get("TGT1", "PKT1", "ITEM5"), [None, None, None, None, None, None, None, None, None]) + + def test_supports_a_specified_limits_set(self): + self.assertEqual(self.limits.get("TGT1", "PKT1", "ITEM1", 'TVAC'), ['TVAC', 1, True, 6.0, 7.0, 12.0, 13.0, 9.0, 10.0]) + + def test_handles_an_item_without_limits_for_the_given_limits_set(self): + self.assertEqual(self.limits.get("TGT1", "PKT2", "ITEM1", 'TVAC'), [None, None, None, None, None, None, None, None, None]) + + def test_sets_limits_for_an_item(self): + self.assertEqual(self.limits.set("TGT1", "PKT1", "ITEM5", 1, 2, 3, 4, None, None, 'DEFAULT'), ['DEFAULT', 1, True, 1.0, 2.0, 3.0, 4.0, None, None]) + + def test_enforces_setting_default_limits_first(self): + with self.assertRaisesRegex(RuntimeError, "DEFAULT limits must be defined for TGT1 PKT1 ITEM5 before setting limits set CUSTOM"): + self.limits.set("TGT1", "PKT1", "ITEM5", 1, 2, 3, 4) + self.assertEqual(self.limits.set("TGT1", "PKT1", "ITEM5", 5, 6, 7, 8, None, None, 'DEFAULT'), ['DEFAULT', 1, True, 5.0, 6.0, 7.0, 8.0, None, None]) + self.assertEqual(self.limits.set("TGT1", "PKT1", "ITEM5", 1, 2, 3, 4), ['CUSTOM', 1, True, 1.0, 2.0, 3.0, 4.0, None, None]) + + def test_allows_setting_other_limits_sets(self): + self.assertEqual(self.limits.set("TGT1", "PKT1", "ITEM1", 1, 2, 3, 4, None, None, 'TVAC'), ['TVAC', 1, True, 1.0, 2.0, 3.0, 4.0, None, None]) + + def test_handles_green_limits(self): + self.assertEqual(self.limits.set("TGT1", "PKT1", "ITEM1", 1, 2, 5, 6, 3, 4, None), ['DEFAULT', 1, True, 1.0, 2.0, 5.0, 6.0, 3.0, 4.0]) diff --git a/openc3/python/test/packets/test_packet.py b/openc3/python/test/packets/test_packet.py index 1f03f5c67..ab57e0b3c 100644 --- a/openc3/python/test/packets/test_packet.py +++ b/openc3/python/test/packets/test_packet.py @@ -44,7 +44,7 @@ def test_sets_the_template(self): def test_complains_if_the_given_template_is_not_a_string(self): p = Packet("tgt", "pkt") - with self.assertRaisesRegex(AttributeError, "template must be bytes but is a int"): + with self.assertRaisesRegex(TypeError, "template must be bytes but is a int"): p.template = 1 @@ -77,7 +77,7 @@ def test_sets_target_name_to_None(self): self.assertIsNone(p.target_name) def test_complains_about_non_string_target_names(self): - with self.assertRaisesRegex(AttributeError, "target_name must be a str but is a float"): + with self.assertRaisesRegex(TypeError, "target_name must be a str but is a float"): Packet(5.1, "pkt") def test_sets_the_packet_name_to_an_uppermatch_string(self): @@ -89,7 +89,7 @@ def test_sets_packet_name_to_None(self): self.assertIsNone(p.packet_name) def test_complains_about_non_string_packet_names(self): - with self.assertRaisesRegex(AttributeError, "packet_name must be a str but is a float"): + with self.assertRaisesRegex(TypeError, "packet_name must be a str but is a float"): Packet("tgt", 5.1) def test_sets_the_description_to_a_string(self): @@ -103,7 +103,7 @@ def test_sets_description_to_None(self): def test_complains_about_non_string_descriptions(self): p = Packet("tgt", "pkt") - with self.assertRaisesRegex(AttributeError, "description must be a str but is a float"): + with self.assertRaisesRegex(TypeError, "description must be a str but is a float"): p.description = 5.1 def test_sets_the_received_time_fast_to_a_time(self): @@ -125,7 +125,7 @@ def test_sets_received_time_to_None(self): def test_complains_about_non_time_received_times(self): p = Packet("tgt", "pkt") - with self.assertRaisesRegex(AttributeError, "received_time must be a datetime but is a str"): + with self.assertRaisesRegex(TypeError, "received_time must be a datetime but is a str"): p.received_time = "1pm" def test_sets_the_received_count_to_a_fixnum(self): @@ -135,12 +135,12 @@ def test_sets_the_received_count_to_a_fixnum(self): def test_complains_about_none_received_count(self): p = Packet("tgt", "pkt") - with self.assertRaisesRegex(AttributeError, "received_count must be an int but is a NoneType"): + with self.assertRaisesRegex(TypeError, "received_count must be an int but is a NoneType"): p.received_count = None def test_complains_about_non_fixnum_received_counts(self): p = Packet("tgt", "pkt") - with self.assertRaisesRegex(AttributeError, "received_count must be an int but is a str"): + with self.assertRaisesRegex(TypeError, "received_count must be an int but is a str"): p.received_count = "5" def test_sets_the_hazardous_description_to_a_string(self): @@ -155,7 +155,7 @@ def test_sets_hazardous_description_to_None(self): def test_complains_about_non_string_hazardous_descriptions(self): p = Packet("tgt", "pkt") - with self.assertRaisesRegex(AttributeError, "hazardous_description must be a str but is a float"): + with self.assertRaisesRegex(TypeError, "hazardous_description must be a str but is a float"): p.hazardous_description = 5.1 def test_sets_the_given_values_to_a_hash(self): @@ -171,7 +171,7 @@ def test_sets_given_values_to_None(self): def test_complains_about_non_hash_given_valuess(self): p = Packet("tgt", "pkt") - with self.assertRaisesRegex(AttributeError, "given_values must be a dict but is a list"): + with self.assertRaisesRegex(TypeError, "given_values must be a dict but is a list"): p.given_values = [] def test_allows_adding_items_to_the_meta_hash(self): @@ -297,7 +297,7 @@ def test_adds_a_packetitem_to_the_end_of_a_packet(self): def test_complains_if_an_item_doesnt_exist(self): p = Packet("tgt", "pkt") - with self.assertRaisesRegex(AttributeError, "Packet item 'TGT PKT TEST' does not exist"): + with self.assertRaisesRegex(RuntimeError, "Packet item 'TGT PKT TEST' does not exist"): p.get_item("test") @@ -309,27 +309,27 @@ def test_complains_about_unknown_value_type(self): self.p.append_item("item", 32, "UINT") i = self.p.get_item("ITEM") with self.assertRaisesRegex( - AttributeError, + ValueError, "Unknown value type 'MINE', must be 'RAW', 'CONVERTED', 'FORMATTED', or 'WITH_UNITS'", ): self.p.read("ITEM", "MINE", b"\x01\x02\x03\x04") with self.assertRaisesRegex( - AttributeError, + ValueError, "Unknown value type 'MINE', must be 'RAW', 'CONVERTED', 'FORMATTED', or 'WITH_UNITS'", ): self.p.read("ITEM", "MINE", b"\x01\x02\x03\x04") with self.assertRaisesRegex( - AttributeError, + ValueError, "Unknown value type 'MINE', must be 'RAW', 'CONVERTED', 'FORMATTED', or 'WITH_UNITS'", ): self.p.read_item(i, "MINE", b"\x01\x02\x03\x04") with self.assertRaisesRegex( - AttributeError, + ValueError, "Unknown value type 'ABCDEFGHIJ...', must be 'RAW', 'CONVERTED', 'FORMATTED', or 'WITH_UNITS'", ): self.p.read_item(i, "ABCDEFGHIJKLMNOPQRSTUVWXYZ", b"\x01\x02\x03\x04") with self.assertRaisesRegex( - AttributeError, + ValueError, "Unknown value type '.*', must be 'RAW', 'CONVERTED', 'FORMATTED', or 'WITH_UNITS'", ): self.p.read("ITEM", b"\00") @@ -609,27 +609,27 @@ def test_complains_about_unknown_value_type(self): self.p.append_item("item", 32, "UINT") i = self.p.get_item("ITEM") with self.assertRaisesRegex( - AttributeError, + ValueError, "Unknown value type 'MINE', must be 'RAW', 'CONVERTED', 'FORMATTED', or 'WITH_UNITS'", ): self.p.write("ITEM", 0, "MINE") with self.assertRaisesRegex( - AttributeError, + ValueError, "Unknown value type 'MINE', must be 'RAW', 'CONVERTED', 'FORMATTED', or 'WITH_UNITS'", ): self.p.write("ITEM", 0, "MINE") with self.assertRaisesRegex( - AttributeError, + ValueError, "Unknown value type 'MINE', must be 'RAW', 'CONVERTED', 'FORMATTED', or 'WITH_UNITS'", ): self.p.write_item(i, 0, "MINE") with self.assertRaisesRegex( - AttributeError, + ValueError, "Unknown value type 'ABCDEFGHIJ...', must be 'RAW', 'CONVERTED', 'FORMATTED', or 'WITH_UNITS'", ): self.p.write_item(i, 0, "ABCDEFGHIJKLMNOPQRSTUVWXYZ") with self.assertRaisesRegex( - AttributeError, + ValueError, "Unknown value type '.*', must be 'RAW', 'CONVERTED', 'FORMATTED', or 'WITH_UNITS'", ): self.p.write("ITEM", 0x01020304, "\x00") @@ -728,17 +728,17 @@ def test_writes_a_float_value_with_nan_infinite(self): def test_complains_about_the_formatted_value_type(self): self.p.append_item("item", 8, "UINT") i = self.p.get_item("ITEM") - with self.assertRaisesRegex(AttributeError, "Invalid value type on write= FORMATTED"): + with self.assertRaisesRegex(ValueError, "Invalid value type on write: FORMATTED"): self.p.write("ITEM", 3, "FORMATTED", self.buffer) - with self.assertRaisesRegex(AttributeError, "Invalid value type on write= FORMATTED"): + with self.assertRaisesRegex(ValueError, "Invalid value type on write: FORMATTED"): self.p.write_item(i, 3, "FORMATTED", self.buffer) def test_complains_about_the_with_units_value_type(self): self.p.append_item("item", 8, "UINT") i = self.p.get_item("ITEM") - with self.assertRaisesRegex(AttributeError, "Invalid value type on write= WITH_UNITS"): + with self.assertRaisesRegex(ValueError, "Invalid value type on write: WITH_UNITS"): self.p.write("ITEM", 3, "WITH_UNITS", self.buffer) - with self.assertRaisesRegex(AttributeError, "Invalid value type on write= WITH_UNITS"): + with self.assertRaisesRegex(ValueError, "Invalid value type on write: WITH_UNITS"): self.p.write_item(i, 3, "WITH_UNITS", self.buffer) diff --git a/openc3/python/test/packets/test_packet_item.py b/openc3/python/test/packets/test_packet_item.py index 1498b9a70..2c3a21d0f 100644 --- a/openc3/python/test/packets/test_packet_item.py +++ b/openc3/python/test/packets/test_packet_item.py @@ -40,22 +40,22 @@ def test_sets_the_format_string_to_None(self): def test_complains_about_non_string_format_strings(self): with self.assertRaisesRegex( - AttributeError, + TypeError, f"{self.pi.name}: format_string must be a str but is a float", ): self.pi.format_string = 5.1 def test_complains_about_badly_formatted_format_strings(self): with self.assertRaisesRegex( - AttributeError, f"{self.pi.name}: format_string invalid '%'" + ValueError, f"{self.pi.name}: format_string invalid '%'" ): self.pi.format_string = "%" with self.assertRaisesRegex( - AttributeError, f"{self.pi.name}: format_string invalid '5'" + ValueError, f"{self.pi.name}: format_string invalid '5'" ): self.pi.format_string = "5" with self.assertRaisesRegex( - AttributeError, f"{self.pi.name}: format_string invalid '%Q'" + ValueError, f"{self.pi.name}: format_string invalid '%Q'" ): self.pi.format_string = "%Q" @@ -71,7 +71,7 @@ def test_sets_the_read_conversion_to_None(self): def test_complains_about_non_conversion_read_conversions(self): with self.assertRaisesRegex( - AttributeError, + TypeError, f"{self.pi.name}: read_conversion must be a Conversion but is a str", ): self.pi.read_conversion = "HI" @@ -88,7 +88,7 @@ def test_sets_the_write_conversion_to_None(self): def test_complains_about_non_conversion_write_conversions(self): with self.assertRaisesRegex( - AttributeError, + TypeError, f"{self.pi.name}: write_conversion must be a Conversion but is a str", ): self.pi.write_conversion = "HI" @@ -166,7 +166,7 @@ def test_sets_the_states_to_None(self): def test_complains_about_states_that_arent_hashes(self): with self.assertRaisesRegex( - AttributeError, f"{self.pi.name}: states must be a dict but is a str" + TypeError, f"{self.pi.name}: states must be a dict but is a str" ): self.pi.states = "state" @@ -185,7 +185,7 @@ def test_sets_the_description_to_None(self): def test_complains_about_description_that_arent_strings(self): with self.assertRaisesRegex( - AttributeError, + TypeError, f"{self.pi.name}: description must be a str but is a float", ): self.pi.description = 5.1 @@ -201,7 +201,7 @@ def test_sets_the_units_full_to_None(self): def test_complains_about_units_full_that_arent_strings(self): with self.assertRaisesRegex( - AttributeError, + TypeError, f"{self.pi.name}: units_full must be a str but is a float", ): self.pi.units_full = 5.1 @@ -219,7 +219,7 @@ def test_sets_the_units_to_None(self): def test_complains_about_units_that_arent_strings(self): with self.assertRaisesRegex( - AttributeError, f"{self.pi.name}: units must be a str but is a float" + TypeError, f"{self.pi.name}: units must be a str but is a float" ): self.pi.units = 5.1 @@ -274,7 +274,7 @@ def test_complains_about_default_not_matching_data_type(self): pi.maximum = 0xFFFF pi.default = 1.1 with self.assertRaisesRegex( - AttributeError, "TEST: default must be a list but is a float" + TypeError, "TEST: default must be a list but is a float" ): pi.check_default_and_range_data_types() pi = PacketItem("test", 0, 8, "UINT", "BIG_ENDIAN", 16) @@ -287,7 +287,7 @@ def test_complains_about_default_not_matching_data_type(self): pi.maximum = 0xFFFF pi.default = 5.5 with self.assertRaisesRegex( - AttributeError, "TEST: default must be a int but is a float" + TypeError, "TEST: default must be a int but is a float" ): pi.check_default_and_range_data_types() pi = PacketItem("test", 0, 32, "UINT", "BIG_ENDIAN", None) @@ -300,7 +300,7 @@ def test_complains_about_default_not_matching_data_type(self): pi.maximum = 0xFFFF pi.default = "test" with self.assertRaisesRegex( - AttributeError, "TEST: default must be a float but is a str" + TypeError, "TEST: default must be a float but is a str" ): pi.check_default_and_range_data_types() pi = PacketItem("test", 0, 32, "FLOAT", "BIG_ENDIAN", None) @@ -318,7 +318,7 @@ def test_complains_about_default_not_matching_data_type(self): pi.maximum = 0xFFFF pi.default = 5.1 with self.assertRaisesRegex( - AttributeError, "TEST: default must be a str but is a float" + TypeError, "TEST: default must be a str but is a float" ): pi.check_default_and_range_data_types() pi = PacketItem("test", 0, 32, "STRING", "BIG_ENDIAN", None) @@ -331,7 +331,7 @@ def test_complains_about_default_not_matching_data_type(self): pi.maximum = 0xFFFF pi.default = 5.5 with self.assertRaisesRegex( - AttributeError, "TEST: default must be a str but is a float" + TypeError, "TEST: default must be a str but is a float" ): pi.check_default_and_range_data_types() pi = PacketItem("test", 0, 32, "BLOCK", "BIG_ENDIAN", None) @@ -346,13 +346,13 @@ def test_complains_about_range_not_matching_data_type(self): pi.minimum = 5.5 pi.maximum = 10 with self.assertRaisesRegex( - AttributeError, "TEST: minimum must be a int but is a float" + TypeError, "TEST: minimum must be a int but is a float" ): pi.check_default_and_range_data_types() pi.minimum = 5 pi.maximum = 10.5 with self.assertRaisesRegex( - AttributeError, "TEST: maximum must be a int but is a float" + TypeError, "TEST: maximum must be a int but is a float" ): pi.check_default_and_range_data_types() pi = PacketItem("test", 0, 32, "FLOAT", "BIG_ENDIAN", None) @@ -363,12 +363,12 @@ def test_complains_about_range_not_matching_data_type(self): pi.minimum = "a" pi.maximum = "z" with self.assertRaisesRegex( - AttributeError, "TEST: minimum must be a float but is a str" + TypeError, "TEST: minimum must be a float but is a str" ): pi.check_default_and_range_data_types() pi.minimum = 5 with self.assertRaisesRegex( - AttributeError, "TEST: maximum must be a float but is a str" + TypeError, "TEST: maximum must be a float but is a str" ): pi.check_default_and_range_data_types() @@ -392,7 +392,7 @@ def test_sets_hazardous_to_None(self): def test_complains_about_hazardous_that_arent_hashes(self): with self.assertRaisesRegex( - AttributeError, f"{self.pi.name}: hazardous must be a dict but is a str" + TypeError, f"{self.pi.name}: hazardous must be a dict but is a str" ): self.pi.hazardous = "" @@ -416,7 +416,7 @@ def test_sets_messages_disabled_to_None(self): def test_complains_about_messages_disabled_that_arent_hashes(self): with self.assertRaisesRegex( - AttributeError, + TypeError, f"{self.pi.name}: messages_disabled must be a dict but is a str", ): self.pi.messages_disabled = "" @@ -439,7 +439,7 @@ def test_sets_the_state_colors_to_None(self): def test_complains_about_state_colors_that_arent_hashes(self): with self.assertRaisesRegex( - AttributeError, f"{self.pi.name}: state_colors must be a dict but is a str" + TypeError, f"{self.pi.name}: state_colors must be a dict but is a str" ): self.pi.state_colors = "" @@ -465,14 +465,14 @@ def test_sets_the_limits_to_None(self): def test_complains_about_limits_that_arent_packetitemlimits(self): with self.assertRaisesRegex( - AttributeError, + TypeError, f"{self.pi.name}: limits must be a PacketItemLimits but is a str", ): self.pi.limits = "" def test_only_allows_a_hash(self): with self.assertRaisesRegex( - AttributeError, + TypeError, f"{self.pi.name}: meta must be a dict but is a int", ): self.pi.meta = 1 diff --git a/openc3/python/test/packets/test_structure.py b/openc3/python/test/packets/test_structure.py index a77fed543..2d6b90ec0 100644 --- a/openc3/python/test/packets/test_structure.py +++ b/openc3/python/test/packets/test_structure.py @@ -35,7 +35,7 @@ def test_complains_about_non_string_buffers(self): def test_complains_about_unknown_data_types(self): self.assertRaisesRegex( - AttributeError, + ValueError, "Unknown endianness 'BLAH', must be 'BIG_ENDIAN' or 'LITTLE_ENDIAN'", Structure, "BLAH", @@ -96,7 +96,7 @@ def test_adds_items_with_negative_bit_offsets(self): def test_adds_item_with_negative_offset(self): self.assertRaisesRegex( - AttributeError, + ValueError, "TEST11: Can't define an item with array_size 128 greater than negative bit_offset -64", self.s.define_item, "test11", @@ -106,7 +106,7 @@ def test_adds_item_with_negative_offset(self): 128, ) self.assertRaisesRegex( - AttributeError, + ValueError, "TEST10: Can't define an item with negative array_size -64 and negative bit_offset -64", self.s.define_item, "test10", @@ -116,7 +116,7 @@ def test_adds_item_with_negative_offset(self): -64, ) self.assertRaisesRegex( - AttributeError, + ValueError, "TEST9: Can't define an item with negative bit_size -64 and negative bit_offset -64", self.s.define_item, "test9", @@ -125,7 +125,7 @@ def test_adds_item_with_negative_offset(self): "BLOCK", ) self.assertRaisesRegex( - AttributeError, + ValueError, "TEST8: bit_size cannot be negative or zero for array items", self.s.define_item, "test8", @@ -135,7 +135,7 @@ def test_adds_item_with_negative_offset(self): 64, ) self.assertRaisesRegex( - AttributeError, + ValueError, "TEST7: bit_size cannot be negative or zero for array items", self.s.define_item, "test7", @@ -145,7 +145,7 @@ def test_adds_item_with_negative_offset(self): 64, ) self.assertRaisesRegex( - AttributeError, + ValueError, "TEST6: Can't define an item with bit_size 32 greater than negative bit_offset -24", self.s.define_item, "test6", @@ -331,7 +331,7 @@ def test_returns_a_defined_item(self): self.assertIsNotNone(self.s.get_item("test1")) def test_complains_if_an_item_doesnt_exist(self): - self.assertRaisesRegex(AttributeError, "Unknown item: test2", self.s.get_item, "test2") + self.assertRaisesRegex(ValueError, "Unknown item: test2", self.s.get_item, "test2") class TestStructureSetItem(unittest.TestCase): @@ -350,7 +350,7 @@ def test_complains_if_an_item_doesnt_exist(self): item = self.s.get_item("test1") item.name = "TEST2" self.assertRaisesRegex( - AttributeError, + ValueError, "Unknown item: TEST2 - Ensure item name is uppercase", self.s.set_item, item, @@ -366,7 +366,7 @@ def test_removes_the_item_and_leaves_a_hole(self): self.s.append_item("test2", 16, "UINT") self.assertEqual(self.s.defined_length, 3) self.s.delete_item("test1") - self.assertRaisesRegex(AttributeError, "Unknown item: test1", self.s.get_item, "test1") + self.assertRaisesRegex(ValueError, "Unknown item: test1", self.s.get_item, "test1") self.assertEqual(self.s.defined_length, 3) self.assertIsNone(self.s.items.get("TEST1")) self.assertIsNotNone(self.s.items["TEST2"]) @@ -449,7 +449,7 @@ def test_writes_array_data_to_the_buffer(self): class TestStructureRead(unittest.TestCase): def test_complains_if_item_doesnt_exist(self): - self.assertRaisesRegex(AttributeError, "Unknown item: BLAH", Structure().read, "BLAH") + self.assertRaisesRegex(ValueError, "Unknown item: BLAH", Structure().read, "BLAH") def test_reads_data_from_the_buffer(self): s = Structure() @@ -478,7 +478,7 @@ def test_reads_array_data_from_the_buffer(self): class TestStructureWrite(unittest.TestCase): def test_complains_if_item_doesnt_exist(self): - with self.assertRaisesRegex(AttributeError, "Unknown item: BLAH"): + with self.assertRaisesRegex(ValueError, "Unknown item: BLAH"): Structure().write("BLAH", 0) def test_writes_data_to_the_buffer(self): @@ -600,13 +600,13 @@ def test_returns_the_buffer(self): def test_complains_if_the_given_buffer_is_too_small(self): s = Structure("BIG_ENDIAN") s.append_item("test1", 16, "UINT") - with self.assertRaisesRegex(AttributeError, "Buffer length less than defined length"): + with self.assertRaisesRegex(ValueError, "Buffer length less than defined length"): s.buffer = b"\x00" def test_complains_if_the_given_buffer_is_too_big(self): s = Structure("BIG_ENDIAN") s.append_item("test1", 16, "UINT") - with self.assertRaisesRegex(AttributeError, "Buffer length greater than defined length"): + with self.assertRaisesRegex(ValueError, "Buffer length greater than defined length"): s.buffer = b"\x00\x00\x00" def test_does_not_complain_if_the_given_buffer_is_too_big_and_were_not_fixed_length( diff --git a/openc3/python/test/packets/test_structure_item.py b/openc3/python/test/packets/test_structure_item.py index 5638a5dd2..2b8c78801 100644 --- a/openc3/python/test/packets/test_structure_item.py +++ b/openc3/python/test/packets/test_structure_item.py @@ -26,7 +26,7 @@ def test_name_creates_new_structure_items(self): def test_name_complains_about_non_string_names(self): self.assertRaisesRegex( - AttributeError, + TypeError, "name must be a String but is a NoneType", StructureItem, None, @@ -37,7 +37,7 @@ def test_name_complains_about_non_string_names(self): None, ) self.assertRaisesRegex( - AttributeError, + TypeError, "name must be a String but is a float", StructureItem, 5.1, @@ -50,7 +50,7 @@ def test_name_complains_about_non_string_names(self): def test_complains_about_blank_names(self): self.assertRaisesRegex( - AttributeError, + ValueError, "name must contain at least one character", StructureItem, "", @@ -73,7 +73,7 @@ def test_endian_accepts_big_and_little(self): def test_complains_about_bad_endianness(self): self.assertRaisesRegex( - AttributeError, + ValueError, "TEST: unknown endianness: BLAH - Must be 'BIG_ENDIAN' or 'LITTLE_ENDIAN'", StructureItem, "TEST", @@ -95,7 +95,7 @@ def test_accepts_data_types(self): def test_complains_about_bad_data_type(self): self.assertRaisesRegex( - AttributeError, + ValueError, "TEST: unknown data_type: UNKNOWN - Must be 'INT', 'UINT', 'FLOAT', 'STRING', 'BLOCK', or 'DERIVED'", StructureItem, "TEST", @@ -115,7 +115,7 @@ def test_accepts_overflow_types(self): def test_complains_about_bad_overflow_types(self): self.assertRaisesRegex( - AttributeError, + ValueError, "TEST: unknown overflow type: UNKNOWN - Must be 'ERROR', 'ERROR_ALLOW_HEX', 'TRUNCATE', or 'SATURATE'", StructureItem, "TEST", @@ -129,7 +129,7 @@ def test_complains_about_bad_overflow_types(self): def test_complains_about_bad_bit_offsets_types(self): self.assertRaisesRegex( - AttributeError, + TypeError, "TEST: bit_offset must be an Integer", StructureItem, "TEST", @@ -143,7 +143,7 @@ def test_complains_about_bad_bit_offsets_types(self): def test_complains_about_unaligned_bit_offsets(self): for type in ["FLOAT", "STRING", "BLOCK"]: self.assertRaisesRegex( - AttributeError, + ValueError, "TEST: bit_offset for 'FLOAT', 'STRING', and 'BLOCK' items must be byte aligned", StructureItem, "TEST", @@ -156,7 +156,7 @@ def test_complains_about_unaligned_bit_offsets(self): def test_complains_about_non_zero_derived_bit_offsets(self): self.assertRaisesRegex( - AttributeError, + ValueError, "TEST: DERIVED items must have bit_offset of zero", StructureItem, "TEST", @@ -169,7 +169,7 @@ def test_complains_about_non_zero_derived_bit_offsets(self): def test_complains_about_bad_bit_sizes_types(self): self.assertRaisesRegex( - AttributeError, + TypeError, "TEST: bit_size must be an Integer", StructureItem, "TEST", @@ -182,7 +182,7 @@ def test_complains_about_bad_bit_sizes_types(self): def test_complains_about_0_size_floats(self): self.assertRaisesRegex( - AttributeError, + ValueError, "TEST: bit_size cannot be negative or zero for 'FLOAT' items: 0", StructureItem, "TEST", @@ -195,7 +195,7 @@ def test_complains_about_0_size_floats(self): def test_complains_about_bad_float_bit_sizes(self): self.assertRaisesRegex( - AttributeError, + ValueError, "TEST: bit_size for FLOAT items must be 32 or 64. Given: 8", StructureItem, "TEST", @@ -212,7 +212,7 @@ def test_creates_32_and_64_bit_floats(self): def test_complains_about_non_zero_derived_bit_sizes(self): self.assertRaisesRegex( - AttributeError, + ValueError, "TEST: DERIVED items must have bit_size of zero", StructureItem, "TEST", @@ -225,7 +225,7 @@ def test_complains_about_non_zero_derived_bit_sizes(self): def test_complains_about_bad_array_size_types(self): self.assertRaisesRegex( - AttributeError, + TypeError, "TEST: array_size must be an Integer", StructureItem, "TEST", @@ -238,7 +238,7 @@ def test_complains_about_bad_array_size_types(self): def test_complains_about_array_size_not_multiple_of_bit_size(self): self.assertRaisesRegex( - AttributeError, + ValueError, "TEST: array_size must be a multiple of bit_size", StructureItem, "TEST", diff --git a/openc3/python/test/script/test_metadata.py b/openc3/python/test/script/test_metadata.py index 05e205117..167f23075 100644 --- a/openc3/python/test/script/test_metadata.py +++ b/openc3/python/test/script/test_metadata.py @@ -69,7 +69,7 @@ def test_metadata(self): self.assertEqual("#123456", json["color"]) def test_metadata_set(self): - with self.assertRaisesRegex(RuntimeError, "metadata must be a dict"): + with self.assertRaisesRegex(TypeError, "metadata must be a dict"): metadata_set("hello") global gData diff --git a/openc3/python/test/streams/test_tcpip_socket_stream.py b/openc3/python/test/streams/test_tcpip_socket_stream.py index 1c76e5f93..5b032c1df 100644 --- a/openc3/python/test/streams/test_tcpip_socket_stream.py +++ b/openc3/python/test/streams/test_tcpip_socket_stream.py @@ -31,7 +31,7 @@ def setUp(self): def test_is_not_be_connected_when_initialized(self): ss = TcpipSocketStream(None, None, 10.0, None) - self.assertFalse(ss.connected) + self.assertFalse(ss.connected()) def test_warns_if_the_write_timeout_is_None(self): for stdout in capture_io(): @@ -155,27 +155,27 @@ def test_closes_the_write_socket(self): write = Mock() ss = TcpipSocketStream(write, None, 10.0, None) ss.connect() - self.assertTrue(ss.connected) + self.assertTrue(ss.connected()) ss.disconnect() - self.assertFalse(ss.connected) + self.assertFalse(ss.connected()) write.close.assert_called_once() def test_closes_the_read_socket(self): read = Mock() ss = TcpipSocketStream(None, read, 10.0, None) ss.connect() - self.assertTrue(ss.connected) + self.assertTrue(ss.connected()) ss.disconnect() - self.assertFalse(ss.connected) + self.assertFalse(ss.connected()) read.close.assert_called_once() def test_does_not_close_the_socket_twice(self): socket = Mock() ss = TcpipSocketStream(socket, socket, 10.0, None) ss.connect() - self.assertTrue(ss.connected) + self.assertTrue(ss.connected()) ss.disconnect() - self.assertFalse(ss.connected) + self.assertFalse(ss.connected()) ss.disconnect() - self.assertFalse(ss.connected) + self.assertFalse(ss.connected()) socket.close.assert_called() diff --git a/openc3/spec/install/config/targets/INST/cmd_tlm/inst_cmds.txt b/openc3/spec/install/config/targets/INST/cmd_tlm/inst_cmds.txt index ad7117a70..dc6f5c39f 100644 --- a/openc3/spec/install/config/targets/INST/cmd_tlm/inst_cmds.txt +++ b/openc3/spec/install/config/targets/INST/cmd_tlm/inst_cmds.txt @@ -1,4 +1,5 @@ COMMAND INST COLLECT BIG_ENDIAN "Starts a collect on the instrument" + META TOPIC COLLECT PARAMETER CCSDSVER 0 3 UINT 0 0 0 "CCSDS primary header version number" PARAMETER CCSDSTYPE 3 1 UINT 1 1 1 "CCSDS primary header packet type" PARAMETER CCSDSSHF 4 1 UINT 0 0 0 "CCSDS primary header secondary header flag" diff --git a/openc3/spec/install/config/targets/INST/cmd_tlm/inst_tlm.txt b/openc3/spec/install/config/targets/INST/cmd_tlm/inst_tlm.txt index fa230008d..1a1a1e7d6 100644 --- a/openc3/spec/install/config/targets/INST/cmd_tlm/inst_tlm.txt +++ b/openc3/spec/install/config/targets/INST/cmd_tlm/inst_tlm.txt @@ -1,4 +1,5 @@ TELEMETRY INST HEALTH_STATUS BIG_ENDIAN "Health and status from the instrument" + META TOPIC HEALTH_STATUS ITEM CCSDSVER 0 3 UINT "CCSDS packet version number (See CCSDS 133.0-B-1)" ITEM CCSDSTYPE 3 1 UINT "CCSDS packet type (command or telemetry)" STATE TLM 0 @@ -80,6 +81,7 @@ TELEMETRY INST HEALTH_STATUS BIG_ENDIAN "Health and status from the instrument" PROCESSOR TEMP1WATER watermark_processor.rb TEMP1 TELEMETRY INST ADCS BIG_ENDIAN "Position and attitude data" + META TOPIC ADCS ITEM CCSDSVER 0 3 UINT "CCSDS packet version number (See CCSDS 133.0-B-1)" ITEM CCSDSTYPE 3 1 UINT "CCSDS packet type (command or telemetry)" STATE TLM 0 diff --git a/openc3/spec/interfaces/mqtt_interface_spec.rb b/openc3/spec/interfaces/mqtt_interface_spec.rb index e743fe699..1f2579784 100644 --- a/openc3/spec/interfaces/mqtt_interface_spec.rb +++ b/openc3/spec/interfaces/mqtt_interface_spec.rb @@ -21,13 +21,15 @@ module OpenC3 describe MqttInterface do + MQTT_CLIENT = 'MQTT::Client'.freeze + before(:all) do setup_system() end describe "initialize" do it "sets all the instance variables" do - i = MqttInterface.new('localhost', '1883', 'false') + i = MqttInterface.new('localhost', '1883') expect(i.name).to eql "MqttInterface" expect(i.instance_variable_get(:@hostname)).to eql 'localhost' expect(i.instance_variable_get(:@port)).to eql 1883 @@ -36,14 +38,119 @@ module OpenC3 describe "connection_string" do it "builds a human readable connection string" do - i = MqttInterface.new('localhost', '1883', 'false') - expect(i.connection_string).to eql "localhost:1883 (ssl: false)" + i = MqttInterface.new('localhost', '1883') + expect(i.connection_string).to eql "localhost:1883" + end + end - i = MqttInterface.new('1.2.3.4', '8080', 'true') - expect(i.connection_string).to eql "1.2.3.4:8080 (ssl: true)" + describe "connect" do + it "sets various ssl settings based on options" do + double = double(MQTT_CLIENT) + expect(double).to receive(:ack_timeout=).with(10.0) + expect(double).to receive(:host=).with('localhost') + expect(double).to receive(:port=).with(1883) + expect(double).to receive(:username=).with('test_user') + expect(double).to receive(:password=).with('test_pass') + expect(double).to receive(:ssl=).with(true).twice + expect(double).to receive(:cert_file=) + expect(double).to receive(:key_file=) + expect(double).to receive(:ca_file=) + expect(double).to receive(:connect) + expect(double).to receive(:connected?).and_return(true) + # inst_tlm.txt declares META TOPIC on the first 2 packets + expect(double).to receive(:subscribe).with('HEALTH_STATUS') + expect(double).to receive(:subscribe).with('ADCS') + allow(MQTT::Client).to receive(:new).and_return(double) + + i = MqttInterface.new('localhost', '1883') + i.set_option('USERNAME', ['test_user']) + i.set_option('PASSWORD', ['test_pass']) + i.set_option('CERT', ['cert_content']) + i.set_option('KEY', ['key_content']) + i.set_option('CA_FILE', ['ca_file_content']) + i.set_option('ACK_TIMEOUT', ['10.0']) + i.connect() + expect(i.connected?).to be true end end - # TODO: This needs more testing + describe "disconnect" do + it "disconnects the mqtt client" do + double = double(MQTT_CLIENT).as_null_object + expect(double).to receive(:connect) + expect(double).to receive(:disconnect) + allow(MQTT::Client).to receive(:new).and_return(double) + + i = MqttInterface.new('localhost', '1883') + i.connect() + i.disconnect() + expect(i.connected?).to be false + i.disconnect() # Safe to call twice + end + end + + describe "read" do + it "reads a message from the mqtt client" do + double = double(MQTT_CLIENT).as_null_object + expect(double).to receive(:connect) + expect(double).to receive(:connected?).and_return(true) + expect(double).to receive(:get).and_return(['HEALTH_STATUS', "\x00\x00\x00\x00\x00\x00"]) + expect(double).to receive(:get).and_return(['ADCS', "\x00\x00\x00\x00\x00\x00"]) + allow(MQTT::Client).to receive(:new).and_return(double) + + i = MqttInterface.new('localhost', '1883') + i.connect() + packet = i.read() + expect(packet.target_name).to eql "INST" + expect(packet.packet_name).to eql "HEALTH_STATUS" + packet = i.read() + expect(packet.target_name).to eql "INST" + expect(packet.packet_name).to eql "ADCS" + end + + it "disconnects if the mqtt client returns no data" do + double = double(MQTT_CLIENT).as_null_object + expect(double).to receive(:connect) + expect(double).to receive(:connected?).and_return(true) + expect(double).to receive(:get).and_return(['HEALTH_STATUS', nil]) + allow(MQTT::Client).to receive(:new).and_return(double) + + capture_io do |stdout| + i = MqttInterface.new('localhost', '1883') + i.connect() + packet = i.read() + expect(stdout.string).to match(/read returned nil/) + expect(stdout.string).to match(/read_interface requested disconnect/) + end + end + end + + describe "write" do + it "writes a message to the mqtt client" do + double = double(MQTT_CLIENT).as_null_object + expect(double).to receive(:connect) + expect(double).to receive(:connected?).and_return(true) + allow(MQTT::Client).to receive(:new).and_return(double) + + i = MqttInterface.new('localhost', '1883') + i.connect() + pkt = System.commands.packet('INST', 'COLLECT') + pkt.restore_defaults() + expect(double).to receive(:publish).with('COLLECT', pkt.buffer) + i.write(pkt) + end + + it "raises on packets without META TOPIC" do + double = double(MQTT_CLIENT).as_null_object + expect(double).to receive(:connect) + allow(MQTT::Client).to receive(:new).and_return(double) + + i = MqttInterface.new('localhost', '1883') + i.connect() + pkt = System.commands.packet('INST', 'CLEAR') + pkt.restore_defaults() + expect { i.write(pkt) }.to raise_error(RuntimeError, "Command packet 'INST CLEAR' requires a META TOPIC or TOPICS") + end + end end end diff --git a/openc3/spec/interfaces/mqtt_stream_interface_spec.rb b/openc3/spec/interfaces/mqtt_stream_interface_spec.rb new file mode 100644 index 000000000..44a505a5b --- /dev/null +++ b/openc3/spec/interfaces/mqtt_stream_interface_spec.rb @@ -0,0 +1,147 @@ +# encoding: ascii-8bit + +# Copyright 2024 OpenC3, Inc. +# All Rights Reserved. +# +# This program is free software; you can modify and/or redistribute it +# under the terms of the GNU Affero General Public License +# as published by the Free Software Foundation; version 3 with +# attribution addendums as found in the LICENSE.txt +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. + +# This file may also be used under the terms of a commercial license +# if purchased from OpenC3, Inc. + +require 'spec_helper' +require 'openc3/interfaces/mqtt_stream_interface' + +module OpenC3 + describe MqttStreamInterface do + MQTT_CLIENT = 'MQTT::Client'.freeze + + before(:all) do + setup_system() + end + + describe "initialize" do + it "sets all the instance variables" do + i = MqttStreamInterface.new('localhost', '1883', 'write_topic', 'read_topic') + expect(i.name).to eql "MqttStreamInterface" + expect(i.instance_variable_get(:@hostname)).to eql 'localhost' + expect(i.instance_variable_get(:@port)).to eql 1883 + expect(i.instance_variable_get(:@write_topic)).to eql 'write_topic' + expect(i.instance_variable_get(:@read_topic)).to eql 'read_topic' + end + end + + describe "connection_string" do + it "builds a human readable connection string" do + i = MqttStreamInterface.new('localhost', '1883', 'write_topic', 'read_topic') + expect(i.connection_string).to eql "localhost:1883 write topic: write_topic read topic: read_topic" + end + end + + describe "connect" do + it "sets various ssl settings based on options" do + double = double(MQTT_CLIENT) + expect(double).to receive(:ack_timeout=).with(10.0) + expect(double).to receive(:host=).with('localhost') + expect(double).to receive(:port=).with(1883) + expect(double).to receive(:username=).with('test_user') + expect(double).to receive(:password=).with('test_pass') + expect(double).to receive(:ssl=).with(true).twice + expect(double).to receive(:cert_file=) + expect(double).to receive(:key_file=) + expect(double).to receive(:ca_file=) + expect(double).to receive(:connect) + expect(double).to receive(:connected?).and_return(true) + # inst_tlm.txt declares META TOPIC on the first 2 packets + expect(double).to receive(:subscribe).with('read_topic') + allow(MQTT::Client).to receive(:new).and_return(double) + + i = MqttStreamInterface.new('localhost', '1883', 'write_topic', 'read_topic') + i.set_option('USERNAME', ['test_user']) + i.set_option('PASSWORD', ['test_pass']) + i.set_option('CERT', ['cert_content']) + i.set_option('KEY', ['key_content']) + i.set_option('CA_FILE', ['ca_file_content']) + i.set_option('ACK_TIMEOUT', ['10.0']) + i.connect() + expect(i.connected?).to be true + end + end + + describe "disconnect" do + it "disconnects the mqtt client" do + double = double(MQTT_CLIENT).as_null_object + expect(double).to receive(:connect) + expect(double).to receive(:disconnect) + allow(MQTT::Client).to receive(:new).and_return(double) + + i = MqttStreamInterface.new('localhost', '1883') + i.connect() + i.disconnect() + expect(i.connected?).to be false + i.disconnect() # Safe to call twice + end + end + + describe "read" do + it "reads a message from the mqtt client" do + double = double(MQTT_CLIENT).as_null_object + expect(double).to receive(:connect) + expect(double).to receive(:connected?).and_return(true) + expect(double).to receive(:get).and_return(['HEALTH_STATUS', "\x00\x01\x02\x03\x04\x05"]) + expect(double).to receive(:get).and_return(['ADCS', "\x06\x07\x08\x09\x0A\x0B"]) + allow(MQTT::Client).to receive(:new).and_return(double) + + i = MqttStreamInterface.new('localhost', '1883', 'write_topic', 'read_topic') + i.connect() + packet = i.read() + expect(packet.target_name).to be_nil + expect(packet.packet_name).to be_nil + expect(packet.buffer).to eql "\x00\x01\x02\x03\x04\x05" + packet = i.read() + expect(packet.target_name).to be_nil + expect(packet.packet_name).to be_nil + expect(packet.buffer).to eql "\x06\x07\x08\x09\x0A\x0B" + end + + it "disconnects if the mqtt client returns no data" do + double = double(MQTT_CLIENT).as_null_object + expect(double).to receive(:connect) + expect(double).to receive(:connected?).and_return(true) + expect(double).to receive(:get).and_return(['HEALTH_STATUS', nil]) + allow(MQTT::Client).to receive(:new).and_return(double) + + capture_io do |stdout| + i = MqttStreamInterface.new('localhost', '1883', 'write_topic', 'read_topic') + i.connect() + packet = i.read() + expect(stdout.string).to match(/read returned nil/) + expect(stdout.string).to match(/read_interface requested disconnect/) + end + end + end + + describe "write" do + it "writes a message to the mqtt client" do + double = double(MQTT_CLIENT).as_null_object + expect(double).to receive(:connect) + expect(double).to receive(:connected?).and_return(true) + allow(MQTT::Client).to receive(:new).and_return(double) + + i = MqttStreamInterface.new('localhost', '1883', 'write_topic', 'read_topic') + i.connect() + pkt = System.commands.packet('INST', 'COLLECT') + pkt.restore_defaults() + expect(double).to receive(:publish).with('write_topic', pkt.buffer) + i.write(pkt) + end + end + end +end diff --git a/openc3/spec/interfaces/protocols/template_protocol_spec.rb b/openc3/spec/interfaces/protocols/template_protocol_spec.rb index b42ef07b5..5305a455e 100644 --- a/openc3/spec/interfaces/protocols/template_protocol_spec.rb +++ b/openc3/spec/interfaces/protocols/template_protocol_spec.rb @@ -14,10 +14,10 @@ # GNU Affero General Public License for more details. # Modified by OpenC3, Inc. -# All changes Copyright 2022, OpenC3, Inc. +# All changes Copyright 2024, OpenC3, Inc. # All Rights Reserved # -# This file may also be used under the terms of a commercial license +# This file may also be used under the terms of a commercial license # if purchased from OpenC3, Inc. require 'spec_helper' @@ -99,7 +99,8 @@ def read; $read_buffer; end @interface.connect $read_buffer = "\x31\x30\xAB\xCD" data = @interface.read - expect(Time.now - start).to be_within(0.1).of(1.5) + expect(Time.now - start > 1.5).to be true # It takes at least 1.5s + expect(Time.now - start < 2.5).to be true # Give it some extra time to connect (especially on CI) expect(data.buffer).to eql("\x31\x30") end end diff --git a/openc3/spec/models/plugin_model_spec.rb b/openc3/spec/models/plugin_model_spec.rb index 5718d9b93..7360a9d7c 100644 --- a/openc3/spec/models/plugin_model_spec.rb +++ b/openc3/spec/models/plugin_model_spec.rb @@ -26,6 +26,9 @@ module OpenC3 describe PluginModel do + # Simple URL that is valid for testing (starts with /) + URL = "URL /myurl" + before(:each) do mock_redis() end @@ -142,7 +145,7 @@ module OpenC3 expect(gem).to receive(:extract_files) do |path| File.open("#{path}/plugin.txt", 'w') do |file| file.puts "TOOL <%= folder %> <%= name %>" - file.puts " URL myurl" + file.puts " #{URL}" file.puts "TARGET <%= folder %> <%= name %>" end end @@ -156,7 +159,7 @@ module OpenC3 expect(GemModel).to receive(:install).and_return(nil) expect_any_instance_of(ToolModel).to receive(:deploy).with(anything, variables, validate_only: false).and_return(nil) expect_any_instance_of(TargetModel).to receive(:deploy).with(anything, variables, validate_only: false).and_return(nil) - plugin_model = PluginModel.install_phase2({"name" => "name", "variables" => variables, "plugin_txt_lines" => ["TOOL THE_FOLDER THE_NAME", " URL myurl", "TARGET THE_FOLDER THE_NAME"]}, scope: "DEFAULT") + plugin_model = PluginModel.install_phase2({"name" => "name", "variables" => variables, "plugin_txt_lines" => ["TOOL THE_FOLDER THE_NAME", " #{URL}", "TARGET THE_FOLDER THE_NAME"]}, scope: "DEFAULT") expect(plugin_model['needs_dependencies']).to eql false end @@ -208,7 +211,7 @@ module OpenC3 plugin_txt_lines = [] plugin_txt_lines << " TOOL THE_FOLDER THE_NAME" - plugin_txt_lines << " URL myurl" + plugin_txt_lines << " #{URL}" plugin_txt_lines << " TARGET THE_FOLDER THE_NAME" expect(GemModel).to receive(:get).and_return("my_plugin.gem") @@ -238,7 +241,7 @@ module OpenC3 plugin_txt_lines = [] plugin_txt_lines << " TOOL THE_FOLDER THE_NAME" - plugin_txt_lines << " URL myurl" + plugin_txt_lines << " #{URL}" plugin_txt_lines << " TARGET THE_FOLDER THE_NAME" expect(GemModel).to receive(:get).and_return("my_plugin.gem") @@ -267,7 +270,7 @@ module OpenC3 plugin_txt_lines = [] plugin_txt_lines << " TOOL THE_FOLDER THE_NAME" - plugin_txt_lines << " URL myurl" + plugin_txt_lines << " #{URL}" plugin_txt_lines << " TARGET THE_FOLDER THE_NAME" plugin_txt_lines << " NEEDS_DEPENDENCIES" diff --git a/openc3/spec/packets/limits_spec.rb b/openc3/spec/packets/limits_spec.rb index ce23ab9ca..02f2d4d65 100644 --- a/openc3/spec/packets/limits_spec.rb +++ b/openc3/spec/packets/limits_spec.rb @@ -106,18 +106,6 @@ module OpenC3 end end - describe "out_of_limits" do - it "returns all out of limits telemetry items" do - @tlm.update!("TGT1", "PKT1", "\x00\x03\x03\x04\x05") - @tlm.packet("TGT1", "PKT1").check_limits - items = @limits.out_of_limits - expect(items[0][0]).to eql "TGT1" - expect(items[0][1]).to eql "PKT1" - expect(items[0][2]).to eql "ITEM1" - expect(items[0][3]).to eql :RED_LOW - end - end - describe "enabled?" do it "complains about non-existent targets" do expect { @limits.enabled?("TGTX", "PKT1", "ITEM1") }.to raise_error(RuntimeError, "Telemetry target 'TGTX' does not exist") diff --git a/scripts/release/openc3_set_versions.rb b/scripts/release/openc3_set_versions.rb index 4e3f22742..59b3421e6 100644 --- a/scripts/release/openc3_set_versions.rb +++ b/scripts/release/openc3_set_versions.rb @@ -149,7 +149,6 @@ shell_scripts = [ 'openc3-cosmos-init/plugins/docker-package-build.sh', 'openc3-cosmos-init/plugins/docker-package-install.sh', - 'examples/hostinstall/centos7/openc3_install_openc3.sh', ] shell_scripts.each do |rel_path|