diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e43b0f9 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.DS_Store diff --git a/trickler/README.md b/trickler/README.md index c6e1035..86518ac 100644 --- a/trickler/README.md +++ b/trickler/README.md @@ -4,7 +4,7 @@ The Open Trickler is described in greater detail [in this article](https://blog. ## Support -Need help? [Join our Discord Server](https://discord.gg/WqTbyK2) to chat with other folks who are building the Open Trickler and helping each other out. +Need help? [Check the FAQ](https://github.com/ammolytics/projects/tree/develop/trickler#frequently-asked-questions) or [join our Discord Server](https://discord.gg/WqTbyK2) to chat with other folks who are building the Open Trickler and helping each other out. This is a free, open-source project which does not come with any official support or warranty. @@ -13,7 +13,7 @@ This is a free, open-source project which does not come with any official suppor The Mobile app for this project uses the [Flutter framework](https://flutter.dev/). The code can be found in the [`mobile/`](https://github.com/ammolytics/projects/blob/develop/trickler/mobile/) directory. -The Controller is a [NodeJS (`v12.x`)](https://nodejs.org/docs/latest-v12.x/api/) application which reads from the scale's serial port and controls the trickler. It was designed to be run on a Raspberry Pi Zero W. The code can be found in the [`peripheral/`](https://github.com/ammolytics/projects/blob/develop/trickler/peripheral/) directory. +The Controller is a [Python (`v3.9`)](https://docs.python.org/3.9/) application which reads from the scale's serial port and controls the trickler. It was designed to be run on a Raspberry Pi Zero W. The code can be found in the [`peripheral/`](https://github.com/ammolytics/projects/blob/develop/trickler/peripheral/) directory. ## Hardware diff --git a/trickler/peripheral/.eslintrc.js b/trickler/peripheral/.eslintrc.js deleted file mode 100644 index 8a201ff..0000000 --- a/trickler/peripheral/.eslintrc.js +++ /dev/null @@ -1,17 +0,0 @@ -module.exports = { - "env": { - "commonjs": true, - "es6": true, - "node": true - }, - "extends": "eslint:recommended", - "globals": { - "Atomics": "readonly", - "SharedArrayBuffer": "readonly" - }, - "parserOptions": { - "ecmaVersion": 11 - }, - "rules": { - } -}; diff --git a/trickler/peripheral/.gitignore b/trickler/peripheral/.gitignore index 028042b..a81c8ee 100644 --- a/trickler/peripheral/.gitignore +++ b/trickler/peripheral/.gitignore @@ -1,90 +1,138 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -lerna-debug.log* - -# Diagnostic reports (https://nodejs.org/api/report.html) -report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json - -# Runtime data -pids -*.pid -*.seed -*.pid.lock +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov +# C extensions +*.so -# Coverage directory used by tools like istanbul -coverage - -# nyc test coverage -.nyc_output +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal -# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) -.grunt +# Flask stuff: +instance/ +.webassets-cache -# Bower dependency directory (https://bower.io/) -bower_components +# Scrapy stuff: +.scrapy -# node-waf configuration -.lock-wscript +# Sphinx documentation +docs/_build/ -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release +# PyBuilder +.pybuilder/ +target/ -# Dependency directories -node_modules/ -jspm_packages/ +# Jupyter Notebook +.ipynb_checkpoints -# TypeScript v1 declaration files -typings/ +# IPython +profile_default/ +ipython_config.py -# Optional npm cache directory -.npm +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version -# Optional eslint cache -.eslintcache +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock -# Optional REPL history -.node_repl_history +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ -# Output of 'npm pack' -*.tgz +# Celery stuff +celerybeat-schedule +celerybeat.pid -# Yarn Integrity file -.yarn-integrity +# SageMath parsed files +*.sage.py -# dotenv environment variables file +# Environments .env -.env.test - -# parcel-bundler cache (https://parceljs.org/) -.cache +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ -# next.js build output -.next +# Spyder project settings +.spyderproject +.spyproject -# nuxt.js build output -.nuxt +# Rope project settings +.ropeproject -# vuepress build output -.vuepress/dist +# mkdocs documentation +/site -# Serverless directories -.serverless/ +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json -# FuseBox cache -.fusebox/ +# Pyre type checker +.pyre/ -# DynamoDB Local files -.dynamodb/ +# pytype static type analyzer +.pytype/ -# NodeEnv -.venv/ - -# Build -build/ +# Cython debug symbols +cython_debug/ diff --git a/trickler/peripheral/.node-version b/trickler/peripheral/.node-version deleted file mode 100644 index dfd39f4..0000000 --- a/trickler/peripheral/.node-version +++ /dev/null @@ -1 +0,0 @@ -12.17.0 diff --git a/trickler/peripheral/.pylintrc b/trickler/peripheral/.pylintrc new file mode 100644 index 0000000..8b9f385 --- /dev/null +++ b/trickler/peripheral/.pylintrc @@ -0,0 +1,590 @@ +[MASTER] + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. +extension-pkg-whitelist= + +# Specify a score threshold to be exceeded before program exits with error. +fail-under=10.0 + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=CVS + +# Add files or directories matching the regex patterns to the blacklist. The +# regex matches against base names, not paths. +ignore-patterns= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the +# number of processors available to use. +jobs=1 + +# Control the amount of potential inferred values when inferring a single +# object. This can help the performance when dealing with large functions or +# complex, nested conditions. +limit-inference-results=120 + +# List of plugins (as comma separated values of python module names) to load, +# usually to register additional checkers. +load-plugins= + +# Pickle collected data for later comparisons. +persistent=yes + +# When enabled, pylint would attempt to guess common misconfiguration and emit +# user-friendly hints instead of false-positive error messages. +suggestion-mode=yes + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. +confidence= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once). You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use "--disable=all --enable=classes +# --disable=W". +disable=print-statement, + parameter-unpacking, + unpacking-in-except, + old-raise-syntax, + backtick, + long-suffix, + old-ne-operator, + old-octal-literal, + import-star-module-level, + non-ascii-bytes-literal, + raw-checker-failed, + bad-inline-option, + locally-disabled, + file-ignored, + suppressed-message, + useless-suppression, + deprecated-pragma, + use-symbolic-message-instead, + apply-builtin, + basestring-builtin, + buffer-builtin, + cmp-builtin, + coerce-builtin, + execfile-builtin, + file-builtin, + long-builtin, + raw_input-builtin, + reduce-builtin, + standarderror-builtin, + unicode-builtin, + xrange-builtin, + coerce-method, + delslice-method, + getslice-method, + setslice-method, + no-absolute-import, + old-division, + dict-iter-method, + dict-view-method, + next-method-called, + metaclass-assignment, + indexing-exception, + raising-string, + reload-builtin, + oct-method, + hex-method, + nonzero-method, + cmp-method, + input-builtin, + round-builtin, + intern-builtin, + unichr-builtin, + map-builtin-not-iterating, + zip-builtin-not-iterating, + range-builtin-not-iterating, + filter-builtin-not-iterating, + using-cmp-argument, + eq-without-hash, + div-method, + idiv-method, + rdiv-method, + exception-message-attribute, + invalid-str-codec, + sys-max-int, + bad-python3-import, + deprecated-string-function, + deprecated-str-translate-call, + deprecated-itertools-function, + deprecated-types-field, + next-method-defined, + dict-items-not-iterating, + dict-keys-not-iterating, + dict-values-not-iterating, + deprecated-operator-function, + deprecated-urllib-function, + xreadlines-attribute, + deprecated-sys-function, + exception-escape, + comprehension-escape, + redefined-outer-name, + unused-argument + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable=c-extension-no-member + + +[REPORTS] + +# Python expression which should return a score less than or equal to 10. You +# have access to the variables 'error', 'warning', 'refactor', and 'convention' +# which contain the number of messages in each category, as well as 'statement' +# which is the total number of statements analyzed. This score is used by the +# global evaluation report (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details. +#msg-template= + +# Set the output format. Available formats are text, parseable, colorized, json +# and msvs (visual studio). You can also give a reporter class, e.g. +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Tells whether to display a full report or only the messages. +reports=no + +# Activate the evaluation score. +score=yes + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + +# Complete name of functions that never returns. When checking for +# inconsistent-return-statements if a never returning function is called then +# it will be considered as an explicit return statement and no message will be +# printed. +never-returning-functions=sys.exit + + +[LOGGING] + +# The type of string formatting that logging methods do. `old` means using % +# formatting, `new` is for `{}` formatting. +logging-format-style=old + +# Logging modules to check that the string format arguments are in logging +# function parameter format. +logging-modules=logging + + +[SPELLING] + +# Limits count of emitted suggestions for spelling mistakes. +max-spelling-suggestions=4 + +# Spelling dictionary name. Available dictionaries: none. To make it work, +# install the python-enchant package. +spelling-dict= + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains the private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to the private dictionary (see the +# --spelling-private-dict-file option) instead of raising a message. +spelling-store-unknown-words=no + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME, + XXX, + TODO + +# Regular expression of note tags to take in consideration. +#notes-rgx= + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# Tells whether to warn about missing members when the owner of the attribute +# is inferred to be None. +ignore-none=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis). It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + +# List of decorators that change the signature of a decorated function. +signature-mutators= + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid defining new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_, + _cb + +# A regular expression matching the name of dummy variables (i.e. expected to +# not be used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore. +ignored-argument-names=_.*|^ignored_|^unused_ + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. +max-line-length=120 + +# Maximum number of lines in a module. +max-module-lines=1000 + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + + +[SIMILARITIES] + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + +# Minimum lines number of a similarity. +min-similarity-lines=4 + + +[BASIC] + +# Naming style matching correct argument names. +argument-naming-style=snake_case + +# Regular expression matching correct argument names. Overrides argument- +# naming-style. +#argument-rgx= + +# Naming style matching correct attribute names. +attr-naming-style=snake_case + +# Regular expression matching correct attribute names. Overrides attr-naming- +# style. +#attr-rgx= + +# Bad variable names which should always be refused, separated by a comma. +bad-names=foo, + bar, + baz, + toto, + tutu, + tata + +# Bad variable names regexes, separated by a comma. If names match any regex, +# they will always be refused +bad-names-rgxs= + +# Naming style matching correct class attribute names. +class-attribute-naming-style=any + +# Regular expression matching correct class attribute names. Overrides class- +# attribute-naming-style. +#class-attribute-rgx= + +# Naming style matching correct class names. +class-naming-style=PascalCase + +# Regular expression matching correct class names. Overrides class-naming- +# style. +#class-rgx= + +# Naming style matching correct constant names. +const-naming-style=UPPER_CASE + +# Regular expression matching correct constant names. Overrides const-naming- +# style. +#const-rgx= + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + +# Naming style matching correct function names. +function-naming-style=snake_case + +# Regular expression matching correct function names. Overrides function- +# naming-style. +#function-rgx= + +# Good variable names which should always be accepted, separated by a comma. +good-names=i, + j, + k, + ex, + Run, + _ + +# Good variable names regexes, separated by a comma. If names match any regex, +# they will always be accepted +good-names-rgxs= + +# Include a hint for the correct naming format with invalid-name. +include-naming-hint=no + +# Naming style matching correct inline iteration names. +inlinevar-naming-style=any + +# Regular expression matching correct inline iteration names. Overrides +# inlinevar-naming-style. +#inlinevar-rgx= + +# Naming style matching correct method names. +method-naming-style=snake_case + +# Regular expression matching correct method names. Overrides method-naming- +# style. +#method-rgx= + +# Naming style matching correct module names. +module-naming-style=snake_case + +# Regular expression matching correct module names. Overrides module-naming- +# style. +#module-rgx= + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +# These decorators are taken in consideration only for invalid-name. +property-classes=abc.abstractproperty + +# Naming style matching correct variable names. +variable-naming-style=snake_case + +# Regular expression matching correct variable names. Overrides variable- +# naming-style. +#variable-rgx= + + +[STRING] + +# This flag controls whether inconsistent-quotes generates a warning when the +# character used as a quote delimiter is used inconsistently within a module. +check-quote-consistency=no + +# This flag controls whether the implicit-str-concat should generate a warning +# on implicit string concatenation in sequences defined over several lines. +check-str-concat-over-line-jumps=no + + +[IMPORTS] + +# List of modules that can be imported at any level, not just the top level +# one. +allow-any-import-level= + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Deprecated modules which should not be used, separated by a comma. +deprecated-modules=optparse,tkinter.tix + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled). +ext-import-graph= + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled). +import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled). +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + +# Couples of modules and preferred modules, separated by a comma. +preferred-modules= + + +[CLASSES] + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp, + __post_init__ + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict, + _fields, + _replace, + _source, + _make + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=cls + + +[DESIGN] + +# Maximum number of arguments for function / method. +max-args=5 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Maximum number of boolean expressions in an if statement (see R0916). +max-bool-expr=5 + +# Maximum number of branch for function / method body. +max-branches=12 + +# Maximum number of locals for function / method body. +max-locals=15 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of return / yield for function / method body. +max-returns=6 + +# Maximum number of statements in function / method body. +max-statements=50 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "BaseException, Exception". +overgeneral-exceptions=BaseException, + Exception diff --git a/trickler/peripheral/MANIFEST.in b/trickler/peripheral/MANIFEST.in new file mode 100644 index 0000000..6333360 --- /dev/null +++ b/trickler/peripheral/MANIFEST.in @@ -0,0 +1,9 @@ +include opentrickler_config.ini +include opentrickler_api.yaml +include bluetooth.sh +include leds.sh +include server.sh +include requirements-to-freeze.txt +include setup.py +include LICENSE +include *.md diff --git a/trickler/peripheral/README.md b/trickler/peripheral/README.md index f7e720c..dba77d5 100644 --- a/trickler/peripheral/README.md +++ b/trickler/peripheral/README.md @@ -1,19 +1,36 @@ # Ammolytics: Open Trickler Controller + +**Now in Python!** +The system was rewritten from scratch. It now supports PID controls, PWM motor control, among other things. [See PR 51 for more info!](https://github.com/ammolytics/projects/pull/51) This portion of the Open Trickler is used to control the scale and trickler motor. It's designed to be run on a Raspberry Pi Zero W, but any similar system which supports Bluetooth may work, but I have not tested them. -# Instructions -1. Download the latest SD card image: - [`opentrickler-20200528-PROD.img.xz`](https://drive.google.com/open?id=18Q4nF9Ur_vaZgMkSsSwSa-qUwd2g9Qhx) - sha256: `469273bf4de0b583aaac2f4a3f7f4382b1884c97cd167113582f8ff267ab235e` -2. Flash the image to your microSD card. +## Support + +Need help? [Check the FAQ](https://github.com/ammolytics/projects/tree/develop/trickler#frequently-asked-questions) or [join our Discord Server](https://discord.gg/WqTbyK2) to chat with other folks who are building the Open Trickler and helping each other out. + +This is a free, open-source project which does not come with any official support or warranty. + + +## Installing Latest Firmware + +1. Download the latest firmware image: [`opentrickler-python-20210814-PROD.img.xz`](https://drive.google.com/file/d/1Fe7pqHpg_yUvkC7nm8q0EPcZy5OtW1Yc/view?usp=sharing) + `SHA256 (opentrickler-python-20210814-PROD.img.xz) = 649b047b9b2ab5906686382008afecd590170a1f62cf16247d34621b7d583025` +1. Flash your microSD card using [balenaEtcher](https://www.balena.io/etcher/) I **highly** recommend the free [balenaEtcher program](https://www.balena.io/etcher/) for this step as it's much smarter and less error/mistake prone. -3. Add your WiFi network details to the `wpa_supplicant.conf` file on the `BOOT` partition. +1. Open the `BOOT` partition (shows up like a USB-drive when plugged into your computer) on the microSD card. Edit the `wpa_supplicant.conf` file with your WiFi settings. Optional, but recommended since it provides more debugging capabilities through your browser at [http://opentrickler.local](http://opentrickler.local). -4. Insert the microSD card into your Raspberry Pi Zero W. -5. Turn on your scale. -6. Boot up your Open Trickler! + See here for more help: https://www.raspberrypi.org/documentation/configuration/wireless/headless.md +1. Open the `CODE` parition on the microSD card. Edit the `opentrickler_config.ini` file to modify the Open Trickler settings. Defaults should work for most people. +1. Plug the microSD card into your Pi. +1. Turn on your scale. +1. Turn on your Pi to boot the Open Trickler system. +1. The onboard LED should "pulse" once it's booted up and ready! +1. Connect with your mobile app. + +Please share any feedback/results on [Discord](https://discord.gg/WqTbyK2) or in a [GitHub issue](https://github.com/ammolytics/projects/issues). + ## Debugging @@ -21,30 +38,53 @@ Once your Open Trickler has booted up, you can visit [http://opentrickler.local] **Note:** Accessing the opentrickler.local website requires putting your Open Trickler onto your wireless network, as described in Step 3 of the Instructions. + ## Customizing Your Open Trickler -To make it easier for anyone to customize and tinker with the software code on their Open Trickler, a `CODE` partition has been added which will appear after you flash your SD card using one of the images listed in this document. The `CODE` partition contains the same JavaScript code you see in this GitHub repository. If you change the code on your SD card, your Open Trickler will run it -- simple! +To make it easier for anyone to customize and tinker with the software code on their Open Trickler, a `CODE` partition has been added which will appear after you flash your SD card using one of the images listed in this document. The `CODE` partition contains the same Python code you see in this GitHub repository. If you change the code on your SD card, your Open Trickler will run it -- simple! + ## For Developers The development SD card image described below provides SSH access to the Raspberry Pi, which is useful for development and advanced debugging. +The main difference is that SSH access is enabled. Don't use this unless you are familiar with the Linux command line. +All firmware images are generated using [buildroot](https://buildroot.org/) and **do not have the same utilities available** as [Raspberry Pi OS](https://www.raspberrypi.org/software/). 1. Clone this repository. -2. Download the latest **development** SD card image: - [`opentrickler-20200528-DEV.img.xz`](https://drive.google.com/open?id=16DJdv4G5Ovct19GoDcUUK3jPjrD0Cf5D) - sha256: `1448eaea651111f944ac7dc19ebe01aa37128de7f448af99fa60cb8d4cf23ab7` -3. Follow the regular instructions above. -4. You can SSH to your running Open Trickler with the following command: + I highly recommend making changes on your computer then copying them to the microSD card. +1. Download the latest **development** firmware image: + [`opentrickler-python-20210814-DEV.img.xz`]()https://drive.google.com/file/d/1q7YvOHOx1h7B_rL1UD9nudy-sgMhuCzV/view?usp=sharing + `SHA256 (opentrickler-python-20210814-DEV.img.xz) = 1b8dbdbcfb76c9c6706ebfebfc981e6238a6107c24640619218087e01b0a277e` +1. Follow the regular instructions above to flash the image. +1. You can log into your running Open Trickler over SSH with the following info: `ssh root@opentrickler.local` (p: `ammolytics`) -### Notes +## Developer Setup Instructions + +If you're going to write and test code on your computer, these steps will help you to set up the dependencies. + +1. Install memcached + ```sudo apt install memcached``` +1. Pull github branch +1. Create virtual environment + ```python3 -m venv .venv``` +1. Activate virtual environment + ```source .venv/bin/activate``` +1. Install dependencies + ```pip install -r requirements-to-freeze.txt``` -- Most of the development was done on Apple and Linux machines, so the code and environment reflects that. -- I use [`nodenv`](https://github.com/nodenv/nodenv) to maintain several different versions of NodeJS on my machine and I recommmend it for this project. -- If you don't have an A&D scale, I created a very simple mock to fill in some of those gaps. You can a server that uses it with the following command: - `./bin/mock.sh` +# References -## References -- [Arduino Lesson 13: DC Motors](https://learn.adafruit.com/adafruit-arduino-lesson-13-dc-motors?view=all) +- https://github.com/Adam-Langley/pybleno +- https://onion.io/2bt-pid-control-python/ +- https://github.com/ivmech/ivPID/blob/master/PID.py +- http://cgkit.sourceforge.net/doc2/pidcontroller.html +- https://github.com/yamins81/cgkit/blob/6ec3f9b32c0330057d3c2c0bfcba573dac267aac/cgkit/pidcontroller.py +- https://gpiozero.readthedocs.io/en/stable/api_output.html#gpiozero.PWMOutputDevice +- https://pythonhosted.org/pyserial/shortintro.html +- https://realpython.com/python-memcache-efficient-caching/ +- https://github.com/pinterest/pymemcache +- https://pymemcache.readthedocs.io/en/latest/ +- https://learn.adafruit.com/adafruit-arduino-lesson-13-dc-motors?view=all diff --git a/trickler/peripheral/VERSION b/trickler/peripheral/VERSION index 26aaba0..d72f262 100644 --- a/trickler/peripheral/VERSION +++ b/trickler/peripheral/VERSION @@ -1 +1 @@ -1.2.0 +2.0.0-dev diff --git a/trickler/peripheral/bin/clean.sh b/trickler/peripheral/bin/clean.sh index 1f04c10..fd5ead1 100755 --- a/trickler/peripheral/bin/clean.sh +++ b/trickler/peripheral/bin/clean.sh @@ -1,7 +1,3 @@ #!/bin/bash -rm -rf build .venv node_modules - -if [[ "$OSTYPE" == "linux-gnu"* ]]; then - sudo rm /usr/bin/node /usr/bin/npm /usr/bin/npx; -fi +rm -rf .venv diff --git a/trickler/peripheral/bin/lint.sh b/trickler/peripheral/bin/lint.sh new file mode 100755 index 0000000..6d52267 --- /dev/null +++ b/trickler/peripheral/bin/lint.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +source .venv/bin/activate + +pylint -E trickler/*.py diff --git a/trickler/peripheral/bin/mock.sh b/trickler/peripheral/bin/mock.sh index b9ee1a7..72a5cea 100755 --- a/trickler/peripheral/bin/mock.sh +++ b/trickler/peripheral/bin/mock.sh @@ -5,4 +5,4 @@ if [[ "$OSTYPE" != "linux-gnu"* ]]; then source .venv/bin/activate; fi -MOCK=1 node index.js /dev/ttyUSB0 +echo "Sorry, this no longer works." diff --git a/trickler/peripheral/bin/package.sh b/trickler/peripheral/bin/package.sh index 5f19a1a..1dc2736 100755 --- a/trickler/peripheral/bin/package.sh +++ b/trickler/peripheral/bin/package.sh @@ -8,12 +8,15 @@ tar --create --transform "$sed_expr" --file $out \ LICENSE \ VERSION \ README.md \ - .eslintrc.js \ - ecosystem.config.js \ - index.js \ - package.json \ - server.js \ + MANIFEST.in \ + .pylintrc \ + setup.cfg \ + setup.py \ + opentrickler_config.ini \ + opentrickler_api.yaml \ + bluetooth.sh \ + leds.sh \ server.sh \ - lib/ + trickler/ xz -c $out > $out.xz diff --git a/trickler/peripheral/bin/setup.sh b/trickler/peripheral/bin/setup.sh index 3223771..1c3dbe1 100755 --- a/trickler/peripheral/bin/setup.sh +++ b/trickler/peripheral/bin/setup.sh @@ -1,41 +1,22 @@ #!/bin/bash -NODE_VER='8.16.0' if [[ "$OSTYPE" == "darwin"* ]]; then # OSX echo "OSX pre-install"; - xcode-select --install; - nodeenv -n $NODE_VER .venv; - source .venv/bin/activate; elif [[ "$OSTYPE" == "linux-gnu"* ]]; then # Linux echo "Linux pre-install"; - if ! [ -x "$(command -v node)" ]; then - echo "Installing Node."; - VERS="node-v$NODE_VER-linux-$(uname -m)" - wget https://nodejs.org/dist/v$NODE_VER/$VERS.tar.xz; - sudo mkdir -p /usr/local/lib/nodejs; - sudo tar -xJvf $VERS.tar.xz -C /usr/local/lib/nodejs; - rm $VERS.tar.xz; - sudo ln -fs /usr/local/lib/nodejs/$VERS/bin/node /usr/bin/node; - sudo ln -fs /usr/local/lib/nodejs/$VERS/bin/npm /usr/bin/npm; - sudo ln -fs /usr/local/lib/nodejs/$VERS/bin/npx /usr/bin/npx; - fi fi - -sudo npm install -g npm@latest; -sudo npm install -g prebuild; -sudo npm install -g node-gyp; -npm install; +python3 -m venv .venv +source .venv/bin/activate +pip install -U pip +pip install wheel +pip install -r requirements-to-freeze.txt if [[ "$OSTYPE" == "linux-gnu"* ]]; then # Linux echo "Linux setup continued..."; - npm install --no-save bluetooth-hci-socket@npm:@abandonware/bluetooth-hci-socket; - sudo npm install -g pm2@latest; - sudo ln -fs /usr/local/lib/nodejs/$VERS/lib/node_modules/pm2/bin/pm2 /usr/bin/pm2; - sudo setcap cap_net_raw+eip $(eval readlink -f `which node`); fi diff --git a/trickler/peripheral/bin/utils.sh b/trickler/peripheral/bin/utils.sh index 562259d..5dfad39 100644 --- a/trickler/peripheral/bin/utils.sh +++ b/trickler/peripheral/bin/utils.sh @@ -7,7 +7,6 @@ upload_files() { --progress \ --exclude=".git/" \ --exclude=".venv/" \ - --exclude="node_modules/" \ --delete \ . -e ssh $USER@$HOST:projects/trickler/peripheral echo "Done." diff --git a/trickler/peripheral/bluetooth.sh b/trickler/peripheral/bluetooth.sh new file mode 100755 index 0000000..642ad69 --- /dev/null +++ b/trickler/peripheral/bluetooth.sh @@ -0,0 +1,4 @@ +#!/bin/sh + +cd /code/opentrickler +exec ./trickler/ble_v1.py opentrickler_config.ini >> "$1" 2>&1 diff --git a/trickler/peripheral/ecosystem.config.js b/trickler/peripheral/ecosystem.config.js deleted file mode 100644 index 85e0201..0000000 --- a/trickler/peripheral/ecosystem.config.js +++ /dev/null @@ -1,28 +0,0 @@ -module.exports = { - apps : [{ - name: 'opentrickler', - script: './index.js', - cwd: '/home/pi/projects/trickler/peripheral/', - args: '/dev/ttyUSB0', - instances: 1, - watch: false, - log_date_format: 'YYYY-MM-DD HH:mm:ss.SSS Z', - env: { - 'NODE_ENV': 'development', - // NOTE(eric): Pin 15 was originally used but pin 12 is now recommended. - // 15 for fake PWM. - //'MOTOR_PIN': 15, - // 12 for real PWM. - 'MOTOR_PIN': 12, - 'SCALE_BAUD_RATE': 19200, - 'SCALE_DEVICE_PATH': '/dev/ttyUSB0', - 'DEVICE_NAME': 'Trickler', - 'BLENO_DEVICE_NAME': 'Trickler', - 'BLENO_ADVERTISING_INTERVAL': 50, - //'DEBUG': 'bleno', - }, - env_production : { - 'NODE_ENV': 'production', - } - }] -} diff --git a/trickler/peripheral/index.js b/trickler/peripheral/index.js deleted file mode 100644 index 0b5b7bc..0000000 --- a/trickler/peripheral/index.js +++ /dev/null @@ -1 +0,0 @@ -require('./lib/index.js') diff --git a/trickler/peripheral/leds.sh b/trickler/peripheral/leds.sh new file mode 100755 index 0000000..cb10d03 --- /dev/null +++ b/trickler/peripheral/leds.sh @@ -0,0 +1,11 @@ +#!/bin/sh + +# Disable the usual trigger for built-in activity LED. +echo none | sudo tee /sys/class/leds/led0/trigger + +# Enter code directory and start the trickler LED daemon. +cd /code/opentrickler +exec ./trickler/leds.py opentrickler_config.ini >> "$1" 2>&1 + +# Revert activity LED to original trigger. +#echo mmc0 | sudo tee /sys/class/leds/led0/trigger diff --git a/trickler/peripheral/lib/and-fxfz.js b/trickler/peripheral/lib/and-fxfz.js deleted file mode 100644 index 6e1c826..0000000 --- a/trickler/peripheral/lib/and-fxfz.js +++ /dev/null @@ -1,312 +0,0 @@ -/** - * Copyright (c) Ammolytics and contributors. All rights reserved. - * Released under the MIT license. See LICENSE file in the project root for details. - */ -const events = require('events') - -const SerialPort = require('serialport') -const Readline = require('@serialport/parser-readline') - - -const UNITS = { - GRAINS: 0, - GRAMS: 1, -} - -const UNIT_PRECISION = { - [UNITS.GRAINS]: 0.02, - [UNITS.GRAMS]: 0.001, - GRAINS: 0.02, - GRAMS: 0.001, -} - -const STATUS = { - STABLE: 0, - UNSTABLE: 1, - OVERLOAD: 2, - ERROR: 3, - MODEL_NUMBER: 4, - SERIAL_NUMBER: 5, - ACKNOWLEDGE: 6, -} - -const UNIT_MAP = { - 'GN': UNITS.GRAINS, - 'g': UNITS.GRAMS, -} - -const STATUS_MAP = { - 'ST': STATUS.STABLE, - // Counting mode - 'QT': STATUS.STABLE, - 'US': STATUS.UNSTABLE, - 'OL': STATUS.OVERLOAD, - 'EC': STATUS.ERROR, - 'AK': STATUS.ACKNOWLEDGE, - 'TN': STATUS.MODEL_NUMBER, - 'SN': STATUS.SERIAL_NUMBER, -} - -const ERROR_CODES = { - 'E00': 'Communications error', - 'E01': 'Undefined command error', - 'E02': 'Not ready', - 'E03': 'Timeout error', - 'E04': 'Excess characters error', - 'E06': 'Format error', - 'E07': 'Parameter setting error', - 'E11': 'Stability error', - 'E17': 'Internal mass error', - 'E20': 'Calibration weight error: The calibration weight is too heavy', - 'E21': 'Calibration weight error: The calibration weight is too light', -} - -const COMMAND_MAP = { - ID: '?ID\r\n', - SERIAL_NUMBER: '?SN\r\n', - MODEL_NUMBER: '?TN\r\n', - TARE_WEIGHT: '?PT\r\n', - CAL_BTN: 'CAL\r\n', - OFF_BTN: 'OFF\r\n', - ON_BTN: 'ON\r\n', - ONOFF_BTN: 'P\r\n', - PRINT_BTN: 'PRT\r\n', - REZERO_BTN: 'R\r\n', - SAMPLE_BTN: 'SMP\r\n', - MODE_BTN: 'U\r\n', -} - - -class Scale extends events.EventEmitter { - constructor (opts) { - super() - opts = (typeof opts === 'object') ? opts : {} - this.BAUD = Number(opts.baud) || 19200 - this.DEV_PATH = opts.device || '/dev/ttyUSB0' - this.encoding = 'ascii' - this.delimiter = '\r\n' - - this._ready = false - this._weight = null - this._unit = null - this._serial = '' - this._model = '' - this._status = null - this.lastStable = new Date() - - this.parser = new Readline({ - delimiter: this.delimiter, - encoding: this.encoding - }) - - // Create mock binding if env MOCK is set. - if (process.env.MOCK) { - console.log('Using Mock interface') - const MockScale = require('./mock-scale') - SerialPort.Binding = MockScale - // Create a port and enable the echo and recording. - MockScale.createPort(this.DEV_PATH, { echo: true, record: true }) - } - - var portOpts = { - autoOpen: false, - baudRate: this.BAUD, - } - - this.port = new SerialPort(this.DEV_PATH, portOpts, this._error) - - this.port.on('error', err => { - console.error(`Serial port error: ${err.message}`) - this.emit('error', err) - }) - this.port.on('open', () => { - console.log(`Serial port open`) - this.emit('open') - - // Listen to weight, unit, and stable to determine when ready. - this.once('weight', () => { this.isReady() }) - this.once('unit', () => { this.isReady() }) - this.once('status', () => { this.isReady() }) - }) - this.port.on('close', () => { - console.log(`Serial port close`) - this.emit('close') - }) - - this.port.pipe(this.parser) - this.parser.on('data', line => { this.lineParser(line) }) - } - - // Error handler/logger. - _error (err) { - if (err) { - console.error(err) - } - } - - open (cb) { - this.port.open(cb) - } - - close (cb) { - this.port.close(cb) - } - - isReady() { - if (typeof this.weight === 'number' && - typeof this.unit === 'number' && - this.stable === true && - this.stableTime > 0) { - console.log('Scale is ready!') - this.ready = true - } - console.log(`weight: ${this.weight}, unit: ${this.unit}, status: ${this.status}, stable: ${this.stable}, stableTime: ${this.stableTime}`) - } - - get ready () { - return this._ready - } - - set ready (value) { - if (typeof value === 'boolean' && this._ready !== value) { - this._ready = value - // Emit the ready signal. - this.emit('ready', value) - } - } - - set weight (value) { - value = Number(value) - if (this._weight !== value) { - this._weight = value - this.emit('weight', this._weight) - } - } - - get weight () { - return this._weight - } - - get stable () { - return this.status === STATUS.STABLE - } - - get stableTime () { - if (this.stable === false) { - return 0 - } - return new Date() - this.lastStable - } - - // Get the current weight in "ticks", or number of units of precision. - get weightTicks () { - return this.weight / UNIT_PRECISION[this.unit] - } - - get status () { - return this._status - } - - set status (value) { - if (this._status !== value) { - this._status = value - this.lastStable = this.stable ? new Date() : this.lastStable - this.emit('status', this._status) - } - } - - set unit (value) { - if (this._unit !== value) { - this._unit = value - this.emit('unit', this._unit) - } - } - - get unit () { - return this._unit - } - - set model (value) { - this._model = value - this.emit('model', this._model) - } - - get model () { - return this._model - } - - set serial (value) { - this._serial = value - this.emit('serial', this._serial) - } - - get serial () { - return this._serial - } - - lineParser (rawLine) { - var now = new Date() - var line = rawLine.trim() - var statusStr = line.substr(0, 2) - - this.emit('data', line) - - if (process.env.DEBUG) { - console.log(`${now} ${line}`) - } - - switch (STATUS_MAP[statusStr]) { - case STATUS.ACKNOWLEDGE: - console.log('Command acknowledged') - break - case STATUS.ERROR: - var errCode = line.substr(3, 3) - var errMsg = ERROR_CODES[errCode] - this._error(`Error code: ${errCode}, message: ${errMsg}, line: ${line}`) - break - case STATUS.MODEL_NUMBER: - this.model = line.substr(3) - break - case STATUS.SERIAL_NUMBER: - this.serial = line.substr(3) - break - case STATUS.STABLE: - case STATUS.UNSTABLE: - var rawWeight = line.substr(3, 9) - var rawUnit = line.substr(12, 3).trim() - this.weight = rawWeight - this.unit = UNIT_MAP[rawUnit] - this.status = STATUS_MAP[statusStr] - break - default: - console.log(`UNHANDLED MESSAGE: ${line}`) - break - } - } - - getModelNumber () { - console.log('WRITE: Requesting model number...') - this.port.write(COMMAND_MAP.MODEL_NUMBER, this.encoding, this._error) - } - - getSerialNumber () { - console.log('WRITE: Requesting serial number...') - this.port.write(COMMAND_MAP.SERIAL_NUMBER, this.encoding, this._error) - } - - pressMode () { - console.log('WRITE: Pressing Mode button to change unit...') - this.port.write(COMMAND_MAP.MODE_BTN, this.encoding, this._error) - } - - reZero () { - console.log('WRITE: Pressing ReZero button...') - this.port.write(COMMAND_MAP.REZERO_BTN, this.encoding, this._error) - } -} - - -module.exports.Scale = Scale -module.exports.UNITS = UNITS -module.exports.UNIT_PRECISION = UNIT_PRECISION -module.exports.STATUS = STATUS diff --git a/trickler/peripheral/lib/auto-mode-characteristic.js b/trickler/peripheral/lib/auto-mode-characteristic.js deleted file mode 100644 index 96e4e5c..0000000 --- a/trickler/peripheral/lib/auto-mode-characteristic.js +++ /dev/null @@ -1,88 +0,0 @@ -/** - * Copyright (c) Ammolytics and contributors. All rights reserved. - * Released under the MIT license. See LICENSE file in the project root for details. - */ -const bleno = require('bleno') - - -class AutoModeCharacteristic extends bleno.Characteristic { - - constructor (trickler) { - super({ - uuid: '10000005-be5f-4b43-a49f-76f2d65c6e28', - properties: ['read', 'write'], - descriptors: [ - new bleno.Descriptor({ - uuid: '2901', - value: 'Start/stop automatic trickle mode' - }) - ] - }) - - this.trickler = trickler - this.listener = this.sendAutoModeNotification.bind(this) - } - - sendAutoModeNotification (result) { - console.log(`this.updateValueCallback: ${this.updateValueCallback}`) - if (this.updateValueCallback) { - var data = Buffer.alloc(1) - data.writeUInt8(result, 0) - console.log(`Calling this.updateValueCallback with ${data}`) - this.updateValueCallback(data) - } - } - - onReadRequest (offset, callback) { - console.log(`autoMode read request`) - if (offset) { - callback(this.RESULT_ATTR_NOT_LONG, null) - } else { - var data = Buffer.alloc(1) - data.writeUInt8(this.trickler.autoMode, 0); - callback(this.RESULT_SUCCESS, data) - } - } - - - onSubscribe (maxValueSize, updateValueCallback) { - console.log(`Subscribe from autoMode`) - this.maxValueSize = maxValueSize - this.updateValueCallback = updateValueCallback - - this.trickler.on('autoMode', this.listener) - } - - - onUnsubscribe () { - console.log(`Unsubscribe from autoMode`) - this.maxValueSize = null - this.updateValueCallback = null - - this.trickler.removeListener('autoMode', this.listener) - } - - - onWriteRequest (data, offset, withoutResponse, callback) { - console.log(`autoMode write request`) - if (offset) { - console.log(`Invalid offset: ${offset}`) - callback(this.RESULT_ATTR_NOT_LONG) - } else if (data.length !== 1) { - console.log(`Invalid data.length: ${data.length}`) - callback(this.RESULT_INVALID_ATTRIBUTE_LENGTH) - } else { - var autoMode = data.readUInt8(0) - console.log(`request to switch autoMode from ${this.trickler.autoMode} to ${autoMode}`) - this.trickler.once('autoMode', result => { - console.log(`autoMode result: ${result}`) - callback(this.RESULT_SUCCESS) - }) - this.trickler.autoMode = autoMode - } - } - -} - - -module.exports = AutoModeCharacteristic diff --git a/trickler/peripheral/lib/bluetooth.js b/trickler/peripheral/lib/bluetooth.js deleted file mode 100644 index 906cf07..0000000 --- a/trickler/peripheral/lib/bluetooth.js +++ /dev/null @@ -1,92 +0,0 @@ -/** - This code was copied from bleno to fix BT advertising: - https://github.com/abandonware/bleno/blob/master/lib/hci-socket/gap.js#L28 - */ -function makeAdData(name, serviceUuids) { - var advertisementDataLength = 3; - var scanDataLength = 0; - - var serviceUuids16bit = []; - var serviceUuids128bit = []; - var i = 0; - - if (name && name.length) { - scanDataLength += 2 + name.length; - } - - if (serviceUuids && serviceUuids.length) { - for (i = 0; i < serviceUuids.length; i++) { - var serviceUuid = Buffer.from(serviceUuids[i].match(/.{1,2}/g).reverse().join(''), 'hex'); - - if (serviceUuid.length === 2) { - serviceUuids16bit.push(serviceUuid); - } else if (serviceUuid.length === 16) { - serviceUuids128bit.push(serviceUuid); - } - } - } - - if (serviceUuids16bit.length) { - advertisementDataLength += 2 + 2 * serviceUuids16bit.length; - } - - if (serviceUuids128bit.length) { - advertisementDataLength += 2 + 16 * serviceUuids128bit.length; - } - - var advertisementData = Buffer.alloc(advertisementDataLength); - var scanData = Buffer.alloc(scanDataLength); - - // flags - advertisementData.writeUInt8(2, 0); - advertisementData.writeUInt8(0x01, 1); - advertisementData.writeUInt8(0x06, 2); - - var advertisementDataOffset = 3; - - if (serviceUuids16bit.length) { - advertisementData.writeUInt8(1 + 2 * serviceUuids16bit.length, advertisementDataOffset); - advertisementDataOffset++; - - advertisementData.writeUInt8(0x03, advertisementDataOffset); - advertisementDataOffset++; - - for (i = 0; i < serviceUuids16bit.length; i++) { - serviceUuids16bit[i].copy(advertisementData, advertisementDataOffset); - advertisementDataOffset += serviceUuids16bit[i].length; - } - } - - if (serviceUuids128bit.length) { - advertisementData.writeUInt8(1 + 16 * serviceUuids128bit.length, advertisementDataOffset); - advertisementDataOffset++; - - advertisementData.writeUInt8(0x06, advertisementDataOffset); - advertisementDataOffset++; - - for (i = 0; i < serviceUuids128bit.length; i++) { - serviceUuids128bit[i].copy(advertisementData, advertisementDataOffset); - advertisementDataOffset += serviceUuids128bit[i].length; - } - } - - // name - if (name && name.length) { - var nameBuffer = Buffer.from(name); - - scanData.writeUInt8(1 + nameBuffer.length, 0); - scanData.writeUInt8(0x08, 1); - nameBuffer.copy(scanData, 2); - } - - console.log('advertisementData:', advertisementData) - console.log('advertisementDataLength:', advertisementDataLength) - console.log('advertisementDataOffset:', advertisementDataOffset) - console.log('nameBuffer:', nameBuffer) - console.log('scanData:', scanData) - console.log('serviceUuids16bit:', serviceUuids16bit) - console.log('serviceUuids128bit:', serviceUuids128bit) - -} - -module.exports.makeAdData = makeAdData diff --git a/trickler/peripheral/lib/device-info-service.js b/trickler/peripheral/lib/device-info-service.js deleted file mode 100644 index 026d58f..0000000 --- a/trickler/peripheral/lib/device-info-service.js +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Copyright (c) Ammolytics and contributors. All rights reserved. - * Released under the MIT license. See LICENSE file in the project root for details. - */ -const bleno = require('bleno') - -const SerialNumberCharacteristic = require('./serial-number-characteristic') -const ModelNumberCharacteristic = require('./model-number-characteristic') - - -class DeviceInfoService extends bleno.PrimaryService { - constructor (trickler) { - super({ - uuid: '180a', - characteristics: [ - new SerialNumberCharacteristic(trickler), - new ModelNumberCharacteristic(trickler), - ] - }) - } -} - - -module.exports = DeviceInfoService diff --git a/trickler/peripheral/lib/index.js b/trickler/peripheral/lib/index.js deleted file mode 100644 index 9dc4856..0000000 --- a/trickler/peripheral/lib/index.js +++ /dev/null @@ -1,125 +0,0 @@ -/** - * Copyright (c) Ammolytics and contributors. All rights reserved. - * Released under the MIT license. See LICENSE file in the project root for details. - */ -const bleno = require('bleno') - -const bluetooth = require('./bluetooth') -const motors = require('./motor') -const scales = require('./and-fxfz') -const trickler = require('./trickler') -const DeviceInfoService = require('./device-info-service') -const TricklerService = require('./trickler-service') -var BT_READY = false -var TRICKLER_READY = false - -console.log('===== STARTING UP =====') -console.log('process.env:', process.env) - -const MOTOR = new motors.MotorControl({ - pin: process.env.MOTOR_PIN, -}) -const SCALE = new scales.Scale({ - baud: process.env.SCALE_BAUD_RATE, - device: process.env.SCALE_DEVICE_PATH, -}) -const TRICKLER = new trickler.Trickler({ - motor: MOTOR, - scale: SCALE, -}) - -const errHandler = (err) => { - if (err) { - console.error(err) - } -} - - -TRICKLER.once('ready', () => { - console.log(`Scale weight reads: ${SCALE.weight} ${SCALE.unit}, stableTime: ${TRICKLER.stableTime}`) - TRICKLER_READY = true -}) - -// Kick off bluetooth after trickler reports it's ready. -bleno.on('stateChange', state => { - console.log(`on -> stateChange: ${state}`) - switch (state) { - case 'poweredOn': - BT_READY = true - break - case 'unknown': - case 'resetting': - case 'unsupported': - case 'unauthorized': - case 'poweredOff': - default: - BT_READY = false - bleno.stopAdvertising() - break - } -}) - - -// Check every 100ms to ensure things are ready to start advertising. -var readyInterval = setInterval(() => { - console.log(`scale: ${TRICKLER.scale.isReady()}`) - console.log(`Checking if ready... TRICKLER: ${TRICKLER_READY} SCALE: ${TRICKLER.scale.ready} BT: ${BT_READY}`) - if ((TRICKLER_READY === true || TRICKLER.scale.ready === true) && BT_READY === true) { - clearInterval(readyInterval) - bluetooth.makeAdData(process.env.DEVICE_NAME, [TricklerService.TRICKLER_SERVICE_UUID]) - bleno.startAdvertising(process.env.DEVICE_NAME, [TricklerService.TRICKLER_SERVICE_UUID], errHandler) - } -}, 250) - - -bleno.on('advertisingStart', err => { - console.log('on -> advertisingStart: ' + (err ? 'error ' + err : 'success')) - if (err) { - // Error handled by advertisingStartError - return - } - - console.log('advertising services...') - bleno.setServices([ - INFO_SERVICE, - TRICKLER_SERVICE, - ]) -}) - -bleno.on('advertisingStop', () => { - console.log('on -> advertisingStop') - TRICKLER.close() -}) - -bleno.on('advertisingStartError', err => { - console.log('on -> advertisingStartError: ' + (err ? 'error ' + err : 'success')) -}) - -bleno.on('accept', clientAddress => { - console.log(`Client accepted: ${clientAddress}`) -}) - -bleno.on('disconnect', clientAddress => { - console.log(`Client disconnected: ${clientAddress}`) -}) - -console.log('Opening trickler...') -TRICKLER.open() - -const INFO_SERVICE = new DeviceInfoService(TRICKLER) -const TRICKLER_SERVICE = new TricklerService.Service(TRICKLER) - - -/** - * Graceful shutdown - */ -const shutdown = () => { - // Close the trickler, scale, and motor. - TRICKLER.close(() => { - clearInterval(readyInterval) - process.exit(0) - }) -} - -process.on('SIGINT', shutdown) -process.on('SIGTERM', shutdown) diff --git a/trickler/peripheral/lib/mock-scale.js b/trickler/peripheral/lib/mock-scale.js deleted file mode 100644 index 400cb50..0000000 --- a/trickler/peripheral/lib/mock-scale.js +++ /dev/null @@ -1,234 +0,0 @@ -/** - * Copyright (c) Ammolytics and contributors. All rights reserved. - * Released under the MIT license. See LICENSE file in the project root for details. - */ -const MockBinding = require('@serialport/binding-mock') -const trickler = require('./trickler') - - -/** -function randomInt() { - return Math.floor(Math.random() * Math.floor(10)) -} -*/ - -function formatGrams(numVal) { - return numVal.toFixed(3).padStart(7, 0) -} - -function formatGrains(numVal) { - // Scale can only display 0.02 GN. - if(numVal.toFixed(2).substr(-1) % 2) { - numVal = numVal += 0.01 - } - return numVal.toFixed(2).padStart(6, 0) -} - -/** -function addPan() { - ` - 019-04-19T22:57:11.559Z] [ST,+00000.00 GN - 2019-04-19T22:57:11.606Z] [ST,+00000.00 GN - 2019-04-19T22:57:11.654Z] [ST,+00000.00 GN - 2019-04-19T22:57:11.702Z] [US,+00001.96 GN - 2019-04-19T22:57:11.749Z] [US,+00010.74 GN - 2019-04-19T22:57:11.797Z] [US,+00033.76 GN - 2019-04-19T22:57:11.845Z] [US,+00076.88 GN - 2019-04-19T22:57:11.892Z] [US,+00142.92 GN - 2019-04-19T22:57:11.940Z] [US,+00228.52 GN - 2019-04-19T22:57:11.987Z] [US,+00321.68 GN - 2019-04-19T22:57:12.035Z] [US,+00407.32 GN - 2019-04-19T22:57:12.083Z] [US,+00474.86 GN - 2019-04-19T22:57:12.130Z] [US,+00522.52 GN - 2019-04-19T22:57:12.179Z] [US,+00558.40 GN - 2019-04-19T22:57:12.227Z] [US,+00593.12 GN - 2019-04-19T22:57:12.274Z] [US,+00630.44 GN - 2019-04-19T22:57:12.322Z] [US,+00666.70 GN - 2019-04-19T22:57:12.370Z] [US,+00695.12 GN - 2019-04-19T22:57:12.417Z] [US,+00711.08 GN - 2019-04-19T22:57:12.465Z] [US,+00716.64 GN - 2019-04-19T22:57:12.513Z] [US,+00717.54 GN - 2019-04-19T22:57:12.560Z] [US,+00717.58 GN - 2019-04-19T22:57:12.609Z] [US,+00717.58 GN - 2019-04-19T22:57:12.658Z] [US,+00717.58 GN - 2019-04-19T22:57:12.706Z] [US,+00717.58 GN - 2019-04-19T22:57:12.753Z] [US,+00717.58 GN - 2019-04-19T22:57:12.802Z] [US,+00717.60 GN - 2019-04-19T22:57:12.850Z] [US,+00717.60 GN - 2019-04-19T22:57:12.897Z] [US,+00717.60 GN - 2019-04-19T22:57:12.945Z] [ST,+00717.60 GN - 2019-04-19T22:57:12.993Z] [ST,+00717.60 GN - ` -} - -function zeroPan() { - `2019-04-19T22:57:14.762Z] [ST,+00717.60 GN -2019-04-19T22:57:14.810Z] [ST,+00717.60 GN -2019-04-19T22:57:16.053Z] [ST,+00000.00 GN -2019-04-19T22:57:16.100Z] [ST,+00000.00 GN -2019-04-19T22:57:16.148Z] [ST,+00000.00 GN -2019-04-19T22:57:16.195Z] [ST,+00000.00 GN -2019-04-19T22:57:16.243Z] [ST,+00000.00 GN` -} - -function removePan() { - `2019-04-19T22:57:03.239Z] [ST,+00000.00 GN -2019-04-19T22:57:03.288Z] [ST,+00000.00 GN -2019-04-19T22:57:03.334Z] [US,+00001.02 GN -2019-04-19T22:57:03.382Z] [US,+00003.32 GN -2019-04-19T22:57:03.429Z] [US,-00000.72 GN -2019-04-19T22:57:03.477Z] [US,-00030.78 GN -2019-04-19T22:57:03.525Z] [US,-00106.12 GN -2019-04-19T22:57:03.572Z] [US,-00228.46 GN -2019-04-19T22:57:03.623Z] [US,-00378.10 GN -2019-04-19T22:57:03.670Z] [US,-00521.36 GN -2019-04-19T22:57:03.718Z] [US,-00627.58 GN -2019-04-19T22:57:03.765Z] [US,-00686.72 GN -2019-04-19T22:57:03.813Z] [US,-00710.30 GN -2019-04-19T22:57:03.861Z] [US,-00716.48 GN -2019-04-19T22:57:03.908Z] [US,-00717.38 GN -2019-04-19T22:57:03.957Z] [US,-00717.48 GN -2019-04-19T22:57:04.005Z] [US,-00717.54 GN -2019-04-19T22:57:04.052Z] [US,-00717.58 GN -2019-04-19T22:57:04.100Z] [US,-00717.60 GN -2019-04-19T22:57:04.148Z] [US,-00717.60 GN -2019-04-19T22:57:04.195Z] [US,-00717.62 GN -2019-04-19T22:57:04.243Z] [US,-00717.62 GN -2019-04-19T22:57:04.290Z] [US,-00717.62 GN -2019-04-19T22:57:04.338Z] [US,-00717.62 GN -2019-04-19T22:57:04.387Z] [US,-00717.62 GN -2019-04-19T22:57:04.435Z] [ST,-00717.60 GN -2019-04-19T22:57:04.482Z] [ST,-00717.60 GN` -} -*/ - - -/** -{ - ON: () => { - function trickleUp () { - var value = 0 - var amount = 0.02 - var rate = 10 - var i = null - i = setInterval(() => { value += amount; this.emitData() }, rate) - this.on('ready', () => { clearInterval(1) }) - } - setTimeout(trickleUp, 500) - }, - OFF: () => { - }, -} -*/ - -/** - * Wait until scale is stable and 0. - * If automode is on, start to trickle up, increase weight. - * Jump in weight (80% or target - 2 gr) to simulate a dump. - * Once target weight is reached, let the app take over. - * Wait a second or two, then drop down to a negative weight (pan removed) - * Wait another second, then go back up to zero. - * Start pretend trickling again. -function fakeUser() { - this.emitData(`US,+000.000 g\r\n`) - this.emitData(`ST,+000.000 g\r\n`) - this.emitData(`US,+000.00 GN\r\n`) - this.emitData(`ST,+000.00 GN\r\n`) - - this.emitData(`US,-046.500 g\r\n`) - this.emitData(`ST,-046.500 g\r\n`) - this.emitData(`US,-717.60 GN\r\n`) - this.emitData(`ST,-717.60 GN\r\n`) - - this.emitData(`US,+000.000 g\r\n`) - this.emitData(`ST,+000.000 g\r\n`) - this.emitData(`US,+000.00 GN\r\n`) - this.emitData(`ST,+000.00 GN\r\n`) - - this.on('autoMode', (mode) => { - switch (mode) { - case AutoModeStatus.ON: - break - case AutoModeStatus.OFF: - break - } - }) -} -*/ - - -class MockScale extends MockBinding { - constructor(opt) { - super(opt) - - this._delay = 50 - this._unit = trickler.TricklerUnits.GRAINS - this._currentWeight = 0 - this._tareWeight = 0 - - // For a given command, the mock should respond accordingly. - var commands = {} - commands[trickler.CommandMap.MODE_BTN] = this.modeBtnAction.bind(this) - commands[trickler.CommandMap.REZERO_BTN] = this.rezeroBtnAction.bind(this) - - // Command runnner - var commander = () => { - if (!this.lastWrite) { - return - } - var last = this.lastWrite.toString() - var cmd = commands[last] - this.lastWrite = null - if (!cmd) { - return - } - cmd() - } - - // Check for a new command every 10 ms. - setInterval(commander, 10) - // emit weight every 50 ms. - setInterval(this._printWeight.bind(this), 50) - } - - // Add or remove weight from the scale. - changeWeight(weight) { - this._currentWeight = weight - } - - getCurrentWeight() { - return this._currentWeight - this._tareWeight - } - - rezeroBtnAction() { - console.log('Rezeroing the scale') - this._tareWeight = this._currentWeight - } - - // Act like the mode button was pressed, change units. - modeBtnAction() { - console.log('Toggling scale mode/unit') - this._unit = trickler.TricklerUnits.GRAINS === this._unit ? trickler.TricklerUnits.GRAMS : trickler.TricklerUnits.GRAINS - } - - _printWeight() { - var sign = Math.sign(this.getCurrentWeight()) - var unit = null - var weight = null - - switch(this._unit) { - case trickler.TricklerUnits.GRAINS: - unit = 'GN' - weight = formatGrains(sign * this.getCurrentWeight()) - break - case trickler.TricklerUnits.GRAMS: - unit = ' g' - weight = formatGrams(sign * this.getCurrentWeight()) - break - } - - this.emitData(`ST,${sign}${weight} ${unit}\r\n`) - } -} - -module.exports = MockScale diff --git a/trickler/peripheral/lib/model-number-characteristic.js b/trickler/peripheral/lib/model-number-characteristic.js deleted file mode 100644 index d7a8397..0000000 --- a/trickler/peripheral/lib/model-number-characteristic.js +++ /dev/null @@ -1,52 +0,0 @@ -/** - * Copyright (c) Ammolytics and contributors. All rights reserved. - * Released under the MIT license. See LICENSE file in the project root for details. - */ -const bleno = require('bleno') - -/** -Requesting model number... -2019-03-20T16:14:48.177Z: ST, +00000.00, GN, 0, 0 -"nknown command: "TN, FX-120i -*/ - - -class ModelNumberCharacteristic extends bleno.Characteristic { - - constructor(trickler) { - super({ - uuid: '2a24', - properties: ['read'], - descriptors: [ - new bleno.Descriptor({ - uuid: '2901', - value: 'Scale model number' - }) - ] - }) - - this.trickler = trickler - } - - - onReadRequest (offset, callback) { - console.log(`model number read request`) - if (offset) { - callback(this.RESULT_ATTR_NOT_LONG, null) - } else { - if (typeof this.trickler.scale.model === 'undefined') { - this.trickler.scale.once('model', model => { - var data = Buffer.from(model) - callback(this.RESULT_SUCCESS, data) - }) - this.trickler.scale.getModelNumber() - } else { - var data = Buffer.from(this.trickler.scale.model) - callback(this.RESULT_SUCCESS, data) - } - } - } -} - - -module.exports = ModelNumberCharacteristic diff --git a/trickler/peripheral/lib/motor.js b/trickler/peripheral/lib/motor.js deleted file mode 100644 index b944e2b..0000000 --- a/trickler/peripheral/lib/motor.js +++ /dev/null @@ -1,154 +0,0 @@ -/** - * Copyright (c) Ammolytics and contributors. All rights reserved. - * Released under the MIT license. See LICENSE file in the project root for details. - */ -const events = require('events') - -const rpio = require('rpio') - - -// TODO(eric): Make variable w/ frequency and duty cycle control. -// Controls the speed of the motor by pulsing it ON (true) and OFF (false) at specific intervals. -const SPEEDS = { - SINGLE_KERNEL: {true: 60, false: 300}, - VERY_SLOW: {true: 60, false: 150}, - SLOW: {true: 60, false: 100}, - MEDIUM: {true: 80, false: 100}, - FAST: {true: 100, false: 50}, - VERY_FAST: {true: 120, false: 20}, -} - -const SPEED_VALS = Object.values(SPEEDS) - - -class MotorControl extends events.EventEmitter { - - constructor (opts) { - super() - opts = (typeof opts === 'object') ? opts : {} - this.PIN = Number(opts.pin) || 15 - this._timeout = null - this._interval = null - this.speed = opts.speed || SPEEDS.VERY_SLOW - this._mode = false - this.timer = null - this.running = false - - this.runner = this._runner.bind(this) - - // Setup GPIO for motor control - var rpioOpts = { - mapping: 'physical', - } - if (process.env.MOCK) { - rpioOpts.mock = 'raspi-zero-w' - } - rpio.init(rpioOpts) - } - - open (cb) { - rpio.open(this.PIN, rpio.OUTPUT, rpio.LOW) - this.emit('open') - if (cb) { - cb() - } - } - - close (cb) { - this.stop() - rpio.close(this.PIN, rpio.PIN_RESET) - this.emit('close') - if (cb) { - cb() - } - } - - faster () { - var currIndex = SPEED_VALS.indexOf(this.speed) - if (currIndex < SPEED_VALS.length - 1) { - this.speed = SPEED_VALS[currIndex + 1] - } - } - - slower () { - var currIndex = SPEED_VALS.indexOf(this.speed) - if (currIndex > 0) { - this.speed = SPEED_VALS[currIndex - 1] - } - } - - get speed() { - return this._speed - } - - get mode () { - return this._mode - } - - set mode (value) { - if (typeof value === 'boolean') { - this._mode = value - switch (this._mode) { - case true: - this.on() - break - case false: - this.off() - break - } - } else { - console.error(`${value} is not a boolean`) - } - } - - set speed(value) { - if (this._speed !== value) { - console.log(`setting speed from ${JSON.stringify(this._speed)} to ${JSON.stringify(value)}`) - this._speed = value - this.emit('speed', this._speed) - } - } - - _runner () { - if (new Date() - this.timer >= this.speed[this.mode]) { - this.mode = ! this.mode - this.timer = new Date() - } - } - - off () { - // Control motor over GPIO. - rpio.write(this.PIN, rpio.LOW) - this.emit('off') - } - - on () { - // Control motor over GPIO. - rpio.write(this.PIN, rpio.HIGH) - this.emit('on') - } - - start () { - this.running = true - this.timer = new Date() - this.mode = true - this._interval = setInterval(this.runner, 1) - this.on() - this.emit('start') - } - - stop () { - clearInterval(this._interval) - this.timer = null - this.mode = false - this.off() - this.running = false - this.emit('stop') - // Reset speed to very slow. - this.speed = SPEEDS.VERY_SLOW - } -} - - -module.exports.MotorControl = MotorControl -module.exports.SPEEDS = SPEEDS diff --git a/trickler/peripheral/lib/serial-number-characteristic.js b/trickler/peripheral/lib/serial-number-characteristic.js deleted file mode 100644 index 1759295..0000000 --- a/trickler/peripheral/lib/serial-number-characteristic.js +++ /dev/null @@ -1,49 +0,0 @@ -/** - * Copyright (c) Ammolytics and contributors. All rights reserved. - * Released under the MIT license. See LICENSE file in the project root for details. - */ -const bleno = require('bleno') - -/** -"nknown command: "SN,15641060 -*/ - -class SerialNumberCharacteristic extends bleno.Characteristic { - - constructor (trickler) { - super({ - uuid: '2a25', - properties: ['read'], - descriptors: [ - new bleno.Descriptor({ - uuid: '2901', - value: 'Scale serial number' - }) - ] - }) - - this.trickler = trickler - } - - - onReadRequest (offset, callback) { - console.log(`serial number read request`) - if (offset) { - callback(this.RESULT_ATTR_NOT_LONG, null) - } else { - if (typeof this.trickler.scale.serial === 'undefined') { - this.trickler.scale.once('serial', serial => { - var data = Buffer.from(serial) - callback(this.RESULT_SUCCESS, data) - }) - this.trickler.scale.getSerialNumber() - } else { - var data = Buffer.from(this.trickler.scale.serial) - callback(this.RESULT_SUCCESS, data) - } - } - } -} - - -module.exports = SerialNumberCharacteristic diff --git a/trickler/peripheral/lib/stability-characteristic.js b/trickler/peripheral/lib/stability-characteristic.js deleted file mode 100644 index 976c40d..0000000 --- a/trickler/peripheral/lib/stability-characteristic.js +++ /dev/null @@ -1,67 +0,0 @@ -/** - * Copyright (c) Ammolytics and contributors. All rights reserved. - * Released under the MIT license. See LICENSE file in the project root for details. - */ -const bleno = require('bleno') - - -class StabilityCharacteristic extends bleno.Characteristic { - - constructor(scale) { - super({ - uuid: '10000002-be5f-4b43-a49f-76f2d65c6e28', - properties: ['read', 'notify'], - descriptors: [ - new bleno.Descriptor({ - uuid: '2901', - value: 'Reads the current stability status of the scale' - }) - ] - }) - - this.scale = scale - this.listener = this.sendStatusNotification.bind(this) - } - - - onReadRequest (offset, callback) { - console.log(`stability read request`) - if (offset) { - callback(this.RESULT_ATTR_NOT_LONG, null) - } else { - var data = Buffer.alloc(1) - data.writeUInt8(this.scale.status, 0) - callback(this.RESULT_SUCCESS, data) - } - } - - - sendStatusNotification (result) { - if (this.updateValueCallback) { - var data = Buffer.alloc(1) - data.writeUInt8(result, 0) - this.updateValueCallback(data) - } - } - - - onSubscribe (maxValueSize, updateValueCallback) { - console.log(`Subscribed to stability.`) - this.maxValueSize = maxValueSize - this.updateValueCallback = updateValueCallback - - this.scale.on('status', this.listener) - } - - - onUnsubscribe () { - console.log(`Unsubscribed from stability.`) - this.maxValueSize = null - this.updateValueCallback = null - - this.scale.removeListener('status', this.listener) - } -} - - -module.exports = StabilityCharacteristic diff --git a/trickler/peripheral/lib/target-weight-characteristic.js b/trickler/peripheral/lib/target-weight-characteristic.js deleted file mode 100644 index 6d62de5..0000000 --- a/trickler/peripheral/lib/target-weight-characteristic.js +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Copyright (c) Ammolytics and contributors. All rights reserved. - * Released under the MIT license. See LICENSE file in the project root for details. - */ -const bleno = require('bleno') -// TOOD: Consider using mathjs.unit for storing target/current weight. -//const mathjs = require('mathjs') - - -class TargetWeightCharacteristic extends bleno.Characteristic { - constructor(trickler) { - super({ - uuid: '10000004-be5f-4b43-a49f-76f2d65c6e28', - properties: ['read', 'write'], - descriptors: [ - new bleno.Descriptor({ - uuid: '2901', - value: 'Target powder weight' - }) - ] - }) - - this.trickler = trickler - } - - - onReadRequest (offset, callback) { - console.log(`target weight read request`) - if (offset) { - callback(this.RESULT_ATTR_NOT_LONG, null) - } else { - var data = Buffer.from(Number(this.trickler.targetWeight).toString()) - callback(this.RESULT_SUCCESS, data) - } - } - - - onWriteRequest (data, offset, withoutResponse, callback) { - console.log(`target weight write request`) - if (offset) { - callback(this.RESULT_ATTR_NOT_LONG) - } else if (data.length === 0) { - callback(this.RESULT_INVALID_ATTRIBUTE_LENGTH) - } else { - var targetWeight = data.toString() - console.log(`request to set the target weight from ${this.trickler.targetWeight} to ${targetWeight}`) - this.trickler.targetWeight = targetWeight - callback(this.RESULT_SUCCESS) - } - } -} - - -module.exports = TargetWeightCharacteristic diff --git a/trickler/peripheral/lib/trickler-service.js b/trickler/peripheral/lib/trickler-service.js deleted file mode 100644 index ac855de..0000000 --- a/trickler/peripheral/lib/trickler-service.js +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Copyright (c) Ammolytics and contributors. All rights reserved. - * Released under the MIT license. See LICENSE file in the project root for details. - */ -const bleno = require('bleno') - -const AutoModeCharacteristic = require('./auto-mode-characteristic') -const StabilityCharacteristic = require('./stability-characteristic') -const WeightCharacteristic = require('./weight-characteristic') -const UnitCharacteristic = require('./unit-characteristic') -const TargetWeightCharacteristic = require('./target-weight-characteristic') -const ModelNumberCharacteristic = require('./model-number-characteristic') -const SerialNumberCharacteristic = require('./serial-number-characteristic') - -const TRICKLER_SERVICE_UUID = '10000000-be5f-4b43-a49f-76f2d65c6e28' - - -class TricklerService extends bleno.PrimaryService { - constructor (trickler) { - super({ - uuid: TRICKLER_SERVICE_UUID, - characteristics: [ - new AutoModeCharacteristic(trickler), - new StabilityCharacteristic(trickler.scale), - new WeightCharacteristic(trickler.scale), - new UnitCharacteristic(trickler.scale), - new TargetWeightCharacteristic(trickler), - new ModelNumberCharacteristic(trickler), - new SerialNumberCharacteristic(trickler), - ] - }) - } -} - - -module.exports.Service = TricklerService -module.exports.TRICKLER_SERVICE_UUID = TRICKLER_SERVICE_UUID diff --git a/trickler/peripheral/lib/trickler.js b/trickler/peripheral/lib/trickler.js deleted file mode 100644 index d12e549..0000000 --- a/trickler/peripheral/lib/trickler.js +++ /dev/null @@ -1,286 +0,0 @@ -/** - * Copyright (c) Ammolytics and contributors. All rights reserved. - * Released under the MIT license. See LICENSE file in the project root for details. - */ -const events = require('events') - -const scales = require('./and-fxfz') -const { SPEEDS } = require('./motor') - - -const AUTO_MODES = { - OFF: 0, - ON: 1, -} - -const RUNNING_MODES = { - NOGO: 0, - GO: 1, -} - -const MAX_TARGET_WEIGHT = 500 - - -class Trickler extends events.EventEmitter { - constructor (opts) { - super() - opts = (typeof opts === 'object') ? opts : {} - - this.scale = opts.scale - this.motor = opts.motor - this._interval = null - this._autoMode = AUTO_MODES.OFF - this._runningMode = RUNNING_MODES.NOGO - this._targetWeight = 0.0 - this._weightListener = this.onWeightUpdate.bind(this) - this._shouldGoBound = this.shouldGo.bind(this) - this.startTime = null - this.endTime = null - - // Listen for the scale's ready event. - this.scale.once('ready', ready => { - console.log(`Trickler is ready! ${ready}`) - this.emit('ready', ready) - }) - // See if it's already ready. - if (this.scale.ready === true) { - console.log(`Trickler is ready!`) - this.emit('ready', true) - } - - this.on('runningMode', runningMode => { - switch (runningMode) { - case RUNNING_MODES.GO: - clearInterval(this._interval) - this._interval = null - this.startWhenReady() - break - case RUNNING_MODES.NOGO: - // Set interval to check for ready state and set GO mode. - if (this._interval === null) { - console.log('NOGO set. Starting checker...') - this._interval = setInterval(this._shouldGoBound, 50) - } - this.motor.stop() - break - } - }) - - this.on('autoMode', autoMode => { - switch (autoMode) { - case AUTO_MODES.ON: - console.log('Auto-mode activated.') - this.scale.on('weight', this._weightListener) - this.setMotorSpeed() - if (this._interval === null) { - console.log('NOGO set. Starting checker...') - this._interval = setInterval(this._shouldGoBound, 50) - } - //this.startWhenReady() - break - case AUTO_MODES.OFF: - this.runningMode = RUNNING_MODES.NOGO - this.scale.removeListener('weight', this._weightListener) - break - } - }) - } - - shouldGo () { - if (this.autoMode === AUTO_MODES.ON && - this.scale.weight >= 0 && - this.scale.stableTime >= 1000 && - this.tickDelta() > 1) { - // Set GO mode. - this.runningMode = RUNNING_MODES.GO - console.log('Ready! Set GO mode.') - } - } - - startWhenReady () { - // Start the motor if it isn't running and scale has been stable for over 1s. - if (this.runningMode === RUNNING_MODES.GO && this.motor.running === false && this.scale.stableTime >= 1000) { - console.log('Ready! Starting motor.') - this.motor.start() - this.startTime = new Date() - } - } - - onWeightUpdate (weight) { - var weightDelta = this.weightDelta() - this.endTime = new Date() - var timeDeltaSec = (this.endTime - this.startTime) / 1000 - - switch (Math.sign(weightDelta)) { - case 0: - case -0: - // Exact weight. - // Turn motor off, wait for pan removal. - this.motor.stop() - this.runningMode = RUNNING_MODES.NOGO - console.log(`EXACT WEIGHT ${weight} delta: ${weightDelta}`) - console.log(`Took ${timeDeltaSec} seconds`) - break - case -1: - // Over (negative delta). - // Turn motor off, wait for pan removal. - this.motor.stop() - this.runningMode = RUNNING_MODES.NOGO - console.log(`OVER WEIGHT ${weight} delta: ${weightDelta}`) - console.log(`Took ${timeDeltaSec} seconds`) - break - case 1: - // Under (positive delta). - console.log(`UNDER WEIGHT ${weight} delta: ${weightDelta}`) - if (weight < 0) { - // Pan removed. - this.motor.stop() - console.log('PAN REMOVED') - } else { - // Set the motor speed based on the current weight. - this.setMotorSpeed() - // Start the motor if it isn't running and scale has been stable for over 1s. - this.startWhenReady() - } - break - } - } - - // Set appropriate motor speed based on weight delta. - setMotorSpeed () { - var tickDelta = this.tickDelta() - - switch (true) { - case (tickDelta <= 3): - this.motor.speed = SPEEDS.SINGLE_KERNEL - break - case (tickDelta > 3 && tickDelta <= 8): - //this.motor.speed = SPEEDS.VERY_SLOW - this.motor.speed = SPEEDS.SLOW - break - case (tickDelta > 8 && tickDelta <= 16): - //this.motor.speed = SPEEDS.SLOW - this.motor.speed = SPEEDS.FAST - break - case (tickDelta > 16 && tickDelta <= 32): - //this.motor.speed = SPEEDS.MEDIUM - this.motor.speed = SPEEDS.VERY_FAST - break - case (tickDelta > 32 && tickDelta <= 48): - this.motor.speed = SPEEDS.VERY_FAST - break - case (tickDelta > 48): - this.motor.speed = SPEEDS.VERY_FAST - break - } - } - - // Don't bother with speeds, just turn the motor on and run until weight changes. - prime () { - this.motor.on() - this.scale.once('weight', this.motor.off) - } - - open (cb) { - // Open the scale first in case it needs to warm up. - this.scale.open(() => { - this.motor.open(cb) - }) - this.emit('open', true) - } - - close (cb) { - this.autoMode = AUTO_MODES.OFF - this.runningMode = RUNNING_MODES.NOGO - // Close the motor first to ensure it turns off. - this.motor.close(() => { - this.scale.close(cb) - }) - this.emit('close', true) - } - - start () { - this.emit('start', true) - } - - stop () { - this.emit('stop', true) - } - - // Difference between current weight and target weight. - weightDelta () { - return this.targetWeight - this.scale.weight - } - - // Difference between current and target weight divided by unit precision. - tickDelta () { - return this.targetWeightTicks - this.scale.weightTicks - } - - get autoMode() { - return this._autoMode - } - - set autoMode(value) { - switch(value) { - case AUTO_MODES.OFF: - case AUTO_MODES.ON: - if (this._autoMode !== value) { - console.log(`setting autoMode from ${this._autoMode} to ${value}`) - this._autoMode = value - this.emit('autoMode', this._autoMode) - } - break - default: - console.error(`Unknown value: ${value}`) - break - } - } - - get runningMode() { - return this._runningMode - } - - set runningMode(value) { - switch(value) { - case RUNNING_MODES.NOGO: - case RUNNING_MODES.GO: - if (this._runningMode !== value) { - console.log(`setting runningMode from ${this._runningMode} to ${value}`) - this._runningMode = value - this.emit('runningMode', this._runningMode) - } - break - default: - console.error(`Unknown value: ${value}`) - break - } - } - - get targetWeight() { - return this._targetWeight - } - - set targetWeight(value) { - value = Number(value) - if (value < 0 || value > MAX_TARGET_WEIGHT) { - console.error(`Invalid target weight requested: ${value}`) - } - if (this._targetWeight !== value) { - console.log(`setting targetWeight from ${this._targetWeight} to ${value}`) - this._targetWeight = value - this.emit('targetWeight', this._targetWeight) - } - } - - get targetWeightTicks () { - return this.targetWeight / scales.UNIT_PRECISION[this.scale.unit] - } - -} - - -module.exports.Trickler = Trickler -module.exports.AUTO_MODES = AUTO_MODES -module.exports.RUNNING_MODES = RUNNING_MODES -module.exports.MAX_TARGET_WEIGHT = MAX_TARGET_WEIGHT diff --git a/trickler/peripheral/lib/unit-characteristic.js b/trickler/peripheral/lib/unit-characteristic.js deleted file mode 100644 index d278a01..0000000 --- a/trickler/peripheral/lib/unit-characteristic.js +++ /dev/null @@ -1,102 +0,0 @@ -/** - * Copyright (c) Ammolytics and contributors. All rights reserved. - * Released under the MIT license. See LICENSE file in the project root for details. - */ -const bleno = require('bleno') -const scales = require('./and-fxfz') - - -class UnitCharacteristic extends bleno.Characteristic { - constructor(scale) { - super({ - uuid: '10000003-be5f-4b43-a49f-76f2d65c6e28', - properties: ['read', 'write', 'notify'], - descriptors: [ - new bleno.Descriptor({ - uuid: '2901', - value: 'Reads the current weight unit of the scale' - }) - ] - }) - - this.scale = scale - this.listener = this.sendUnitNotification.bind(this) - } - - - onReadRequest (offset, callback) { - console.log(`unit read request`) - if (offset) { - callback(this.RESULT_ATTR_NOT_LONG, null) - } else { - var data = Buffer.alloc(1) - data.writeUInt8(this.scale.unit, 0) - callback(this.RESULT_SUCCESS, data) - } - } - - - sendUnitNotification (result) { - if (this.updateValueCallback) { - var data = Buffer.alloc(1) - data.writeUInt8(result, 0) - this.updateValueCallback(data) - } - } - - - onSubscribe (maxValueSize, updateValueCallback) { - console.log(`Subscribed to unit`) - this.maxValueSize = maxValueSize - this.updateValueCallback = updateValueCallback - - this.scale.on('unit', this.listener) - } - - - onUnsubscribe () { - console.log(`Unsubscribed from unit`) - this.maxValueSize = null - this.updateValueCallback = null - - this.scale.removeListener('unit', this.listener) - } - - - onWriteRequest (data, offset, withoutResponse, callback) { - console.log(`unit write request`) - if (offset) { - callback(this.RESULT_ATTR_NOT_LONG) - } else if (data.length !== 1) { - callback(this.RESULT_INVALID_ATTRIBUTE_LENGTH) - } else { - var unit = data.readUInt8(0) - console.log(`request to switch unit from ${this.scale.unit} to ${unit}`) - - switch(unit) { - case scales.UNITS.GRAINS: - case scales.UNITS.GRAMS: - if (this.scale.unit === unit) { - // Nothing to do. - console.log('Unit already set') - callback(this.RESULT_SUCCESS) - } else { - this.scale.once('unit', () => { - callback(this.RESULT_SUCCESS) - }) - this.scale.pressMode() - } - break - default: - console.log(`Invalid unit: ${unit}`) - callback(this.RESULT_UNLIKELY_ERROR) - break - } - } - } -} - - - - -module.exports = UnitCharacteristic diff --git a/trickler/peripheral/lib/weight-characteristic.js b/trickler/peripheral/lib/weight-characteristic.js deleted file mode 100644 index 69548cc..0000000 --- a/trickler/peripheral/lib/weight-characteristic.js +++ /dev/null @@ -1,70 +0,0 @@ -/** - * Copyright (c) Ammolytics and contributors. All rights reserved. - * Released under the MIT license. See LICENSE file in the project root for details. - */ -const bleno = require('bleno') - - -class WeightCharacteristic extends bleno.Characteristic { - constructor(scale) { - super({ - uuid: '10000001-be5f-4b43-a49f-76f2d65c6e28', - properties: ['read', 'notify'], - descriptors: [ - new bleno.Descriptor({ - uuid: '2901', - value: 'Reads the current weight value of the scale' - }) - ] - }) - - this.scale = scale - this.listener = this.sendWeightNotification.bind(this) - } - - - onReadRequest (offset, callback) { - console.log(`weight read request`) - if (offset) { - callback(this.RESULT_ATTR_NOT_LONG, null) - } else { - var data = Buffer.from(Number(this.scale.weight).toString()) - callback(this.RESULT_SUCCESS, data) - } - } - - - sendWeightNotification (result) { - if (process.env.DEBUG) { - console.log(`Trying to send weight: ${result}`) - } - if (this.updateValueCallback) { - var data = Buffer.from(Number(result).toString()) - if (process.env.DEBUG) { - console.log(`Sending weight notification: ${data}`) - } - this.updateValueCallback(data) - } - } - - - onSubscribe (maxValueSize, updateValueCallback) { - this.maxValueSize = maxValueSize - this.updateValueCallback = updateValueCallback - console.log(`Subscribed to weight characteristic.`) - - this.scale.on('weight', this.listener) - } - - - onUnsubscribe () { - this.maxValueSize = null - this.updateValueCallback = null - console.log('Unsubscribed from weight characteristic') - - this.scale.removeListener('weight', this.listener) - } -} - - -module.exports = WeightCharacteristic diff --git a/trickler/peripheral/opentrickler_api.yaml b/trickler/peripheral/opentrickler_api.yaml new file mode 100644 index 0000000..8dd6283 --- /dev/null +++ b/trickler/peripheral/opentrickler_api.yaml @@ -0,0 +1,53 @@ +openapi: 3.0.0 + +info: + version: 1.0.0 + title: OpenTrickler API + license: + name: MIT + +servers: + - url: http://opentrickler.local/v1 + - url: http://localhost/v1 + +paths: + /settings: + get: + summary: Read Open Trickler settings + operationId: getSettings + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Setting' + put: + summary: Update Open Trickler settings + operationId: updateSettings + requestBody: + description: New setting values + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/Setting' + responses: + '204': + description: The settings were updated + +components: + schemas: + Setting: + type: object + properties: + auto_mode: + type: boolean + target_weight: + type: string + pattern: '^\d{1,3}.\d{1,3}$' + target_unit: + type: string + enum: + - g + - gn diff --git a/trickler/peripheral/opentrickler_config.ini b/trickler/peripheral/opentrickler_config.ini new file mode 100644 index 0000000..c34652f --- /dev/null +++ b/trickler/peripheral/opentrickler_config.ini @@ -0,0 +1,57 @@ +[general] +verbose = False + + +[bluetooth] +name = Trickler + + +[scale] +# See the SCALES variable in scales.py for options. +model = and-fx120 +port = /dev/ttyUSB0 +baudrate = 19200 +timeout = 0.1 + + +[motors] +trickler_pin = 18 +trickler_max_pwm = 100 +trickler_min_pwm = 32 +#servo_pin = + + +[leds] +# https://gpiozero.readthedocs.io/en/stable/recipes_advanced.html#controlling-the-pi-s-own-leds +status_led_pin = 47 +active_high = False +enable_status_leds = True +ready_status_led_mode = pulse +running_status_led_mode = on +done_status_led_mode = fast_blink + + +[PID] +# Higher Kp values will: +# - decrease rise time +# - increase overshoot +# - slightly increase settling time +# - decrease steady-state error +# - degrade stability +Kp = 10 +# Higher Ki values will: +# - slightly decrease rise time +# - increase overshoot +# - increase settling time +# - largely decrease steady-state error +# - degrade stability +Ki = 2.3 +# Higher Kd values will: +# - slightly decrease rise time +# - decrease overshoot +# - decrease settling time +# - minorly affect steady-state error +# - improve stability +Kd = 3.75 +# Enable for use with pidtuner.com +pid_tuner_mode = False diff --git a/trickler/peripheral/package-lock.json b/trickler/peripheral/package-lock.json deleted file mode 100644 index 991a7e1..0000000 --- a/trickler/peripheral/package-lock.json +++ /dev/null @@ -1,2100 +0,0 @@ -{ - "name": "trickler", - "version": "1.2.0", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "@abandonware/bleno": { - "version": "github:abandonware/bleno#de0f0645bc71c7397f5e936e89e2c0ccd14433e5", - "from": "github:abandonware/bleno", - "requires": { - "@abandonware/bluetooth-hci-socket": "^0.5.3-5", - "bplist-parser": "0.2.0", - "debug": "^4.1.1", - "xpc-connect": "^2.0.0" - } - }, - "@abandonware/bluetooth-hci-socket": { - "version": "0.5.3-5", - "resolved": "https://registry.npmjs.org/@abandonware/bluetooth-hci-socket/-/bluetooth-hci-socket-0.5.3-5.tgz", - "integrity": "sha512-q9DupPXYcqLyLFrmJqYDaqXoN0fOR4qOZA39dJbEeu1M583Ghr5Bn+JlEAnA6l88DJSBZiyjtCgItDeUfuRwMA==", - "optional": true, - "requires": { - "debug": "^4.1.1", - "nan": "^2.14.0", - "node-pre-gyp": "^0.14.0", - "usb": "^1.6.2" - } - }, - "@babel/code-frame": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", - "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", - "dev": true, - "requires": { - "@babel/highlight": "^7.10.4" - } - }, - "@babel/helper-validator-identifier": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", - "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", - "dev": true - }, - "@babel/highlight": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", - "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.10.4", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - }, - "dependencies": { - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - } - } - }, - "@serialport/binding-abstract": { - "version": "8.0.6", - "resolved": "https://registry.npmjs.org/@serialport/binding-abstract/-/binding-abstract-8.0.6.tgz", - "integrity": "sha512-1swwUVoRyQ9ubxrkJ8JPppykohUpTAP4jkGr36e9NjbVocSPfqeX6tFZFwl/IdUlwJwxGdbKDqq7FvXniCQUMw==", - "requires": { - "debug": "^4.1.1" - } - }, - "@serialport/binding-mock": { - "version": "8.0.6", - "resolved": "https://registry.npmjs.org/@serialport/binding-mock/-/binding-mock-8.0.6.tgz", - "integrity": "sha512-BIbY5/PsDDo0QWDNCCxDgpowAdks+aZR8BOsEtK2GoASTTcJCy1fBwPIfH870o7rnbH901wY3C+yuTfdOvSO9A==", - "requires": { - "@serialport/binding-abstract": "^8.0.6", - "debug": "^4.1.1" - } - }, - "@serialport/bindings": { - "version": "8.0.8", - "resolved": "https://registry.npmjs.org/@serialport/bindings/-/bindings-8.0.8.tgz", - "integrity": "sha512-xMJHr7CyOPq+wwC/S2RNI+tY+WZW4gXY3tE8QUOIRp0K7lSyLYOzKdyGUtk2uI0ohDMV3OcB+TEhhffT2S2DHQ==", - "requires": { - "@serialport/binding-abstract": "^8.0.6", - "@serialport/parser-readline": "^8.0.6", - "bindings": "^1.5.0", - "debug": "^4.1.1", - "nan": "^2.14.0", - "prebuild-install": "^5.3.0" - } - }, - "@serialport/parser-byte-length": { - "version": "8.0.6", - "resolved": "https://registry.npmjs.org/@serialport/parser-byte-length/-/parser-byte-length-8.0.6.tgz", - "integrity": "sha512-92mrFxFEvq3gRvSM7ANK/jfbmHslz91a5oYJy/nbSn4H/MCRXjxR2YOkQgVXuN+zLt+iyDoW3pcOP4Sc1nWdqQ==" - }, - "@serialport/parser-cctalk": { - "version": "8.0.6", - "resolved": "https://registry.npmjs.org/@serialport/parser-cctalk/-/parser-cctalk-8.0.6.tgz", - "integrity": "sha512-pqtCYQPgxnxHygiXUPCfgX7sEx+fdR/ObjpscidynEULUq2fFrC5kBkrxRbTfHRtTaU2ii9DyjFq0JVRCbhI0Q==" - }, - "@serialport/parser-delimiter": { - "version": "8.0.6", - "resolved": "https://registry.npmjs.org/@serialport/parser-delimiter/-/parser-delimiter-8.0.6.tgz", - "integrity": "sha512-ogKOcPisPMlVtirkuDu3SFTF0+xT0ijxoH7XjpZiYL41EVi367MwuCnEmXG+dEKKnF0j9EPqOyD2LGSJxaFmhQ==" - }, - "@serialport/parser-readline": { - "version": "8.0.6", - "resolved": "https://registry.npmjs.org/@serialport/parser-readline/-/parser-readline-8.0.6.tgz", - "integrity": "sha512-OYBT2mpczh9QUI3MTw8j0A0tIlPVjpVipvuVnjRkYwxrxPeq04RaLFhaDpuRzua5rTKMt89c1y3btYeoDXMjAA==", - "requires": { - "@serialport/parser-delimiter": "^8.0.6" - } - }, - "@serialport/parser-ready": { - "version": "8.0.6", - "resolved": "https://registry.npmjs.org/@serialport/parser-ready/-/parser-ready-8.0.6.tgz", - "integrity": "sha512-xcEqv4rc119WR5JzAuu8UeJOlAwET2PTdNb6aIrrLlmTxhvuBbuRFcsnF3BpH9jUL30Kh7a6QiNXIwVG+WR/1Q==" - }, - "@serialport/parser-regex": { - "version": "8.0.6", - "resolved": "https://registry.npmjs.org/@serialport/parser-regex/-/parser-regex-8.0.6.tgz", - "integrity": "sha512-J8KY75Azz5ZyExmyM5YfUxbXOWBkZCytKgCCmZ966ttwZS0bUZOuoCaZj2Zp4VILJAiLuxHoqc0foi67Fri5+g==" - }, - "@serialport/stream": { - "version": "8.0.6", - "resolved": "https://registry.npmjs.org/@serialport/stream/-/stream-8.0.6.tgz", - "integrity": "sha512-ym1PwM0rwLrj90vRBB66I1hwMXbuMw9wGTxqns75U3N/tuNFOH85mxXaYVF2TpI66aM849NoI1jMm50fl9equg==", - "requires": { - "debug": "^4.1.1" - } - }, - "@types/color-name": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", - "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", - "dev": true - }, - "abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "optional": true - }, - "acorn": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.0.tgz", - "integrity": "sha512-+G7P8jJmCHr+S+cLfQxygbWhXy+8YTVGzAkpEbcLo2mLoL7tij/VG41QSHACSf5QgYRhMZYHuNc6drJaO0Da+w==", - "dev": true - }, - "acorn-jsx": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.2.0.tgz", - "integrity": "sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ==", - "dev": true - }, - "ajv": { - "version": "6.12.4", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.4.tgz", - "integrity": "sha512-eienB2c9qVQs2KWexhkrdMLVDoIQCz5KSeLxwg9Lzk4DOfBtIK9PQwwufcsn1jjGuf9WZmqPMbGxOzfcuphJCQ==", - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ansi-colors": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", - "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", - "dev": true - }, - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" - }, - "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==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "aproba": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" - }, - "are-we-there-yet": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", - "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "requires": { - "sprintf-js": "~1.0.2" - } - }, - "asn1": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", - "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", - "optional": true, - "requires": { - "safer-buffer": "~2.1.0" - } - }, - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "optional": true - }, - "astral-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", - "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", - "dev": true - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", - "optional": true - }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", - "optional": true - }, - "aws4": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.10.1.tgz", - "integrity": "sha512-zg7Hz2k5lI8kb7U32998pRRFin7zJlkfezGJjUc2heaD4Pw2wObakCDVzkKztTm/Ln7eiVvYsjqak0Ed4LkMDA==", - "optional": true - }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" - }, - "base64-js": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", - "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==" - }, - "bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", - "optional": true, - "requires": { - "tweetnacl": "^0.14.3" - } - }, - "big-integer": { - "version": "1.6.48", - "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.48.tgz", - "integrity": "sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w==", - "optional": true - }, - "bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "requires": { - "file-uri-to-path": "1.0.0" - } - }, - "bl": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.0.2.tgz", - "integrity": "sha512-j4OH8f6Qg2bGuWfRiltT2HYGx0e1QcBTrK9KAHNMwMZdQnDZFk0ZSYIpADjYCB3U12nicC5tVJwSIhwOWjb4RQ==", - "requires": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - }, - "dependencies": { - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - } - } - }, - "bleno": { - "version": "github:abandonware/bleno#de0f0645bc71c7397f5e936e89e2c0ccd14433e5", - "from": "github:abandonware/bleno", - "requires": { - "@abandonware/bluetooth-hci-socket": "^0.5.3-5", - "bplist-parser": "0.2.0", - "debug": "^4.1.1", - "xpc-connect": "^2.0.0" - } - }, - "block-stream": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", - "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", - "optional": true, - "requires": { - "inherits": "~2.0.0" - } - }, - "bplist-parser": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.2.0.tgz", - "integrity": "sha512-z0M+byMThzQmD9NILRniCUXYsYpjwnlO8N5uCFaCqIOpqRsJCrQL9NK3JsD67CN5a08nF5oIL2bD6loTdHOuKw==", - "optional": true, - "requires": { - "big-integer": "^1.6.44" - } - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "buffer": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz", - "integrity": "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==", - "requires": { - "base64-js": "^1.0.2", - "ieee754": "^1.1.4" - } - }, - "callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true - }, - "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", - "optional": true - }, - "chalk": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", - "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "dev": true, - "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - } - }, - "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==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "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==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" - }, - "code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" - }, - "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==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", - "dev": true - }, - "combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "optional": true, - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "complex.js": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/complex.js/-/complex.js-2.0.11.tgz", - "integrity": "sha512-6IArJLApNtdg1P1dFtn3dnyzoZBEF0MwMnrfF1exSBRpZYoy4yieMkpZhQDC0uwctw48vii0CFVyHfpgZ/DfGw==" - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, - "console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" - }, - "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "dependencies": { - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - } - } - }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "optional": true, - "requires": { - "assert-plus": "^1.0.0" - } - }, - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "requires": { - "ms": "^2.1.1" - } - }, - "decimal.js": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.2.0.tgz", - "integrity": "sha512-vDPw+rDgn3bZe1+F/pyEwb1oMG2XTlRVgAa6B4KccTEpYgF8w6eQllVbQcfIJnZyvzFtFpxnpGtx8dd7DJp/Rw==" - }, - "decompress-response": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", - "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", - "requires": { - "mimic-response": "^2.0.0" - } - }, - "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==" - }, - "deep-is": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", - "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", - "dev": true - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", - "optional": true - }, - "delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" - }, - "detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" - }, - "doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } - }, - "ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", - "optional": true, - "requires": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true - }, - "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==", - "requires": { - "once": "^1.4.0" - } - }, - "enquirer": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", - "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", - "dev": true, - "requires": { - "ansi-colors": "^4.1.1" - } - }, - "escape-latex": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/escape-latex/-/escape-latex-1.2.0.tgz", - "integrity": "sha512-nV5aVWW1K0wEiUIEdZ4erkGGH8mDxGyxSeqPzRNtWP7ataw+/olFObw7hujFWlVjNsaDFw5VZ5NzVSIqRgfTiw==" - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true - }, - "eslint": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.7.0.tgz", - "integrity": "sha512-1KUxLzos0ZVsyL81PnRN335nDtQ8/vZUD6uMtWbF+5zDtjKcsklIi78XoE0MVL93QvWTu+E5y44VyyCsOMBrIg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "ajv": "^6.10.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.0.1", - "doctrine": "^3.0.0", - "enquirer": "^2.3.5", - "eslint-scope": "^5.1.0", - "eslint-utils": "^2.1.0", - "eslint-visitor-keys": "^1.3.0", - "espree": "^7.2.0", - "esquery": "^1.2.0", - "esutils": "^2.0.2", - "file-entry-cache": "^5.0.1", - "functional-red-black-tree": "^1.0.1", - "glob-parent": "^5.0.0", - "globals": "^12.1.0", - "ignore": "^4.0.6", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "js-yaml": "^3.13.1", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash": "^4.17.19", - "minimatch": "^3.0.4", - "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "progress": "^2.0.0", - "regexpp": "^3.1.0", - "semver": "^7.2.1", - "strip-ansi": "^6.0.0", - "strip-json-comments": "^3.1.0", - "table": "^5.2.3", - "text-table": "^0.2.0", - "v8-compile-cache": "^2.0.3" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", - "dev": true - }, - "semver": { - "version": "7.3.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", - "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", - "dev": true - }, - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.0" - } - }, - "strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true - } - } - }, - "eslint-scope": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.0.tgz", - "integrity": "sha512-iiGRvtxWqgtx5m8EyQUJihBloE4EnYeGE/bz1wSPwJE6tZuJUtHlhqDM4Xj2ukE8Dyy1+HCZ4hE0fzIVMzb58w==", - "dev": true, - "requires": { - "esrecurse": "^4.1.0", - "estraverse": "^4.1.1" - } - }, - "eslint-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", - "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", - "dev": true, - "requires": { - "eslint-visitor-keys": "^1.1.0" - } - }, - "eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", - "dev": true - }, - "espree": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-7.2.0.tgz", - "integrity": "sha512-H+cQ3+3JYRMEIOl87e7QdHX70ocly5iW4+dttuR8iYSPr/hXKFb+7dBsZ7+u1adC4VrnPlTkv0+OwuPnDop19g==", - "dev": true, - "requires": { - "acorn": "^7.3.1", - "acorn-jsx": "^5.2.0", - "eslint-visitor-keys": "^1.3.0" - } - }, - "esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true - }, - "esquery": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.3.1.tgz", - "integrity": "sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ==", - "dev": true, - "requires": { - "estraverse": "^5.1.0" - }, - "dependencies": { - "estraverse": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", - "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", - "dev": true - } - } - }, - "esrecurse": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", - "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", - "dev": true, - "requires": { - "estraverse": "^4.1.0" - } - }, - "estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true - }, - "esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true - }, - "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==" - }, - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "optional": true - }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", - "optional": true - }, - "fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" - }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" - }, - "fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", - "dev": true - }, - "file-entry-cache": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", - "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", - "dev": true, - "requires": { - "flat-cache": "^2.0.1" - } - }, - "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==" - }, - "flat-cache": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", - "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", - "dev": true, - "requires": { - "flatted": "^2.0.0", - "rimraf": "2.6.3", - "write": "1.0.3" - }, - "dependencies": { - "rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - } - } - }, - "flatted": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", - "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", - "dev": true - }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", - "optional": true - }, - "form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "optional": true, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - } - }, - "fraction.js": { - "version": "4.0.12", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.0.12.tgz", - "integrity": "sha512-8Z1K0VTG4hzYY7kA/1sj4/r1/RWLBD3xwReT/RCrUCbzPszjNQCCsy3ktkU/eaEqX3MYa4pY37a52eiBlPMlhA==" - }, - "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==" - }, - "fs-minipass": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", - "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", - "optional": true, - "requires": { - "minipass": "^2.6.0" - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" - }, - "fstream": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", - "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", - "optional": true, - "requires": { - "graceful-fs": "^4.1.2", - "inherits": "~2.0.0", - "mkdirp": ">=0.5 0", - "rimraf": "2" - } - }, - "functional-red-black-tree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", - "dev": true - }, - "gauge": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", - "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - } - }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "optional": true, - "requires": { - "assert-plus": "^1.0.0" - } - }, - "github-from-package": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4=" - }, - "glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-parent": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", - "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", - "dev": true, - "requires": { - "is-glob": "^4.0.1" - } - }, - "globals": { - "version": "12.4.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", - "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", - "dev": true, - "requires": { - "type-fest": "^0.8.1" - } - }, - "graceful-fs": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", - "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", - "optional": true - }, - "har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", - "optional": true - }, - "har-validator": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", - "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", - "optional": true, - "requires": { - "ajv": "^6.12.3", - "har-schema": "^2.0.0" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true - }, - "has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" - }, - "http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "optional": true, - "requires": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - } - }, - "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "optional": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "ieee754": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", - "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" - }, - "ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", - "dev": true - }, - "ignore-walk": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.3.tgz", - "integrity": "sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==", - "optional": true, - "requires": { - "minimatch": "^3.0.4" - } - }, - "import-fresh": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz", - "integrity": "sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==", - "dev": true, - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - } - }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "ini": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "is-glob": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", - "optional": true - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" - }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", - "optional": true - }, - "javascript-natural-sort": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz", - "integrity": "sha1-+eIwPUUH9tdDVac2ZNFED7Wg71k=" - }, - "js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true - }, - "js-yaml": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz", - "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==", - "dev": true, - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - }, - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", - "optional": true - }, - "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", - "optional": true - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" - }, - "json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", - "dev": true - }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", - "optional": true - }, - "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "optional": true, - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" - } - }, - "levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - } - }, - "lodash": { - "version": "4.17.20", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", - "dev": true - }, - "mathjs": { - "version": "6.6.5", - "resolved": "https://registry.npmjs.org/mathjs/-/mathjs-6.6.5.tgz", - "integrity": "sha512-jvRqk7eoEHBcx/lskmy05m+8M7xDHAJcJzRJoqIsqExtlTHPDQO0Zv85g5F0rasDAXF+DLog/70hcqCJijSzPQ==", - "requires": { - "complex.js": "^2.0.11", - "decimal.js": "^10.2.0", - "escape-latex": "^1.2.0", - "fraction.js": "^4.0.12", - "javascript-natural-sort": "^0.7.1", - "seed-random": "^2.2.0", - "tiny-emitter": "^2.1.0", - "typed-function": "^1.1.1" - } - }, - "mime-db": { - "version": "1.44.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", - "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==", - "optional": true - }, - "mime-types": { - "version": "2.1.27", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", - "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", - "optional": true, - "requires": { - "mime-db": "1.44.0" - } - }, - "mimic-response": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", - "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==" - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" - }, - "minipass": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", - "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", - "optional": true, - "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - } - }, - "minizlib": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz", - "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", - "optional": true, - "requires": { - "minipass": "^2.9.0" - } - }, - "mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "requires": { - "minimist": "^1.2.5" - } - }, - "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==" - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "nan": { - "version": "2.14.1", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.1.tgz", - "integrity": "sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw==" - }, - "napi-build-utils": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", - "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==" - }, - "natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", - "dev": true - }, - "needle": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/needle/-/needle-2.5.0.tgz", - "integrity": "sha512-o/qITSDR0JCyCKEQ1/1bnUXMmznxabbwi/Y4WwJElf+evwJNFNwIDMCCt5IigFVxgeGBJESLohGtIS9gEzo1fA==", - "optional": true, - "requires": { - "debug": "^3.2.6", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" - }, - "dependencies": { - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "optional": true, - "requires": { - "ms": "^2.1.1" - } - } - } - }, - "node-abi": { - "version": "2.18.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.18.0.tgz", - "integrity": "sha512-yi05ZoiuNNEbyT/xXfSySZE+yVnQW6fxPZuFbLyS1s6b5Kw3HzV2PHOM4XR+nsjzkHxByK+2Wg+yCQbe35l8dw==", - "requires": { - "semver": "^5.4.1" - } - }, - "node-gyp": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.8.0.tgz", - "integrity": "sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA==", - "optional": true, - "requires": { - "fstream": "^1.0.0", - "glob": "^7.0.3", - "graceful-fs": "^4.1.2", - "mkdirp": "^0.5.0", - "nopt": "2 || 3", - "npmlog": "0 || 1 || 2 || 3 || 4", - "osenv": "0", - "request": "^2.87.0", - "rimraf": "2", - "semver": "~5.3.0", - "tar": "^2.0.0", - "which": "1" - }, - "dependencies": { - "nopt": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", - "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", - "optional": true, - "requires": { - "abbrev": "1" - } - }, - "semver": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", - "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=", - "optional": true - }, - "tar": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.2.tgz", - "integrity": "sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA==", - "optional": true, - "requires": { - "block-stream": "*", - "fstream": "^1.0.12", - "inherits": "2" - } - } - } - }, - "node-pre-gyp": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.14.0.tgz", - "integrity": "sha512-+CvDC7ZttU/sSt9rFjix/P05iS43qHCOOGzcr3Ry99bXG7VX953+vFyEuph/tfqoYu8dttBkE86JSKBO2OzcxA==", - "optional": true, - "requires": { - "detect-libc": "^1.0.2", - "mkdirp": "^0.5.1", - "needle": "^2.2.1", - "nopt": "^4.0.1", - "npm-packlist": "^1.1.6", - "npmlog": "^4.0.2", - "rc": "^1.2.7", - "rimraf": "^2.6.1", - "semver": "^5.3.0", - "tar": "^4.4.2" - } - }, - "noop-logger": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/noop-logger/-/noop-logger-0.1.1.tgz", - "integrity": "sha1-lKKxYzxPExdVMAfYlm/Q6EG2pMI=" - }, - "nopt": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", - "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", - "optional": true, - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } - }, - "npm-bundled": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.1.tgz", - "integrity": "sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA==", - "optional": true, - "requires": { - "npm-normalize-package-bin": "^1.0.1" - } - }, - "npm-normalize-package-bin": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", - "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==", - "optional": true - }, - "npm-packlist": { - "version": "1.4.8", - "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.8.tgz", - "integrity": "sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==", - "optional": true, - "requires": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1", - "npm-normalize-package-bin": "^1.0.1" - } - }, - "npmlog": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", - "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" - }, - "oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", - "optional": true - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "requires": { - "wrappy": "1" - } - }, - "optionator": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", - "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", - "dev": true, - "requires": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" - } - }, - "os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", - "optional": true - }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", - "optional": true - }, - "osenv": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", - "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", - "optional": true, - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "requires": { - "callsites": "^3.0.0" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" - }, - "path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true - }, - "performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", - "optional": true - }, - "prebuild-install": { - "version": "5.3.5", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-5.3.5.tgz", - "integrity": "sha512-YmMO7dph9CYKi5IR/BzjOJlRzpxGGVo1EsLSUZ0mt/Mq0HWZIHOKHHcHdT69yG54C9m6i45GpItwRHpk0Py7Uw==", - "requires": { - "detect-libc": "^1.0.3", - "expand-template": "^2.0.3", - "github-from-package": "0.0.0", - "minimist": "^1.2.3", - "mkdirp": "^0.5.1", - "napi-build-utils": "^1.0.1", - "node-abi": "^2.7.0", - "noop-logger": "^0.1.1", - "npmlog": "^4.0.1", - "pump": "^3.0.0", - "rc": "^1.2.7", - "simple-get": "^3.0.3", - "tar-fs": "^2.0.0", - "tunnel-agent": "^0.6.0", - "which-pm-runs": "^1.0.0" - } - }, - "prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true - }, - "process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" - }, - "progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true - }, - "psl": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", - "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==", - "optional": true - }, - "pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" - }, - "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", - "optional": true - }, - "rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "requires": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - } - }, - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "regexpp": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz", - "integrity": "sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==", - "dev": true - }, - "request": { - "version": "2.88.2", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", - "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", - "optional": true, - "requires": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.3", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.5.0", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - } - }, - "resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true - }, - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "optional": true, - "requires": { - "glob": "^7.1.3" - } - }, - "rpio": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/rpio/-/rpio-2.2.0.tgz", - "integrity": "sha512-zuOHIqKKxdS7RuY6wde70r+Nx4MsrypFotsgPuCkpxYlJQlq27KinJg5VlsKcU6b1ukwxTx9zA5UtWXaZFjn5g==", - "requires": { - "bindings": "~1.5.0", - "nan": "^2.14.1" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "optional": true - }, - "sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", - "optional": true - }, - "seed-random": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/seed-random/-/seed-random-2.2.0.tgz", - "integrity": "sha1-KpsZ4lCoFwmSMaW5mk2vgLf77VQ=" - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" - }, - "serialport": { - "version": "8.0.8", - "resolved": "https://registry.npmjs.org/serialport/-/serialport-8.0.8.tgz", - "integrity": "sha512-GEaMYbAk9chfGyxoVC27PHnKMUMOQOCAg+9umOhAgk88vH0H6DbQ9/Tj3lRwoj7lE+TLra75P/0l1RXMfX4yQg==", - "requires": { - "@serialport/binding-mock": "^8.0.6", - "@serialport/bindings": "^8.0.8", - "@serialport/parser-byte-length": "^8.0.6", - "@serialport/parser-cctalk": "^8.0.6", - "@serialport/parser-delimiter": "^8.0.6", - "@serialport/parser-readline": "^8.0.6", - "@serialport/parser-ready": "^8.0.6", - "@serialport/parser-regex": "^8.0.6", - "@serialport/stream": "^8.0.6", - "debug": "^4.1.1" - } - }, - "set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" - }, - "shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true - }, - "signal-exit": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", - "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==" - }, - "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==" - }, - "simple-get": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.0.tgz", - "integrity": "sha512-bCR6cP+aTdScaQCnQKbPKtJOKDp/hj9EDLJo3Nw4y1QksqaovlW/bnptB6/c1e+qmNIDHRK+oXFDdEqBT8WzUA==", - "requires": { - "decompress-response": "^4.2.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, - "slice-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", - "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.0", - "astral-regex": "^1.0.0", - "is-fullwidth-code-point": "^2.0.0" - }, - "dependencies": { - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - } - } - }, - "sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", - "dev": true - }, - "sshpk": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", - "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", - "optional": true, - "requires": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - } - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - }, - "table": { - "version": "5.4.6", - "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", - "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", - "dev": true, - "requires": { - "ajv": "^6.10.2", - "lodash": "^4.17.14", - "slice-ansi": "^2.1.0", - "string-width": "^3.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "requires": { - "ansi-regex": "^4.1.0" - } - } - } - }, - "tar": { - "version": "4.4.13", - "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz", - "integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==", - "optional": true, - "requires": { - "chownr": "^1.1.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.8.6", - "minizlib": "^1.2.1", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.2", - "yallist": "^3.0.3" - } - }, - "tar-fs": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.0.tgz", - "integrity": "sha512-9uW5iDvrIMCVpvasdFHW0wJPez0K4JnMZtsuIeDI7HyMGJNxmDZDOCQROr7lXyS+iL/QMpj07qcjGYTSdRFXUg==", - "requires": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", - "pump": "^3.0.0", - "tar-stream": "^2.0.0" - } - }, - "tar-stream": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.1.3.tgz", - "integrity": "sha512-Z9yri56Dih8IaK8gncVPx4Wqt86NDmQTSh49XLZgjWpGZL9GK9HKParS2scqHCC4w6X9Gh2jwaU45V47XTKwVA==", - "requires": { - "bl": "^4.0.1", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "dependencies": { - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - } - } - }, - "text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", - "dev": true - }, - "tiny-emitter": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", - "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==" - }, - "tough-cookie": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", - "optional": true, - "requires": { - "psl": "^1.1.28", - "punycode": "^2.1.1" - } - }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "optional": true - }, - "type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "requires": { - "prelude-ls": "^1.2.1" - } - }, - "type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", - "dev": true - }, - "typed-function": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/typed-function/-/typed-function-1.1.1.tgz", - "integrity": "sha512-RbN7MaTQBZLJYzDENHPA0nUmWT0Ex80KHItprrgbTPufYhIlTePvCXZxyQK7wgn19FW5bnuaBIKcBb5mRWjB1Q==" - }, - "uri-js": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", - "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", - "requires": { - "punycode": "^2.1.0" - } - }, - "usb": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/usb/-/usb-1.6.3.tgz", - "integrity": "sha512-23KYMjaWydACd8wgGKMQ4MNwFspAT6Xeim4/9Onqe5Rz/nMb4TM/WHL+qPT0KNFxzNKzAs63n1xQWGEtgaQ2uw==", - "optional": true, - "requires": { - "bindings": "^1.4.0", - "nan": "2.13.2", - "prebuild-install": "^5.3.3" - }, - "dependencies": { - "nan": { - "version": "2.13.2", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.13.2.tgz", - "integrity": "sha512-TghvYc72wlMGMVMluVo9WRJc0mB8KxxF/gZ4YYFy7V2ZQX9l7rgbPg7vjS9mt6U5HXODVFVI2bOduCzwOMv/lw==", - "optional": true - } - } - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, - "uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "optional": true - }, - "v8-compile-cache": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz", - "integrity": "sha512-8OQ9CL+VWyt3JStj7HX7/ciTL2V3Rl1Wf5OL+SNTm0yK1KvtReVulksyeRnCANHHuUxHlQig+JJDlUhBt1NQDQ==", - "dev": true - }, - "verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "optional": true, - "requires": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "optional": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "which-pm-runs": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.0.0.tgz", - "integrity": "sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs=" - }, - "wide-align": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", - "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", - "requires": { - "string-width": "^1.0.2 || 2" - } - }, - "word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", - "dev": true - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - }, - "write": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", - "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", - "dev": true, - "requires": { - "mkdirp": "^0.5.1" - } - }, - "xpc-connect": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/xpc-connect/-/xpc-connect-2.0.0.tgz", - "integrity": "sha512-r7J493GkXt+c931hju91N0UDSZCjqQ4/02MJNSe+948myXSAf/oyBd8zQ9XP1l/IZWuQhdkroMyCaj7gmg413Q==", - "optional": true, - "requires": { - "bindings": "^1.5.0", - "nan": "^2.14.0", - "node-gyp": "^3.8.0" - } - }, - "yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "optional": true - } - } -} diff --git a/trickler/peripheral/package.json b/trickler/peripheral/package.json deleted file mode 100644 index 2c821bb..0000000 --- a/trickler/peripheral/package.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "name": "trickler", - "version": "1.2.0", - "description": "Automatic, Bluetooth-controlled powder trickler controller", - "main": "index.js", - "scripts": { - "start": "node index.js", - "lint": "./node_modules/.bin/eslint *.js lib/*.js" - }, - "repository": "git@github.com:ammolytics/projects.git", - "author": "Eric Higgins ", - "license": "MIT", - "dependencies": { - "@abandonware/bleno": "github:abandonware/bleno", - "bleno": "github:abandonware/bleno", - "mathjs": "^6.6.5", - "rpio": "^2.1.1", - "serialport": "^8.0.8" - }, - "devDependencies": { - "eslint": "^7.1.0" - } -} diff --git a/trickler/peripheral/requirements-to-freeze.txt b/trickler/peripheral/requirements-to-freeze.txt new file mode 100644 index 0000000..347e393 --- /dev/null +++ b/trickler/peripheral/requirements-to-freeze.txt @@ -0,0 +1,7 @@ +bluezero +pybleno +pyserial +gpiozero +pymemcache +RPi.GPIO; platform_machine == 'armv6l' +grpcio diff --git a/trickler/peripheral/server.js b/trickler/peripheral/server.js deleted file mode 100755 index b89538f..0000000 --- a/trickler/peripheral/server.js +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env node - - -// Parse the config file to load env vars before starting the application. -var config = require('./' + process.argv[2]) - -for (var k in config.apps[0].env) { - console.log('Setting: process.env[', k, '] = ', config.apps[0].env[k]) - process.env[k] = config.apps[0].env[k] -} - - -require('./index.js') diff --git a/trickler/peripheral/server.sh b/trickler/peripheral/server.sh index 545bda5..f07b772 100755 --- a/trickler/peripheral/server.sh +++ b/trickler/peripheral/server.sh @@ -1,4 +1,5 @@ #!/bin/sh +# Enter code directory and start the trickler daemon. cd /code/opentrickler -exec ./server.js ecosystem.config.js >> "$1" 2>&1 +exec ./trickler/main.py opentrickler_config.ini >> "$1" 2>&1 diff --git a/trickler/peripheral/setup.cfg b/trickler/peripheral/setup.cfg new file mode 100644 index 0000000..6a02cb8 --- /dev/null +++ b/trickler/peripheral/setup.cfg @@ -0,0 +1,5 @@ +[metadata] +license-file = LICENSE + +[wheel] +universal = 1 diff --git a/trickler/peripheral/setup.py b/trickler/peripheral/setup.py new file mode 100644 index 0000000..4caf6e8 --- /dev/null +++ b/trickler/peripheral/setup.py @@ -0,0 +1,48 @@ +from setuptools import setup + +SHORT_DESCRIPTION = """ +""".strip() + +LONG_DESCRIPTION = """ +DIY powder trickler control software.""".strip() + +DEPENDENCIES = [ + 'pybleno', + 'pyserial', + 'gpiozero', + 'pymemcache', + 'RPi.GPIO', +] + +TEST_DEPENDENCIES = [] + +VERSION = '2.0.0-dev' +URL = 'https://github.com/ammolytics/projects/trickler' + +setup( + name='opentrickler', + version=VERSION, + description=SHORT_DESCRIPTION, + long_description=LONG_DESCRIPTION, + url=URL, + + author='Eric Higgins', + author_email='eric@ammolytics.com', + license='MIT', + + classifiers=[ + 'License :: OSI Approved :: MIT License', + + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + ], + + keywords='', + + packages=[], + + install_requires=DEPENDENCIES, + tests_require=TEST_DEPENDENCIES, +) diff --git a/trickler/peripheral/trickler/PID.py b/trickler/peripheral/trickler/PID.py new file mode 100644 index 0000000..3385e71 --- /dev/null +++ b/trickler/peripheral/trickler/PID.py @@ -0,0 +1,129 @@ +#!/usr/bin/python +# +# This file is part of IvPID. +# Copyright (C) 2015 Ivmech Mechatronics Ltd. +# +# IvPID is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# IvPID is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# title :PID.py +# description :python pid controller +# author :Caner Durmusoglu +# date :20151218 +# version :0.1 +# notes : +# python_version :2.7 +# ============================================================================== + +"""Ivmech PID Controller is simple implementation of a Proportional-Integral-Derivative (PID) Controller in the Python Programming Language. +More information about PID Controller: http://en.wikipedia.org/wiki/PID_controller +""" +import time + +class PID: + """PID Controller + """ + + def __init__(self, P=0.2, I=0.0, D=0.0, current_time=None): + + self.Kp = P + self.Ki = I + self.Kd = D + + self.sample_time = 0.00 + self.current_time = current_time if current_time is not None else time.time() + self.last_time = self.current_time + + self.clear() + + def clear(self): + """Clears PID computations and coefficients""" + self.SetPoint = 0.0 + + self.PTerm = 0.0 + self.ITerm = 0.0 + self.DTerm = 0.0 + self.last_error = 0.0 + + # Windup Guard + self.int_error = 0.0 + self.windup_guard = 20.0 + + self.output = 0.0 + + def update(self, feedback_value, current_time=None): + """Calculates PID value for given reference feedback + + .. math:: + u(t) = K_p e(t) + K_i \int_{0}^{t} e(t)dt + K_d {de}/{dt} + + .. figure:: images/pid_1.png + :align: center + + Test PID with Kp=1.2, Ki=1, Kd=0.001 (test_pid.py) + + """ + error = self.SetPoint - feedback_value + + self.current_time = current_time if current_time is not None else time.time() + delta_time = self.current_time - self.last_time + delta_error = error - self.last_error + + if (delta_time >= self.sample_time): + self.PTerm = self.Kp * error + self.ITerm += error * delta_time + + if (self.ITerm < -self.windup_guard): + self.ITerm = -self.windup_guard + elif (self.ITerm > self.windup_guard): + self.ITerm = self.windup_guard + + self.DTerm = 0.0 + if delta_time > 0: + self.DTerm = delta_error / delta_time + + # Remember last time and last error for next calculation + self.last_time = self.current_time + self.last_error = error + + self.output = self.PTerm + (self.Ki * self.ITerm) + (self.Kd * self.DTerm) + + def setKp(self, proportional_gain): + """Determines how aggressively the PID reacts to the current error with setting Proportional Gain""" + self.Kp = proportional_gain + + def setKi(self, integral_gain): + """Determines how aggressively the PID reacts to the current error with setting Integral Gain""" + self.Ki = integral_gain + + def setKd(self, derivative_gain): + """Determines how aggressively the PID reacts to the current error with setting Derivative Gain""" + self.Kd = derivative_gain + + def setWindup(self, windup): + """Integral windup, also known as integrator windup or reset windup, + refers to the situation in a PID feedback controller where + a large change in setpoint occurs (say a positive change) + and the integral terms accumulates a significant error + during the rise (windup), thus overshooting and continuing + to increase as this accumulated error is unwound + (offset by errors in the other direction). + The specific problem is the excess overshooting. + """ + self.windup_guard = windup + + def setSampleTime(self, sample_time): + """PID that should be updated at a regular interval. + Based on a pre-determined sampe time, the PID decides if it should compute or return immediately. + """ + self.sample_time = sample_time diff --git a/trickler/peripheral/trickler/ble.py b/trickler/peripheral/trickler/ble.py new file mode 100755 index 0000000..64c3313 --- /dev/null +++ b/trickler/peripheral/trickler/ble.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +""" +Copyright (c) Ammolytics and contributors. All rights reserved. +Released under the MIT license. See LICENSE file in the project root for details. + +OpenTrickler +https://github.com/ammolytics/projects/tree/develop/trickler +""" + +import logging + +import bluezero # pylint: disable=import-error; + +import helpers + + +TRICKLER_UUID = '10000000-be5f-4b43-a49f-76f2d65c6e28' +AUTO_MODE_UUID = '10000005-be5f-4b43-a49f-76f2d65c6e28' +SCALE_STATUS_UUID = '10000002-be5f-4b43-a49f-76f2d65c6e28' +TARGET_WEIGHT_UUID = '10000004-be5f-4b43-a49f-76f2d65c6e28' +SCALE_UNIT_UUID = '10000003-be5f-4b43-a49f-76f2d65c6e28' +SCALE_WEIGHT_UUID = '10000001-be5f-4b43-a49f-76f2d65c6e28' + + +CHARACTERISTICS = dict( + auto_mode=dict( + uuid=AUTO_MODE_UUID, + flags=['read', 'write'], + description='Start/stop automatic trickle mode', + ), + target_weight=dict( + uuid=TARGET_WEIGHT_UUID, + flags=['read', 'write'], + description='Target powder weight', + ), + scale_status=dict( + uuid=SCALE_STATUS_UUID, + flags=['read', 'notify'], + description='Reads the current stability status of the scale', + ), + scale_unit=dict( + uuid=SCALE_UNIT_UUID, + flags=['read', 'write', 'notify'], + description='Reads the current weight unit of the scale', + ), + scale_weight=dict( + uuid=SCALE_WEIGHT_UUID, + flags=['read', 'notify'], + description='Reads the current weight value of the scale', + ), +) + + +def graceful_exit(): + logging.info('Stopping OpenTrickler Bluetooth...') + pass + + +def main(config, args): + memcache = helpers.get_mc_client() + + adapters = list(bluezero.adapter.Adapter.available()) + logging.info('Available bluetooth adapters: %s', adapters) + + adapter_address = adapters[0].address + logging.info('First adapter address: %s', adapter_address) + + device_name = config['bluetooth']['name'] + opentrickler = bluezero.peripheral.Peripheral(adapter_address, local_name=device_name) + + #atexit.register(functools.partial(graceful_exit, bleno)) + opentrickler.add_service(srv_id=1, uuid=TRICKLER_UUID, primary=True) + for i, key, char in enumerate(CHARACTERISTICS.items(), start=1): + char['srv_id'] = 1 + char['chr_id'] = i + char['notifying'] = 'notify' in char['flags'] + opentrickler.add_characteristic(**char) + + opentrickler.publish() + + +if __name__ == '__main__': + import argparse + import configparser + + parser = argparse.ArgumentParser(description='Test bluetooth') + parser.add_argument('config_file') + args = parser.parse_args() + + config = configparser.ConfigParser() + config.read_file(open(args.config_file)) + + helpers.setup_logging() + + main(config, args) diff --git a/trickler/peripheral/trickler/ble_v1.py b/trickler/peripheral/trickler/ble_v1.py new file mode 100755 index 0000000..e28412a --- /dev/null +++ b/trickler/peripheral/trickler/ble_v1.py @@ -0,0 +1,349 @@ +#!/usr/bin/env python3 +""" +Copyright (c) Ammolytics and contributors. All rights reserved. +Released under the MIT license. See LICENSE file in the project root for details. + +OpenTrickler +https://github.com/ammolytics/projects/tree/develop/trickler +""" + +import atexit +import functools +import logging +import os +import time + +import pybleno # pylint: disable=import-error; + +import constants +import helpers +import scales + + +TRICKLER_UUID = '10000000-be5f-4b43-a49f-76f2d65c6e28' + + +class BasicCharacteristic(pybleno.Characteristic): + + def __init__(self, *args, **kwargs): + super(BasicCharacteristic, self).__init__(*args, **kwargs) + self._memcache = None + self._mc_key = None + self._updateValueCallback = None + self._send_fn = helpers.noop + self._recv_fn = helpers.noop + self.__value = None + + def onSubscribe(self, maxValueSize, updateValueCallback): + self._maxValueSize = maxValueSize + self._updateValueCallback = updateValueCallback + + def onUnsubscribe(self): + self._maxValueSize = None + self._updateValueCallback = None + + def onReadRequest(self, offset, callback): + if offset: + callback(pybleno.Characteristic.RESULT_ATTR_NOT_LONG, None) + else: + data = self._send_fn(self.mc_value) # pylint: disable=assignment-from-none; + callback(pybleno.Characteristic.RESULT_SUCCESS, data) + + @property + def mc_value(self): + return self.__value + + @mc_value.setter + def mc_value(self, value): + if value == self.__value: + return + logging.info('Updating %s: from %r to %r', self._mc_key, self.__value, value) + self.__value = value + if self._updateValueCallback: + self._updateValueCallback(self._send_fn(self.__value)) + + def mc_get(self): + for _ in range(2): + try: + value = self._memcache.get(self._mc_key) + except (KeyError, ValueError): + logging.exception('Cache miss.') + else: + return value + + def mc_update(self): + value = self.mc_get() + self.mc_value = value + + +class AutoMode(BasicCharacteristic): + + def __init__(self, memcache): + super(AutoMode, self).__init__({ + 'uuid': '10000005-be5f-4b43-a49f-76f2d65c6e28', + 'properties': ['read', 'write'], + 'descriptors': [ + pybleno.Descriptor(dict( + uuid='2901', + value='Start/stop automatic trickle mode' + ))], + 'value': False, + }) + self._memcache = memcache + self._mc_key = constants.AUTO_MODE + self._updateValueCallback = None + self._send_fn = helpers.bool_to_bytes + self._recv_fn = helpers.bytes_to_bool + self.__value = self.mc_get() + + def onWriteRequest(self, data, offset, withoutResponse, callback): + if offset: + callback(pybleno.Characteristic.RESULT_ATTR_NOT_LONG) + elif len(data) != 1: + callback(pybleno.Characteristic.RESULT_INVALID_ATTRIBUTE_LENGTH) + else: + value = self._recv_fn(data) + logging.info('Changing %s to %r', self._mc_key, value) + self._memcache.set(self._mc_key, value) + # This will notify subscribers. + self.mc_value = value + callback(pybleno.Characteristic.RESULT_SUCCESS) + + +class ScaleStatus(BasicCharacteristic): + + def __init__(self, memcache): + super(ScaleStatus, self).__init__({ + 'uuid': '10000002-be5f-4b43-a49f-76f2d65c6e28', + 'properties': ['read', 'notify'], + 'descriptors': [ + pybleno.Descriptor(dict( + uuid='2901', + value='Reads the current stability status of the scale' + ))], + }) + self._memcache = memcache + self._mc_key = constants.SCALE_STATUS + self._updateValueCallback = None + self._send_fn = helpers.enum_to_bytes + self._recv_fn = helpers.bytes_to_enum + self.__value = self.mc_get() + + +class TargetWeight(BasicCharacteristic): + + def __init__(self, memcache): + super(TargetWeight, self).__init__({ + 'uuid': '10000004-be5f-4b43-a49f-76f2d65c6e28', + 'properties': ['read', 'write'], + 'descriptors': [ + pybleno.Descriptor(dict( + uuid='2901', + value='Target powder weight' + ))], + }) + self._memcache = memcache + self._mc_key = constants.TARGET_WEIGHT + self._updateValueCallback = None + self._send_fn = helpers.decimal_to_bytes + self._recv_fn = helpers.bytes_to_decimal + self.__value = self.mc_get() + + def onWriteRequest(self, data, offset, withoutResponse, callback): + if offset: + callback(pybleno.Characteristic.RESULT_ATTR_NOT_LONG) + elif len(data) == 0: + callback(pybleno.Characteristic.RESULT_INVALID_ATTRIBUTE_LENGTH) + else: + value = self._recv_fn(data) + logging.info('Changing %s to %r', self._mc_key, value) + self._memcache.set(self._mc_key, value) + # This will notify subscribers. + self.mc_value = value + callback(pybleno.Characteristic.RESULT_SUCCESS) + + +class ScaleUnit(BasicCharacteristic): + + def __init__(self, memcache): + super(ScaleUnit, self).__init__({ + 'uuid': '10000003-be5f-4b43-a49f-76f2d65c6e28', + 'properties': ['read', 'write', 'notify'], + 'descriptors': [ + pybleno.Descriptor(dict( + uuid='2901', + value='Reads the current weight unit of the scale' + ))], + }) + self._memcache = memcache + self._mc_key = constants.SCALE_UNIT + self._updateValueCallback = None + self._send_fn = helpers.enum_to_bytes + self._recv_fn = functools.partial(helpers.bytes_to_enum, scales.Units) + self.__value = self.mc_get() + + def onWriteRequest(self, data, offset, withoutResponse, callback): + if offset: + callback(pybleno.Characteristic.RESULT_ATTR_NOT_LONG) + elif len(data) != 1: + callback(pybleno.Characteristic.RESULT_INVALID_ATTRIBUTE_LENGTH) + else: + value = self._recv_fn(data) + logging.info('Changing %s to %r', constants.TARGET_UNIT, value) + # NOTE: Cannot set the scale unit directly, but can change the target unit. + self._memcache.set(constants.TARGET_UNIT, value) + # Notify subscribers. + if self._updateValueCallback: + self._updateValueCallback(data) + callback(pybleno.Characteristic.RESULT_SUCCESS) + + +class ScaleWeight(BasicCharacteristic): + + def __init__(self, memcache): + super(ScaleWeight, self).__init__({ + 'uuid': '10000001-be5f-4b43-a49f-76f2d65c6e28', + 'properties': ['read', 'notify'], + 'descriptors': [ + pybleno.Descriptor(dict( + uuid='2901', + value='Reads the current weight value of the scale' + ))], + }) + self._memcache = memcache + self._mc_key = constants.SCALE_WEIGHT + self._updateValueCallback = None + self._send_fn = helpers.decimal_to_bytes + self._recv_fn = helpers.bytes_to_decimal + self.__value = self.mc_get() + + +class TricklerService(pybleno.BlenoPrimaryService): + + def __init__(self, memcache): + super(TricklerService, self).__init__({ + 'uuid': TRICKLER_UUID, + 'characteristics': [ + AutoMode(memcache), + ScaleStatus(memcache), + ScaleUnit(memcache), + ScaleWeight(memcache), + TargetWeight(memcache), + ], + }) + + def all_mc_update(self): + for characteristic in self['characteristics']: + characteristic.mc_update() + + +def error_handler(error): + if error: + logging.error(error) + + +def on_state_change(device_name, bleno, trickler_service, state): + if state == 'poweredOn': + bleno.startAdvertising(device_name, [TRICKLER_UUID], error_handler) + else: + bleno.stopAdvertising() + + +def on_advertising_start(bleno, trickler_service, error): + if error: + logging.error(error) + else: + logging.info('Starting advertising') + bleno.setServices([trickler_service]) + + +def on_advertising_stop(): + logging.info('Stopping advertising') + + +def on_accept(client_address): + logging.info('Client connected: %r', client_address) + + +def on_disconnect(client_address): + logging.info('Client disconnected: %r', client_address) + + +def graceful_exit(bleno): + bleno.stopAdvertising() + bleno.disconnect() + logging.info('Stopping OpenTrickler Bluetooth...') + + +def all_variables_set(memcache): + variables = ( + memcache.get(constants.AUTO_MODE, None) != None, + memcache.get(constants.SCALE_STATUS, None) != None, + memcache.get(constants.SCALE_WEIGHT, None) != None, + memcache.get(constants.SCALE_UNIT, None) != None, + memcache.get(constants.TARGET_WEIGHT, None) != None, + memcache.get(constants.TARGET_UNIT, None) != None, + ) + logging.info('Variables: %r', variables) + return all(variables) + + +def run(config, args): + memcache = helpers.get_mc_client() + + logging.info('Setting up Bluetooth...') + trickler_service = TricklerService(memcache) + device_name = config['bluetooth']['name'] + os.environ['BLENO_DEVICE_NAME'] = device_name + logging.info('Bluetooth device will be advertised as %s', device_name) + bleno = pybleno.Bleno() + atexit.register(functools.partial(graceful_exit, bleno)) + + # pylint: disable=no-member + bleno.on('stateChange', functools.partial(on_state_change, device_name, bleno, trickler_service)) + bleno.on('advertisingStart', functools.partial(on_advertising_start, bleno, trickler_service)) + bleno.on('advertisingStop', on_advertising_stop) + bleno.on('accept', on_accept) + bleno.on('disconnect', on_disconnect) + # pylint: enable=no-member + + logging.info('Checking if ready to advertise...') + while 1: + if all_variables_set(memcache): + logging.info('Ready to advertise!') + break + time.sleep(0.1) + + logging.info('Advertising OpenTrickler over Bluetooth...') + bleno.start() + + logging.info('Starting OpenTrickler Bluetooth daemon...') + # Loop and keep TricklerService property values up to date from memcache. + while 1: + try: + trickler_service.all_mc_update() + except (AttributeError, OSError): + logging.exception('Caught possible bluetooth exception.') + time.sleep(0.1) + + logging.info('Stopping bluetooth daemon...') + + +if __name__ == '__main__': + import argparse + import configparser + + parser = argparse.ArgumentParser(description='Test bluetooth') + parser.add_argument('config_file') + args = parser.parse_args() + + config = configparser.ConfigParser() + config.read_file(open(args.config_file)) + + log_level = logging.INFO + if config['general'].getboolean('verbose'): + log_level = logging.DEBUG + + helpers.setup_logging(log_level) + + run(config, args) diff --git a/trickler/peripheral/trickler/constants.py b/trickler/peripheral/trickler/constants.py new file mode 100644 index 0000000..5157aed --- /dev/null +++ b/trickler/peripheral/trickler/constants.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 +""" +Copyright (c) Ammolytics and contributors. All rights reserved. +Released under the MIT license. See LICENSE file in the project root for details. + +OpenTrickler +https://github.com/ammolytics/projects/tree/develop/trickler +""" + +AUTO_MODE = 'auto_mode' +SCALE_IS_STABLE = 'scale_is_stable' +SCALE_RESOLUTION = 'scale_resolution' +SCALE_STATUS = 'scale_status' +SCALE_UNIT = 'scale_unit' +SCALE_WEIGHT = 'scale_weight' +TARGET_WEIGHT = 'target_weight' +TARGET_UNIT = 'target_unit' +TRICKLER_MOTOR_SPEED = 'trickler_motor_speed' diff --git a/trickler/peripheral/trickler/helpers.py b/trickler/peripheral/trickler/helpers.py new file mode 100644 index 0000000..a2f07a1 --- /dev/null +++ b/trickler/peripheral/trickler/helpers.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +""" +Copyright (c) Ammolytics and contributors. All rights reserved. +Released under the MIT license. See LICENSE file in the project root for details. + +OpenTrickler +https://github.com/ammolytics/projects/tree/develop/trickler +""" +import array +import decimal +import logging +import struct + +import pymemcache.client.base # pylint: disable=import-error; +import pymemcache.serde # pylint: disable=import-error; + + +def get_mc_client(server='127.0.0.1:11211'): + return pymemcache.client.base.Client( + server, + serde=pymemcache.serde.PickleSerde(), + connect_timeout=10, + timeout=2) + + +def setup_logging(level=logging.DEBUG): + logging.basicConfig( + level=level, + format='%(asctime)s.%(msecs)06dZ %(levelname)-4s %(message)s', + datefmt='%Y-%m-%dT%H:%M:%S') + + +def is_even(dec): + """Returns True if a decimal.Decimal is even, False if odd.""" + exp = dec.as_tuple().exponent + factor = 10 ** (exp * -1) + return (dec * factor) % 2 == 0 + + +def noop(*args, **kwargs): + return None + + +def bool_to_bytes(value): + data_bytes = array.array('B', [0] * 1) + struct.pack_into(" 0 + auto_mode = memcache.get(constants.AUTO_MODE) + except (KeyError, ValueError): + logging.exception('Possible cache miss, trying again.') + break + + try: + status = TricklerStatus((auto_mode, motor_on)) + except ValueError: + logging.info('Bad state. auto_mode:%r and motor_on:%r', auto_mode, motor_on) + break + + led_fn = LED_MODES.get(config['leds'][STATUS_MAP[status]]) + if led_fn != last_led_fn: + led_fn(status_led) + last_led_fn = led_fn + time.sleep(1) + + +if __name__ == '__main__': + import argparse + import configparser + + parser = argparse.ArgumentParser(description='Test bluetooth') + parser.add_argument('config_file') + args = parser.parse_args() + + config = configparser.ConfigParser() + config.read_file(open(args.config_file)) + + log_level = logging.INFO + if config['general'].getboolean('verbose'): + log_level = logging.DEBUG + + helpers.setup_logging(log_level) + + if config['leds'].getboolean('enable_status_leds'): + run(config, args) diff --git a/trickler/peripheral/trickler/main.py b/trickler/peripheral/trickler/main.py new file mode 100755 index 0000000..ae2ec7c --- /dev/null +++ b/trickler/peripheral/trickler/main.py @@ -0,0 +1,186 @@ +#!/usr/bin/env python3 +""" +Copyright (c) Ammolytics and contributors. All rights reserved. +Released under the MIT license. See LICENSE file in the project root for details. + +OpenTrickler +https://github.com/ammolytics/projects/tree/develop/trickler +""" + +import datetime +import decimal +import logging +import time + +import constants +import helpers +import PID +import motors +import scales + + +# Components: +# 0. Server (Pi) +# 1. Scale (serial) +# 2. Trickler (gpio/PWM) +# 3. Dump (gpio/servo?) +# 4. API +# 6. Bluetooth? +# 7: Powder pan/cup? + +# TODO +# - document specific python version +# - handle case where scale is booted with pan on -- shows error instead of negative value +# - detect scale that's turned off (blank values) +# - validate inputs (target weight) + + +def trickler_loop(memcache, pid, trickler_motor, scale, target_weight, target_unit, pidtune_logger): + pidtune_logger.info('timestamp, input (motor %), output (weight %)') + logging.info('Starting trickling process...') + + while 1: + # Stop running if auto mode is disabled. + if not memcache.get(constants.AUTO_MODE): + logging.debug('auto mode disabled.') + break + + # Read scale values (weight/unit/stable) + scale.update() + + # Stop running if scale's unit no longer matches target unit. + if scale.unit != target_unit: + logging.debug('Target unit does not match scale unit.') + break + + # Stop running if pan removed. + if scale.weight < 0: + logging.debug('Pan removed.') + break + + remainder_weight = target_weight - scale.weight + logging.debug('remainder_weight: %r', remainder_weight) + + pidtune_logger.info( + '%s, %s, %s', + datetime.datetime.now().timestamp(), + trickler_motor.speed, + scale.weight / target_weight) + + # Trickling complete. + if remainder_weight <= 0: + logging.debug('Trickling complete, motor turned off and PID reset.') + break + + # PID controller requires float value instead of decimal.Decimal + pid.update(float(scale.weight / target_weight) * 100) + trickler_motor.update(pid.output) + logging.debug('trickler_motor.speed: %r, pid.output: %r', trickler_motor.speed, pid.output) + logging.info( + 'remainder: %s %s scale: %s %s motor: %s', + remainder_weight, + target_unit, + scale.weight, + scale.unit, + trickler_motor.speed) + + # Clean up tasks. + trickler_motor.off() + # Clear PID values. + pid.clear() + logging.info('Trickling process stopped.') + + +def main(config, args, pidtune_logger): + memcache = helpers.get_mc_client() + + pid = PID.PID( + float(config['PID']['Kp']), + float(config['PID']['Ki']), + float(config['PID']['Kd'])) + logging.debug('pid: %r', pid) + + trickler_motor = motors.TricklerMotor( + memcache=memcache, + motor_pin=int(config['motors']['trickler_pin']), + min_pwm=int(config['motors']['trickler_min_pwm']), + max_pwm=int(config['motors']['trickler_max_pwm'])) + logging.debug('trickler_motor: %r', trickler_motor) + #servo_motor = gpiozero.AngularServo(int(config['motors']['servo_pin'])) + + scale = scales.SCALES[config['scale']['model']]( + memcache=memcache, + port=config['scale']['port'], + baudrate=int(config['scale']['baudrate']), + timeout=float(config['scale']['timeout'])) + logging.debug('scale: %r', scale) + + memcache.set(constants.AUTO_MODE, args.auto_mode or False) + memcache.set(constants.TARGET_WEIGHT, args.target_weight or decimal.Decimal('0.0')) + memcache.set(constants.TARGET_UNIT, scales.UNIT_MAP.get(args.target_unit, 'GN')) + + while 1: + # Update settings. + auto_mode = memcache.get(constants.AUTO_MODE) + target_weight = memcache.get(constants.TARGET_WEIGHT) + target_unit = memcache.get(constants.TARGET_UNIT) + # Use percentages for PID control to avoid complexity w/ different units of weight. + pid.SetPoint = 100.0 + scale.update() + + # Set scale to match target unit. + if target_unit != scale.unit: + logging.info('scale.unit: %r, target_unit: %r', scale.unit, target_unit) + scale.change_unit() + + logging.info( + 'target: %s %s scale: %s %s auto_mode: %s', + target_weight, + target_unit, + scale.weight, + scale.unit, + auto_mode) + + # Powder pan in place, scale stable, ready to trickle. + if (scale.weight >= 0 and + scale.weight < target_weight and + scale.unit == target_unit and + scale.is_stable and + auto_mode): + # Wait a second to start trickling. + time.sleep(1) + # Run trickler loop. + trickler_loop(memcache, pid, trickler_motor, scale, target_weight, target_unit, pidtune_logger) + + +if __name__ == '__main__': + import argparse + import configparser + + parser = argparse.ArgumentParser(description='Run OpenTrickler.') + parser.add_argument('config_file') + parser.add_argument('--verbose', action='store_true') + parser.add_argument('--auto_mode', action='store_true') + parser.add_argument('--pid_tune', action='store_true') + parser.add_argument('--target_weight', type=decimal.Decimal, default=0) + parser.add_argument('--target_unit', choices=scales.UNIT_MAP.keys(), default='GN') + args = parser.parse_args() + + config = configparser.ConfigParser() + config.read_file(open(args.config_file)) + + log_level = logging.INFO + if args.verbose or config['general'].getboolean('verbose'): + log_level = logging.DEBUG + + helpers.setup_logging(log_level) + + pidtune_logger = logging.getLogger('pid_tune') + pid_handler = logging.StreamHandler() + pid_handler.setFormatter(logging.Formatter('%(message)s')) + + pidtune_logger.setLevel(logging.ERROR) + if args.pid_tune or config['PID'].getboolean('pid_tuner_mode'): + pidtune_logger.setLevel(logging.INFO) + + main(config, args, pidtune_logger) diff --git a/trickler/peripheral/trickler/motors.py b/trickler/peripheral/trickler/motors.py new file mode 100755 index 0000000..6b62b3e --- /dev/null +++ b/trickler/peripheral/trickler/motors.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 +""" +Copyright (c) Ammolytics and contributors. All rights reserved. +Released under the MIT license. See LICENSE file in the project root for details. + +OpenTrickler +https://github.com/ammolytics/projects/tree/develop/trickler +""" + +import atexit +import logging + +import gpiozero # pylint: disable=import-error; + +import constants + + +class TricklerMotor: + """Controls a small vibration DC motor with the PWM controller on the Pi.""" + + def __init__(self, memcache, motor_pin=18, min_pwm=15, max_pwm=100): + """Constructor.""" + self._memcache = memcache + self.pwm = gpiozero.PWMOutputDevice(motor_pin) + self.min_pwm = min_pwm + self.max_pwm = max_pwm + logging.debug( + 'Created pwm motor on PIN %r with min %r and max %r: %r', + motor_pin, + self.min_pwm, + self.max_pwm, + self.pwm) + atexit.register(self._graceful_exit) + + def _graceful_exit(self): + """Graceful exit function, turn off motor and close GPIO pin.""" + logging.debug('Closing trickler motor...') + self.pwm.off() + self.pwm.close() + + def update(self, target_pwm): + """Change PWM speed of motor (int), enforcing clamps.""" + logging.debug('Updating target_pwm to %r', target_pwm) + target_pwm = max(min(int(target_pwm), self.max_pwm), self.min_pwm) + logging.debug('Adjusted clamped target_pwm to %r', target_pwm) + self.set_speed(target_pwm / 100) + + def set_speed(self, speed): + """Sets the PWM speed (float) and circumvents any clamps.""" + # TODO(eric): must be 0 - 1. + logging.debug('Setting speed from %r to %r', self.speed, speed) + self.pwm.value = speed + self._memcache.set(constants.TRICKLER_MOTOR_SPEED, self.speed) + + def off(self): + """Turns motor off.""" + self.set_speed(0) + + @property + def speed(self): + """Returns motor speed (float).""" + return self.pwm.value + + +if __name__ == '__main__': + import argparse + import time + + import helpers + + parser = argparse.ArgumentParser(description='Test motors.') + parser.add_argument('--trickler_motor_pin', type=int, default=18) + #parser.add_argument('--servo_motor_pin', type=int) + parser.add_argument('--max_pwm', type=float, default=100) + parser.add_argument('--min_pwm', type=float, default=15) + args = parser.parse_args() + + memcache_client = helpers.get_mc_client() + + helpers.setup_logging() + + motor = TricklerMotor( + memcache=memcache_client, + motor_pin=args.trickler_motor_pin, + min_pwm=args.min_pwm, + max_pwm=args.max_pwm) + print('Spinning up trickler motor in 3 seconds...') + time.sleep(3) + for x in range(1, 101): + motor.set_speed(x / 100) + time.sleep(.05) + for x in range(100, 0, -1): + motor.set_speed(x / 100) + time.sleep(.05) + motor.off() + print('Done.') diff --git a/trickler/peripheral/trickler/scales.py b/trickler/peripheral/trickler/scales.py new file mode 100755 index 0000000..b49494c --- /dev/null +++ b/trickler/peripheral/trickler/scales.py @@ -0,0 +1,218 @@ +#!/usr/bin/env python3 +""" +Copyright (c) Ammolytics and contributors. All rights reserved. +Released under the MIT license. See LICENSE file in the project root for details. + +OpenTrickler +https://github.com/ammolytics/projects/tree/develop/trickler +""" + +import atexit +import decimal +import enum +import logging +import time + +import serial # pylint: disable=import-error; + +import constants + + +class Units(enum.Enum): + GRAINS = 0 + GRAMS = 1 + + +UNIT_MAP = { + 'GN': Units.GRAINS, + 'g': Units.GRAMS, +} + + +UNIT_REVERSE_MAP = { + Units.GRAINS: 'GN', + Units.GRAMS: 'g', +} + + +def noop(*args, **kwargs): + """No-op function for scales to use on throwaway status updates.""" + return + + +class ANDFx120: + """Class for controlling an A&D FX120 scale.""" + + class ScaleStatusV1(enum.Enum): + """Supports the first version of OpenTrickler software.""" + STABLE = 0 + UNSTABLE = 1 + OVERLOAD = 2 + ERROR = 3 + MODEL_NUMBER = 4 + SERIAL_NUMBER = 5 + ACKNOWLEDGE = 6 + + class ScaleStatusV2(enum.Enum): + """New version avoids zero (0) which can be confused with null/None.""" + STABLE = 1 + UNSTABLE = 2 + OVERLOAD = 3 + ERROR = 4 + MODEL_NUMBER = 5 + SERIAL_NUMBER = 6 + ACKNOWLEDGE = 7 + + def __init__(self, memcache, port='/dev/ttyUSB0', baudrate=19200, timeout=0.1, _version=1, **kwargs): + """Controller.""" + self._memcache = memcache + self._serial = serial.Serial(port=port, baudrate=baudrate, timeout=timeout, **kwargs) + # Set default values, which should be overwritten quickly. + self.raw = b'' + self.unit = Units.GRAINS + self.resolution = decimal.Decimal(0.02) + self.weight = decimal.Decimal('0.00') + + self.StatusMap = self.ScaleStatusV1 + if _version == 2: + self.StatusMap = self.ScaleStatusV2 + + self.status = self.StatusMap.STABLE + self.model_number = None + self.serial_number = None + atexit.register(self._graceful_exit) + + def _graceful_exit(self): + """Graceful exit, closes serial port.""" + logging.debug('Closing serial port...') + self._serial.close() + + def change_unit(self): + """Changes the unit of weight on the scale.""" + logging.debug('changing weight unit on scale from: %r', self.unit) + # Send Mode button command. + self._serial.write(b'U\r\n') + # Sleep 1s and wait for change to take effect. + time.sleep(1) + # Run update fn to set latest values. + self.update() + + @property + def is_stable(self): + """Returns True if the scale is stable, otherwise False.""" + return self.status == self.StatusMap.STABLE + + def update(self): + """Read from the serial port and update an instance of this class with the most recent values.""" + handlers = { + 'ST': self._stable, + 'US': self._unstable, + 'OL': self._overload, + 'EC': self._error, + 'AK': self._acknowledge, + 'TN': self._model_number, + 'SN': self._serial_number, + None: noop, + } + + # Note: The input buffer can fill up, causing latency. Clear it before reading. + self._serial.reset_input_buffer() + raw = self._serial.readline() + self.raw = raw + logging.debug(raw) + try: + line = raw.strip().decode('utf-8') + except UnicodeDecodeError: + logging.debug('Could not decode bytes to unicode.') + else: + status = line[0:2] + handler = handlers.get(status, noop) + handler(line) + + def _stable_unstable(self, line): + """Update the scale when status is stable or unstable.""" + weight = line[3:12].strip() + self.weight = decimal.Decimal(weight) + + unit = line[12:15].strip() + self.unit = UNIT_MAP[unit] + + resolution = {} + resolution[Units.GRAINS] = decimal.Decimal(0.02) + resolution[Units.GRAMS] = decimal.Decimal(0.001) + + self.resolution = resolution[self.unit] + # Update memcache values. + self._memcache.set(constants.SCALE_STATUS, self.status) + self._memcache.set(constants.SCALE_WEIGHT, self.weight) + self._memcache.set(constants.SCALE_UNIT, self.unit) + self._memcache.set(constants.SCALE_RESOLUTION, self.resolution) + self._memcache.set(constants.SCALE_IS_STABLE, self.is_stable) + + def _stable(self, line): + """Scale is stable.""" + self.status = self.StatusMap.STABLE + self._stable_unstable(line) + + def _unstable(self, line): + """Scale is unstable.""" + self.status = self.StatusMap.UNSTABLE + self._stable_unstable(line) + + def _overload(self, line): + """Scale is overloaded.""" + self.status = self.StatusMap.OVERLOAD + self._memcache.set(constants.SCALE_STATUS, self.status) + + def _error(self, line): + """Scale has an error.""" + self.status = self.StatusMap.ERROR + self._memcache.set(constants.SCALE_STATUS, self.status) + + def _acknowledge(self, line): + """Scale has acknowledged a command.""" + self.status = self.StatusMap.ACKNOWLEDGE + self._memcache.set(constants.SCALE_STATUS, self.status) + + def _model_number(self, line): + """Gets & sets the scale's model number.""" + self.status = self.StatusMap.MODEL_NUMBER + self.model_number = line[3:] + + def _serial_number(self, line): + """Gets & sets the scale's serial number.""" + self.status = self.StatusMap.SERIAL_NUMBER + self.serial_number = line[3:] + + +SCALES = { + 'and-fx120': ANDFx120, +} + + +if __name__ == '__main__': + import argparse + + import helpers + + parser = argparse.ArgumentParser(description='Test scale.') + parser.add_argument('--scale', choices=SCALES.keys(), default='and-fx120') + parser.add_argument('--scale_port', default='/dev/ttyUSB0') + parser.add_argument('--scale_baudrate', type=int, default='19200') + parser.add_argument('--scale_timeout', type=float, default='0.1') + parser.add_argument('--scale_version', type=int, default='1') + args = parser.parse_args() + + memcache_client = helpers.get_mc_client() + + helpers.setup_logging() + + scale = SCALES[args.scale]( + port=args.scale_port, + baudrate=args.scale_baudrate, + timeout=args.scale_timeout, + memcache=memcache_client, + _version=args.scale_version) + + while 1: + scale.update()