diff --git a/.actrc b/.actrc new file mode 100644 index 0000000..d270424 --- /dev/null +++ b/.actrc @@ -0,0 +1 @@ +-P ubuntu-latest=catthehacker/ubuntu:runner-latest diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..06cae95 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,41 @@ +name: Run RaspberryPi-Gateway tests +on: + pull_request: + push: + workflow_dispatch: +jobs: + flake: + name: Run Nix flake checks + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Initialize Nix store paths + run: | + sudo mkdir -p /nix/store + sudo chmod -R 777 /nix + # https://github.com/cachix/install-nix-action/issues/56#issuecomment-1030697681 + - name: Configure Nix store cache + uses: actions/cache@v4 + with: + key: ${{ runner.os }}-${{ runner.arch }}-nix-store + # See https://github.com/actions/cache/pull/726 and + # https://github.com/actions/cache/issues/494 for caveats on negative + # patterns. + path: | + /nix/store/** + /nix/var/nix/*/* + /nix/var/nix/db/* + /nix/var/nix/db/*/** + !/nix/var/nix/daemon-socket/socket + !/nix/var/nix/userpool/* + !/nix/var/nix/gc.lock + !/nix/var/nix/db/big-lock + !/nix/var/nix/db/reserved + - name: Install Nix + uses: cachix/install-nix-action@v31 + with: + extra_nix_config: | + system-features = benchmark big-parallel kvm nixos-test uid-range + - name: Run Nix flake checks + run: | + nix flake check -L diff --git a/.gitignore b/.gitignore index 43607a7..6aff2e3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,14 @@ node_modules db/* *.log + +# VM disk images +*.qcow2 + +# Nix build results +result +result-* +repl-result-* + +# NixOS test driver REPL history +.nixos-test-history diff --git a/.setup/gateway b/.setup/gateway index 4a4fb15..bdb430c 100644 --- a/.setup/gateway +++ b/.setup/gateway @@ -63,33 +63,12 @@ server{ proxy_set_header X-Forwarded-User $remote_user; } - # enable and configure PHP - location ~* \.php$ { - fastcgi_split_path_info ^(.+\.php)(.*)$; - - # typically the fpm.sock is used, but check to make sure by using: - # sudo grep -ri "listen = " /etc/php - # should get something like: - # /etc/php/7.3/fpm/pool.d/www.conf:listen = /run/php/php7.3-fpm.sock - # then you know it's fpm.sock - fastcgi_pass unix:/run/php/PHPFPMSOCK; #uncomment if PHP runs on fpm.sock - #fastcgi_pass 127.0.0.1:9000; #uncomment if PHP is running on port 9000 - - fastcgi_index index.php; - fastcgi_param SCRIPT_FILENAME /home/pi/gateway/www$fastcgi_script_name; - fastcgi_param QUERY_STRING $query_string; - fastcgi_param REQUEST_METHOD $request_method; - fastcgi_param CONTENT_TYPE $content_type; - fastcgi_param CONTENT_LENGTH $content_length; - include fastcgi_params; - } - # redirect server error pages to the static page /50x.html error_page 500 502 503 504 /50x.html; location = /50x.html { root html; } - + ## Disable viewing .htaccess & .htpassword location ~ /\.ht { deny all; diff --git a/.setup/gatewaysetup.sh b/.setup/gatewaysetup.sh index 7c7e217..9f9dcc4 100644 --- a/.setup/gatewaysetup.sh +++ b/.setup/gatewaysetup.sh @@ -108,9 +108,6 @@ sudo apt-get -y install git apache2-utils echo -e "${CYAN}************* STEP: Install latest NGINX *************${NC}" sudo apt-get -y install nginx -echo -e "${CYAN}************* STEP: Install latest PHP *********************${NC}" -sudo apt-get -y install php-common php-cli php-fpm - echo -e "${CYAN}************* STEP: Install nodeJS & npm *************${NC}" #install latest NodeJS --- https://www.raspberrypi.org/forums/viewtopic.php?t=141770 sudo wget -O - https://raw.githubusercontent.com/audstanley/NodeJs-Raspberry-Pi/master/Install-Node.sh | sudo bash @@ -179,10 +176,6 @@ echo -e "${YLW}Done. You can add/change http_auth credentials using ${RED}htpass echo -e "${CYAN}************* STEP: Copy gateway site config to sites-available *************${NC}" cp -rf $APPSRVDIR/.setup/gateway /etc/nginx/sites-available/gateway -#determine php-fpm version and replace in gateway site config -phpfpmsock=$(grep -ri "listen = /" /etc/php) #search for file containing "listen =" in php path -phpfpmsock=${phpfpmsock##*/} #extract everything after last / -sudo sed -i "s/PHPFPMSOCK/${phpfpmsock}/g" /etc/nginx/sites-available/gateway #replace PHPFPMSOCK with it in site config file cd /etc/nginx/sites-enabled sudo rm /etc/nginx/sites-enabled/default sudo ln -s /etc/nginx/sites-available/gateway diff --git a/app.nix b/app.nix new file mode 100644 index 0000000..2f9fcdd --- /dev/null +++ b/app.nix @@ -0,0 +1,36 @@ +{ + lib, + config, + dream2nix, + pkgs, + ... +} @ args: let + packageJSON = lib.importJSON ./package.json; +in { + imports = [ + dream2nix.modules.dream2nix.nodejs-package-json-v3 + dream2nix.modules.dream2nix.nodejs-granular-v3 + ]; + + inherit (packageJSON) name version; + + # Take advantage of a shell injection vector in dream2nix in order to apply a + # workaround for an issue with `npm i` converting dependencies specified with + # `git+https://` or `github:` to `git+ssh://`. + # See: + # - https://github.com/npm/cli/issues/2610 + # - https://github.com/npm/cli/issues/2631 + nodejs-package-json.npmArgs = lib.mkAfter [ + "--package-lock-only' ; ${lib.getExe pkgs.gnused} -i -e 's|git+ssh://|git+https://|g' ./package-lock.json #" + ]; + + mkDerivation = { + src = lib.cleanSource ./.; + doCheck = false; + meta = { + # XXX broken somehow? `lib.licenses.gpl3` works... + #license = lib.licenses.cc-by-nc-40; + homepage = "https://github.com/LowPowerLab/RaspberryPi-Gateway"; + }; + }; +} diff --git a/config.js b/config.js new file mode 100644 index 0000000..bee8e12 --- /dev/null +++ b/config.js @@ -0,0 +1,86 @@ +// ********************************************************************************** +// Websocket server backend for the RaspberryPi-Gateway App +// http://lowpowerlab.com/gateway +// ********************************************************************************** +// Common application configuration settings. +// ********************************************************************************** + +const JSON5 = require('json5'); // https://github.com/aseemk/json5 +const nconf = require('nconf'); // https://github.com/indexzero/nconf +const path = require('path'); + +exports.load = function({defaultStateDir = __dirname, defaultContentDir = null} = {}) { + nconf.argv().env() + + const stateDir = path.normalize(nconf.get('MOTEINO_GATEWAY_STATE_DIRECTORY') || defaultStateDir); + + const resolvePath = (base) => (...dirs) => path.resolve(base, ...dirs); + + const resolveCoreStatePath = resolvePath(defaultStateDir); + const resolveUserStatePath = resolvePath(stateDir); + + const coreContentDir = resolveCoreStatePath('www'); + const contentDir = path.normalize(nconf.get('MOTEINO_GATEWAY_CONTENT_DIRECTORY') || defaultContentDir || coreContentDir); + + const resolveCoreContentPath = resolvePath(coreContentDir); + const resolveUserContentPath = resolvePath(contentDir); + + const coreImagesDir = resolveCoreContentPath('images'); + const userImagesDir = resolveUserContentPath('images'); + + const coreMetricsDir = resolveCoreStatePath('metrics'); + const userMetricsDir = resolveUserStatePath('metrics'); + + const dbDir = resolveUserStatePath('data/db'); + + if (stateDir == defaultStateDir) + { + nconf.file('mutable5', { + file: resolveCoreStatePath('settings.json5'), + format: JSON5, + }); + + nconf.file('mutable', { + file: resolveCoreStatePath('settings.json'), + }); + } + else + { + nconf.file('mutable5', { + file: resolveUserStatePath('settings.json5'), + format: JSON5, + }); + + nconf.file('mutable', { + file: resolveUserStatePath('settings.json'), + }); + + nconf.file('immutable5', { + file: resolveCoreStatePath('settings.json5'), + format: JSON5, + }); + + nconf.file('immutable', { + file: resolveCoreStatePath('settings.json'), + }); + } + + return { + 'nconf': nconf, + 'stateDir': stateDir, + 'contentDir': contentDir, + 'coreContentDir': coreContentDir, + 'coreImagesDir': coreImagesDir, + 'userImagesDir': userImagesDir, + 'coreMetricsDir': coreMetricsDir, + 'userMetricsDir': userMetricsDir, + 'dbDir': dbDir, + 'resolveCoreContentPath': resolveCoreContentPath, + 'resolveCoreStatePath': resolveCoreStatePath, + 'resolveUserContentPath': resolveUserContentPath, + 'resolveUserStatePath': resolveUserStatePath, + }; +}; + +// Load configuration and export it +Object.assign(exports, exports.load()); diff --git a/data/db/.gitignore b/data/db/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/data/db/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/doc/nixos-modules.md b/doc/nixos-modules.md new file mode 100644 index 0000000..e2db80c --- /dev/null +++ b/doc/nixos-modules.md @@ -0,0 +1,490 @@ +## profiles\.mightyhat\.enable + + + +Whether to enable the LowPowerLab Moteino Gateway Raspberry Pi profile\. + + + +*Type:* +boolean + + + +*Default:* +` false ` + + + +*Example:* +` true ` + +*Declared by:* + - [flake\.nix](/flake.nix) + + + +## profiles\.mightyhat\.apply-overlays-dtmerge\.enable + +Merge device tree overlays with ` dtmerge ` from +` libraspberrypi `\. + +Additionally, supports merging compiled overlays from +` device-tree_rpi `, even if they specify a ` compatible ` +stanza that is different than that nominally required by the target +device tree\. + + + +*Type:* +boolean + + + +*Default:* +` true ` + +*Declared by:* + - [flake\.nix](/flake.nix) + + + +## profiles\.mightyhat\.bluetooth\.disable + + + +Apply the ` disable-bt ` device tree overlay from +` raspberrypi/firmware `\. + + + +*Type:* +boolean + + + +*Default:* +` true ` + +*Declared by:* + - [flake\.nix](/flake.nix) + + + +## profiles\.mightyhat\.bluetooth\.miniuart + + + +Apply the ` miniuart-bt ` device tree overlay from +` raspberrypi/firmware `\. + + + +*Type:* +boolean + + + +*Default:* +` false ` + +*Declared by:* + - [flake\.nix](/flake.nix) + + + +## profiles\.mightyhat\.deviceTree + + + +The device-tree_rpi package to use\. + + + +*Type:* +package + + + +*Default:* +` pkgs.device-tree_rpi.device-tree_rpi ` + +*Declared by:* + - [flake\.nix](/flake.nix) + + + +## profiles\.mightyhat\.uboot\.disableConsole + + + +Apply an overlay that redefines the Raspberry-Pi-specific uboot +packages in order to disable the serial console\. + +**Important:** Enabling this option may prevent you from selecting a NixOS boot +generation at the uboot loader screen! + + + +*Type:* +boolean + + + +*Default:* +` false ` + +*Declared by:* + - [flake\.nix](/flake.nix) + + + +## services\.moteino-gateway\.enable + + + +Whether to enable the LowPowerLab Moteino Gateway\. + + + +*Type:* +boolean + + + +*Default:* +` false ` + + + +*Example:* +` true ` + +*Declared by:* + - [flake\.nix](/flake.nix) + + + +## services\.moteino-gateway\.package + + + +The package providing the LowPowerLab Moteino Gateway\. + + + +*Type:* +package + + + +*Default:* +` "perSystem.config.packages.default" ` + +*Declared by:* + - [flake\.nix](/flake.nix) + + + +## services\.moteino-gateway\.contentDir + + + +Directory for holding static content\. At present, used only for +uploaded node icon images\. + +**Note:** Because the gateway systemd service uses ` DynamicUser=yes `, this +directory has to be outside of ` StateDirectory `, ` CacheDirectory `, +and so on, so that the nginx webserver can read its contents\. + + + +*Type:* +absolute path + + + +*Default:* +` "/srv/www/moteino-gateway" ` + +*Declared by:* + - [flake\.nix](/flake.nix) + + + +## services\.moteino-gateway\.metrics + + + +JavaScript scripts for exporting node metrics data\. + +See https://github\.com/LowPowerLab/RaspberryPi-Gateway/blob/master/metrics/examples/_example\.js +for an example metrics script definition\. + + + +*Type:* +attribute set of (submodule) + + + +*Default:* +` { } ` + +*Declared by:* + - [flake\.nix](/flake.nix) + + + +## services\.moteino-gateway\.metrics\.\\.name + + + +Name of the metrics script\. + + + +*Type:* +string + + + +*Default:* +` "‹name›" ` + +*Declared by:* + - [flake\.nix](/flake.nix) + + + +## services\.moteino-gateway\.metrics\.\\.source + + + +Path of the source file containing the metrics script\. + + + +*Type:* +absolute path + +*Declared by:* + - [flake\.nix](/flake.nix) + + + +## services\.moteino-gateway\.metrics\.\\.text + + + +Text of the metrics script\. + + + +*Type:* +null or strings concatenated with “\\n” + + + +*Default:* +` null ` + +*Declared by:* + - [flake\.nix](/flake.nix) + + + +## services\.moteino-gateway\.nodejs + + + +The nodejs package to use\. + + + +*Type:* +package + + + +*Default:* +` pkgs.nodejs.nodejs ` + +*Declared by:* + - [flake\.nix](/flake.nix) + + + +## services\.moteino-gateway\.persistLogs + + + +Whether to persist gateway info and error logs to the systemd +service’s configured ` LogsDirectory ` (by default, +` /var/log/moteino-gateway `)\. + + + +*Type:* +boolean + + + +*Default:* +` false ` + +*Declared by:* + - [flake\.nix](/flake.nix) + + + +## services\.moteino-gateway\.settings + + + +Settings for the LowPowerLab Moteino Gateway\. Defaults are loaded from +` ${services.moteino-gateway.package}/lib/node_modules/RaspberryPi-Gateway/settings.json5 `\. + + + +*Type:* +JSON value + + + +*Default:* +` { } ` + +*Declared by:* + - [flake\.nix](/flake.nix) + + + +## services\.moteino-gateway\.settings\.general + + + +General settings\. + + + +*Type:* +JSON value + + + +*Default:* +` { } ` + +*Declared by:* + - [flake\.nix](/flake.nix) + + + +## services\.moteino-gateway\.settings\.general\.port + + + +Listening port for the web application of the +LowPowerLab Moteino Gateway\. + + + +*Type:* +16 bit unsigned integer; between 0 and 65535 (both inclusive) + + + +*Default:* +` 8081 ` + +*Declared by:* + - [flake\.nix](/flake.nix) + + + +## services\.moteino-gateway\.settings\.general\.socketPort + + + +Listening port for the websocket of the LowPowerLab Moteino Gateway\. + + + +*Type:* +16 bit unsigned integer; between 0 and 65535 (both inclusive) + + + +*Default:* +` 8080 ` + +*Declared by:* + - [flake\.nix](/flake.nix) + + + +## services\.moteino-gateway\.settings\.serial + + + +Serial port settings\. + + + +*Type:* +JSON value + + + +*Default:* +` { } ` + +*Declared by:* + - [flake\.nix](/flake.nix) + + + +## services\.moteino-gateway\.settings\.serial\.baud + + + +Needs to match the serial baud speed in the sketch +running on the Moteino or MightyHat, if any, that is +attached to this machine\. + + + +*Type:* +positive integer, meaning >0 + + + +*Default:* +` 115200 ` + +*Declared by:* + - [flake\.nix](/flake.nix) + + + +## services\.moteino-gateway\.settings\.serial\.port + + + +Serial port for the LowPowerLab Moteino Gateway\. + + + +*Type:* +absolute path + + + +*Default:* +` "/dev/ttyAMA0" ` + +*Declared by:* + - [flake\.nix](/flake.nix) + + diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..67768f8 --- /dev/null +++ b/flake.lock @@ -0,0 +1,221 @@ +{ + "nodes": { + "avrdude-rpi": { + "flake": false, + "locked": { + "lastModified": 1452284151, + "narHash": "sha256-ZLQhaxvB7mkWWDQ5Xwn8bwBzWhngze39FZ4/jkwY9L8=", + "owner": "LowPowerLab", + "repo": "avrdude-rpi", + "rev": "b44376c4e3e89c1a0c7b972ee5b53b4a2be5c5d5", + "type": "github" + }, + "original": { + "owner": "LowPowerLab", + "repo": "avrdude-rpi", + "type": "github" + } + }, + "devshell": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1741473158, + "narHash": "sha256-kWNaq6wQUbUMlPgw8Y+9/9wP0F8SHkjy24/mN3UAppg=", + "owner": "numtide", + "repo": "devshell", + "rev": "7c9e793ebe66bcba8292989a68c0419b737a22a0", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "devshell", + "type": "github" + } + }, + "dream2nix": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ], + "purescript-overlay": "purescript-overlay", + "pyproject-nix": "pyproject-nix" + }, + "locked": { + "lastModified": 1735160684, + "narHash": "sha256-n5CwhmqKxifuD4Sq4WuRP/h5LO6f23cGnSAuJemnd/4=", + "owner": "nix-community", + "repo": "dream2nix", + "rev": "8ce6284ff58208ed8961681276f82c2f8f978ef4", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "dream2nix", + "type": "github" + } + }, + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1696426674, + "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", + "owner": "edolstra", + "repo": "flake-compat", + "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", + "type": "github" + }, + "original": { + "owner": "edolstra", + "repo": "flake-compat", + "type": "github" + } + }, + "flake-parts": { + "inputs": { + "nixpkgs-lib": "nixpkgs-lib" + }, + "locked": { + "lastModified": 1741352980, + "narHash": "sha256-+u2UunDA4Cl5Fci3m7S643HzKmIDAe+fiXrLqYsR2fs=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "f4330d22f1c5d2ba72d3d22df5597d123fdb60a9", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1742669843, + "narHash": "sha256-G5n+FOXLXcRx+3hCJ6Rt6ZQyF1zqQ0DL0sWAMn2Nk0w=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "1e5b653dff12029333a6546c11e108ede13052eb", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-lib": { + "locked": { + "lastModified": 1740877520, + "narHash": "sha256-oiwv/ZK/2FhGxrCkQkB83i7GnWXPPLzoqFHpDD3uYpk=", + "owner": "nix-community", + "repo": "nixpkgs.lib", + "rev": "147dee35aab2193b174e4c0868bd80ead5ce755c", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "nixpkgs.lib", + "type": "github" + } + }, + "purescript-overlay": { + "inputs": { + "flake-compat": "flake-compat", + "nixpkgs": [ + "dream2nix", + "nixpkgs" + ], + "slimlock": "slimlock" + }, + "locked": { + "lastModified": 1728546539, + "narHash": "sha256-Sws7w0tlnjD+Bjck1nv29NjC5DbL6nH5auL9Ex9Iz2A=", + "owner": "thomashoneyman", + "repo": "purescript-overlay", + "rev": "4ad4c15d07bd899d7346b331f377606631eb0ee4", + "type": "github" + }, + "original": { + "owner": "thomashoneyman", + "repo": "purescript-overlay", + "type": "github" + } + }, + "pyproject-nix": { + "flake": false, + "locked": { + "lastModified": 1702448246, + "narHash": "sha256-hFg5s/hoJFv7tDpiGvEvXP0UfFvFEDgTdyHIjDVHu1I=", + "owner": "davhau", + "repo": "pyproject.nix", + "rev": "5a06a2697b228c04dd2f35659b4b659ca74f7aeb", + "type": "github" + }, + "original": { + "owner": "davhau", + "ref": "dream2nix", + "repo": "pyproject.nix", + "type": "github" + } + }, + "root": { + "inputs": { + "avrdude-rpi": "avrdude-rpi", + "devshell": "devshell", + "dream2nix": "dream2nix", + "flake-parts": "flake-parts", + "nixpkgs": "nixpkgs", + "treefmt-nix": "treefmt-nix" + } + }, + "slimlock": { + "inputs": { + "nixpkgs": [ + "dream2nix", + "purescript-overlay", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1688756706, + "narHash": "sha256-xzkkMv3neJJJ89zo3o2ojp7nFeaZc2G0fYwNXNJRFlo=", + "owner": "thomashoneyman", + "repo": "slimlock", + "rev": "cf72723f59e2340d24881fd7bf61cb113b4c407c", + "type": "github" + }, + "original": { + "owner": "thomashoneyman", + "repo": "slimlock", + "type": "github" + } + }, + "treefmt-nix": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1742370146, + "narHash": "sha256-XRE8hL4vKIQyVMDXykFh4ceo3KSpuJF3ts8GKwh5bIU=", + "owner": "numtide", + "repo": "treefmt-nix", + "rev": "adc195eef5da3606891cedf80c0d9ce2d3190808", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "treefmt-nix", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..e65eb8d --- /dev/null +++ b/flake.nix @@ -0,0 +1,82 @@ +{ + description = "Description for the project"; + + inputs = { + avrdude-rpi.url = "github:LowPowerLab/avrdude-rpi"; + avrdude-rpi.flake = false; + + dream2nix.url = "github:nix-community/dream2nix"; + dream2nix.inputs.nixpkgs.follows = "nixpkgs"; + + devshell.url = "github:numtide/devshell"; + devshell.inputs.nixpkgs.follows = "nixpkgs"; + + flake-parts.url = "github:hercules-ci/flake-parts"; + + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + + treefmt-nix.url = "github:numtide/treefmt-nix"; + treefmt-nix.inputs.nixpkgs.follows = "nixpkgs"; + }; + + outputs = inputs: + inputs.flake-parts.lib.mkFlake {inherit inputs;} ({ + inputs, + self, + moduleWithSystem, + ... + } @ toplevel: { + systems = [ + "aarch64-darwin" + "aarch64-linux" + "x86_64-darwin" + "x86_64-linux" + ]; + + imports = [ + inputs.devshell.flakeModule + inputs.treefmt-nix.flakeModule + + ./nix/checks.nix + ./nix/devshells.nix + ./nix/nixos-modules.nix + ./nix/overlays.nix + ./nix/packages.nix + ]; + + perSystem = { + config, + lib, + ... + }: { + apps = lib.mapAttrs (lib.const (lib.getAttr "flakeApp")) config.devShells; + + treefmt = { + flakeFormatter = true; + projectRootFile = "flake.nix"; + programs.alejandra.enable = true; + #programs.prettier.enable = true; + #programs.shfmt.enable = true; + }; + }; + + flake = { + nixosConfigurations = { + default = inputs.nixpkgs.lib.nixosSystem { + system = "x86_64-linux"; + modules = [ + self.nixosModules.mightyhat + self.nixosModules.moteino-gateway + ({pkgs, ...}: { + boot.isContainer = true; + services.moteino-gateway.enable = true; + system.stateVersion = "24.05"; + environment.systemPackages = with pkgs; [curl]; + fileSystems."/".fsType = "tmpfs"; + }) + ]; + }; + }; + }; + }); +} diff --git a/gateway.js b/gateway.js index bd65d4d..7d75073 100644 --- a/gateway.js +++ b/gateway.js @@ -29,14 +29,25 @@ // ******************************************************************************************** // Note: In NodeJS modules are loaded synchronously and processed in the order they occur // ******************************************************************************************** -var nconf = require('nconf'); //https://github.com/indexzero/nconf -var JSON5 = require('json5'); //https://github.com/aseemk/json5 +var fs = require('fs'); var path = require('path'); -var dbDir = 'data/db'; + +const config = require('./config'); + +const nconf = config.nconf; +const coreImagesDir = config.coreImagesDir; +const userImagesDir = config.userImagesDir; +const coreMetricsDir = config.coreMetricsDir; +const userMetricsDir = config.userMetricsDir; +const dbDir = config.dbDir; + +fs.mkdirSync(dbDir, {'recursive': true}); +fs.mkdirSync(userImagesDir, {'recursive': true}); + +global.settings = nconf.get('settings'); + var packageJson = require('./package.json') var coreMetricsFilePath = './metrics/core.js'; -nconf.argv().file({ file: './settings.json5', format: JSON5 }); -global.settings = nconf.get('settings'); var dbLog = require('./logUtil.js'); io = require('socket.io')().listen(settings.general.socketPort.value) //usage in 2.3.0: io = require('socket.io').listen(settings.general.socketPort.value); var serialport = require("serialport"); //https://github.com/node-serialport/node-serialport @@ -44,20 +55,19 @@ var Datastore = require('nedb'); //https://github var nodemailer = require('nodemailer'); //https://github.com/andris9/Nodemailer var http = require('http'); var url = require('url'); -db = new Datastore({ filename: path.join(__dirname, dbDir, settings.database.name.value), autoload: true }); //used to keep all node/metric data +db = new Datastore({ filename: path.join(dbDir, settings.database.name.value), autoload: true }); //used to keep all node/metric data var dbCompacted = Date.now(); -var fs = require('fs'); var gatewayUptime=''; var gatewayFrequency=''; global.port=undefined; global.parser=undefined; var unmatchedDataDB = null; if (settings.database.nonMatchesName.value) - unmatchedDataDB = new Datastore({ filename: path.join(__dirname, dbDir, settings.database.nonMatchesName.value), autoload: true }); + unmatchedDataDB = new Datastore({ filename: path.join(dbDir, settings.database.nonMatchesName.value), autoload: true }); require("console-stamp")(console, settings.general.consoleLogDateFormat.value); //timestamp logs - https://github.com/starak/node-console-stamp //HTTP ENDPOINT - accept HTTP: data from the internet/LAN -http.createServer(httpEndPointHandler).listen(8081); +http.createServer(httpEndPointHandler).listen(settings.general.port.value); console.info('*********************************************************************'); console.info('************************* GATEWAY APP START *************************'); @@ -229,7 +239,7 @@ global.handleNodeEvents = function(node) { global.getGraphData = function(nodeId, metricKey, start, end, exportMode) { var sts = Math.floor(start / 1000); //get timestamp in whole seconds var ets = Math.floor(end / 1000); //get timestamp in whole seconds - var logfile = path.join(__dirname, dbDir, dbLog.getLogName(nodeId,metricKey)); + var logfile = path.join(dbDir, dbLog.getLogName(nodeId,metricKey)); var graphData = dbLog.getData(logfile, sts, ets, exportMode ? 100000 : settings.general.graphMaxPoints.value); //100k points when exporting, more points is really pointless var graphOptions={}; for(var k in metricsDef.metrics) @@ -245,19 +255,42 @@ global.getGraphData = function(nodeId, metricKey, start, end, exportMode) { return { graphData:graphData, options : graphOptions }; } -global.getNodeIcons = function(dir, files_, steps){ - files_ = files_ || []; - dir = dir || __dirname + '/www/images'; - steps = steps || 0; - var files = fs.readdirSync(dir); - for (var i in files){ - var name = dir + '/' + files[i]; - if (fs.statSync(name).isDirectory() && steps==0) //recurse 1 level only - getNodeIcons(name, files_, steps+1); - else if (files[i].match(/^icon_.+\.(bmp|png|jpg|jpeg|ico)$/ig)) //images only - files_.push(name.replace(__dirname+'/www/images/','')); +global.getNodeIcons = function({ + dir = coreImagesDir, + root = null, + files = [], + steps = 0, + prefix = 'icon_', + extensions = ['.bmp', '.png', '.jpg', '.jpeg', '.ico'], +} = {}) { + dir = path.resolve(dir); + root = path.resolve(root || dir); + + console.log({'dir': dir, 'root': root, 'files': files, 'steps': steps, 'prefix': prefix, 'extensions': extensions}); + + var basenames = fs.readdirSync(dir); + for (var i in basenames) { + var basename = basenames[i]; + var file = path.join(dir, basename); + + if (fs.statSync(file).isDirectory() && steps==0) //recurse 1 level only + getNodeIcons({'dir': file, 'root': root, 'files': files, 'steps': steps+1, 'prefix': prefix, 'extensions': extensions}); + else if (basename.startsWith(prefix) && extensions.includes(path.extname(file))) + files.push(path.relative(root, file)); } - return files_; + + return files; +} + +global.allNodeIcons = function () { + var files; + + if (coreImagesDir == userImagesDir) + files = []; + else + files = getNodeIcons({'dir': userImagesDir, 'prefix': ''}); + + return getNodeIcons({'dir': coreImagesDir, 'files': files}); } //authorize handshake - make sure the request is proxied from localhost, not from the outside world @@ -302,7 +335,7 @@ io.sockets.on('connection', function (socket) { socket.emit('SETTINGSDEF', settings); broadcastServerInfo(socket); socket.emit('DBCOMPACTED', dbCompacted); - socket.emit('NODEICONS', getNodeIcons()); + socket.emit('NODEICONS', allNodeIcons()); //pull all nodes from the database and send them to client db.find({ _id : { $exists: true } }, function (err, entries) { @@ -320,7 +353,7 @@ io.sockets.on('connection', function (socket) { }); socket.on('REFRESHICONS', function () { - io.sockets.emit('NODEICONS', getNodeIcons()); + io.sockets.emit('NODEICONS', allNodeIcons()); }); socket.on('UPDATENODELISTORDER', function (newOrder) { @@ -460,7 +493,7 @@ io.sockets.on('connection', function (socket) { if (dbNode.metrics) { Object.keys(dbNode.metrics).forEach(function(mKey,index) { //syncronous/blocking call if (dbNode.metrics[mKey].graph == 1) - dbLog.removeMetricLog(path.join(__dirname, dbDir, dbLog.getLogName(dbNode._id, mKey))); + dbLog.removeMetricLog(path.join(dbDir, dbLog.getLogName(dbNode._id, mKey))); }); } } @@ -494,7 +527,7 @@ io.sockets.on('connection', function (socket) { delete(dbNode.metrics[metricKey]); db.update({ _id: dbNode._id }, { $set : dbNode}, {}, function (err, numReplaced) { console.info('DELETENODEMETRIC DB-Replaced:' + numReplaced); }); if (settings.general.keepMetricLogsOnDelete.value != 'true') - dbLog.removeMetricLog(path.join(__dirname, dbDir, dbLog.getLogName(dbNode._id, metricKey))); + dbLog.removeMetricLog(path.join(dbDir, dbLog.getLogName(dbNode._id, metricKey))); io.sockets.emit('UPDATENODE', dbNode); //post it back to all clients to confirm UI changes } }); @@ -522,7 +555,7 @@ io.sockets.on('connection', function (socket) { if (entries.length == 1) { var dbNode = entries[0]; - var logfile = path.join(__dirname, dbDir, dbLog.getLogName(dbNode._id, metricKey)); + var logfile = path.join(dbDir, dbLog.getLogName(dbNode._id, metricKey)); var count = dbLog.deleteData(logfile, sts, ets); console.info('DELETEMETRICDATA DB-Removed points:' + count); //if (settings.general.keepMetricLogsOnDelete.value != 'true') @@ -538,7 +571,7 @@ io.sockets.on('connection', function (socket) { if (entries.length == 1) { var dbNode = entries[0]; - var logfile = path.join(__dirname, dbDir, dbLog.getLogName(dbNode._id, metricKey)); + var logfile = path.join(dbDir, dbLog.getLogName(dbNode._id, metricKey)); var count = dbLog.editData(logfile, sts, ets, newValue); console.info(`EDITMETRICDATA DB-Updated points:${count} to:${newValue}`); socket.emit('EDITMETRICDATA_OK', count); //post it back to requesting client only @@ -669,7 +702,7 @@ io.sockets.on('connection', function (socket) { var dbNode = entries[0]; Object.keys(dbNode.metrics).forEach(function(mKey,index) { //syncronous/blocking call if (dbNode.metrics[mKey].graph == 1) { - var logfile = path.join(__dirname, dbDir, dbLog.getLogName(dbNode._id, mKey)); + var logfile = path.join(dbDir, dbLog.getLogName(dbNode._id, mKey)); var theData = dbLog.getData(logfile, sts, ets, howManyPoints /*settings.general.graphMaxPoints.value*/); theData.label = dbNode.metrics[mKey].label || mKey; sets.push(theData); //100k points when exporting, more points is really pointless @@ -744,6 +777,25 @@ io.sockets.on('connection', function (socket) { console.log('PI SHUTDOWN REQUESTED from ' + address); require('child_process').exec('sudo /sbin/shutdown now "GATEWAY SHUTDOWN REQUEST"', function (msg) { console.log(msg) }); }); + + // https://socket.io/how-to/upload-a-file + socket.on('UPLOADIMAGE', function (name, buffer, callback) { + if (!name) { + callback(null, "missing file name"); + return; + } + + if (!buffer) { + callback(null, "missing file data"); + return; + } + + target = path.join(userImagesDir, name); + + fs.writeFile(target, buffer, function(err) { + callback(name, err); + }); + }); }); //entries should contain the node list and also a node that contains the order (if this was ever added) @@ -883,7 +935,7 @@ global.processSerialData = function (data, simulated) { if (isNumeric(graphValue)) { var ts = Math.floor(Date.now() / 1000); //get timestamp in whole seconds - var logfile = path.join(__dirname, dbDir, dbLog.getLogName(id, matchingMetric.name)); + var logfile = path.join(dbDir, dbLog.getLogName(id, matchingMetric.name)); try { console.log('post: ' + logfile + '[' + ts + ','+graphValue + ']'); dbLog.postData(logfile, ts, graphValue, matchingMetric.duplicateInterval || null); @@ -1093,7 +1145,7 @@ function httpEndPointHandler(req, res) { if (isNumeric(graphValue)) { var ts = Math.floor(Date.now() / 1000); //get timestamp in whole seconds - var logfile = path.join(__dirname, dbDir, dbLog.getLogName(id, matchingMetric.name)); + var logfile = path.join(dbDir, dbLog.getLogName(id, matchingMetric.name)); try { console.log(`post: ${logfile} [${ts},${graphValue}]`); dbLog.postData(logfile, ts, graphValue, matchingMetric.duplicateInterval || null); diff --git a/lock.json b/lock.json new file mode 100644 index 0000000..5a675be --- /dev/null +++ b/lock.json @@ -0,0 +1,1535 @@ +{ + "package-lock": { + "name": "RaspberryPi-Gateway", + "version": "9.2.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "RaspberryPi-Gateway", + "version": "9.2.1", + "license": "CC-BY-NC-SA-4.0", + "dependencies": { + "colors": "~1.4", + "console-stamp": "^3.0.2", + "json5": "~2.2.0", + "merge": "^2.1.1", + "nconf": "~0.11.2", + "nedb": "git+https://github.com/LowPowerLab/nedb.git", + "nodemailer": "^6.6.0", + "serialport": "~9.0.7", + "socket.io": "~4.1.0", + "speedtest-net": "^1.6.2", + "suncalc": "~1.9" + } + }, + "node_modules/@serialport/binding-abstract": { + "version": "9.2.3", + "resolved": "https://registry.npmjs.org/@serialport/binding-abstract/-/binding-abstract-9.2.3.tgz", + "integrity": "sha512-cQs9tbIlG3P0IrOWyVirqlhWuJ7Ms2Zh9m2108z6Y5UW/iVj6wEOiW8EmK9QX9jmJXYllE7wgGgvVozP5oCj3w==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.2" + }, + "engines": { + "node": ">=10.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/binding-mock": { + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/@serialport/binding-mock/-/binding-mock-9.2.4.tgz", + "integrity": "sha512-dpEhACCs44oQhh6ajJfJdvQdK38Vq0N4W6iD/gdplglDCK7qXRQCMUjJIeKdS/HSEiWkC3bwumUhUufdsOyT4g==", + "license": "MIT", + "dependencies": { + "@serialport/binding-abstract": "9.2.3", + "debug": "^4.3.2" + }, + "engines": { + "node": ">=10.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/bindings": { + "version": "9.2.8", + "resolved": "https://registry.npmjs.org/@serialport/bindings/-/bindings-9.2.8.tgz", + "integrity": "sha512-hSLxTe0tADZ3LMMGwvEJWOC/TaFQTyPeFalUCsJ1lSQ0k6bPF04JwrtB/C81GetmDBTNRY0GlD0SNtKCc7Dr5g==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@serialport/binding-abstract": "9.2.3", + "@serialport/parser-readline": "9.2.4", + "bindings": "^1.5.0", + "debug": "^4.3.2", + "nan": "^2.15.0", + "prebuild-install": "^7.0.0" + }, + "engines": { + "node": ">=10.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-byte-length": { + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/@serialport/parser-byte-length/-/parser-byte-length-9.2.4.tgz", + "integrity": "sha512-sQD/iw4ZMU3xW9PLi0/GlvU6Y623jGeWecbMkO7izUo/6P7gtfv1c9ikd5h0kwL8AoAOpQA1lxdHIKox+umBUg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-cctalk": { + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/@serialport/parser-cctalk/-/parser-cctalk-9.2.4.tgz", + "integrity": "sha512-T4TU5vQMwmo9AB3gQLFDWbfJXlW5jd9guEsB/nqKjFHTv0FXPdZ7DQ2TpSp8RnHWxU3GX6kYTaDO20BKzc8GPQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-delimiter": { + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/@serialport/parser-delimiter/-/parser-delimiter-9.2.4.tgz", + "integrity": "sha512-4nvTAoYAgkxFiXrkI+3CA49Yd43CODjeszh89EK+I9c8wOZ+etZduRCzINYPiy26g7zO+GRAb9FoPCsY+sYcbQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-inter-byte-timeout": { + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/@serialport/parser-inter-byte-timeout/-/parser-inter-byte-timeout-9.2.4.tgz", + "integrity": "sha512-SOAdvr0oBQIOCXX198hiTlxs4JTKg9j5piapw5tNq52fwDOWdbYrFneT/wN04UTMKaDrJuEvXq6T4rv4j7nJ5A==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-readline": { + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/@serialport/parser-readline/-/parser-readline-9.2.4.tgz", + "integrity": "sha512-Z1/qrZTQUVhNSJP1hd9YfDvq0o7d87rNwAjjRKbVpa7Qi51tG5BnKt43IV3NFMyBlVcRe0rnIb3tJu57E0SOwg==", + "license": "MIT", + "dependencies": { + "@serialport/parser-delimiter": "9.2.4" + }, + "engines": { + "node": ">=10.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-ready": { + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/@serialport/parser-ready/-/parser-ready-9.2.4.tgz", + "integrity": "sha512-Pyi94Itjl6qAURwIZr/gmZpMAyTmKXThm6vL5DoAWGQjcRHWB0gwv2TY2v7N+mQLJYUKU3cMnvnATXxHm7xjxw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/parser-regex": { + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/@serialport/parser-regex/-/parser-regex-9.2.4.tgz", + "integrity": "sha512-sI/cVvPOYz+Dbv4ZdnW16qAwvXiFf/1pGASQdbveRTlgJDdz7sRNlCBifzfTN2xljwvCTZYqiudKvDdC1TepRQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@serialport/stream": { + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/@serialport/stream/-/stream-9.2.4.tgz", + "integrity": "sha512-bLye8Ub4vUFQGmkh8qEqehr7SE7EJs2yDs0h9jzuL5oKi+F34CFmWkEErO8GAOQ8YNn7p6b3GxUgs+0BrHHDZQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.3.2" + }, + "engines": { + "node": ">=10.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/@types/component-emitter": { + "version": "1.2.14", + "resolved": "https://registry.npmjs.org/@types/component-emitter/-/component-emitter-1.2.14.tgz", + "integrity": "sha512-lmPil1g82wwWg/qHSxMWkSKyJGQOK+ejXeMAAWyxNtVUD0/Ycj2maL63RAqpxVfdtvTfZkRnqzB0A9ft59y69g==", + "license": "MIT" + }, + "node_modules/@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==", + "license": "MIT" + }, + "node_modules/@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "22.13.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.13.tgz", + "integrity": "sha512-ClsL5nMwKaBRwPcCvH8E7+nU4GxHVx1axNvMZTFHMEfNI7oahimt26P5zjVCRrjiIWj6YFXfE1v3dEp94wLcGQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/agent-base": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz", + "integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==", + "license": "MIT", + "dependencies": { + "es6-promisify": "^5.0.0" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha512-nSVgobk4rv61R9PUSDtYt7mPVB2olxNR5RWJcAsH676/ef11bUZwvu7+RGYrYauVdDPcO519v68wRhXQtxsV9w==", + "license": "MIT" + }, + "node_modules/base64-arraybuffer": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz", + "integrity": "sha512-a1eIFi4R9ySrbiMuyTGx5e92uRH5tQY6kArNcFaKBUleIoLjdjBg7Zxm3Mqm3Kmkf27HLR/1fnxX9q8GQ7Iavg==", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "node_modules/binary-search-tree": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/binary-search-tree/-/binary-search-tree-0.2.5.tgz", + "integrity": "sha512-CvNVKS6iXagL1uGwLagSXz1hzSMezxOuGnFi5FHGKqaTO3nPPWrAbyALUzK640j+xOTVm7lzD9YP8W1f/gvUdw==", + "dependencies": { + "underscore": "~1.4.4" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==", + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/console-stamp": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/console-stamp/-/console-stamp-3.1.2.tgz", + "integrity": "sha512-ab66x3NxOTxPuq71dI6gXEiw2X6ql4Le5gZz0bm7FW3FSCB00eztra/oQUuCoCGlsyKOxtULnHwphzMrRtzMBg==", + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "dateformat": "^4.6.3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/dateformat": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", + "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/draftlog": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/draftlog/-/draftlog-1.0.13.tgz", + "integrity": "sha512-GeMWOpXERBpfVDK6v7m0x1hPg8+g8ZsZWqJl2T17wHqrm4h8fnjiZmXcnCrmwogAc6R3YTxFXax15wezfuyCUw==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/engine.io": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-5.1.1.tgz", + "integrity": "sha512-aMWot7H5aC8L4/T8qMYbLdvKlZOdJTH54FxfdFunTGvhMx1BHkJOntWArsVfgAZVwAO9LC2sryPWRcEeUzCe5w==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.4.1", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~4.0.0", + "ws": "~7.4.2" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io-parser": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-4.0.3.tgz", + "integrity": "sha512-xEAAY0msNnESNPc00e19y5heTPX4y/TJ36gr8t1voOaNmTojP9b3oK3BbJLFufW2XFPQaaijpFewm2g2Um3uqA==", + "license": "MIT", + "dependencies": { + "base64-arraybuffer": "0.1.4" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==", + "license": "MIT" + }, + "node_modules/es6-promisify": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", + "integrity": "sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==", + "license": "MIT", + "dependencies": { + "es6-promise": "^4.0.3" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/http-proxy-agent": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz", + "integrity": "sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg==", + "license": "MIT", + "dependencies": { + "agent-base": "4", + "debug": "3.1.0" + }, + "engines": { + "node": ">= 4.5.0" + } + }, + "node_modules/http-proxy-agent/node_modules/debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/http-proxy-agent/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/https-proxy-agent": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-3.0.1.tgz", + "integrity": "sha512-+ML2Rbh6DAuee7d07tYGEKOEi2voWPUGan+ExdPbPW6Z3svq+JCqr0v8WmKPOkz1vOVykPCBSuobe7G8GJUtVg==", + "license": "MIT", + "dependencies": { + "agent-base": "^4.3.0", + "debug": "^3.1.0" + }, + "engines": { + "node": ">= 4.5.0" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", + "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lie": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", + "integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/localforage": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz", + "integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==", + "license": "Apache-2.0", + "dependencies": { + "lie": "3.1.1" + } + }, + "node_modules/merge": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/merge/-/merge-2.1.1.tgz", + "integrity": "sha512-jz+Cfrg9GWOZbQAnDQ4hlVnQky+341Yk5ru8bZSe6sIDTCIg8n9i/u7hSQGSVOF3C7lH6mGtqjkiT9G4wFLL0w==", + "license": "MIT" + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nan": { + "version": "2.22.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.2.tgz", + "integrity": "sha512-DANghxFkS1plDdRsX0X9pm0Z6SJNN6gBdtXfanwoZ8hooC5gosGFSBGRYHUVPz1asKA/kMRqDRdHrluZ61SpBQ==", + "license": "MIT" + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/nconf": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/nconf/-/nconf-0.11.4.tgz", + "integrity": "sha512-YaDR846q11JnG1vTrhJ0QIlhiGY6+W1bgWtReG9SS3vkTl3AoNwFvUItdhG6/ZjGCfWpUVuRTNEBTDAQ3nWhGw==", + "license": "MIT", + "dependencies": { + "async": "^1.4.0", + "ini": "^2.0.0", + "secure-keys": "^1.0.0", + "yargs": "^16.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/nedb": { + "version": "1.8.0", + "resolved": "git+https://git@github.com/LowPowerLab/nedb.git#76ecbb380fe89b5fe494525f26650df835c41a42", + "license": "SEE LICENSE IN LICENSE", + "dependencies": { + "async": "0.2.10", + "binary-search-tree": "0.2.5", + "localforage": "^1.3.0", + "mkdirp": "~0.5.1", + "underscore": "~1.4.4" + } + }, + "node_modules/nedb/node_modules/async": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz", + "integrity": "sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ==" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-abi": { + "version": "3.74.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.74.0.tgz", + "integrity": "sha512-c5XK0MjkGBrQPGYG24GBADZud0NCbznxNx0ZkS+ebUTrmV1qTDxPxSL8zEAPURXSbLRWVexxmP4986BziahL5w==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/nodemailer": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.0.tgz", + "integrity": "sha512-SQ3wZCExjeSatLE/HBaXS5vqUOQk6GtBdIIKxiFdmm01mOQZX/POJkO3SUX1wDiYcwUOJwT23scFSC9fY2H8IA==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pump": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", + "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/sax": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", + "license": "ISC" + }, + "node_modules/secure-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/secure-keys/-/secure-keys-1.0.0.tgz", + "integrity": "sha512-nZi59hW3Sl5P3+wOO89eHBAAGwmCPd2aE1+dLZV5MO+ItQctIvAqihzaAXIQhvtH4KJPxM080HsnqltR2y8cWg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/serialport": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/serialport/-/serialport-9.0.8.tgz", + "integrity": "sha512-+G/Mp0sBoxC6iKSUmClOFKVMK0Nirdz6ui7zRWyK9CEXy3t3dCDhWKvzDDtfKxILYtHYPR3G1xP5H/ISDKrzcg==", + "license": "MIT", + "dependencies": { + "@serialport/binding-mock": "^9.0.7", + "@serialport/bindings": "^9.0.8", + "@serialport/parser-byte-length": "^9.0.7", + "@serialport/parser-cctalk": "^9.0.7", + "@serialport/parser-delimiter": "^9.0.7", + "@serialport/parser-inter-byte-timeout": "^9.0.7", + "@serialport/parser-readline": "^9.0.7", + "@serialport/parser-ready": "^9.0.7", + "@serialport/parser-regex": "^9.0.7", + "@serialport/stream": "^9.0.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=10.0.0" + }, + "funding": { + "url": "https://opencollective.com/serialport/donate" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/socket.io": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.1.3.tgz", + "integrity": "sha512-tLkaY13RcO4nIRh1K2hT5iuotfTaIQw7cVIe0FUykN3SuQi0cm7ALxuyT5/CtDswOMWUzMGTibxYNx/gU7In+Q==", + "license": "MIT", + "dependencies": { + "@types/cookie": "^0.4.0", + "@types/cors": "^2.8.10", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "debug": "~4.3.1", + "engine.io": "~5.1.1", + "socket.io-adapter": "~2.3.1", + "socket.io-parser": "~4.0.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.3.3.tgz", + "integrity": "sha512-Qd/iwn3VskrpNO60BeRyCyr8ZWw9CPZyitW4AQwmRZ8zCiyDiL+znRnWX6tDHXnWn1sJrM1+b6Mn6wEDJJ4aYQ==", + "license": "MIT" + }, + "node_modules/socket.io-parser": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.5.tgz", + "integrity": "sha512-sNjbT9dX63nqUFIOv95tTVm6elyIU4RvB1m8dOeZt+IgWwcWklFDOdmGcfo3zSiRsnR/3pJkjY5lfoGqEe4Eig==", + "license": "MIT", + "dependencies": { + "@types/component-emitter": "^1.2.10", + "component-emitter": "~1.3.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/speedtest-net": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/speedtest-net/-/speedtest-net-1.6.2.tgz", + "integrity": "sha512-0rfBDPVLzvDbMUKrFWvF6kvtXxllXGZRkFL0aCWJ5y8uXlEfZDo6IEPACZwJbKt/fGqFjJLJRHnG62vJmLLpRA==", + "license": "MIT", + "dependencies": { + "chalk": "^2.4.1", + "draftlog": "^1.0.12", + "http-proxy-agent": "^2.0.0", + "https-proxy-agent": "^3.0.0", + "xml2js": "^0.4.4" + }, + "bin": { + "speedtest-net": "bin/index.js" + } + }, + "node_modules/speedtest-net/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/speedtest-net/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/speedtest-net/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/speedtest-net/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "license": "MIT" + }, + "node_modules/speedtest-net/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/speedtest-net/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/suncalc": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/suncalc/-/suncalc-1.9.0.tgz", + "integrity": "sha512-vMJ8Byp1uIPoj+wb9c1AdK4jpkSKVAywgHX0lqY7zt6+EWRRC3Z+0Ucfjy/0yxTVO1hwwchZe4uoFNqrIC24+A==" + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar-fs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.2.tgz", + "integrity": "sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/underscore": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.4.4.tgz", + "integrity": "sha512-ZqGrAgaqqZM7LGRzNjLnw5elevWb5M8LEoDMadxIW3OWbcv72wMMgKdwOKpd5Fqxe8choLD8HN3iSj3TUh/giQ==" + }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "7.4.6", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", + "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml2js": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", + "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "license": "ISC", + "engines": { + "node": ">=10" + } + } + } + }, + "invalidationHash": "cda6215f02c82351e6e59e67b7d327b58c11af5d73f3af8ba6778c31ae978aa0" +} \ No newline at end of file diff --git a/metrics/core.js b/metrics/core.js index b08934d..6f10bc6 100644 --- a/metrics/core.js +++ b/metrics/core.js @@ -11,11 +11,11 @@ // ******************************************************************************************** // (C) Felix Rusu, Low Power Lab LLC (2020), http://lowpowerlab.com/contact // ******************************************************************************************** -var config = require('nconf'); -var JSON5 = require('json5'); var suncalc = require('suncalc'); //https://github.com/mourner/suncalc -config.argv().file({ file: require('path').resolve(__dirname, '../settings.json5'), format: JSON5 }); -var settings = config.get('settings'); //these are local to avoid runtime errors but in events they will reference the global settings declared in gateway.js + +//these are local to avoid runtime errors but in events they will reference the +//global settings declared in gateway.js +var settings = require('../config').nconf.get('settings'); // ****************************************************************************************************************************************** // SAMPLE METRICS DEFINITIONS diff --git a/nix/checks.nix b/nix/checks.nix new file mode 100644 index 0000000..5ea3ee1 --- /dev/null +++ b/nix/checks.nix @@ -0,0 +1,30 @@ +{self, ...}: { + perSystem = { + lib, + pkgs, + ... + }: { + checks = { + default = pkgs.nixosTest { + name = "moteino-gateway"; + nodes.default = { + lib, + pkgs, + ... + }: { + imports = [ + self.nixosModules.default + ]; + + services.moteino-gateway.enable = true; + }; + testScript = '' + start_all() + default.wait_for_unit("moteino-gateway.service") + default.wait_for_open_port(8080) + default.wait_until_succeeds("${lib.getExe pkgs.curl} --resolve moteino-gateway:80:127.0.0.1 -fLI http://moteino-gateway/ 1>&2") + ''; + }; + }; + }; +} diff --git a/nix/devshells.nix b/nix/devshells.nix new file mode 100644 index 0000000..6286e88 --- /dev/null +++ b/nix/devshells.nix @@ -0,0 +1,49 @@ +{ + perSystem = { + config, + lib, + pkgs, + ... + }: { + devshells = { + default = { + commands = + [ + { + name = "dream2nix-lock"; + command = '' + cd "$(git rev-parse --show-cdup)" || exit + exec -a "$0" nix run "$@" '.#default.lock' + ''; + help = "Lock dependencies in lock.json"; + } + + { + name = "mkoptdocs"; + command = '' + cd "$(git rev-parse --show-cdup)" || exit + while read -r out_path; do + install -Dm0644 "$out_path" ./doc/nixos-modules.md + done < <(nix build "$@" --no-link --print-out-paths '.#docs') + ''; + help = "Build NixOS module options documentation"; + } + + { + package = config.treefmt.build.wrapper; + } + + { + package = pkgs.act; + } + ] + ++ lib.pipe config.packages [ + (lib.flip builtins.removeAttrs ["default"]) + (builtins.attrValues) + (lib.filter (package: package ? meta.mainProgram)) + (map (package: {inherit package;})) + ]; + }; + }; + }; +} diff --git a/nix/modules/nixos/mightyhat.nix b/nix/modules/nixos/mightyhat.nix new file mode 100644 index 0000000..e93f9a0 --- /dev/null +++ b/nix/modules/nixos/mightyhat.nix @@ -0,0 +1,230 @@ +{ + self, + moduleWithSystem, + ... +}: +moduleWithSystem ( + {config} @ perSystem: { + config, + options, + lib, + pkgs, + utils, + ... + } @ nixos: let + inherit (lib) mkOption types; + cfg = config.profiles.mightyhat; + description = "LowPowerLab Moteino Gateway Raspberry Pi profile"; + mgCfg = config.services.moteino-gateway; + in { + imports = [ + # In order to access the configured serial device so that we can + # determine whether we should set pins 14 and 15 to their `ALT0` + # functions. + self.nixosModules.moteino-gateway + ]; + + options.profiles.mightyhat = { + enable = lib.mkEnableOption "the ${description}"; + + apply-overlays-dtmerge = { + enable = mkOption { + type = types.bool; + default = true; + description = '' + Merge device tree overlays with {command}`dtmerge` from + {package}`libraspberrypi`. + + Additionally, supports merging compiled overlays from + {package}`device-tree_rpi`, even if they specify a `compatible` + stanza that is different than that nominally required by the target + device tree. + ''; + }; + }; + + uboot = { + disableConsole = mkOption { + type = types.bool; + default = false; + description = '' + Apply an overlay that redefines the Raspberry-Pi-specific uboot + packages in order to disable the serial console. + + ::: {.important} + Enabling this option may prevent you from selecting a NixOS boot + generation at the uboot loader screen! + ::: + ''; + }; + }; + + # TODO see if there is a way to work backward from + # `hardware.deviceTree.kernelPackage` to the relevant device + # tree/firmware source. + firmwarePackage = lib.mkPackageOption pkgs "raspberrypifw" { + default = "raspberrypifw"; + pkgsText = "pkgs.raspberrypifw"; + }; + + deviceTreePackage = lib.mkOption { + type = + (types.addCheck types.package (p: types.path.check (p.overlays or null))) + // { + description = "${types.package.description} with an attribute `overlays` representing a path"; + }; + default = pkgs.device-tree_rpi.override { + raspberrypifw = cfg.firmwarePackage; + }; + defaultText = '' + pkgs.device-tree_rpi.override { + raspberrypifw = config.profiles.mightyhat.firmwarePackage; + } + ''; + description = '' + Package providing device tree files for the Raspberry Pi. + + :::{.note} + You could consider using this option as the value of + {option}`hardwire.deviceTree.dtbSource`. + ::: + ''; + }; + + overlays = mkOption { + type = types.path; + default = cfg.deviceTreePackage.overlays; + description = '' + Path containing compiled device tree overlay files. + ''; + }; + + dtbo = mkOption { + type = types.attrsOf (types.submodule ({ + config, + name, + ... + }: { + options = { + name = mkOption { + type = types.str; + default = name; + description = '' + Name of this DTBO. Used as the default basename (minus + extension) of {option}`file`. + ''; + }; + + file = mkOption { + type = types.path; + default = "${cfg.overlays}/${config.name}.dtbo"; + defaultText = "\${config.profiles.mightyhat.overlays}/${config.name}.dtbo"; + description = '' + Path of the DTBO file. + ''; + }; + + enable = mkOption { + type = types.bool; + default = true; + description = '' + Apply this DTBO to the device tree. + ''; + }; + }; + })); + }; + + # Disable bluetooth per + # https://lowpowerlab.com/guide/mightyhat/pi3-compatibility/, or + # put it on the miniuart. + bluetooth = { + disable = mkOption { + type = types.bool; + default = true; + description = '' + Apply the `disable-bt` device tree overlay from + `raspberrypi/firmware`. + ''; + }; + + miniuart = mkOption { + type = types.bool; + default = false; + description = '' + Apply the `miniuart-bt` device tree overlay from + `raspberrypi/firmware`. + ''; + }; + }; + }; + + config = lib.mkIf cfg.enable (lib.mkMerge [ + { + environment.systemPackages = [ + perSystem.config.packages.avrdude-rpi + perSystem.config.packages.ubootToolsWithEnvtools + ]; + + boot.blacklistedKernelModules = lib.optionals cfg.bluetooth.disable [ + "bluetooth" + "btusb" + "btsdio" + ]; + + hardware.bluetooth.enable = !cfg.bluetooth.disable; + + hardware.deviceTree = { + enable = true; + + # FIXME no bluetooth on RPi Zero + #filter = lib.mkDefault "*rpi*.dtb"; + + overlays = lib.pipe cfg.dtbo [ + builtins.attrValues + (lib.filter (dtbo: dtbo.enable)) + (map (dtbo: { + inherit (dtbo) name; + dtboFile = dtbo.file; + })) + ]; + }; + + hardware.enableRedistributableFirmware = lib.mkDefault true; + + profiles.mightyhat.dtbo = { + disable-bt.enable = cfg.bluetooth.disable; + miniuart-bt.enable = cfg.bluetooth.miniuart; + }; + + nixpkgs.overlays = + (lib.optional cfg.apply-overlays-dtmerge.enable self.overlays.device-tree-apply-overlays-dtmerge) + ++ (lib.optional cfg.uboot.disableConsole self.overlays.uboot-disable-console); + + warnings = lib.optional (!cfg.apply-overlays-dtmerge.enable) (lib.concatStringsSep " " [ + "it appears that this configuration is using the default implementation of `deviceTree.applyOverlays`." + "This is problematic on Raspberry Pis." + "Please consider setting `profiles.mightyhat.apply-overlays-dtmerge = true` in your configuration." + "Without this, Bluetooth is likely to remain enabled on the non-mini UART, and `/dev/ttyAMA0` will be unavailable." + ]); + } + + (lib.mkIf (mgCfg.enable && mgCfg.settings.serial.port == "/dev/ttyAMA0") { + systemd.services.moteino-gateway-uart-pin-setup = { + path = [perSystem.config.packages.raspberrypi-utils]; + partOf = ["moteino-gateway.service"]; + requiredBy = ["moteino-gateway.service"]; + before = ["moteino-gateway.service"]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + script = '' + pinctrl set 14 a0 + pinctrl set 15 a0 + ''; + }; + }) + ]); + } +) diff --git a/nix/modules/nixos/moteino-gateway.nix b/nix/modules/nixos/moteino-gateway.nix new file mode 100644 index 0000000..3ab500e --- /dev/null +++ b/nix/modules/nixos/moteino-gateway.nix @@ -0,0 +1,386 @@ +{moduleWithSystem, ...}: +moduleWithSystem ( + {config} @ perSystem: { + config, + options, + lib, + pkgs, + utils, + ... + } @ nixos: let + inherit (lib) mkOption types; + + cfg = config.services.moteino-gateway; + opts = options.services.moteino-gateway; + + jsonFmt = pkgs.formats.json {}; + + description = "LowPowerLab Moteino Gateway"; + in { + options.services.moteino-gateway = { + enable = lib.mkEnableOption "the ${description}"; + + package = mkOption { + type = types.package; + default = perSystem.config.packages.default; + defaultText = "perSystem.config.packages.default"; + description = '' + The package providing the ${description}. + ''; + }; + + nodejs = lib.mkPackageOption pkgs "nodejs" { + default = "nodejs"; + pkgsText = "pkgs.nodejs"; + }; + + contentDir = mkOption { + type = types.path; + default = "/srv/www/moteino-gateway"; + description = '' + Directory for holding static content. At present, used only for + uploaded node icon images. + + ::: {.note} + Because the gateway systemd service uses `DynamicUser=yes`, this + directory has to be outside of `StateDirectory`, `CacheDirectory`, + and so on, so that the nginx webserver can read its contents. + ::: + ''; + }; + + persistLogs = mkOption { + type = types.bool; + default = false; + description = '' + Whether to persist gateway info and error logs to the systemd + service's configured `LogsDirectory` (by default, + `/var/log/moteino-gateway`). + ''; + }; + + metrics = mkOption { + default = {}; + description = '' + JavaScript scripts for exporting node metrics data. + + See https://github.com/LowPowerLab/RaspberryPi-Gateway/blob/master/metrics/examples/_example.js + for an example metrics script definition. + ''; + type = types.attrsOf (types.submodule ({ + name, + config, + options, + ... + }: { + options = { + name = mkOption { + # FIXME remove "/" characters and ensure that the name has + # the extension `.js`. + type = types.str; + default = name; + description = '' + Name of the metrics script. + ''; + }; + + basename = mkOption { + type = types.strMatching "^[^/]+\\.js$"; + internal = true; + readOnly = true; + defaultText = "${name}.js"; + }; + + text = mkOption { + type = types.nullOr types.lines; + default = null; + description = '' + Text of the metrics script. + ''; + }; + + source = mkOption { + type = types.path; + description = '' + Path of the source file containing the metrics script. + ''; + }; + }; + + config = { + basename = let + noSlashes = lib.replaceStrings ["/"] ["__"] config.name; + in + if lib.hasSuffix ".js" noSlashes + then noSlashes + else "${noSlashes}.js"; + + source = lib.mkIf (config.text != null) (lib.mkDerivedConfig options.text (pkgs.writeTextDir "metrics/${config.basename}")); + }; + })); + }; + + settings = mkOption { + default = {}; + description = '' + Settings for the ${description}. Defaults are loaded from + {option}`''${services.moteino-gateway.package}/lib/node_modules/RaspberryPi-Gateway/settings.json5`. + ''; + type = types.submodule { + freeformType = jsonFmt.type; + options = { + general = mkOption { + description = "General settings."; + default = {}; + type = types.submodule { + freeformType = jsonFmt.type; + options = { + port = mkOption { + type = types.port; + default = 8081; + description = '' + Listening port for the web application of the + ${description}. + ''; + }; + + socketPort = mkOption { + type = types.port; + default = 8080; + description = '' + Listening port for the websocket of the ${description}. + ''; + }; + }; + }; + }; + + serial = mkOption { + description = "Serial port settings."; + default = {}; + type = types.submodule { + freeformType = jsonFmt.type; + options = { + port = mkOption { + type = types.path; + default = + if config.profiles.mightyhat.bluetooth.disable + then "/dev/ttyAMA0" + else "/dev/ttyS0"; + defaultText = lib.literalExpression '' + if config.profiles.mightyhat.bluetooth.disable then "/dev/ttyAMA0" else "/dev/ttyS0" + ''; + description = '' + Serial port for the ${description}. + ''; + }; + + baud = mkOption { + type = types.ints.positive; + default = 115200; + description = '' + Needs to match the serial baud speed in the sketch + running on the Moteino or MightyHat, if any, that is + attached to this machine. + ''; + }; + }; + }; + }; + }; + }; + }; + }; + + config = lib.mkIf cfg.enable { + services.nginx = { + enable = lib.mkDefault true; + + virtualHosts.moteino-gateway = {name, ...}: let + unit = nixos.config.systemd.services.${name}; + root = "${cfg.package}/lib/node_modules/RaspberryPi-Gateway/www"; + in { + inherit root; + + extraConfig = '' + index index.html index.htm; + error_page 500 502 503 504 /50x.html; + ''; + + locations = { + "/images/" = { + root = cfg.contentDir; + extraConfig = '' + try_files $uri @fallback; + ''; + }; + + "/socket.io/" = { + proxyWebsockets = true; + recommendedProxySettings = true; + proxyPass = "http://localhost:${toString cfg.settings.general.socketPort}"; + }; + + "/httpendpoint/" = { + proxyWebsockets = true; + recommendedProxySettings = true; + proxyPass = "http://localhost:${toString cfg.settings.general.port}"; + }; + + "/50x.html" = { + extraConfig = '' + root html; + ''; + }; + + "@fallback" = { + inherit root; + }; + }; + }; + }; + + services.logrotate = lib.mkIf cfg.persistLogs { + enable = lib.mkDefault true; + settings = { + moteino-gateway = {name, ...}: let + unit = nixos.config.systemd.services.${name}; + logsDirectories = lib.toList (unit.serviceConfig.LogsDirectory or ["moteino-gateway"]); + logsDirectory = lib.head logsDirectories; + in { + files = [ + "/var/log/${logsDirectory}/*.log" + "/var/log/${logsDirectory}/*.err" + ]; + size = "20M"; + missingok = true; + rotate = 20; + dateext = true; + dateformat = "-%Y-%m-%d"; + compress = true; + notifempty = true; + nocreate = true; + copytruncate = true; + }; + }; + }; + + systemd.services.moteino-gateway = {name, ...}: let + app = "${cfg.package}/lib/node_modules/RaspberryPi-Gateway"; + metrics = pkgs.buildEnv { + name = "${name}-metrics"; + paths = lib.mapAttrsToList (_: metric: "${metric.source}") cfg.metrics; + pathsToLink = ["/metrics"]; + }; + in { + inherit description; + script = let + secretSnippet = lib.pipe cfg.settings [ + (lib.mapAttrsRecursive (_: value: {inherit value;})) + (settings: {inherit settings;}) + (lib.flip utils.genJqSecretsReplacementSnippet "settings.json") + ]; + in '' + export MOTEINO_GATEWAY_STATE_DIRECTORY="''${STATE_DIRECTORY%%:*}" + (cd "$MOTEINO_GATEWAY_STATE_DIRECTORY" && ${secretSnippet}) + ${lib.optionalString cfg.persistLogs '' + logsdir="''${LOGS_DIRECTORY%%:*}" + exec 1>>"''${logsdir}/info.log" + exec 2> >(exec ${pkgs.coreutils}/bin/tee -a "''${logsdir}/error.log" 1>&2) + ''} + exec ${cfg.nodejs}/bin/node ${app}/gateway.js + ''; + after = ["network.target"]; + wantedBy = ["multi-user.target"]; + environment = { + NODE_ENV = "production"; + MOTEINO_GATEWAY_CONTENT_DIRECTORY = cfg.contentDir; + }; + serviceConfig = { + DynamicUser = true; + + SupplementaryGroups = [ + "dialout" # for serial port access + config.services.nginx.group # for static files + ]; + + Restart = "always"; + RestartSec = 3; + + ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID"; + + CacheDirectory = name; + LogsDirectory = lib.mkIf cfg.persistLogs name; + RuntimeDirectory = name; + StateDirectory = name; + + StateDirectoryMode = "0750"; + + RootDirectory = "%t/${name}"; + WorkingDirectory = app; # to load files relative to `__dirname` + + MountAPIVFS = true; + + BindPaths = [ + "-${cfg.settings.serial.port}" + cfg.contentDir + ]; + + BindReadOnlyPaths = [ + builtins.storeDir + "${metrics}/metrics:%S/${name}/metrics" + "/dev/log" + "/run/systemd/journal/socket" + "/run/systemd/journal/stdout" + ]; + + # Uploaded image files need to be readable to nginx, so + # no `0027` here. + UMask = "0022"; + + # Security settings. + CapabilityBoundingSet = [""]; + DeviceAllow = [cfg.settings.serial.port]; + DevicePolicy = "closed"; + LockPersonality = true; + #MemoryDenyWriteExecute = true; # evidently enabling this breaks NodeJS + NoNewPrivileges = true; + PrivateDevices = true; + PrivateIPC = true; + PrivateTmp = true; + ProcSubset = "pid"; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectProc = "invisible"; + ProtectSystem = "strict"; + RemoveIPC = true; + RestrictAddressFamilies = [ + "AF_INET" + "AF_INET6" + "AF_NETLINK" + "AF_UNIX" + ]; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + SystemCallArchitectures = "native"; + # `@pkey` needed; NodeJS calls `pkey_alloc`. + SystemCallFilter = [ + "@system-service @resources @pkey" + "~@privileged" + ]; + }; + }; + + systemd.tmpfiles.rules = [ + "d ${cfg.contentDir} 0770 ${config.services.nginx.user} ${config.services.nginx.group} - -" + "d ${cfg.contentDir}/images 0770 ${config.services.nginx.user} ${config.services.nginx.group} - -" + ]; + }; + } +) diff --git a/nix/nixos-modules.nix b/nix/nixos-modules.nix new file mode 100644 index 0000000..4641c8d --- /dev/null +++ b/nix/nixos-modules.nix @@ -0,0 +1,24 @@ +{ + self, + flake-parts-lib, + lib, + moduleWithSystem, + ... +}: { + flake = { + nixosModules = let + importApply = lib.importApply or flake-parts-lib.importApply; + importModule = src: let + mod = importApply src {inherit self moduleWithSystem;}; + in + { + key = mod._file; + } + // mod; + in { + default = self.nixosModules.moteino-gateway; + mightyhat = importModule ./modules/nixos/mightyhat.nix; + moteino-gateway = importModule ./modules/nixos/moteino-gateway.nix; + }; + }; +} diff --git a/nix/overlays.nix b/nix/overlays.nix new file mode 100644 index 0000000..71c9a36 --- /dev/null +++ b/nix/overlays.nix @@ -0,0 +1,8 @@ +{ + flake = { + overlays = { + device-tree-apply-overlays-dtmerge = import ./overlays/device-tree-apply-overlays-dtmerge.nix; + uboot-disable-console = import ./overlays/uboot-disable-console.nix; + }; + }; +} diff --git a/nix/overlays/device-tree-apply-overlays-dtmerge.nix b/nix/overlays/device-tree-apply-overlays-dtmerge.nix new file mode 100644 index 0000000..aab803b --- /dev/null +++ b/nix/overlays/device-tree-apply-overlays-dtmerge.nix @@ -0,0 +1,93 @@ +# Modification of nixpkgs deviceTree.applyOverlays to resolve https://github.com/NixOS/nixpkgs/issues/125354. +# +# Derived from https://github.com/NixOS/nixpkgs/blob/916ca8f2b0c208def051f8ea9760c534a40309db/pkgs/os-specific/linux/device-tree/default.nix. +# +# This replaces the default device tree overlay application process +# to use `dtmerge` from `libraspberrypi`. The default overlay +# application process can be problematic on Raspberry Pis due to +# (among other things) issues like this: +# +# https://github.com/raspberrypi/firmware/issues/1718 +# +# and `fdtget`-based compatibility checks, which yield false +# negatives (overlay is compatible, but the check says it is not): +# +# $ dtbCompat="$(fdtget -t s ./boot/bcm2710-rpi-3-b-plus.dtb / compatible | tee /dev/stderr)" +# raspberrypi,3-model-b-plus brcm,bcm2837 +# $ overlayCompat="$(fdtget -t s ./boot/overlays/disable-bt.dtbo / compatible | tee /dev/stderr)" +# brcm,bcm2835 +# $ [[ "$dtbCompat" =~ "$overlayCompat" ]] ; echo "$?" +# 1 +# +# The upstream/default merging logic is here: +# +# https://github.com/NixOS/nixpkgs/blob/c999f2e369508fca13646ca5a0fb926b3ce0063e/pkgs/os-specific/linux/device-tree/default.nix#L27-L65 +# +# The `nixos-hardware` override is here: +# +# https://github.com/NixOS/nixos-hardware/blob/33a97b5814d36ddd65ad678ad07ce43b1a67f159/raspberry-pi/4/pkgs-overlays.nix#L5 +final: prev: { + deviceTree = + prev.deviceTree + // { + applyOverlays = final.callPackage ({ + lib, + stdenvNoCC, + dtc, + libraspberrypi, + ... + } @ ctx: base: overlays': + stdenvNoCC.mkDerivation { + name = "device-tree-overlays"; + nativeBuildInputs = [dtc libraspberrypi]; + buildCommand = let + overlays = lib.toList overlays'; + in '' + mkdir -p "$out" + cd ${lib.escapeShellArg base} + find . -type f -name '*.dtb' -exec cp -v --no-preserve=mode --target-directory "$out" --parents '{}' '+' + + while read -d $'\0' dtb; do + dtbName="$(basename "$dtb")" + dtbCompat=$(fdtget -t s "$dtb" / compatible 2>/dev/null || true) + + # skip files without `compatible` string + if [[ -z "$dtbCompat" ]]; then + echo "Cannot apply overlays to ''${dtbName} since it lacks a `compatible` string" + continue + fi + + ${lib.concatMapStringsSep "\n" (o: '' + name=${lib.escapeShellArg o.name} + dtboFile=${lib.escapeShellArg o.dtboFile} + filter=${lib.escapeShellArg o.filter} + + overlayCompat="$(fdtget -t s "$dtboFile" / compatible)" + + # skip incompatible and non-matching overlays + if ! { [[ "$dtbCompat" =~ "$overlayCompat" ]] || [[ "$overlayCompat" = brcm,bcm2835 ]]; }; then + echo "Skipping overlay ''${name}: incompatible with ''${dtbName}: wanted ''${dtbCompat} or generic brcm,bcm2835, got ''${overlayCompat}" + elif [[ -n "$filter" ]] && [[ "''${dtb//''${filter}/}" = "$dtb" ]]; then + echo "Skipping overlay ''${name}: filter does not match ''${dtbName}" + else + echo -n "Applying overlay ''${name} to ''${dtbName}... " + dtbIn="''${dtb}.in" + mv -f "$dtb" "$dtbIn" + + # dtmerge requires a .dtbo ext for dtbo files, otherwise it adds it to the given file implicitly + dtboWithExt="''${TMPDIR}/$(basename "$dtboFile}")" + dtboWithExt="''${dtboWithExt%.dtbo}.dtbo" + cp -r "$dtboFile" "$dtboWithExt" + + dtmerge "$dtbIn" "$dtb" "$dtboWithExt" + + echo "ok" + rm -f "$dtbIn" "$dtboWithExt" + fi + '') + overlays} + done < <(find "$out" -type f -name '*.dtb' -print0) + ''; + }) {}; + }; +} diff --git a/nix/overlays/uboot-disable-console.nix b/nix/overlays/uboot-disable-console.nix new file mode 100644 index 0000000..89c8112 --- /dev/null +++ b/nix/overlays/uboot-disable-console.nix @@ -0,0 +1,56 @@ +let + # https://raspberrypi.stackexchange.com/questions/116074/how-can-i-disable-the-serial-console-on-distributions-that-use-u-boot + # https://github.com/u-boot/u-boot/blob/eac52e4be4e234d563d6911737ee7ccdc0ada1f1/doc/README.autoboot + # https://github.com/u-boot/u-boot/blob/eac52e4be4e234d563d6911737ee7ccdc0ada1f1/doc/README.silent + # > The config option CONFIG_SILENT_CONSOLE can be used to quiet messages + # > on the console. If the option has been enabled, the output can be + # > silenced by setting the environment variable "silent". + # > + # > - CONFIG_SILENT_CONSOLE_UPDATE_ON_SET + # > When the "silent" variable is changed with env set, the change + # > will take effect immediately. + # > + # > - CONFIG_SILENT_CONSOLE_UPDATE_ON_RELOC + # > Some environments are not available until relocation (e.g. NAND) + # > so this will make the value in the flash env take effect at + # > relocation. + # > + # > The following actions are taken if "silent" is set at boot time: + # > + # > - Until the console devices have been initialized, output has to be + # > suppressed by testing for the flag "GD_FLG_SILENT" in "gd->flags". + # > + # > - When the console devices have been initialized, "stdout" and + # > "stderr" are set to "nulldev", so subsequent messages are + # > suppressed automatically. Make sure to enable "nulldev" by + # > enabling CONFIG_SYS_DEVICE_NULLDEV in your board defconfig file. + # > + # > - When booting a linux kernel, the "bootargs" are fixed up so that + # > the argument "console=" will be in the command line, no matter how + # > it was set in "bootargs" before. If you don't want the linux command + # > line to be affected, define CONFIG_SILENT_U_BOOT_ONLY in your board + # > config file as well, and this part of the feature will be disabled. + enableConfigSilentConsole = p: + p.overrideAttrs (oldAttrs: { + extraConfig = + (p.extraConfig or "") + + '' + CONFIG_BOOTDELAY=-2 + CONFIG_SILENT_CONSOLE=y + CONFIG_SILENT_U_BOOT_ONLY=y + CONFIG_SYS_DEVICE_NULLDEV=y + CONFIG_SILENT_CONSOLE_UPDATE_ON_SET=y + ''; + }); +in + final: prev: + prev.lib.mapAttrs (prev.lib.const enableConfigSilentConsole) (prev.lib.getAttrs [ + "ubootRaspberryPi" + "ubootRaspberryPi2" + "ubootRaspberryPi3_32bit" + "ubootRaspberryPi3_64bit" + "ubootRaspberryPi4_32bit" + "ubootRaspberryPi4_64bit" + "ubootRaspberryPiZero" + ] + prev) diff --git a/nix/packages.nix b/nix/packages.nix new file mode 100644 index 0000000..37e5f71 --- /dev/null +++ b/nix/packages.nix @@ -0,0 +1,148 @@ +{ + self, + inputs, + ... +}: { + perSystem = { + config, + pkgs, + system, + ... + }: { + packages = { + default = config.packages.raspberrypi-gateway; + + docs = pkgs.callPackage ({ + nixosOptionsDoc, + lib, + eval, + }: + (nixosOptionsDoc { + options = { + inherit (eval.options.profiles) mightyhat; + inherit (eval.options.services) moteino-gateway; + }; + + # Default is currently "appendix". + documentType = "none"; + + warningsAreErrors = true; + + transformOptions = let + ourPrefix = "${toString self}/"; + moduleSource = "flake.nix"; + link = { + url = "/${moduleSource}"; + name = moduleSource; + }; + in + opt: + opt + // { + visible = opt.visible && (lib.any (lib.hasPrefix ourPrefix) opt.declarations); + declarations = map (decl: + if lib.hasPrefix ourPrefix decl + then link + else decl) + opt.declarations; + }; + }) + .optionsCommonMark) { + eval = self.nixosConfigurations.default.extendModules { + modules = [ + { + nixpkgs.hostPlatform = system; + } + ]; + }; + }; + + raspberrypi-gateway = inputs.dream2nix.lib.evalModules { + packageSets.nixpkgs = pkgs; + modules = let + projectRoot = self; + app = self + "/app.nix"; + in [ + app + { + paths = { + inherit projectRoot; + projectRootFile = "flake.nix"; + package = self; + }; + } + ]; + }; + + avrdude-rpi-autoreset = pkgs.callPackage ({writers}: + writers.makeScriptWriter { + inherit (pkgs.python3.withPackages (p: [p.rpi-gpio])) interpreter; + } "/bin/avrdude-rpi-autoreset" (builtins.readFile "${inputs.avrdude-rpi}/autoreset")) {}; + + # Likely has to be run with `sudo` or another privilege-escalation + # tool. + avrdude-rpi = pkgs.callPackage ({ + avrdude, + strace, + avrdude-rpi-autoreset, + }: + pkgs.writers.writeDashBin "avrdude-rpi" '' + exec ${strace}/bin/strace -o '| ${avrdude-rpi-autoreset}/bin/avrdude-rpi-autoreset' -eioctl ${avrdude}/bin/avrdude "$@" + '') { + inherit (config.packages) avrdude-rpi-autoreset; + }; + + # For (among other things) `pinctrl`, which allows us to work around an + # issue with setting up the UART on `/dev/ttyAMA0` by directly setting + # pins 14 and 15 to their `ALT0` functions (`TXD0` and `RXD0`, + # respectively). + raspberrypi-utils = pkgs.callPackage ({ + cmake, + dtc, + fetchFromGitHub, + stdenv, + lib, + }: let + owner = "raspberrypi"; + repo = "utils"; + version = "71a596c8e62ff458e2760b558fb224bba41b3437"; + in + stdenv.mkDerivation { + inherit version; + + pname = "${owner}-${repo}"; + + src = fetchFromGitHub { + inherit owner repo; + rev = version; + hash = "sha256-7O6xyBsy3SPJKHFLsiDuhSACRfrLoh9szilk0Y9gT1o="; + }; + + nativeBuildInputs = [ + cmake + dtc + ]; + + cmakeFlags = ["-DBUILD_SHARED_LIBS=1"]; + + meta = { + description = "A collection of scripts and simple applications"; + homepage = "https://github.com/${owner}/${repo}"; + license = lib.licenses.bsd3; + }; + }) {}; + + ubootToolsWithEnvtools = pkgs.callPackage ({ubootTools}: + ubootTools.override { + extraMakeFlags = (ubootTools.extraMakeFlags or []) ++ ["envtools"]; + filesToInstall = (ubootTools.filesToInstall or []) ++ ["tools/env/fw_printenv"]; + postInstall = + (ubootTools.postInstall or "") + + '' + ln -sfT $out/bin/fw_printenv $out/bin/fw_setenv + install -D tools/env/fw_env.config $out/share/doc/uboot/tools/env/fw_env.config + ''; + }) {}; + }; + }; +} diff --git a/package.json b/package.json index e656328..32bceda 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "json5": "~2.2.0", "merge": "^2.1.1", "nconf": "~0.11.2", - "nedb": "https://github.com/LowPowerLab/nedb.git", + "nedb": "git+https://github.com/LowPowerLab/nedb.git", "nodemailer": "^6.6.0", "serialport": "~9.0.7", "socket.io": "~4.1.0", diff --git a/settings.json5 b/settings.json5 index e0bde28..d1cec45 100644 --- a/settings.json5 +++ b/settings.json5 @@ -71,6 +71,12 @@ editable: false, description: 'the port at which the gateway.js socket app is listening', }, + port: { + value: 8081, + type: 'number', + editable: false, + description: 'the port at which the web app is listening', + }, genNodeIfNoMatch: { value: 'false', description: 'generate a new node even if the data received does not match any metric definition, default = false', diff --git a/www/images/user/.gitignore b/www/images/user/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/www/images/user/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/www/index.html b/www/index.html index e091927..8780c13 100644 --- a/www/index.html +++ b/www/index.html @@ -2285,35 +2285,33 @@

