From bfa44fdb331653d447443eadfc8e1d81b513cfb5 Mon Sep 17 00:00:00 2001 From: Attila Kovacs Date: Sat, 14 Sep 2024 11:38:25 +0200 Subject: [PATCH] Added and updated documentation --- .cproject | 42 ++ .github/dependabot.yml | 11 + .github/workflows/build.yml | 53 +++ .github/workflows/check.yml | 43 ++ .github/workflows/codeql.yml | 95 ++++ .github/workflows/dox.yml | 150 ++++++ .gitignore | 13 + .project | 28 ++ .settings/language.settings.xml | 15 + CHANGELOG.md | 12 + CODE_OF_CONDUCT.md | 128 +++++ CONTRIBUTING.md | 43 ++ LICENSE | 24 + README.md | 683 ++++++++++++++++++++++++++- resources/CfA-logo-dark.png | Bin 0 -> 11338 bytes resources/CfA-logo.png | Bin 0 -> 13407 bytes resources/doxygen_extra.css | 181 +++++++ resources/header.html | 93 ++++ resources/smithsonian-logo-55x55.png | Bin 0 -> 5834 bytes 19 files changed, 1588 insertions(+), 26 deletions(-) create mode 100644 .cproject create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/check.yml create mode 100644 .github/workflows/codeql.yml create mode 100644 .github/workflows/dox.yml create mode 100644 .gitignore create mode 100644 .project create mode 100644 .settings/language.settings.xml create mode 100644 CHANGELOG.md create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 resources/CfA-logo-dark.png create mode 100644 resources/CfA-logo.png create mode 100644 resources/doxygen_extra.css create mode 100644 resources/header.html create mode 100644 resources/smithsonian-logo-55x55.png diff --git a/.cproject b/.cproject new file mode 100644 index 0000000..64218e3 --- /dev/null +++ b/.cproject @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..6867e71 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file + +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..58de1ec --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,53 @@ +name: Build + +on: + push: + paths: + - 'src/**' + - 'include/**' + - 'Makefile' + - '*.mk' + - '.github/workflows/build.yml' + +jobs: + + build: + name: Build library + + runs-on: ubuntu-latest + env: + CC: gcc + XCHANGE: ../xchange + REDISX: ../redisx + steps: + - name: Check out smax-clib + uses: actions/checkout@v4 + with: + path: smax-clib + + - name: Check out xchange dependency + uses: actions/checkout@v4 + with: + repository: Smithsonian/xchange + path: xchange + + - name: Check out RedisX dependency + uses: actions/checkout@v4 + with: + repository: Smithsonian/redisx + path: redisx + + - name: Build static library + run: make -C smax-clib static + + - name: Build xchange dependency + run: make -C xchange shared + + - name: Build xchange dependency + run: make -C redisx shared + + - name: Build shared library + run: make -C smax-clib shared + + + diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml new file mode 100644 index 0000000..ec99360 --- /dev/null +++ b/.github/workflows/check.yml @@ -0,0 +1,43 @@ +name: Static Analysis + +on: + push: + paths: + - 'src/**' + - 'include/**' + - 'Makefile' + - '*.mk' + - '.github/workflows/check.yml' + +jobs: + + cppcheck: + name: Check source code + + runs-on: ubuntu-latest + env: + CC: gcc + XCHANGE: xchange + REDISX: redisx + steps: + + - name: Check out smax-clib + uses: actions/checkout@v4 + + - name: Check out xchange dependency + uses: actions/checkout@v4 + with: + repository: Smithsonian/xchange + path: xchange + + - name: Check out RedisX dependency + uses: actions/checkout@v4 + with: + repository: Smithsonian/redisx + path: redisx + + - name: install package dependencies + run: sudo apt-get install -y cppcheck + + - name: Run cppcheck + run: make check diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..04e84e8 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,95 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL Advanced" + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + schedule: + - cron: '35 18 * * 5' + +jobs: + analyze: + name: Analyze (${{ matrix.language }}) + # Runner size impacts CodeQL analysis time. To learn more, please see: + # - https://gh.io/recommended-hardware-resources-for-running-codeql + # - https://gh.io/supported-runners-and-hardware-resources + # - https://gh.io/using-larger-runners (GitHub.com only) + # Consider using larger runners or machines with greater resources for possible analysis time improvements. + runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + permissions: + # required for all workflows + security-events: write + + # required to fetch internal or private CodeQL packs + packages: read + + # only required for workflows in private repositories + actions: read + contents: read + + strategy: + fail-fast: false + matrix: + include: + - language: c-cpp + build-mode: autobuild + # CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' + # Use `c-cpp` to analyze code written in C, C++ or both + # Use 'java-kotlin' to analyze code written in Java, Kotlin or both + # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both + # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, + # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. + # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how + # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + path: smax-clib + + - name: Checkout xchange dependency + uses: actions/checkout@v4 + with: + repository: Smithsonian/xchange + path: xchange + + - name: Checkout RedisX dependency + uses: actions/checkout@v4 + with: + repository: Smithsonian/redisx + path: redisx + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: ${{ matrix.language }} + build-mode: manual + + - name: Manual build + shell: bash + env: + XCHANGE: ../xchange + REDISX: ../redisx + run: | + make -C xchange shared + make -C redisx shared + make -C smax-clib shared + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + working-directory: smax-clib + category: "/language:${{matrix.language}}" diff --git a/.github/workflows/dox.yml b/.github/workflows/dox.yml new file mode 100644 index 0000000..f282372 --- /dev/null +++ b/.github/workflows/dox.yml @@ -0,0 +1,150 @@ +name: API documentation + +on: + release: + types: [published] + + push: + paths: + - 'src/**' + - 'include/**' + - 'css/**' + - 'resources/**' + - 'Doxyfile' + - '*.md' + - '.github/workflows/dox.yml' + +jobs: + + apidocs: + name: Generate API documentation + + runs-on: ubuntu-latest + env: + CC: gcc + XCHANGE: xchange + REDISX: redisx + steps: + + - name: install doxygen + run: sudo apt-get install doxygen + + - name: Check out smax-clib + uses: actions/checkout@v4 + + - name: Generate docs + run: make local-dox + + site-update: + name: Update github pages + needs: apidocs + if: github.repository_owner == 'Smithsonian' && (github.event_name == 'release' || contains(github.event.head_commit.message, 'site update')) + + runs-on: ubuntu-latest + steps: + + - name: Checkout smax-clib + uses: actions/checkout@v4 + with: + repository: Smithsonian/smax-clib + path: smax-clib + + - name: Check out xchange dependency + uses: actions/checkout@v4 + with: + repository: Smithsonian/xchange + path: xchange + + - name: Check out RedisX dependency + uses: actions/checkout@v4 + with: + repository: Smithsonian/redisx + path: redisx + + - name: Generate headless README for xchange + run: make -C xchange README-orig.md + + - name: Generate headless README for RedisX + run: make -C redisx README-orig.md + + - name: Generate xchange apidocs + uses: mattnotmitt/doxygen-action@v1.9.8 + with: + additional-packages: font-roboto + working-directory: ./xchange + + - name: Generate RedisX apidocs + uses: mattnotmitt/doxygen-action@v1.9.8 + with: + additional-packages: font-roboto + working-directory: ./redisx + + - name: Generate headless README for smax-clib + run: make -C redisx README-orig.md + + - name: Generate smax-clib apidocs + uses: mattnotmitt/doxygen-action@v1.9.8 + with: + additional-packages: font-roboto + working-directory: ./smax-clib + + - name: Checkout gh-pages + uses: actions/checkout@v4 + with: + ref: 'gh-pages' + path: site + + - name: Assert site/doc/ + run: mkdir -p site/doc + + - name: Copy README + run: | + echo 'CfA logo' > site/doc/README.md + echo '
' >> site/doc/README.md + cat redisx/README-orig.md >> site/doc/README.md + + - name: Copy CHANGELOG + run: cp redisx/CHANGELOG.md site/doc/ + + - name: Copy API documentation + run: cp -a redisx/apidoc site/ + + - name: Push to pages + run: | + cd site + git config --global user.email "$GITHUB_JOB+github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions" + git add -A + git commit -m "[automated site update]" && git push || true + + + changelog-update: + name: Update CHANGELOG on github pages + if: github.repository_owner == 'Smithsonian' && contains(github.event.head_commit.message, 'changelog update') + + runs-on: ubuntu-latest + steps: + + - name: Checkout source + uses: actions/checkout@v4 + + - name: Checkout gh-pages + uses: actions/checkout@v4 + with: + ref: 'gh-pages' + path: site + + - name: Assert site/doc/ + run: mkdir -p site/doc + + - name: Copy CHANGELOG + run: cp CHANGELOG.md site/doc/ + + - name: Push to pages + run: | + cd site + git config --global user.email "$GITHUB_JOB+github-actions[bot]@users.noreply.github.com" + git config --global user.name "github-actions" + git add -A + git commit -m "[automated site update]" && git push || true + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..17a57a4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +**/apidoc +**/obj +**/bin +**/dep +**/lib +*.d +*.o +*~ +*.swp +README-orig.md +TODO +xchange/** +redisx/** diff --git a/.project b/.project new file mode 100644 index 0000000..7ddd480 --- /dev/null +++ b/.project @@ -0,0 +1,28 @@ + + + smax-clib + + + redisx + xchange + + + + org.eclipse.cdt.managedbuilder.core.genmakebuilder + clean,full,incremental, + + + + + org.eclipse.cdt.managedbuilder.core.ScannerConfigBuilder + full,incremental, + + + + + + org.eclipse.cdt.core.cnature + org.eclipse.cdt.managedbuilder.core.managedBuildNature + org.eclipse.cdt.managedbuilder.core.ScannerConfigNature + + diff --git a/.settings/language.settings.xml b/.settings/language.settings.xml new file mode 100644 index 0000000..2b6f529 --- /dev/null +++ b/.settings/language.settings.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..6f5756d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,12 @@ +# Changelog + +All notable changes to the [Smithsonian/redisx](https://github.com/Smithsonian/smax-clib) library will be +documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to +[Semantic Versioning](https://semver.org/spec/v2.0.0.html). + + +## [Unreleased] + +Initial public release. diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..9c72ea1 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +the Center for Astrophysics | Harvard & Smithsonian. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..df71794 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,43 @@ +# Contributing to RedisX + + +The _SMA-X C/C++_ library is for everyone. And, it is developers like you who can make it better. Whether there is a +nagging issue you would like to fix, or a new feature you'd like to see, you can make a difference yourself. Make this +project a little bit your own, by submitting pull requests with fixes and enhancement. When you are ready, here are +the typical steps for contributing to the project: + +1. Old or new __Issue__? Whether you just found a bug, or you are missing a much needed feature, start by checking +open (and closed) [Issues](https://github.com/Smithsonian/smax/issues). If an existing issue seems like a +good match to yours, feel free to speak up, comment, or to offer help in resolving it. If you find no issues that +match, go ahead and create a new one. + +2. __Fork__. Is it something you'd like to help resolve? Great! You should start by creating your own fork of the +repository so you can work freely on your solution. I recommend that you place your work on a branch of your fork, +which is named either after the issue number, e.g. `issue-192`, or some other descriptive name, such as +`sentinel-support`. + +3. __Develop__. Experiment on your fork/branch freely. If you run into a dead-end, you can always abandon it (which is +why branches are great) and start anew. You can run `make all` to ensure that all components of the package and its +API documentation are also in order. Remember to synchronize your `main` branch by fetching changes from upstream +every once in a while, and merging them into your development branch. Don't forget to: + + - Add __doxygen__ markup your new code. You can keep it sweet and simple, but make sure it properly explains your + globally exposed functions, their arguments and return values. You should also cross-reference other functions / + constants that are similar, related, or relevant to what you just added. + +4. __Pull Request__. Once you feel your work can be integrated, create a pull request from your fork/branch. You can +do that easily from the github page of your fork/branch directly. In the pull request, provide a concise description +of what you added or changed. Your pull request will be reviewed. You may get some feedback at this point, and maybe +there will be discussions about possible improvements or regressions etc. It's a good thing too, and your changes will +likely end up with added polish as a result. You can be all the more proud of it in the end! + +5. If all goes well, your pull-request will get merged, and will be included in the upcoming release of _smax-clib_. +Congratulations for your excellent work, and many thanks for dedicating some of your time for making this library a +little bit better. There will be many who will appreciate it. :-) + + +If at any point you have questions, or need feedback, don't be afraid to ask. You can put your questions into the +issue you found or created, or your pull-request, or as a Q&A in +[Discussions](https://github.com/Smithsonian/smax_clib/discussions). + + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..fdddb29 --- /dev/null +++ b/LICENSE @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to diff --git a/README.md b/README.md index 6b77f95..cde9b21 100644 --- a/README.md +++ b/README.md @@ -1,73 +1,704 @@ -# SMA information exchange (SMA-X) over Redis +![Build Status](https://github.com/Smithsonian/smax-clib/actions/workflows/build.yml/badge.svg) +![Static Analysis](https://github.com/Smithsonian/smax-clib/actions/workflows/check.yml/badge.svg) + + ![API documentation](https://github.com/Smithsonian/smax-clib/actions/workflows/dox.yml/badge.svg) + + + ![Project page](https://github.com/Smithsonian/smax-clib/actions/workflows/pages/pages-build-deployment/badge.svg) + + + + + + CfA logo + +
+ +# SMA-X: SMA information exchange Author: Attila Kovacs -Last Updated: 7 January 2021 +Last Updated: 14 September 2024 +## Table of Contents + - [Introduction](#introduction) + - [Prerequisites](#prerequisites) + - [Building the SMA-X C library](#building) + - [Initial configuration](#configuration) + - [Connecting to / disconnecting from SMA-X](#connecting) + - [Sharing and pulling data](#sharing-and-pulling) + - [Lazy pulling](#lazy-pulling) + - [Pipelined pulls](#pipelined-pulls) + - [Custom notification and update handling](#notifications) + - [Optional metadata](#optional-metadata) + - [Error handling](#error-handling) + - [Debug support](#debug-support) + - [Future plans](#future-plans) -## 1. Introduction +------------------------------------------------------------------------------ -### 1.1. Features at a glance + +## Introduction -#### Pipelined (queued) pulls +The SMA information eXchange (SMA-X) is a high-throughput and versatile data sharing platform for distributed software +systems. It is built around a central Redis database, and provides atomic access to structured data, including +specific branches and/or leaf nodes, with associated metadadata. -#### Lazy pulling +SMA-X consists of a set of server-side [LUA](https://lua.org/) scripts that run on [Redis](https://redis.io) (or one +of its forks / clones such as [Valkey](https://valkey.io) or [Dragonfly](https://dragonfly.io)); a set of libraries to +interface client applications; and a set of command-line tools build with them. Currently we provide client libraries +for C/C++ and Python 3. We may provide Java and/or Rust client libraries too in the future. +### Features at a glance +TODO -## 2. Building your program with SMA-X +------------------------------------------------------------------------------ + +## Prerequisites -## 3. API description and usage +The SMA-X C/C++ library has a build and runtime dependency on the __xchange__ and __RedisX__ libraries also available +at the Smithsonian Github repositories: + - [Smithsonian/xchange](https://github.com/Smithsonian/xchange) + - [Smithsonian/redisx](https://github.com/Smithsonian/redisx) -### 3.1. Basic usage +Additionally, to configure your Redis (or Valkey / Dragonfly) servers for SMA-X, you will need the +[Smithsonian/smax-server](https://github.com/Smithsonian/smax-server) repo also. -#### Connecting / Disconnecting -#### Sharing & Pulling +------------------------------------------------------------------------------ -#### Standard metadata + +## Building the SMA-X C library +The __smax-clib__ library can be built either as a shared (`libsmax.so[.1]`) and as a static (`libsmax.a`) library, +depending on what suits your needs best. +You can configure the build, either by editing `config.mk` or else by defining the relevant environment variables +prior to invoking `make`. The following build variables can be configured: -### 3.2. Configuration + - `XCHANGE`: the root of the location where the [Smithsonian/xchange](https://github.com/Smithsonian/xchange) library + is installed. It expects to find `xchange.h` under `$(XCHANGE)/include` and `libxchange.so` under `$(XCHANGE)/lib` + or else in the default `LD_LIBRARY_PATH`. + + - `REDISX`: the root of the location where the [Smithsonian/redisx](https://github.com/Smithsonian/redisx) library + is installed. It expects to find `redisx.h` under `$(REDISX)/include` and `libredisx.so` under `$(REDISX)/lib` + or else in the default `LD_LIBRARY_PATH`. + + - `CC`: The C compiler to use (default: `gcc`). -### 3.3. Simplified (easy) access + - `CPPFLAGS`: C pre-processor flags, such as externally defined compiler constants. + + - `CFLAGS`: Flags to pass onto the C compiler (default: `-Os -Wall`). Note, `-Iinclude` will be added automatically. + + - `LDFLAGS`: Linker flags (default is `-lm`). Note, `-lxchange -lredisx` will be added automatically. + - `BUILD_MODE`: You can set it to `debug` to enable debugging features: it will initialize the global `xDebug` + variable to `TRUE` and add `-g` to `CFLAGS`. + - `CHECKEXTRA`: Extra options to pass to `cppcheck` for the `make check` target + +After configuring, you can simply run `make`, which will build the `shared` (`lib/libsmax.so[.1]`) and `static` +(`lib/libsmax.a`) libraries, local HTML documentation (provided `doxygen` is available), and performs static +analysis via the `check` target. Or, you may build just the components you are interested in, by specifying the +desired `make` target(s). (You can use `make help` to get a summary of the available `make` targets). -### 3.4. Pipelined (queued) pulling +------------------------------------------------------------------------------ -#### Synchronization points + +## Initial configuration -#### Callbacks +Bu default, the library assumes that the Redis server used for SMA-X runs on a machine called `smax` (e.g. you may assign +`smax` to the IP address in `/etc/hosts`), and that the Redis is on the default port 6379/tcp. However, you can configure +to use a specific host and/or an alternative Redis port number to use instead. -### 3.5. Lazy pulling +```c + smaxSetServer("my-smax.example.com", 7033); +``` +Also, while SMA-X will normally run on database index 0, you can also specify a different database number to use. E.g.: +```c + smaxSetDB(3); +``` -### 3.6. Notifications & (remote) updates +(Note, you can switch the database later also, but beware that if you have an active subscription client open, you cannot +switch that client until the subscriptions are terminated.) -#### Monitoring updates +You can also set up the authentication credentials for using the SMA-X database on the Redis server: -#### Waiting for updates +```c + smaxSetUser("johndoe"); + smaxSetPassword("mySecretPassword); +``` +And finally, you can select the option to automatically try reconnect to the SMA-X server in case of lost connection or +network errors (and keep track of changes locally until then): -### 3.7. Optional extra metadata +```c + smaxSetResilient(TRUE); +``` -#### Descriptions +------------------------------------------------------------------------------ -#### Physical units + +## Connecting to / disconnecting from SMA-X + +Once you have configured the connection parameters, you can connec to the server by: + +```c + int status = smaxConnect(); + if(status < 0) { + // Oops, we could not connect to the server + ... + } +``` + +And, when you are done, you should disconnect with: + +```c + smaxDisconnect(); +``` + +------------------------------------------------------------------------------ + + +## Sharing and pulling data + + - [The basics](#basics) + - [Flexible types and sizes](#flexible-types-and-sizes) + - [Standard metadata](#metadata) + - [Scalar quantities](#scalars) + - [Arrays](#arrays) + - [Structures / substructures](#structures) + + +### The basics + +For SMA-X we use the terms sharing and pulling, instead of the more generic get/set terminology. The intention is to +make programmers conscious of the fact that the transactions are not some local access to memory, but that they +involve networking, and as such may be subject to unpredictable latencies, network outages, or other errors. + +At the lowest level, the library provides two functions accordingly: `smaxShare()` and `smaxPull()`, either to send +local data to store in SMA-X, or to retrieve data from SMA-X for local use, respectively. There are higher-level +functions too, which build on these, providing a simpler API for specific data types, or extra features, such as +lazy or pipelined pulls. These will be discussed in the sections below. But, before that, let's look into the basics +of how data is handled between you machine local data that you use in your C/C++ application and its +machine-independent representation in SMA-X. + +Here is an example for a generic sharing of a `double[]` array from C/C++: + +```c + double data[8] = ... // your local data + + // Share (send) this data to SMA-X as "system:subsystem:some_data" + int status = smaxShare("system:subsystem", "some_data", X_DOUBLE, 8); + if(status < 0) { + // Ooops, that did not work... + ... + } + +``` + + + +Pulling data back from SMA-X works similarly, e.g.: + +```c + double data[4][2] = ... // buffer in which we will receive data + XMeta meta; // (optional) metadata we can obtain + + // Retrieve 'data' from the SMA-X "system:subsystem:some_data", as 8 doubles + int status = smaxPull("system:subsystem", "some_data", X_DOUBLE, 8, data, &meta); + if(status < 0) { + // Oops, something went wrong... + return; + } + +``` + +The metadata argument is optional, and can be `NULL` if not required. + + +### Flexible types and sizes + +One nifty feature of the library is that as a consumer you need not be too concerned about what type or size of data +the producer provides. The program that produces the data may sometimes change, for example from writing 32-bit +floating point types to a 64-bit floating point types. Also while it produced data for say 10 units before (as an +array of 10), now it might report for just 9, or perhaps it now reports for 12. + +The point is that if your consumer application was written to expect ten 32-bit floating floating point values, it can +get that even if the producer changed the exact type or element count since you have written your client. The library +will simply apply the necessaty type conversion automatically, and then truncate, or else pad (with zeroes), the data +as necessary to get what you want. + +The type conversion can be both widening or narrowing. Strings and numerical values can be converted to one another +through the expected string representation of numbers and vice versa. Boolean `true` values are treated equivalently +to a numerical value of 1 and all non-zero numerical values will convert to boolean `true`. + +And, if you are concerned about the actual type or size (or shape) of the data stored, you have the option to inspect +the metadata, and make decisions based on it. Otherwise, the library will just take care of giving you the available +data in the format you expect. + + + +### Standard metadata + +For every variable (structure or leaf node) in SMA-X there is also a set of essential metadata that is stored in the +Redis database, which describe the data themselves, such as the native type (at the origin); the size as stored in +Redis; the array dimension and shape; the host (and program) which provided the data; the time it was last updated; +and a serial number. + +The user of the library has the option to retrieve metadata together with the actual data, and thus gain access to +this information. The header file `smax.h` defines the `Xmeta` type as: + + +```c + typedef struct XMeta { + int status; // Error code or X_SUCCESS. + XType storeType; // Type of variable as stored. + int storeDim; // Dimensionality of the data as stored. + int storeSizes[X_MAX_DIMS]; // Sizes along each dimension of the data as stored. + int storeBytes; // Total number of bytes stored. + char origin[SMAX_ORIGIN_LENGTH]; // Host name that last modified. + struct timespec timestamp; // Timestamp of the last modification. + int serial; // Number of times the variable was updated. + } XMeta; +``` + + +### Scalar quantities + +Often enough we deal with scalar quantities (not arrays), such as a single number, boolean value, or a +string (yes, we treat strings i.e., `char *`, as a scalar too!). + +Here are some examples of sharing scalars to SMA-X. Easy-peasy: + +```c + int status; + + // Put a boolean value into SMA-X + status = smaxShareBoolean("system:subsystem", "is_online", TRUE); + + // Put an integer value into SMA-X + status = smaxShareInt("system:subsystem", "some_int_value", 1012); + + // Put a floating-point value into SMA-X + status = smaxShareDouble("system:subsystem", "some_value", -3.4032e-11); + + // Put a string value into SMA-X + status = smaxShareString("system:subsystem", "name", "blah-blah"); +``` + +Or pulling them from SMA-X: + +```c + // Retrieve "system:subsystem:is_online" as a boolean, defaulting to FALSE + boolean isTrue = smaxPullBoolean("system:subsystem", "is_online", FALSE); + + // Retrieve "system:subsystem:some_int_value" as an int, with a default value of -1 + int c = smaxPullInt("system:subsystem", "some_int_value", -1); + + // Retrieve "system:subsystem:some_value" as a double (or else NAN if cannot). + double value = smaxPullDouble("system:subsystem", "some_value"); + if(!isnan(value)) { // check for NAN if need be + ... + } + + // Retrieve "system:subsystem:name" as a 0-terminated C string (or NULL if cannot). + char *str = smaxPullDouble("system:subsystem", "name"); + if(str != NULL) { // check for NULL + ... + } + + ... + + // Once the pulled string is no longer needed, destroy it. + free(str) + +``` + + +### Arrays + +The generic `smaxShare()` function readily handles 1D arrays, and the `smaxPull()` handles native (monolithic) arrays +of all types (e.g. `double[][]`, or `boolean[][][]`). However, you may want to share multidimensional arrays, noting +their specific shapes, or else pull array data for which you may not know in advance what size (or shape) of the +storage needed locally for the values stored in SMA-X for some variable. For these reasons, the library provides a set +of functions to make the handling of arrays a simpler too. + +Let's begin with sharing multi-dimensional array. Instead of `smaxShare()`, you can use `smaxShareArray()` which allows +you to define the multi-dimensional shape of the data beyond just the number of elements stored. E.g.: + +``` + float data[4][2] = ... // Your local 2D data array + int shape[] = { 4, 2 }; // The array shape as stored in SMA-X + + int status = smaxShareArray("system:subsystem", "my_2d_array", X_FLOAT, 2, shape); + if (status < 0) { + // Oops, did not work... + ... + } +``` + +Note, that the dimensions of how the data is stored in SMA-X is determined solely by the '2' dimensions specified +as the 4th argument, and the corresponding 2 elements in the `shape` array. The `data` could have been any pointer +to an array of floats containing at least the required number of element (8 in the example above). + +For 1D arrays, you have some convenience methods for shharing specific types. These can be convenient because they +eliminate one potential source of bugs, where the type argument to `smaxShare()` does not match the pointer type of +the data. Using, say, `smaxShareFloats()` instead to share a 1D floating-point array instead will allow the compiler +to check and warn if the data array is not the `float *` type. E.g.: + +```c + float *data = ... // pointer to a one or higher-dimensional C array of floats. + + // Send N elements from data to SMA-X as "system:subsystem:my_array" + int status = smaxShareFloats("system:subsystem", "my_array", data, N); + if (status < 0) { + // Oops, did not work... + ... + } +``` + +Similar functions are available for every built-in type (plus strings and booleans). For pulling arrays without +knowing a-priori the element count or shape, there are also convenience functions, such as: + +``` + XMeta meta; // (optional) we'll return the metadata in this + int status; // we'll return the status in this + + // Get whatever data is in "system:subsystem:my_array" as doubles. + double *data = smaxPullDoubles("system:subsystem", "my_array", &meta, &status); + if(status < 0) { + // Oops, we got an error + } + if(data == NULL) { + // Oops the data is NULL + } + + ... + + // When done using the data we obtained, destroy it. + free(data); +``` + +As illustrated, the above will return a dynamically allocated array with the required size to hold the data, and the +size and shape of the data is returned in the metadata that was also supplied with the call. After using the returned +data (and ensuring it is not `NULL`), you should always be sure to call `free()` on it to avoid memory leaks in your +application. + + +### Structures / substructures... + +You can share entire data structures, represented by an appropriate `XStructure` type (see the __xchange__ library for +a description and usage): + +```c + XStructure s = ... // The structured data you have prepared locally. + + int status = smaxShareStruct("syste:subsystem", &s); + if(status < 0) { + // Oops, something did not work + ... + } +``` + +Or, you can read a structure, including all embedded substructures in it, with `smaxPullStruct()`: + + +```c + XMeta meta; + int nElements; + + XStructure *s = smaxPullStruct("system", "subsystem", &meta, &n); + if(n < 0) { + // Oops there was an error... + return; + } + + double value = smaxGetDoubleField(s, "some_value", 0.0); + + ... + + // Once the pulled structure is no longer needed, destroy it... + xDestroyStruct(s); +``` + +Note, that the structure returned by `smaxPullStruct()` is in the serialized format of SMA-X. That is, all leaf nodes +are stored as strings, just as they appear in the Redis database. Hence, we used the `smaxGet...Field()` methods above +to deserialize the leaf nodes as needed on demand. If you want to use the methods of __xchange__ to access the +structure, you will need to convert to binary format first, using `smax2xStruct(XStructure *)`. + +Note also, that pulling large structures can be an expensive operation on the Redis server, and may block the server +for longer than usual periods, causing latencies for other programs that use SMA-X. It's best to use this method for +smallish structures only (with, say, a hundred or so or fewer leaf nodes). + + +------------------------------------------------------------------------------ + + +## Lazy pulling (high-frequency queries) + +What happens if you need the data frequently? Do you pound on the database at some high-frequency? No, you probably +no not want to do that, especially if the data you need is not necessaily changing fast. There is no point on wasting +network bandwidth only to return the same values again and again. This is where 'lazy' pulling excels. + +From the caller's perspective lazy pulling works just like regular SMA-X pulls, e.g.: + +```c + int data[10][4][2]; + int sizes[] = { 10, 4, 2 }; + XMeta meta; + + int status = smaxLazyPull("some_table", "some_data", X_INT, 3, sizes, data, &meta); +``` + +or + +```c + int status = smaxLazyPullDouble("some_table", "some_var"); +``` + +But, under the hood, it does something different. The first time a new variable is lazy pulled it is fetched from the +Redis database just like a regular pull. But, it also will cache the value, and watch for update notifications from +the SMA-X server. Thus, as long as no update notification is received, successive calls will simply return the locally +cached value. This can save big on network usage, and also provides orders of magnitude faster access so long as the +variable remains unchanged. + +When the vatiable is updated in SMA-X, our client library will be notified, and one of two things can happen: + + 1. it invalidates the cache, so that the next lazy pull will again work just like a regular pull, fetching the + updated value from SMA-X on demand. And again the library will cache that value and watch for notifications for + the next update. Or, + + 2. it will trigger a background process to update the cached value in the background with a pipelined + (high-throughput) pull. However, until the new value is actually fetched, it will return the previously cached + value promptly. + +The choice between the two is yours, and you can control which suits your need best. The default behavior for lazy pulls +is (1), but you may call `smaxLazyCache()` after the first pull of a variable, to indicate that you want to enable +background cache updates (2) for it. The advantage of (1) is that it will never serve you outdated data even if there +are significant network latencies -- but you may have to wait a little to fetch updates. On the other hand (2) will +always provide a recent value with effectively no latency, but this value may be outdated if there are delays on the +network updating the cache. The difference is typically at the micro-seconds level on a local LAN. However, (2) may +be preferable when you need to access SMA-X data from timing critical code blocks, where it is more important to ensure +that the value is returned quickly, rather than whether it is a millisecond too old or not. + +In either case, when you are done using lazy variables, you should let the library know that it no longer needs to watch +updates for these, by calling either `smaxLazyEnd()` on specific variables, or else `smaxLazyFlush()` to stop watching +updates for all lazy variables. (A successive lazy pull will automatically start watching for updates again, in case you +wish to re-enable). + +```c + // Lazy pull a bunch of data (typically in a loop). + for(...) { + smaxLazyPull("some_table", "some_var", ...); + smaxLaxyPull(...); + ... + } + + // Once we do not need "some_table:some_var" any more: + smaxLazyEnd("some_table", "some_var"); + + ... + + // And to stop lazy accessing all + smaxLazyFlush(); +``` + + +------------------------------------------------------------------------------ + + +## Pipelined pulling (high volume data retrievals) + +The regular pulling of data from SMA-X requires a separate round-trip for each and every request. That is, successive +pulls are sent only after the responses from the prior pull has been received. A lot of the time is spent on waiting +for responses to come back. With round trip times in the 100 μs range, this means that this method of fetching data +from SMA-X is suitable for obtaining at most a a few thousand values per second. + +However, sometimes you want to get access to a large number of values faster. This is what pipelined pulling is for. +In pipelined mode, a batch of pull requests are sent to the SMA-X Redis server in quick succession, without waiting +for responses. The values, when received are processed by a dedicated background thread. And, the user has an option +of either waiting until all data is collected, or ask for as callback when the data is ready. + +Again it works similarly to the basic pulling, except that you submit your pull request to a queue with +`smaxQueue()`. For example: + +```c + double d; // A value we will fill + XMeta meta; // (optional) metadata to fill (for the above value). + + int status = smaxQueue("some_table", "some_var", X_DOUBLE, 1, &d, &meta); +``` + +Pipelined (batched) pulls have dramatic effects on performance. Rather than being limited by round-trip times, you will +be limited by the performance of the Redis server itself (or the network bandwidth on some older infrastructure). As +such, instead of thousand of queries per second, you can pull 2-3 orders of magnitude more in a given time, with hudreds +of thousands to even millions of pull per second this way. + +### Synchronization points and waiting + +After you have submitted a batch of pull request to the queue, you can create a synchronization point as: + +```c + XSyncPoint *syncPoint = smaxCreateSyncPoint(); +``` + +A synchronization point is a marker in the queue that we can wait on. After the synchronization point is created, you +can sumbit more pull request to the same queue (e.g. for another processing block), or do some other things for a bit +(since it will take at least some microseconds before the data is ready). Then, when ready you can wait on the +specific synchronization point to ensure that data submitted prior to its creation is delivered from SMA-X: + +``` + // Wait for data submitted prior to syncPoint to be ready, or time out after 1000 ms. + int status = smaxSync(syncPoint, 1000); + + // Destroy the synchronization point if we no longer need it. + xDestroySyncPoint(syncPoint); + + // Check return status... + if(status == X_TIMEOUT) { + // We timed out + ... + } + else if(status < 0) { + // Some other error + ... + } +``` + +### Callbacks + +The alternative to synchronization points and waiting, is to provide a callback function, which will process your data +as soon as it is available, e.g.: + +```c + void my_pull_processor(void *arg) { + // Say, we expect a string tag passed along to identify what we need to process... + char *tag = (char *) arg; + + // Do what we need to do... + ... + } +``` + +Then submit this callback routine to the queue after the set of variables it requires with: + +```c + // We'll call my_pull_processor, with the argument "some_tag", when prior data has arrived. + smaxQueueCallback(my_pull_processor, "some_tag"); +``` + +### Finishing up + +If you might still have some pending pipelined pulls that have not received responses yet, you may want to wait until +all previously sumbitted requests have been collected. You can do that with: + +```c + // Wait for up to 3000 ms for all pipelined pulls to collect responses from SMA-X. + int status = smaxWaitQueueComplete(3000); + + // Check return status... + if(status == X_TIMEOUT) { + // We timed out + ... + } + else if(status < 0) { + // Some other error + ... + } +``` + + + + +------------------------------------------------------------------------------ + +------------------------------------------------------------------------------ + + +## Custom notifications and update handling + +### Monitoring updates + +### Waiting for updates + +### Status / error messages + + +------------------------------------------------------------------------------ + + +## Optional metadata + + +### Descriptions + +### Coordinate Systems + +### Physical units #### Coordinate systems -### 3.8. Program messages +----------------------------------------------------------------------------- + + +## Error handling + +The principal error handling of the library is an extension of that of __xchange__, with further error codes defined +in `smax.h` and `redisx.h`. The functions that return an error status (either directly, or into the integer designated +by a pointer argument), can be inspected by `smaxErrorDescription()`, e.g.: + +```c + int status = smaxShare(...); + if (status != X_SUCCESS) { + // Ooops, something went wrong... + fprintf(stderr, "WARNING! set value: %s", smaxErrorDescription(status)); + ... + } +``` + +----------------------------------------------------------------------------- + + +## Debug support + +You can enable verbose output of the library with `smaxSetVerbose(boolean)`. When enabled, it will produce status +messages to `stderr`so you can follow what's going on. In addition (or alternatively), you can enable debug messages +with `xSetDebug(boolean)`. When enabled, all errors encountered by the library (such as invalid arguments passed) will +be printed to `stderr`, including call traces, so you can walk back to see where the error may have originated from. +(You can also enable debug messages by default by defining the `DEBUG` constant for the compiler, e.g. by adding +`-DDEBUG` to `CFLAGS` prior to calling `make`). + +For helping to debug your application, the __xchange__ library provides two macros: `xvprintf()` and `xdprintf()`, +for printing verbose and debug messages to `stderr`. Both work just like `printf()`, but they are conditional on +verbosity being enabled via `xSetVerbose(boolean)` and `xSetDebug(boolean)`, respectively. Applications using this +library may use these macros to produce their own verbose and/or debugging outputs conditional on the same global +settings. + + + +----------------------------------------------------------------------------- + + +## Future plans + +Some obvious ways the library could evolve and grow in the not too distant future: + + - Automated regression testing and coverage tracking. -### 3.9. Miscellaneous utility functions +If you have an idea for a must have feature, please let me (Attila) know. Pull requests, for new features or fixes to +existing ones, are especially welcome! + +----------------------------------------------------------------------------- +Copyright (C) 2024 Attila Kovács diff --git a/resources/CfA-logo-dark.png b/resources/CfA-logo-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..2b5a13ca8da097d16647432a3ca0bfc01b5aa6d0 GIT binary patch literal 11338 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 literal 0 HcmV?d00001 diff --git a/resources/doxygen_extra.css b/resources/doxygen_extra.css new file mode 100644 index 0000000..953f03b --- /dev/null +++ b/resources/doxygen_extra.css @@ -0,0 +1,181 @@ +* { + font-family: "Roboto", "Calibri", "Arial", sans-serif; +} + +.headertitle { + font-size: 125%; +} + +#nav-tree .label { + font-size: 110%; +} + +p, li { + font-size: 110%; +} + +h1, h2, h3, h4, h5, h6 { + margin-top: 24px; + margin-bottom: 16px; + font-weight: 600; + line-height: 1.25; + color: #8d0034; +} + +h1 { + font-size: 200%; + padding-top: 1em; + padding-bottom: 0.3em; + border-bottom: 2px solid; +} + +h2 { + font-size: 170%; + padding-top: 0.3em; + padding-bottom: 0.2em; + border-bottom: 1px solid; +} + +h3 { + font-size: 150%; +} + +h4 { + font-size: 125%; +} + +h5 { + font-size: 110%; +} + +h6 { + font-size: 87.5%; + color: #283266; +} + +div.fragment { + padding: 4px 16px 4px 16px; + border: 0px solid; +} + +.line, .keywordflow { + font-family: "Roboto Mono", "Calibri Mono", "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; + line-height: 1.8; +} + +span.comment { + color: #555D68; +} + + +div.line.glow { + background-color: auto; + box-shadow: none; + font-weight: bold; +} + +hr { + display: none; +} + +code { + font-family: "Roboto Mono", "Calibri Mono", "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; + margin: 0px 2px 0px 2px; + font-size: 87.5%; + font-weight: bold; +} + +a.hl_function, a.hl_struct, span.keywordtype, span.keywordflow { + font-weight: bold; +} + +span.keywordflow { + color: #C6809A; +} + +a { + color: var(--page-link-color); +} + +a:visited, a.dark-mode { + color: #894270; +} + +#main-nav a { + color: var(--menu-link-color); +} + +#main-menu a { + color: var(--menu-link-color); +} + +#nav-tree-contents a { + color: var(--menu-link-color); +} + +/* tint 15/85 80%: 894270 */ +/* tint 30/70 482562 -> 80%: 6D5182 */ +/* tint 50/50 5C1B55 */ +/* tint 70/30 701048 */ +/* 80% red A4335D */ +/* 100% blue 283266 */ +/* 80% blue 555DA3 */ +/* 70% blue 6B719E */ +/* 60% blue 8086AC */ +/* 50% blue 959ABA */ +/* 40% blue AAAEC8 */ +html { + --title-background-color: #D8D8D8; + --nav-background-color: #F0F0F0; + --memdef-title-background-color: #D8D8D8; + --memdef-proto-background-color: #D8D8D8; + --memdef-doc-background-color: #F0F0F0; + --fragment-foreground-color: #283266; + --menu-link-color: #303030; + --page-link-color: #8d0034; + --code-link-color: #8d0034; + --page-visited-link-color: #701048; + --code-type-keyword-color: #5C1B55; + --memdef-proto-text-color: #283266; +} + +html.dark-mode { + --title-background-color: #181818; + --page-background-color: black; + --nav-background-color: black; + --memdef-title-background-color: #181818; + --memdef-proto-background-color: #181818; + --memdef-doc-background-color: #303030; + --fragment-foreground-color: #959ABA; + --menu-link-color: #D0D0D0; + --page-link-color: #A4335D; + --code-link-color: #A4335D; + --page-visited-link-color: #894270; + --code-type-keyword-color: #6D5182; + --memdef-proto-text-color: #555DA3; +} + +.paramtype { + color: var(--code-type-keyword-color); +} + +.paramname { + color: #BB0085; +} + +.memtitle { + font-weight: bold; +} + +.memname { + font-weight: bold; + color: var(--fragment-foreground-color); +} + + +blockquote { + border-left: 2px solid #606060; + margin: 0 24px 0 4px; + padding: 0px 12px 0px 16px; +} + diff --git a/resources/header.html b/resources/header.html new file mode 100644 index 0000000..7de7c54 --- /dev/null +++ b/resources/header.html @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + +$projectname: $title +$title + + + + + + + + +$treeview +$search +$mathjax +$darkmode + +$extrastylesheet + + + + +
+ + + +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
$projectname $projectnumber +
+
$projectbrief
+
+
$projectbrief
+
$searchbox
$searchbox
+
+ + diff --git a/resources/smithsonian-logo-55x55.png b/resources/smithsonian-logo-55x55.png new file mode 100644 index 0000000000000000000000000000000000000000..defb2293e67246d8bd1c3ddfacecc32096220cbb GIT binary patch literal 5834 zcmeHLdsK{T7oQ|5O>(**9AnC*%QW|CW|S^dX;MkjP2`=Kcc!6c#>`ZcbWyofR7$CI z8KpvrE)$7vIHeAbLy;^_}l8t+~AK-oO3Zd;gyOJga$= zy*w8qwT-nA2n3Sp>f{Z+t(9+0b?{lNftG@=_66WwZBM@pydM|VedQ@= zLyd*osN<_V|0+SJDYTvU>#g1FJImc_Byk>+nUY$1J5cnLSM%rX{;JBvN|fficTt8zPX!?@VD0AI2GMDEPgo)MKX7D4u^sQJUtAwO{&%blBppU*UV zm3BCK{HxfhzX!Yvk4uXs3-Kt_4;^{X_0uy>6ZbT*-lTx^73WtLDBN}^o}3My*7eAuJE(%3 zVro<~UJGsTo0p3Qg;oD(pTFy+sP+!K{x@^QW_QgKce2%1gk-F;do}y*lS3 zwdBKR)-U>LQ_(7UKxDxHyMteZXye^>6Ao7**9>J1ZdjQ*Q%$|M^-TE6=MGrK! zpiz8|E!vOlf%6bC;1Is+1~I&RgQpLB!#XyNgSK0wZ6l)tf-qPLp=4p90tsDai&pW{ z!L`ziMWa*@={j4qzlRr!Ar!+Xk`>7chjEtiBM9h4+9(?_hfDW%a``|3?rhN^QmKfJ z#YRR(T167Agkl~RPovSWI0BYHzyJhB5+#sAGK@fCqNEt*aDpXlF<&I*3k4`8C&Utl zOKs6;;75I|FHGd&F-9+td{6=Cft5ibEZz!-4GY759wCuBM*xx!1${L_;sZ7*)*F@x z!^LdaIRX|)O+KgKu*c#>;o?wLI~+C^4u!)2R06W%zeu^5>ESgNp_G8f4-=`PfY@Ik zrF`xuvA(EH=~1=wc_AQtjQ0!l$KF+7fb#I5I|@Kv+mQr+`3Z2fff4Vi=MN#Xdq|s4ZGq5=t34=Jo*Fi33R?CrAndQXGLq#}UEH z2X9R$k?3TK1rATgeTElu_}r-fhE{GKl+EapyYeLqqZKZ$tU2t%O3U zHU%AGkG3FzB4CcHPQW$lVuwHi9t=*8599jL&i~3Nun7b!G7Ru~PbYuvuTm>K)1_zRi2}b=O z81_TMSml}Vv11$TziDEl0(^GJfVfc`IK04Fh#fl&KWL^rcK*S?4{PxcW&qURlYEoD z-{tx)*EcEfP2lg-^2(;CYSc7pHr{^{1+4nek>)p5vCy!>Ig4)ALqp) zcSLOI|Me9E(v|$Dj4}IWZ%zAgYx>tQ8_R-n1S_)Ff7P3@&nvmaXUsJ&zNqOiSy4?> ziz^;8f(wJqS-r>RTsdkwB`N!pJ*JG77XP$a|H(-M()L7P=jHL!)-eJd~QL7>x?ZKh}gXv zoA!*0FGR%2i}t&`X+yqlL+&nj-@MnivkqgLoLYX+@k}kQx@ytReM^yvITI4{f2y?_ zzo`J1y7@uV^c%-eE<2;(ypXy>^t;DrylmASY11BP*8fGZ0KIMd?ZzJ+GB$#w7ZnS1 z5_h|2Mh>>en2?yFYhhN4x{0*JCBK|3+G^njCmzl;9nT^oyo z07gB&keMkvT21xJ3O`qG-cg4M-NSi&a_YS%3Dj1|P1n-G9*G>QR&FTXq7>1%=sPCk-z(%%7i}Vwtivzrt}tou#IO-IOGb(YUs zrc_qiVN$mqsI+gWrXH-cw@Z%=$>W_kLf$0zZ?D0dr6gySdFmzQ#LEL;x9J@0PJRp? zI;cs#J(nZw?_UK^w-9QqC5xR9+cajmf*UQ7Yk&lSn53_Kt0D3Wz}7)blrlY>Cq7VD z*R!O@pL+?OXsAO>CkG$b4ux+`+8%qo7qOQVy;i{)M#;nKOQ^M$rp8N_rOZM4FvIU@ zl}*wfGW3#bnjODyo>8_j{@%I_L5+b19O>V>6-iAdXf*vaoLgw!^qfm+3ULbDbm7`% zmt8enF|xDbc;Ly-*aLaK9uCYYff1+ID}=AKr}hTB6!w=8Hd$*{$-R$_tZFd6RvoBN z^Y6kZd7ad1y>n_k#c~(o>_YtU+dbH4Iz8`D4#@3Z>T9qeOjJN;-wngrZz!j$inAj8 z5I(JEo)%`*)3+s8*bMV-J!2mbEv)gxl_WcuRL;k@#>9;8Vtbya1YBP|GB71$+T@!! zt!NkDl&MXR!wj#q#820|%Q*c!hr{X|9KUveH9YUcAgk)7U+!&L{}Ctg!+A$fUvi8r zmoI)PXtUgKmy|?BJV%{T80l9f%qeW2m^ncb-z$#{>ecDYD1f&|OXTFP-JTu%iKZ4$ z*s+6+K@p-m+1GNla;^U;G|(Q(pgkIYR%hlk!u*h%o5PLB!@3gFPS4$m$KdNljMScE zve})liWM4nwyIs7!`&HuL!L9_yCwL&K`@Utqj_=$J_a?}W~Lpi72VfyQxxcY z3BQFOd9SSWo}%7oRhb@6Vk}2fA1@Uxi#fE<_BbQ$fo{u_`(g0)-m~TV+MYacvHbDG zK!Iib(##(FbaIvcZ|1$-q}9Q1qi@u%y6k4q@YwTmUHg(A&mpJ;#MrBiKQVU#9sSMjL1}%NU-~+`w zzRW26H@o(GOmX(RD4sF4`{f^j!V#XdV$kUFQ`ZTDLr#5@e!h6g?cu_n%d1LtcyDgK z(Es(^UbjcFjtav=28A#6k+jBb9cLU5yqRj{U&g5-R_$pdbGjrqu{TZE?D`!!_ttBH z-$6V5#Sz4dTT@z^xkJq>j8ByHN6fP;zwYPw<759j`R}q8Hy9da6;_s;-JtYR+8svT g_XG!DyYPO3;)lKEmv&Z61^W-dboO+rc3hM2FV$`4^8f$< literal 0 HcmV?d00001