' + (nodeResolveString(node.label||'', node.metrics) || node._id) + ' ' + re $("#changeNodeIcon_OK").click("tap", function(event) { if ($("#nodeIconUpload").val()) { event.preventDefault(); - var file_data = $('#nodeIconUpload').prop('files')[0]; - var form_data = new FormData(); - form_data.append('file', file_data); - $.ajax({ - type: "POST", - url: "upload.php", - enctype: 'multipart/form-data', - dataType: 'json', - cache: false, - contentType: false, - processData: false, - data: form_data, - success: function (data, textStatus, jqXHR) { - data['status']; - if (data['status']=='success') + + var file = $('#nodeIconUpload').prop('files')[0]; + + var types = ['image/jpeg', 'image/png', 'image/gif']; + if (!types.includes(file.type)) { + alert('Invalid file type; only JPG/JPEG/PNG/GIF are allowed'); + return; + } + + if ((file.size || 0) > 500000) { + alert('File too large (>500K)'); + return; + } + + socket.emit('UPLOADIMAGE', file.name, file, function(filePath, err) { + if (err) { + alert('Upload failed: ' + err.message); + } + else { + if (filePath) { - if (data['filePath']) - { - $('#nodeDetailImage').attr('src', 'images/'+data['filePath']); - nodes[selectedNodeId].icon = data['filePath']; - socket.emit('REFRESHICONS'); - $.mobile.navigate('#nodedetails'); - } - else alert('Expected filePath back, got nothing'); + $('#nodeDetailImage').attr('src', 'images/'+filePath); + nodes[selectedNodeId].icon = filePath; + socket.emit('REFRESHICONS'); + $.mobile.navigate('#nodedetails'); } - else alert(data['message']||'Upload failed'); - }, - error: function (data, textStatus, msg) { - alert(msg||'Upload failed'); + else alert('Expected filePath back, got nothing'); } }); } diff --git a/www/upload.php b/www/upload.php deleted file mode 100644 index b29b92b..0000000 --- a/www/upload.php +++ /dev/null @@ -1,77 +0,0 @@ - 500000) { - $response['message'] .= "File too large (>500K) "; - $uploadOk = 0; - } - - // Allow certain file formats - if($imageFileType != "jpg" && $imageFileType != "png" && $imageFileType != "jpeg" && $imageFileType != "gif" ) { - $response['message'] .= "Only JPG/JPEG/PNG/GIF are allowed "; - $uploadOk = 0; - } - - // Check if $uploadOk is set to 0 by an error - if ($uploadOk == 0) { - $response['status'] = 'error'; - $response['message'] .= "Upload FAILED"; - } else { //all ok, try to upload file - if (move_uploaded_file($_FILES[$postFileId]["tmp_name"], $target_file)) { - $response['message'] = "Upload OK"; - $response['filePath'] = $uploadsDir . basename($target_file); - } else { - $response['status'] = 'error'; - $response['message'] .= "Upload FAILED"; - } - } - - echo json_encode($response); -?> \ No newline at end of file