commit 090a1096902460c294e4b71da7f77c559bbd93ae Author: Relintai Date: Sat Jun 5 16:41:54 2021 +0200 Initial commit. (from the data collector sample app) diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..cb67d39 --- /dev/null +++ b/.clang-format @@ -0,0 +1,128 @@ +# Commented out parameters are those with the same value as base LLVM style +# We can uncomment them if we want to change their value, or enforce the +# chosen value in case the base style changes (last sync: Clang 6.0.1). +--- +### General config, applies to all languages ### +BasedOnStyle: LLVM +AccessModifierOffset: -4 +AlignAfterOpenBracket: DontAlign +# AlignConsecutiveAssignments: false +# AlignConsecutiveDeclarations: false +# AlignEscapedNewlines: Right +# AlignOperands: true +AlignTrailingComments: false +AllowAllParametersOfDeclarationOnNextLine: false +# AllowShortBlocksOnASingleLine: false +AllowShortCaseLabelsOnASingleLine: true +AllowShortFunctionsOnASingleLine: Inline +AllowShortIfStatementsOnASingleLine: true +# AllowShortLoopsOnASingleLine: false +# AlwaysBreakAfterDefinitionReturnType: None +# AlwaysBreakAfterReturnType: None +# AlwaysBreakBeforeMultilineStrings: false +# AlwaysBreakTemplateDeclarations: false +# BinPackArguments: true +# BinPackParameters: true +# BraceWrapping: +# AfterClass: false +# AfterControlStatement: false +# AfterEnum: false +# AfterFunction: false +# AfterNamespace: false +# AfterObjCDeclaration: false +# AfterStruct: false +# AfterUnion: false +# AfterExternBlock: false +# BeforeCatch: false +# BeforeElse: false +# IndentBraces: false +# SplitEmptyFunction: true +# SplitEmptyRecord: true +# SplitEmptyNamespace: true +# BreakBeforeBinaryOperators: None +# BreakBeforeBraces: Attach +# BreakBeforeInheritanceComma: false +BreakBeforeTernaryOperators: false +# BreakConstructorInitializersBeforeComma: false +BreakConstructorInitializers: AfterColon +# BreakStringLiterals: true +ColumnLimit: 0 +# CommentPragmas: '^ IWYU pragma:' +# CompactNamespaces: false +ConstructorInitializerAllOnOneLineOrOnePerLine: true +ConstructorInitializerIndentWidth: 8 +ContinuationIndentWidth: 8 +Cpp11BracedListStyle: false +# DerivePointerAlignment: false +# DisableFormat: false +# ExperimentalAutoDetectBinPacking: false +# FixNamespaceComments: true +# ForEachMacros: +# - foreach +# - Q_FOREACH +# - BOOST_FOREACH +# IncludeBlocks: Preserve +IncludeCategories: + - Regex: '".*"' + Priority: 1 + - Regex: '^<.*\.h>' + Priority: 2 + - Regex: '^<.*' + Priority: 3 +# IncludeIsMainRegex: '(Test)?$' +IndentCaseLabels: true +# IndentPPDirectives: None +IndentWidth: 4 +# IndentWrappedFunctionNames: false +# JavaScriptQuotes: Leave +# JavaScriptWrapImports: true +# KeepEmptyLinesAtTheStartOfBlocks: true +# MacroBlockBegin: '' +# MacroBlockEnd: '' +# MaxEmptyLinesToKeep: 1 +# NamespaceIndentation: None +# PenaltyBreakAssignment: 2 +# PenaltyBreakBeforeFirstCallParameter: 19 +# PenaltyBreakComment: 300 +# PenaltyBreakFirstLessLess: 120 +# PenaltyBreakString: 1000 +# PenaltyExcessCharacter: 1000000 +# PenaltyReturnTypeOnItsOwnLine: 60 +# PointerAlignment: Right +# RawStringFormats: +# - Delimiter: pb +# Language: TextProto +# BasedOnStyle: google +# ReflowComments: true +# SortIncludes: true +# SortUsingDeclarations: true +# SpaceAfterCStyleCast: false +# SpaceAfterTemplateKeyword: true +# SpaceBeforeAssignmentOperators: true +# SpaceBeforeParens: ControlStatements +# SpaceInEmptyParentheses: false +# SpacesBeforeTrailingComments: 1 +# SpacesInAngles: false +# SpacesInContainerLiterals: true +# SpacesInCStyleCastParentheses: false +# SpacesInParentheses: false +# SpacesInSquareBrackets: false +TabWidth: 4 +UseTab: Always +--- +### C++ specific config ### +Language: Cpp +Standard: Cpp03 +--- +### ObjC specific config ### +Language: ObjC +Standard: Cpp03 +ObjCBlockIndentWidth: 4 +# ObjCSpaceAfterProperty: false +# ObjCSpaceBeforeProtocolList: true +--- +### Java specific config ### +Language: Java +# BreakAfterJavaFieldAnnotations: false +JavaImportGroups: ['org.godotengine', 'android', 'androidx', 'com.android', 'com.google', 'java', 'javax'] +... diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..689234b --- /dev/null +++ b/.gitignore @@ -0,0 +1,31 @@ +engine +modules/* +logs/* + +*.d +*.o +*.meta +game/.import/** +game/.prop_tool_temp/** +.sconsign.dblite +.DS_Store + +export/** +release/** + +.vs/* +.kdev4/* +.vscode/* + +TestRWTextures + +_build/* +_binaries/* +game/android/build/* + +*.blend1 +.dir-locals.el + +build.config + +database.sqlite diff --git a/HEADS b/HEADS new file mode 100644 index 0000000..9288942 --- /dev/null +++ b/HEADS @@ -0,0 +1 @@ +{"engine": {"master": "a47cb57da73a27369df2575a432adbeb6f2dd790"}} \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..190891a --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2020 Péter Magyar + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..51baa72 --- /dev/null +++ b/Readme.md @@ -0,0 +1,80 @@ +# Data collection test app for rcpp framework + +Simple data collector app written with rcpp framework for a university project. + +## Compilation + +Will only work on linux! Works on the rasberry pi. + +### Dependencies + +Arch/Manjaro: + +``` +pacman -S --needed scons pkgconf gcc yasm libevent +``` + +Debian/Raspian: + +``` +sudo apt-get install build-essential scons pkg-config libudev-dev yasm libevent libevent-dev +``` + +Optionally if you install MariaDB/MySQL and/or PostgreSQL the compile system should pick it up. Make sure to get a version +whoch contains the development headers (A bunch of .h files). + +### Initial setup + +clone this repo, then call `scons`, it will clone rcpp cms into a new engine directory. Run this every time you update the project. +You don't have to run it before / between builds. + +``` +# git clone https://github.com/Relintai/rcpp_sample_simple_data_collector_app.git rcpp_sample_simple_data_collector_app +# cd school_industrial_comm_2021 +# scons +``` + +Now you can build the project like: `scons bl`. ([b]uild [l]inux) + +Adding -jX to the build command will run the build on that many threads. Like: `scons bl -j4`. + +``` +# scons bl -j4 +- or - +# ./build.sh +``` +Now you can run it. + +First run migrations, this will create the necessary database tables: + +``` +# ./engine/bin/server m +- or - +# ./migrate.sh +``` + +Now you can start the server: + +``` +# ./engine/bin/server +- or - +# ./run.sh +``` + +Make sure to run it from the project's directory, as it needs data files. + +Now just open http://127.0.0.1:8080 + +You can push floats to the "a/b" MQTT topics, and the new values will be save in the `database.sqlite` file, and will appear +in your browser. + +If you have mosqitto installed you can use the `publish_random_data.py` or `publish_data.sh` scripts to automatically +push things into the proper MQTT topic. + +## Structure + +The main Application implementation is `app/ic_application.h`. + +The `main.cpp` contains the initialization code for the framework. + +The `content/www` folder is the wwwroot. diff --git a/SConstruct b/SConstruct new file mode 100644 index 0000000..a8ccf84 --- /dev/null +++ b/SConstruct @@ -0,0 +1,505 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Copyright (c) 2019-2020 Péter Magyar +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +EnsureSConsVersion(0, 98, 1) + +import sys +import os +import subprocess +import json +import shutil +import traceback + + +folders = [ + 'app', +] + +module_folders = [ + '../modules', + '../custom_modules', +] + +main_file = 'main.cpp' + +repository_index = 0 +module_clone_path = '/modules/' +clone_command = 'git clone {0} {1}' + +visual_studio_call_vcvarsall = False +visual_studio_vcvarsall_path = 'C:\\Program Files (x86)\\Microsoft Visual Studio\\2019\\Community\\VC\\Auxiliary\\Build\\vcvarsall.bat' +visual_studio_arch = 'amd64' + +exports = { + 'global': [], + 'linux': [], + 'windows': [], + 'android': [], + 'javascript': [], +} + +engine_repository = [ ['https://github.com/Relintai/rcpp_framework.git', 'git@github.com:Relintai/rcpp_framework.git'], 'engine', '' ] + +module_repositories = [ +] + +addon_repositories = [ +] + +third_party_addon_repositories = [ +] + +target_commits = {} + +engine_branch = 'master' + +def onerror(func, path, exc_info): + """ + https://stackoverflow.com/questions/2656322/shutil-rmtree-fails-on-windows-with-access-is-denied + + Because Windows. + + Error handler for ``shutil.rmtree``. + + If the error is due to an access error (read only file) + it attempts to add write permission and then retries. + + If the error is for another reason it re-raises the error. + + Usage : ``shutil.rmtree(path, onerror=onerror)`` + """ + import stat + if not os.access(path, os.W_OK): + # Is the error an access error ? + os.chmod(path, stat.S_IWUSR) + func(path) + else: + raise + +def load_target_commits_array(): + global target_commits + + if os.path.isfile('./HEADS'): + with open('./HEADS', 'r') as infile: + target_commits = json.load(infile) + else: + target_commits = {} + +def save_target_commits_array(): + with open('./HEADS', 'w') as outfile: + json.dump(target_commits, outfile) + +def update_repository(data, clone_path, branch = 'master'): + cwd = os.getcwd() + + full_path = cwd + clone_path + data[1] + '/' + + if not os.path.isdir(full_path): + os.chdir(cwd + clone_path) + + subprocess.call(clone_command.format(data[0][repository_index], data[1]), shell=True) + + os.chdir(full_path) + + subprocess.call('git reset', shell=True) + subprocess.call('git reset --hard', shell=True) + subprocess.call('git clean -f -d', shell=True) + subprocess.call('git checkout -B ' + branch + ' origin/' + branch, shell=True) + subprocess.call('git reset', shell=True) + subprocess.call('git reset --hard', shell=True) + subprocess.call('git clean -f -d', shell=True) + subprocess.call('git pull origin ' + branch, shell=True) + + process = subprocess.Popen('git rev-parse HEAD', shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + output = process.communicate()[0].decode().strip() + + if data[1] not in target_commits: + target_commits[data[1]] = {} + + target_commits[data[1]][branch] = output + + os.chdir(cwd) + +def setup_repository(data, clone_path, branch = 'master'): + cwd = os.getcwd() + + full_path = cwd + clone_path + data[1] + '/' + + if not os.path.isdir(full_path): + os.chdir(cwd + clone_path) + + subprocess.call(clone_command.format(data[0][repository_index], data[1]), shell=True) + + os.chdir(full_path) + + subprocess.call('git reset', shell=True) + subprocess.call('git reset --hard', shell=True) + subprocess.call('git clean -f -d', shell=True) + subprocess.call('git checkout -B ' + branch + ' origin/' + branch, shell=True) + subprocess.call('git pull origin ' + branch, shell=True) + subprocess.call('git reset', shell=True) + subprocess.call('git reset --hard', shell=True) + + if data[1] in target_commits: + target = target_commits[data[1]][branch] + + subprocess.call('git checkout -B ' + branch + ' ' + target, shell=True) + subprocess.call('git clean -f -d', shell=True) + subprocess.call('git reset', shell=True) + subprocess.call('git reset --hard', shell=True) + + os.chdir(cwd) + +def copy_repository(data, target_folder, clone_path): + copytree(os.path.abspath(clone_path + data[1] + '/' + data[2]), os.path.abspath(target_folder + data[1])) + +def copytree(src, dst): + for item in os.listdir(src): + sp = os.path.join(src, item) + dp = os.path.join(dst, item) + + if os.path.isdir(sp): + if os.path.isdir(dp): + shutil.rmtree(dp, onerror=onerror) + + shutil.copytree(sp, dp) + else: + if not os.path.isdir(dst): + os.makedirs(dst) + + shutil.copy2(sp, dp) + +def update_engine(): + update_repository(engine_repository, '/', engine_branch) + +def update_modules(): + for rep in module_repositories: + update_repository(rep, module_clone_path) + copy_repository(rep, './engine/modules/', '.' + module_clone_path) + +def update_addons(): + for rep in addon_repositories: + update_repository(rep, module_clone_path) + copy_repository(rep, './game/addons/', '.' + module_clone_path) + +def update_addons_third_party_addons(): + for rep in third_party_addon_repositories: + update_repository(rep, module_clone_path) + copy_repository(rep, './game/addons/', '.' + module_clone_path) + +def update_all(): + update_engine() + update_modules() + update_addons() + update_addons_third_party_addons() + + save_target_commits_array() + + +def setup_engine(): + setup_repository(engine_repository, '/', engine_branch) + +def setup_modules(): + for rep in module_repositories: + setup_repository(rep, module_clone_path) + copy_repository(rep, './engine/modules/', '.' + module_clone_path) + +def setup_addons(): + for rep in addon_repositories: + setup_repository(rep, module_clone_path) + copy_repository(rep, './game/addons/', '.' + module_clone_path) + +def setup_addons_third_party_addons(): + for rep in third_party_addon_repositories: + setup_repository(rep, module_clone_path) + copy_repository(rep, './game/addons/', '.' + module_clone_path) + +def setup_all(): + setup_engine() + setup_modules() + setup_addons() + setup_addons_third_party_addons() + +def format_path(path): + if 'win' in sys.platform: + path = path.replace('/', '\\') + path = path.replace('~', '%userprofile%') + + return path + +def get_exports_for(platform): + export_command = 'export ' + command_separator = ';' + + if 'win' in sys.platform: + command_separator = '&' + export_command = 'set ' + + command = '' + + for p in exports[platform]: + command += export_command + p + command_separator + + return command + + +def parse_config(): + global visual_studio_vcvarsall_path + global visual_studio_arch + global visual_studio_call_vcvarsall + global exports + + if not os.path.isfile('build.config'): + return + + with open('build.config', 'r') as f: + + for line in f: + ls = line.strip() + if ls == '' or ls.startswith('#'): + continue + + words = line.split() + + if (len(words) < 2): + print('This build.config line is malformed, and got ignored: ' + ls) + continue + + if words[0] == 'visual_studio_vcvarsall_path': + visual_studio_vcvarsall_path = format_path(ls[29:]) + elif words[0] == 'visual_studio_arch': + visual_studio_arch = format_path(ls[19:]) + elif words[0] == 'visual_studio_call_vcvarsall': + visual_studio_call_vcvarsall = words[1].lower() in [ 'true', '1', 't', 'y', 'yes' ] + elif words[0] == 'export': + if (len(words) < 3) or not words[1] in exports: + print('This build.config line is malformed, and got ignored: ' + ls) + continue + + export_path = format_path(ls[8 + len(words[1]):]) + + exports[words[1]].append(export_path) + +parse_config() + +env = Environment() + +if len(sys.argv) > 1: + + arg = sys.argv[1] + + arg_split = arg.split('_') + arg = arg_split[0] + arg_split = arg_split[1:] + + if arg[0] == 'b': + build_string = get_exports_for('global') + 'scons ' + + build_string += 'target=' + if 'r' in arg: + build_string += 'release' + elif 'd' in arg: + build_string += 'debug' + else: + build_string += 'release_debug' + build_string += ' ' + + if 'm' in arg: + build_string += 'use_mingw=yes' + else: + if 'win' in sys.platform and visual_studio_call_vcvarsall: + build_string = 'call "{0}" {1}&'.format(visual_studio_vcvarsall_path, visual_studio_arch) + build_string + + if 'o' in arg: + build_string += 'use_llvm=yes' + + if 'v' in arg: + build_string += 'vsproj=yes' + + build_string += 'folders="' + + for f in folders: + build_string += '../' + f + build_string += ';' + + build_string += '" ' + + build_string += 'module_folders="' + + for f in module_folders: + build_string += f + build_string += ';' + + build_string += '" ' + + build_string += 'main_file="../' + main_file + '" ' + + for i in range(2, len(sys.argv)): + build_string += ' ' + sys.argv[i] + ' ' + + cwd = os.getcwd() + full_path = cwd + '/engine/' + + if not os.path.isdir(full_path): + print('engine directory doesnt exists.') + exit() + + os.chdir(full_path) + + if 'l' in arg: + build_string += 'platform=x11' + + build_string = get_exports_for('linux') + build_string + + print('Running command: ' + build_string) + + subprocess.call(build_string, shell=True) + elif 'w' in arg: + build_string += 'platform=windows' + + build_string = get_exports_for('windows') + build_string + + print('Running command: ' + build_string) + + subprocess.call(build_string, shell=True) + elif 'a' in arg: + build_string += 'platform=android' + + build_string = get_exports_for('android') + build_string + + print('Running command: ' + build_string + ' android_arch=armv7') + subprocess.call(build_string + ' android_arch=armv7', shell=True) + print('Running command: ' + build_string + ' android_arch=arm64v8') + subprocess.call(build_string + ' android_arch=arm64v8', shell=True) + print('Running command: ' + build_string + ' android_arch=x86') + subprocess.call(build_string + ' android_arch=x86', shell=True) + + os.chdir(full_path + 'platform/android/java/') + + print('Running command: ' + get_exports_for('global') + get_exports_for('android') + './gradlew generateGodotTemplates') + subprocess.call(get_exports_for('global') + get_exports_for('android') + './gradlew generateGodotTemplates', shell=True) + elif 'j' in arg: + build_string += 'platform=javascript' + + build_string = get_exports_for('javascript') + build_string + + print('Running command: ' + build_string) + subprocess.call(build_string, shell=True) + + else: + print('No platform specified') + exit() + + exit() + elif arg[0] == 'p': + if arg == 'p': + #print("Applies a patch. Append c for the compilation database patch. For example: pc") + print("Applies a patch. No Patches right now.") + exit() + + cwd = os.getcwd() + full_path = cwd + '/engine/' + + if not os.path.isdir(full_path): + print('engine directory doesnt exists.') + exit() + + os.chdir(full_path) + + #apply the patch to just the working directory, without creating a commit + + #if 'c' in arg: + # subprocess.call('git apply --index ../patches/compilation_db.patch', shell=True) + + #unstage all files + subprocess.call('git reset', shell=True) + + exit() + +opts = Variables(args=ARGUMENTS) + +opts.Add('a', 'What to do', '') +opts.Add(EnumVariable('action', 'What to do', 'setup', ('setup', 'update'))) +opts.Add('t', 'Action target', '') +opts.Add(EnumVariable('target', 'Action target', 'all', ('all', 'engine', 'modules', 'all_addons', 'addons', 'third_party_addons'))) +opts.Add(EnumVariable('repository_type', 'Type of repositories to clone from first', 'http', ('http', 'ssh'))) + +opts.Update(env) +Help(opts.GenerateHelpText(env)) + +load_target_commits_array() + +rt = env['repository_type'] + +if rt == 'ssh': + repository_index = 1 + +action = env['action'] +target = env['target'] + +if env['a']: + action = env['a'] + +if env['t']: + target = env['t'] + +if not os.path.isdir('./modules'): + os.mkdir('./modules') + +if 'm' in action: + godot_branch = 'master' + +if 'setup' in action or action[0] == 's': + if target == 'all': + setup_all() + elif target == 'engine': + setup_engine() + elif target == 'modules': + setup_modules() + elif target == 'all_addons': + setup_addons() + setup_addons_third_party_addons() + elif target == 'addons': + setup_addons() + elif target == 'third_party_addons': + setup_addons_third_party_addons() +elif 'update' in action or action[0] == 'u': + if target == 'all': + update_all() + elif target == 'engine': + update_engine() + save_target_commits_array() + elif target == 'modules': + update_modules() + save_target_commits_array() + elif target == 'all_addons': + update_addons() + update_addons_third_party_addons() + save_target_commits_array() + elif target == 'addons': + update_addons() + save_target_commits_array() + elif target == 'third_party_addons': + update_addons_third_party_addons() + save_target_commits_array() + diff --git a/app/ic_application.cpp b/app/ic_application.cpp new file mode 100644 index 0000000..74c9a43 --- /dev/null +++ b/app/ic_application.cpp @@ -0,0 +1,151 @@ +#include "ic_application.h" + +#include +#include + +#include + +#include "core/database/database_manager.h" +#include "core/file_cache.h" +#include "core/http/handler_instance.h" +#include "core/html/html_builder.h" +#include "core/database/query_result.h" +#include "core/http/request.h" +#include "core/utils.h" + +void ICApplication::index(Object *instance, Request *request) { + if (FileCache::get_singleton()->wwwroot_has_file("/index.html")) { + std::string fp = FileCache::get_singleton()->wwwroot + "/index.html"; + + request->send_file(fp); + + return; + } + + request->send_error(404); +} + +void ICApplication::get_sensor_data(Object *instance, Request *request) { + std::string sql = "SELECT * FROM sensor_data;"; + + QueryResult *res = DatabaseManager::get_singleton()->ddb->query(sql); + + std::string json; + + json += "["; + + bool first = true; + while (res->next_row()) { + if (first) { + first = false; + } else { + json += ","; + } + + json += "{"; + + json += "\"id\":" + std::string(res->get_cell(0)) + ","; + json += "\"client_id\":\"" + std::string(res->get_cell(1)) + "\","; + json += "\"measurement\":" + std::string(res->get_cell(2)); + + json += "}"; + } + + json += "]"; + + request->response->setContentType("application/json"); + request->response->setBody(json); + request->send(); +} + +void ICApplication::app_docs_page(Object *instance, Request *request) { + request->response->setBody(app_docs); + request->send(); +} + +void ICApplication::engine_docs_page(Object *instance, Request *request) { + request->response->setBody(engine_docs); + request->send(); +} + +void ICApplication::setup_routes() { + WebApplication::setup_routes(); + + index_func = HandlerInstance(index); + + main_route_map["sensor_data"] = HandlerInstance(get_sensor_data); + main_route_map["app_docs"] = HandlerInstance(app_docs_page); + main_route_map["engine_docs"] = HandlerInstance(engine_docs_page); +} + +void ICApplication::setup_middleware() { + WebApplication::setup_middleware(); + + //middlewares.push_back(ICApplication::session_middleware_func); +} + +void ICApplication::migrate() { + std::string sql = "CREATE TABLE sensor_data(" + "id INTEGER PRIMARY KEY AUTOINCREMENT," + "client_id TEXT NOT NULL," + "measurement REAL);"; + + DatabaseManager::get_singleton()->ddb->query_run(sql); +} + +void ICApplication::mqtt_sensor_callback(const std::string &client_id, const std::vector &data) { + if (client_id != "1") { + return; + } + + if (data.size() == 0) { + return; + } + + std::string d; + d.reserve(data.size()); + + for (int i = 0; i < data.size(); ++i) { + d += data[i]; + } + + //IMPORTANT: SQL INJECTION WILL WORK ON THIS, IF YOU DON"T FILTER THE CLINET IDS!!!! No prepared statement support yet! :( + std::string sql = "INSERT INTO sensor_data (client_id, measurement)" + "VALUES ('" + + client_id + "'," + d + ");"; + + DatabaseManager::get_singleton()->ddb->query_run(sql); +} + +void ICApplication::load_md(const std::string &file_name, std::string *str) { + FILE *f = fopen(file_name.c_str(), "r"); + + if (!f) { + printf("ICApplication::load_md: Error opening file!\n"); + return; + } + + fseek(f, 0, SEEK_END); + long fsize = ftell(f); + fseek(f, 0, SEEK_SET); /* same as rewind(f); */ + + str->resize(fsize); + + fread(&(*str)[0], 1, fsize, f); + fclose(f); + + Utils::markdown_to_html(str); +} + +ICApplication::ICApplication() : + WebApplication() { + + load_md("./engine/Readme.md", &engine_docs); + load_md("./Readme.md", &app_docs); +} + +ICApplication::~ICApplication() { +} + +std::string ICApplication::engine_docs; +std::string ICApplication::app_docs; \ No newline at end of file diff --git a/app/ic_application.h b/app/ic_application.h new file mode 100644 index 0000000..14de151 --- /dev/null +++ b/app/ic_application.h @@ -0,0 +1,37 @@ +#ifndef IC_APPLICATION_H +#define IC_APPLICATION_H + +#include + +#include "core/http/web_application.h" +#include "core/object.h" + +#include "modules/message_page/message_page.h" +#include "modules/list_page/list_page.h" +#include "modules/paged_article/paged_article.h" + +class ICApplication : public WebApplication { +public: + static void index(Object *instance, Request *request); + static void get_sensor_data(Object *instance, Request *request); + + static void app_docs_page(Object *instance, Request *request); + static void engine_docs_page(Object *instance, Request *request); + + virtual void setup_routes(); + virtual void setup_middleware(); + + virtual void migrate(); + + void mqtt_sensor_callback(const std::string &client_id, const std::vector &data); + + void load_md(const std::string &file_name, std::string *str); + + ICApplication(); + ~ICApplication(); + + static std::string engine_docs; + static std::string app_docs; +}; + +#endif \ No newline at end of file diff --git a/build.config.example b/build.config.example new file mode 100644 index 0000000..317567f --- /dev/null +++ b/build.config.example @@ -0,0 +1,44 @@ +# Copyright (c) 2019-2020 Péter Magyar +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +# Rename this file to build.config to use it + +# Lines starting with # are comments. (Only works at the start of the line!) + +# Note: +# ~ will be converted to %userprofile% on windows +# / will be converted to \ on windows +# so you don't have to worry about it + +# Visual studio related setup: +visual_studio_call_vcvarsall True +visual_studio_vcvarsall_path C:/Program Files (x86)/Microsoft Visual Studio/2019/Community/VC/Auxiliary/Build/vcvarsall.bat +visual_studio_arch amd64 + +# export related setup +# available export targets: global, linux, windows, android, javascript + +export global SCONS_CACHE=~/.scons_cache +export global SCONS_CACHE_LIMIT=5000 + +export android ANDROID_NDK_ROOT=~/SDKs/Android/NDK/android-ndk-r20b +export android ANDROID_NDK_HOME=~/SDKs/Android/NDK/android-ndk-r20b +export android ANDROID_HOME=~/SDKs/Android/SDK + diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..8830c0c --- /dev/null +++ b/build.sh @@ -0,0 +1 @@ +scons bl -j4 \ No newline at end of file diff --git a/content/www/css/main.css b/content/www/css/main.css new file mode 100644 index 0000000..09fe427 --- /dev/null +++ b/content/www/css/main.css @@ -0,0 +1,130 @@ +body { + padding: 0; + margin: 0; + background-color: rgb(255, 255, 255); +} + +a:visited { + color: blue; +} + +header { + color: white; + margin: 0; + padding: 0.8em 0.8em 0.8em 0.8em; + font-size: 24; + background-color: rgb(26, 29, 31); +} + +.header_link a { + color: rgb(129, 186, 240); + font-size: 18; +} + +.header_link { + font-size: 18; + margin: 0em 1em; +} + +.menu_active { + background-color: rgb(89, 21, 13); +} + +ul.menu { + background-color: rgb(46, 45, 45); + padding: 1em 1em 1em 1em; + margin: 0; +} + +ul.menu li { + display: inline; + color: white; + padding: 0em 0.3em; +} + +ul.menu li a { + color: white; + text-align: center; + padding: 1em 1em; + text-decoration: none; +} + +ul.menu li a:hover { + background-color: rgb(60, 60, 60); +} + +.content { + background-color: rgb(255, 255, 255); + margin: 1em 6em; + background-color: rgb(240, 240, 240); +} + +.inner_content { + padding: 1em 1em 0em 1em; + background-color: rgb(240, 240, 240); +} + +.list_entry { + margin: 0.5em 0em; + padding: 0.5em 0.5em; + border: 1px solid rgb(100, 100, 100); + border-radius: 3px; + background-color: rgb(255, 255, 255); +} + +footer { + border-top: 1px solid rgb(107, 107, 107); + text-align: center; + font-size: 12; + margin-top: 2em; + padding-top: 1em; + padding-bottom: 1em; + background-color: rgb(36, 40, 48); + color: white; +} + +footer a { + color: rgb(129, 186, 240); +} + +footer a:visited { + color: rgb(129, 186, 240); +} + +ul.pagination { + padding: 1em 1em 1em 1em; + margin: 0; + text-align: center; +} + +ul.pagination li { + display: inline; + border: 1px solid black; + padding: 0.2em 0em; + margin: 0em 0.2em 0em 0.2em; +} + +ul.pagination li a { + color: black; + text-align: center; + text-decoration: none; + padding: 0.2em 0.4em; +} + +ul.pagination li.disabled { + color: rgb(138, 138, 138); + text-align: center; + text-decoration: none; + padding: 0.2em 0.4em; +} + +ul.pagination li a:hover { + background-color: rgb(60, 60, 60); + color: white; +} + +.chart { + margin-left: auto; + margin-right: auto; + width: 800px; +} \ No newline at end of file diff --git a/content/www/index.html b/content/www/index.html new file mode 100644 index 0000000..5ecea16 --- /dev/null +++ b/content/www/index.html @@ -0,0 +1,26 @@ + + + + + + + + + + + +
+ +
+ + + + + + + + + \ No newline at end of file diff --git a/content/www/js/chart.min.js b/content/www/js/chart.min.js new file mode 100644 index 0000000..6f839dd --- /dev/null +++ b/content/www/js/chart.min.js @@ -0,0 +1,13 @@ +/*! + * Chart.js v3.2.1 + * https://www.chartjs.org + * (c) 2021 Chart.js Contributors + * Released under the MIT License + */ +!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).Chart=e()}(this,(function(){"use strict";const t="undefined"==typeof window?function(t){return t()}:window.requestAnimationFrame;function e(e,i,n){const o=n||(t=>Array.prototype.slice.call(t));let s=!1,a=[];return function(...n){a=o(n),s||(s=!0,t.call(window,(()=>{s=!1,e.apply(i,a)})))}}function i(t,e){let i;return function(){return e?(clearTimeout(i),i=setTimeout(t,e)):t(),e}}const n=t=>"start"===t?"left":"end"===t?"right":"center",o=(t,e,i)=>"start"===t?e:"end"===t?i:(e+i)/2,s=(t,e,i)=>"right"===t?i:"center"===t?(e+i)/2:e;var a=new class{constructor(){this._request=null,this._charts=new Map,this._running=!1,this._lastDate=void 0}_notify(t,e,i,n){const o=e.listeners[n],s=e.duration;o.forEach((n=>n({chart:t,initial:e.initial,numSteps:s,currentStep:Math.min(i-e.start,s)})))}_refresh(){const e=this;e._request||(e._running=!0,e._request=t.call(window,(()=>{e._update(),e._request=null,e._running&&e._refresh()})))}_update(t=Date.now()){const e=this;let i=0;e._charts.forEach(((n,o)=>{if(!n.running||!n.items.length)return;const s=n.items;let a,r=s.length-1,l=!1;for(;r>=0;--r)a=s[r],a._active?(a._total>n.duration&&(n.duration=a._total),a.tick(t),l=!0):(s[r]=s[s.length-1],s.pop());l&&(o.draw(),e._notify(o,n,t,"progress")),s.length||(n.running=!1,e._notify(o,n,t,"complete"),n.initial=!1),i+=s.length})),e._lastDate=t,0===i&&(e._running=!1)}_getAnims(t){const e=this._charts;let i=e.get(t);return i||(i={running:!1,initial:!0,items:[],listeners:{complete:[],progress:[]}},e.set(t,i)),i}listen(t,e,i){this._getAnims(t).listeners[e].push(i)}add(t,e){e&&e.length&&this._getAnims(t).items.push(...e)}has(t){return this._getAnims(t).items.length>0}start(t){const e=this._charts.get(t);e&&(e.running=!0,e.start=Date.now(),e.duration=e.items.reduce(((t,e)=>Math.max(t,e._duration)),0),this._refresh())}running(t){if(!this._running)return!1;const e=this._charts.get(t);return!!(e&&e.running&&e.items.length)}stop(t){const e=this._charts.get(t);if(!e||!e.items.length)return;const i=e.items;let n=i.length-1;for(;n>=0;--n)i[n].cancel();e.items=[],this._notify(t,e,Date.now(),"complete")}remove(t){return this._charts.delete(t)}}; +/*! + * @kurkle/color v0.1.9 + * https://github.com/kurkle/color#readme + * (c) 2020 Jukka Kurkela + * Released under the MIT License + */const r={0:0,1:1,2:2,3:3,4:4,5:5,6:6,7:7,8:8,9:9,A:10,B:11,C:12,D:13,E:14,F:15,a:10,b:11,c:12,d:13,e:14,f:15},l="0123456789ABCDEF",c=t=>l[15&t],h=t=>l[(240&t)>>4]+l[15&t],d=t=>(240&t)>>4==(15&t);function u(t){var e=function(t){return d(t.r)&&d(t.g)&&d(t.b)&&d(t.a)}(t)?c:h;return t?"#"+e(t.r)+e(t.g)+e(t.b)+(t.a<255?e(t.a):""):t}function f(t){return t+.5|0}const g=(t,e,i)=>Math.max(Math.min(t,i),e);function p(t){return g(f(2.55*t),0,255)}function m(t){return g(f(255*t),0,255)}function x(t){return g(f(t/2.55)/100,0,1)}function b(t){return g(f(100*t),0,100)}const _=/^rgba?\(\s*([-+.\d]+)(%)?[\s,]+([-+.e\d]+)(%)?[\s,]+([-+.e\d]+)(%)?(?:[\s,/]+([-+.e\d]+)(%)?)?\s*\)$/;const y=/^(hsla?|hwb|hsv)\(\s*([-+.e\d]+)(?:deg)?[\s,]+([-+.e\d]+)%[\s,]+([-+.e\d]+)%(?:[\s,]+([-+.e\d]+)(%)?)?\s*\)$/;function v(t,e,i){const n=e*Math.min(i,1-i),o=(e,o=(e+t/30)%12)=>i-n*Math.max(Math.min(o-3,9-o,1),-1);return[o(0),o(8),o(4)]}function w(t,e,i){const n=(n,o=(n+t/60)%6)=>i-i*e*Math.max(Math.min(o,4-o,1),0);return[n(5),n(3),n(1)]}function M(t,e,i){const n=v(t,1,.5);let o;for(e+i>1&&(o=1/(e+i),e*=o,i*=o),o=0;o<3;o++)n[o]*=1-e-i,n[o]+=e;return n}function k(t){const e=t.r/255,i=t.g/255,n=t.b/255,o=Math.max(e,i,n),s=Math.min(e,i,n),a=(o+s)/2;let r,l,c;return o!==s&&(c=o-s,l=a>.5?c/(2-o-s):c/(o+s),r=o===e?(i-n)/c+(i>16&255,s>>8&255,255&s]}return t}(),T.transparent=[0,0,0,0]);const e=T[t.toLowerCase()];return e&&{r:e[0],g:e[1],b:e[2],a:4===e.length?e[3]:255}}function L(t,e,i){if(t){let n=k(t);n[e]=Math.max(0,Math.min(n[e]+n[e]*i,0===e?360:1)),n=P(n),t.r=n[0],t.g=n[1],t.b=n[2]}}function E(t,e){return t?Object.assign(e||{},t):t}function I(t){var e={r:0,g:0,b:0,a:255};return Array.isArray(t)?t.length>=3&&(e={r:t[0],g:t[1],b:t[2],a:255},t.length>3&&(e.a=m(t[3]))):(e=E(t,{r:0,g:0,b:0,a:1})).a=m(e.a),e}function z(t){return"r"===t.charAt(0)?function(t){const e=_.exec(t);let i,n,o,s=255;if(e){if(e[7]!==i){const t=+e[7];s=255&(e[8]?p(t):255*t)}return i=+e[1],n=+e[3],o=+e[5],i=255&(e[2]?p(i):i),n=255&(e[4]?p(n):n),o=255&(e[6]?p(o):o),{r:i,g:n,b:o,a:s}}}(t):C(t)}class F{constructor(t){if(t instanceof F)return t;const e=typeof t;let i;var n,o,s;"object"===e?i=I(t):"string"===e&&(s=(n=t).length,"#"===n[0]&&(4===s||5===s?o={r:255&17*r[n[1]],g:255&17*r[n[2]],b:255&17*r[n[3]],a:5===s?17*r[n[4]]:255}:7!==s&&9!==s||(o={r:r[n[1]]<<4|r[n[2]],g:r[n[3]]<<4|r[n[4]],b:r[n[5]]<<4|r[n[6]],a:9===s?r[n[7]]<<4|r[n[8]]:255})),i=o||R(t)||z(t)),this._rgb=i,this._valid=!!i}get valid(){return this._valid}get rgb(){var t=E(this._rgb);return t&&(t.a=x(t.a)),t}set rgb(t){this._rgb=I(t)}rgbString(){return this._valid?(t=this._rgb)&&(t.a<255?`rgba(${t.r}, ${t.g}, ${t.b}, ${x(t.a)})`:`rgb(${t.r}, ${t.g}, ${t.b})`):this._rgb;var t}hexString(){return this._valid?u(this._rgb):this._rgb}hslString(){return this._valid?function(t){if(!t)return;const e=k(t),i=e[0],n=b(e[1]),o=b(e[2]);return t.a<255?`hsla(${i}, ${n}%, ${o}%, ${x(t.a)})`:`hsl(${i}, ${n}%, ${o}%)`}(this._rgb):this._rgb}mix(t,e){const i=this;if(t){const n=i.rgb,o=t.rgb;let s;const a=e===s?.5:e,r=2*a-1,l=n.a-o.a,c=((r*l==-1?r:(r+l)/(1+r*l))+1)/2;s=1-c,n.r=255&c*n.r+s*o.r+.5,n.g=255&c*n.g+s*o.g+.5,n.b=255&c*n.b+s*o.b+.5,n.a=a*n.a+(1-a)*o.a,i.rgb=n}return i}clone(){return new F(this.rgb)}alpha(t){return this._rgb.a=m(t),this}clearer(t){return this._rgb.a*=1-t,this}greyscale(){const t=this._rgb,e=f(.3*t.r+.59*t.g+.11*t.b);return t.r=t.g=t.b=e,this}opaquer(t){return this._rgb.a*=1+t,this}negate(){const t=this._rgb;return t.r=255-t.r,t.g=255-t.g,t.b=255-t.b,this}lighten(t){return L(this._rgb,2,t),this}darken(t){return L(this._rgb,2,-t),this}saturate(t){return L(this._rgb,1,t),this}desaturate(t){return L(this._rgb,1,-t),this}rotate(t){return function(t,e){var i=k(t);i[0]=D(i[0]+e),i=P(i),t.r=i[0],t.g=i[1],t.b=i[2]}(this._rgb,t),this}}function V(t){return new F(t)}const B=t=>t instanceof CanvasGradient||t instanceof CanvasPattern;function W(t){return B(t)?t:V(t)}function H(t){return B(t)?t:V(t).saturate(.5).darken(.1).hexString()}function N(){}const j=function(){let t=0;return function(){return t++}}();function $(t){return null==t}function Y(t){if(Array.isArray&&Array.isArray(t))return!0;const e=Object.prototype.toString.call(t);return"[object"===e.substr(0,7)&&"Array]"===e.substr(-6)}function U(t){return null!==t&&"[object Object]"===Object.prototype.toString.call(t)}const X=t=>("number"==typeof t||t instanceof Number)&&isFinite(+t);function q(t,e){return X(t)?t:e}function K(t,e){return void 0===t?e:t}const G=(t,e)=>"string"==typeof t&&t.endsWith("%")?parseFloat(t)/100:t/e,Z=(t,e)=>"string"==typeof t&&t.endsWith("%")?parseFloat(t)/100*e:+t;function Q(t,e,i){if(t&&"function"==typeof t.call)return t.apply(i,e)}function J(t,e,i,n){let o,s,a;if(Y(t))if(s=t.length,n)for(o=s-1;o>=0;o--)e.call(i,t[o],o);else for(o=0;oi;)t=t[e.substr(i,n-i)],i=n+1,n=rt(e,i);return t}function ct(t){return t.charAt(0).toUpperCase()+t.slice(1)}const ht=t=>void 0!==t,dt=t=>"function"==typeof t,ut=(t,e)=>{if(t.size!==e.size)return!1;for(const i of t)if(!e.has(i))return!1;return!0},ft=Object.create(null),gt=Object.create(null);function pt(t,e){if(!e)return t;const i=e.split(".");for(let e=0,n=i.length;et.chart.platform.getDevicePixelRatio(),this.elements={},this.events=["mousemove","mouseout","click","touchstart","touchmove"],this.font={family:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",size:12,style:"normal",lineHeight:1.2,weight:null},this.hover={},this.hoverBackgroundColor=(t,e)=>H(e.backgroundColor),this.hoverBorderColor=(t,e)=>H(e.borderColor),this.hoverColor=(t,e)=>H(e.color),this.indexAxis="x",this.interaction={mode:"nearest",intersect:!0},this.maintainAspectRatio=!0,this.onHover=null,this.onClick=null,this.parsing=!0,this.plugins={},this.responsive=!0,this.scale=void 0,this.scales={},this.showLine=!0,this.describe(t)}set(t,e){return mt(this,t,e)}get(t){return pt(this,t)}describe(t,e){return mt(gt,t,e)}override(t,e){return mt(ft,t,e)}route(t,e,i,n){const o=pt(this,t),s=pt(this,i),a="_"+e;Object.defineProperties(o,{[a]:{value:o[e],writable:!0},[e]:{enumerable:!0,get(){const t=this[a],e=s[n];return U(t)?Object.assign({},e,t):K(t,e)},set(t){this[a]=t}}})}}({_scriptable:t=>!t.startsWith("on"),_indexable:t=>"events"!==t,hover:{_fallback:"interaction"},interaction:{_scriptable:!1,_indexable:!1}});const bt=Math.PI,_t=2*bt,yt=_t+bt,vt=Number.POSITIVE_INFINITY,wt=bt/180,Mt=bt/2,kt=bt/4,St=2*bt/3,Pt=Math.log10,Dt=Math.sign;function Ct(t){const e=Math.pow(10,Math.floor(Pt(t))),i=t/e;return(i<=1?1:i<=2?2:i<=5?5:10)*e}function Ot(t){const e=[],i=Math.sqrt(t);let n;for(n=1;nt-e)).pop(),e}function At(t){return!isNaN(parseFloat(t))&&isFinite(t)}function Tt(t,e,i){return Math.abs(t-e)=t}function Lt(t,e,i){let n,o,s;for(n=0,o=t.length;nr&&ln&&(n=s),n}function Ut(t,e,i,n){let o=(n=n||{}).data=n.data||{},s=n.garbageCollect=n.garbageCollect||[];n.font!==e&&(o=n.data={},s=n.garbageCollect=[],n.font=e),t.save(),t.font=e;let a=0;const r=i.length;let l,c,h,d,u;for(l=0;li.length){for(l=0;l0&&t.stroke()}}function Gt(t,e,i){return i=i||.5,t&&t.x>e.left-i&&t.xe.top-i&&t.y0&&""!==s.strokeColor;let l,c;for(t.save(),s.translation&&t.translate(s.translation[0],s.translation[1]),$(s.rotation)||t.rotate(s.rotation),t.font=o.string,s.color&&(t.fillStyle=s.color),s.textAlign&&(t.textAlign=s.textAlign),s.textBaseline&&(t.textBaseline=s.textBaseline),l=0;lt[i]1;)n=s+o>>1,i(n)?s=n:o=n;return{lo:s,hi:o}}const oe=(t,e,i)=>ne(t,i,(n=>t[n][e]ne(t,i,(n=>t[n][e]>=i));function ae(t,e,i){let n=0,o=t.length;for(;nn&&t[o-1]>i;)o--;return n>0||o{const i="_onData"+ct(e),n=t[e];Object.defineProperty(t,e,{configurable:!0,enumerable:!1,value(...e){const o=n.apply(this,e);return t._chartjs.listeners.forEach((t=>{"function"==typeof t[i]&&t[i](...e)})),o}})})))}function ce(t,e){const i=t._chartjs;if(!i)return;const n=i.listeners,o=n.indexOf(e);-1!==o&&n.splice(o,1),n.length>0||(re.forEach((e=>{delete t[e]})),delete t._chartjs)}function he(t){const e=new Set;let i,n;for(i=0,n=t.length;i{o.push(t)})),o}function de(t){let e=t.parentNode;return e&&"[object ShadowRoot]"===e.toString()&&(e=e.host),e}function ue(t,e,i){let n;return"string"==typeof t?(n=parseInt(t,10),-1!==t.indexOf("%")&&(n=n/100*e.parentNode[i])):n=t,n}const fe=t=>window.getComputedStyle(t,null);function ge(t,e){return fe(t).getPropertyValue(e)}const pe=["top","right","bottom","left"];function me(t,e,i){const n={};i=i?"-"+i:"";for(let o=0;o<4;o++){const s=pe[o];n[s]=parseFloat(t[e+"-"+s+i])||0}return n.width=n.left+n.right,n.height=n.top+n.bottom,n}function xe(t,e){const{canvas:i,currentDevicePixelRatio:n}=e,o=fe(i),s="border-box"===o.boxSizing,a=me(o,"padding"),r=me(o,"border","width"),{x:l,y:c,box:h}=function(t,e){const i=t.native||t,n=i.touches,o=n&&n.length?n[0]:i,{offsetX:s,offsetY:a}=o;let r,l,c=!1;if(((t,e,i)=>(t>0||e>0)&&(!i||!i.shadowRoot))(s,a,i.target))r=s,l=a;else{const t=e.getBoundingClientRect();r=o.clientX-t.left,l=o.clientY-t.top,c=!0}return{x:r,y:l,box:c}}(t,i),d=a.left+(h&&r.left),u=a.top+(h&&r.top);let{width:f,height:g}=e;return s&&(f-=a.width+r.width,g-=a.height+r.height),{x:Math.round((l-d)/f*i.width/n),y:Math.round((c-u)/g*i.height/n)}}const be=t=>Math.round(10*t)/10;function _e(t,e,i,n){const o=fe(t),s=me(o,"margin"),a=ue(o.maxWidth,t,"clientWidth")||vt,r=ue(o.maxHeight,t,"clientHeight")||vt,l=function(t,e,i){let n,o;if(void 0===e||void 0===i){const s=de(t);if(s){const t=s.getBoundingClientRect(),a=fe(s),r=me(a,"border","width"),l=me(a,"padding");e=t.width-l.width-r.width,i=t.height-l.height-r.height,n=ue(a.maxWidth,s,"clientWidth"),o=ue(a.maxHeight,s,"clientHeight")}else e=t.clientWidth,i=t.clientHeight}return{width:e,height:i,maxWidth:n||vt,maxHeight:o||vt}}(t,e,i);let{width:c,height:h}=l;if("content-box"===o.boxSizing){const t=me(o,"border","width"),e=me(o,"padding");c-=e.width+t.width,h-=e.height+t.height}return c=Math.max(0,c-s.width),h=Math.max(0,n?Math.floor(c/n):h-s.height),c=be(Math.min(c,a,l.maxWidth)),h=be(Math.min(h,r,l.maxHeight)),c&&!h&&(h=be(c/2)),{width:c,height:h}}function ye(t,e,i){const n=t.currentDevicePixelRatio=e||1,{canvas:o,width:s,height:a}=t;o.height=a*n,o.width=s*n,t.ctx.setTransform(n,0,0,n,0,0),o.style&&(i||!o.style.height&&!o.style.width)&&(o.style.height=a+"px",o.style.width=s+"px")}const ve=function(){let t=!1;try{const e={get passive(){return t=!0,!1}};window.addEventListener("test",null,e),window.removeEventListener("test",null,e)}catch(t){}return t}();function we(t,e){const i=ge(t,e),n=i&&i.match(/^(\d+)(\.\d+)?px$/);return n?+n[1]:void 0}function Me(t,e){return"native"in t?{x:t.x,y:t.y}:xe(t,e)}function ke(t,e,i,n){const{controller:o,data:s,_sorted:a}=t,r=o._cachedMeta.iScale;if(r&&e===r.axis&&a&&s.length){const t=r._reversePixels?se:oe;if(!n)return t(s,e,i);if(o._sharedOptions){const n=s[0],o="function"==typeof n.getRange&&n.getRange(e);if(o){const n=t(s,e,i-o),a=t(s,e,i+o);return{lo:n.lo,hi:a.hi}}}}return{lo:0,hi:s.length-1}}function Se(t,e,i,n,o){const s=t.getSortedVisibleDatasetMetas(),a=i[e];for(let t=0,i=s.length;t{t[r](o[a],n)&&s.push({element:t,datasetIndex:e,index:i}),t.inRange(o.x,o.y,n)&&(l=!0)})),i.intersect&&!l?[]:s}var Oe={modes:{index(t,e,i,n){const o=Me(e,t),s=i.axis||"x",a=i.intersect?Pe(t,o,s,n):De(t,o,s,!1,n),r=[];return a.length?(t.getSortedVisibleDatasetMetas().forEach((t=>{const e=a[0].index,i=t.data[e];i&&!i.skip&&r.push({element:i,datasetIndex:t.index,index:e})})),r):[]},dataset(t,e,i,n){const o=Me(e,t),s=i.axis||"xy";let a=i.intersect?Pe(t,o,s,n):De(t,o,s,!1,n);if(a.length>0){const e=a[0].datasetIndex,i=t.getDatasetMeta(e).data;a=[];for(let t=0;tPe(t,Me(e,t),i.axis||"xy",n),nearest:(t,e,i,n)=>De(t,Me(e,t),i.axis||"xy",i.intersect,n),x:(t,e,i,n)=>(i.axis="x",Ce(t,e,i,n)),y:(t,e,i,n)=>(i.axis="y",Ce(t,e,i,n))}};const Ae=new RegExp(/^(normal|(\d+(?:\.\d+)?)(px|em|%)?)$/),Te=new RegExp(/^(normal|italic|initial|inherit|unset|(oblique( -?[0-9]?[0-9]deg)?))$/);function Re(t,e){const i=(""+t).match(Ae);if(!i||"normal"===i[1])return 1.2*e;switch(t=+i[2],i[3]){case"px":return t;case"%":t/=100}return e*t}function Le(t,e){const i={},n=U(e),o=n?Object.keys(e):e,s=U(t)?n?i=>K(t[i],t[e[i]]):e=>t[e]:()=>t;for(const t of o)i[t]=+s(t)||0;return i}function Ee(t){return Le(t,{top:"y",right:"x",bottom:"y",left:"x"})}function Ie(t){return Le(t,["topLeft","topRight","bottomLeft","bottomRight"])}function ze(t){const e=Ee(t);return e.width=e.left+e.right,e.height=e.top+e.bottom,e}function Fe(t,e){t=t||{},e=e||xt.font;let i=K(t.size,e.size);"string"==typeof i&&(i=parseInt(i,10));let n=K(t.style,e.style);n&&!(""+n).match(Te)&&(console.warn('Invalid font style specified: "'+n+'"'),n="");const o={family:K(t.family,e.family),lineHeight:Re(K(t.lineHeight,e.lineHeight),i),size:i,style:n,weight:K(t.weight,e.weight),string:""};return o.string=$t(o),o}function Ve(t,e,i,n){let o,s,a,r=!0;for(o=0,s=t.length;ot.pos===e))}function Ne(t,e){return t.filter((t=>-1===We.indexOf(t.pos)&&t.box.axis===e))}function je(t,e){return t.sort(((t,i)=>{const n=e?i:t,o=e?t:i;return n.weight===o.weight?n.index-o.index:n.weight-o.weight}))}function $e(t,e,i,n){return Math.max(t[i],e[i])+Math.max(t[n],e[n])}function Ye(t,e){t.top=Math.max(t.top,e.top),t.left=Math.max(t.left,e.left),t.bottom=Math.max(t.bottom,e.bottom),t.right=Math.max(t.right,e.right)}function Ue(t,e,i){const n=i.box,o=t.maxPadding;U(i.pos)||(i.size&&(t[i.pos]-=i.size),i.size=i.horizontal?n.height:n.width,t[i.pos]+=i.size),n.getPadding&&Ye(o,n.getPadding());const s=Math.max(0,e.outerWidth-$e(o,t,"left","right")),a=Math.max(0,e.outerHeight-$e(o,t,"top","bottom")),r=s!==t.w,l=a!==t.h;return t.w=s,t.h=a,i.horizontal?{same:r,other:l}:{same:l,other:r}}function Xe(t,e){const i=e.maxPadding;function n(t){const n={left:0,top:0,right:0,bottom:0};return t.forEach((t=>{n[t]=Math.max(e[t],i[t])})),n}return n(t?["left","right"]:["top","bottom"])}function qe(t,e,i){const n=[];let o,s,a,r,l,c;for(o=0,s=t.length,l=0;ot.box.fullSize)),!0),n=je(He(e,"left"),!0),o=je(He(e,"right")),s=je(He(e,"top"),!0),a=je(He(e,"bottom")),r=Ne(e,"x"),l=Ne(e,"y");return{fullSize:i,leftAndTop:n.concat(s),rightAndBottom:o.concat(l).concat(a).concat(r),chartArea:He(e,"chartArea"),vertical:n.concat(o).concat(l),horizontal:s.concat(a).concat(r)}}(t.boxes),l=r.vertical,c=r.horizontal;J(t.boxes,(t=>{"function"==typeof t.beforeLayout&&t.beforeLayout()}));const h=l.reduce(((t,e)=>e.box.options&&!1===e.box.options.display?t:t+1),0)||1,d=Object.freeze({outerWidth:e,outerHeight:i,padding:o,availableWidth:s,availableHeight:a,vBoxMaxWidth:s/2/h,hBoxMaxHeight:a/2}),u=Object.assign({},o);Ye(u,ze(n));const f=Object.assign({maxPadding:u,w:s,h:a,x:o.left,y:o.top},o);!function(t,e){let i,n,o;for(i=0,n=t.length;i{const i=e.box;Object.assign(i,t.chartArea),i.update(f.w,f.h)}))}};class Ze{acquireContext(t,e){}releaseContext(t){return!1}addEventListener(t,e,i){}removeEventListener(t,e,i){}getDevicePixelRatio(){return 1}getMaximumSize(t,e,i,n){return e=Math.max(0,e||t.width),i=i||t.height,{width:e,height:Math.max(0,n?Math.floor(e/n):i)}}isAttached(t){return!0}}class Qe extends Ze{acquireContext(t){return t&&t.getContext&&t.getContext("2d")||null}}const Je={touchstart:"mousedown",touchmove:"mousemove",touchend:"mouseup",pointerenter:"mouseenter",pointerdown:"mousedown",pointermove:"mousemove",pointerup:"mouseup",pointerleave:"mouseout",pointerout:"mouseout"},ti=t=>null===t||""===t;const ei=!!ve&&{passive:!0};function ii(t,e,i){t.canvas.removeEventListener(e,i,ei)}function ni(t,e,i){const n=t.canvas,o=n&&de(n)||n,s=new MutationObserver((t=>{const e=de(o);t.forEach((t=>{for(let n=0;n{t.forEach((t=>{for(let e=0;e{i.currentDevicePixelRatio!==t&&e()})))}function li(t,i,n){const o=t.canvas,s=o&&de(o);if(!s)return;const a=e(((t,e)=>{const i=s.clientWidth;n(t,e),i{const e=t[0],i=e.contentRect.width,n=e.contentRect.height;0===i&&0===n||a(i,n)}));return r.observe(s),function(t,e){si.size||window.addEventListener("resize",ri),si.set(t,e)}(t,a),r}function ci(t,e,i){i&&i.disconnect(),"resize"===e&&function(t){si.delete(t),si.size||window.removeEventListener("resize",ri)}(t)}function hi(t,i,n){const o=t.canvas,s=e((e=>{null!==t.ctx&&n(function(t,e){const i=Je[t.type]||t.type,{x:n,y:o}=xe(t,e);return{type:i,chart:e,native:t,x:void 0!==n?n:null,y:void 0!==o?o:null}}(e,t))}),t,(t=>{const e=t[0];return[e,e.offsetX,e.offsetY]}));return function(t,e,i){t.addEventListener(e,i,ei)}(o,i,s),s}class di extends Ze{acquireContext(t,e){const i=t&&t.getContext&&t.getContext("2d");return i&&i.canvas===t?(function(t,e){const i=t.style,n=t.getAttribute("height"),o=t.getAttribute("width");if(t.$chartjs={initial:{height:n,width:o,style:{display:i.display,height:i.height,width:i.width}}},i.display=i.display||"block",i.boxSizing=i.boxSizing||"border-box",ti(o)){const e=we(t,"width");void 0!==e&&(t.width=e)}if(ti(n))if(""===t.style.height)t.height=t.width/(e||2);else{const e=we(t,"height");void 0!==e&&(t.height=e)}}(t,e),i):null}releaseContext(t){const e=t.canvas;if(!e.$chartjs)return!1;const i=e.$chartjs.initial;["height","width"].forEach((t=>{const n=i[t];$(n)?e.removeAttribute(t):e.setAttribute(t,n)}));const n=i.style||{};return Object.keys(n).forEach((t=>{e.style[t]=n[t]})),e.width=e.width,delete e.$chartjs,!0}addEventListener(t,e,i){this.removeEventListener(t,e);const n=t.$proxies||(t.$proxies={}),o={attach:ni,detach:oi,resize:li}[e]||hi;n[e]=o(t,e,i)}removeEventListener(t,e){const i=t.$proxies||(t.$proxies={}),n=i[e];if(!n)return;({attach:ci,detach:ci,resize:ci}[e]||ii)(t,e,n),i[e]=void 0}getDevicePixelRatio(){return window.devicePixelRatio}getMaximumSize(t,e,i,n){return _e(t,e,i,n)}isAttached(t){const e=de(t);return!(!e||!de(e))}}var ui=Object.freeze({__proto__:null,BasePlatform:Ze,BasicPlatform:Qe,DomPlatform:di});const fi=t=>0===t||1===t,gi=(t,e,i)=>-Math.pow(2,10*(t-=1))*Math.sin((t-e)*_t/i),pi=(t,e,i)=>Math.pow(2,-10*t)*Math.sin((t-e)*_t/i)+1,mi={linear:t=>t,easeInQuad:t=>t*t,easeOutQuad:t=>-t*(t-2),easeInOutQuad:t=>(t/=.5)<1?.5*t*t:-.5*(--t*(t-2)-1),easeInCubic:t=>t*t*t,easeOutCubic:t=>(t-=1)*t*t+1,easeInOutCubic:t=>(t/=.5)<1?.5*t*t*t:.5*((t-=2)*t*t+2),easeInQuart:t=>t*t*t*t,easeOutQuart:t=>-((t-=1)*t*t*t-1),easeInOutQuart:t=>(t/=.5)<1?.5*t*t*t*t:-.5*((t-=2)*t*t*t-2),easeInQuint:t=>t*t*t*t*t,easeOutQuint:t=>(t-=1)*t*t*t*t+1,easeInOutQuint:t=>(t/=.5)<1?.5*t*t*t*t*t:.5*((t-=2)*t*t*t*t+2),easeInSine:t=>1-Math.cos(t*Mt),easeOutSine:t=>Math.sin(t*Mt),easeInOutSine:t=>-.5*(Math.cos(bt*t)-1),easeInExpo:t=>0===t?0:Math.pow(2,10*(t-1)),easeOutExpo:t=>1===t?1:1-Math.pow(2,-10*t),easeInOutExpo:t=>fi(t)?t:t<.5?.5*Math.pow(2,10*(2*t-1)):.5*(2-Math.pow(2,-10*(2*t-1))),easeInCirc:t=>t>=1?t:-(Math.sqrt(1-t*t)-1),easeOutCirc:t=>Math.sqrt(1-(t-=1)*t),easeInOutCirc:t=>(t/=.5)<1?-.5*(Math.sqrt(1-t*t)-1):.5*(Math.sqrt(1-(t-=2)*t)+1),easeInElastic:t=>fi(t)?t:gi(t,.075,.3),easeOutElastic:t=>fi(t)?t:pi(t,.075,.3),easeInOutElastic(t){const e=.1125;return fi(t)?t:t<.5?.5*gi(2*t,e,.45):.5+.5*pi(2*t-1,e,.45)},easeInBack(t){const e=1.70158;return t*t*((e+1)*t-e)},easeOutBack(t){const e=1.70158;return(t-=1)*t*((e+1)*t+e)+1},easeInOutBack(t){let e=1.70158;return(t/=.5)<1?t*t*((1+(e*=1.525))*t-e)*.5:.5*((t-=2)*t*((1+(e*=1.525))*t+e)+2)},easeInBounce:t=>1-mi.easeOutBounce(1-t),easeOutBounce(t){const e=7.5625,i=2.75;return t<1/i?e*t*t:t<2/i?e*(t-=1.5/i)*t+.75:t<2.5/i?e*(t-=2.25/i)*t+.9375:e*(t-=2.625/i)*t+.984375},easeInOutBounce:t=>t<.5?.5*mi.easeInBounce(2*t):.5*mi.easeOutBounce(2*t-1)+.5},xi="transparent",bi={boolean:(t,e,i)=>i>.5?e:t,color(t,e,i){const n=W(t||xi),o=n.valid&&W(e||xi);return o&&o.valid?o.mix(n,i).hexString():e},number:(t,e,i)=>t+(e-t)*i};class _i{constructor(t,e,i,n){const o=e[i];n=Ve([t.to,n,o,t.from]);const s=Ve([t.from,o,n]);this._active=!0,this._fn=t.fn||bi[t.type||typeof s],this._easing=mi[t.easing]||mi.linear,this._start=Math.floor(Date.now()+(t.delay||0)),this._duration=this._total=Math.floor(t.duration),this._loop=!!t.loop,this._target=e,this._prop=i,this._from=s,this._to=n,this._promises=void 0}active(){return this._active}update(t,e,i){const n=this;if(n._active){n._notify(!1);const o=n._target[n._prop],s=i-n._start,a=n._duration-s;n._start=i,n._duration=Math.floor(Math.max(a,t.duration)),n._total+=s,n._loop=!!t.loop,n._to=Ve([t.to,e,o,t.from]),n._from=Ve([t.from,o,e])}}cancel(){const t=this;t._active&&(t.tick(Date.now()),t._active=!1,t._notify(!1))}tick(t){const e=this,i=t-e._start,n=e._duration,o=e._prop,s=e._from,a=e._loop,r=e._to;let l;if(e._active=s!==r&&(a||i1?2-l:l,l=e._easing(Math.min(1,Math.max(0,l))),e._target[o]=e._fn(s,r,l))}wait(){const t=this._promises||(this._promises=[]);return new Promise(((e,i)=>{t.push({res:e,rej:i})}))}_notify(t){const e=t?"res":"rej",i=this._promises||[];for(let t=0;t"onProgress"!==t&&"onComplete"!==t&&"fn"!==t}),xt.set("animations",{colors:{type:"color",properties:["color","borderColor","backgroundColor"]},numbers:{type:"number",properties:["x","y","borderWidth","radius","tension"]}}),xt.describe("animations",{_fallback:"animation"}),xt.set("transitions",{active:{animation:{duration:400}},resize:{animation:{duration:0}},show:{animations:{colors:{from:"transparent"},visible:{type:"boolean",duration:0}}},hide:{animations:{colors:{to:"transparent"},visible:{type:"boolean",easing:"linear",fn:t=>0|t}}}});class vi{constructor(t,e){this._chart=t,this._properties=new Map,this.configure(e)}configure(t){if(!U(t))return;const e=this._properties;Object.getOwnPropertyNames(t).forEach((i=>{const n=t[i];if(!U(n))return;const o={};for(const t of yi)o[t]=n[t];(Y(n.properties)&&n.properties||[i]).forEach((t=>{t!==i&&e.has(t)||e.set(t,o)}))}))}_animateOptions(t,e){const i=e.options,n=function(t,e){if(!e)return;let i=t.options;if(!i)return void(t.options=e);i.$shared&&(t.options=i=Object.assign({},i,{$shared:!1,$animations:{}}));return i}(t,i);if(!n)return[];const o=this._createAnimations(n,i);return i.$shared&&function(t,e){const i=[],n=Object.keys(e);for(let e=0;e{t.options=i}),(()=>{})),o}_createAnimations(t,e){const i=this._properties,n=[],o=t.$animations||(t.$animations={}),s=Object.keys(e),a=Date.now();let r;for(r=s.length-1;r>=0;--r){const l=s[r];if("$"===l.charAt(0))continue;if("options"===l){n.push(...this._animateOptions(t,e));continue}const c=e[l];let h=o[l];const d=i.get(l);if(h){if(d&&h.active()){h.update(d,c,a);continue}h.cancel()}d&&d.duration?(o[l]=h=new _i(d,t,l,c),n.push(h)):t[l]=c}return n}update(t,e){if(0===this._properties.size)return void Object.assign(t,e);const i=this._createAnimations(t,e);return i.length?(a.add(this._chart,i),!0):void 0}}function wi(t,e){const i=t&&t.options||{},n=i.reverse,o=void 0===i.min?e:0,s=void 0===i.max?e:0;return{start:n?s:o,end:n?o:s}}function Mi(t,e){const i=[],n=t._getSortedDatasetMetas(e);let o,s;for(o=0,s=n.length;o0||!i&&e<0)return n.index}return null}function Ci(t,e){const{chart:i,_cachedMeta:n}=t,o=i._stacks||(i._stacks={}),{iScale:s,vScale:a,index:r}=n,l=s.axis,c=a.axis,h=function(t,e,i){return`${t.id}.${e.id}.${i.stack||i.type}`}(s,a,n),d=e.length;let u;for(let t=0;ti[t].axis===e)).shift()}function Ai(t,e){e=e||t._parsed;for(const i of e){const e=i._stacks;if(!e||void 0===e[t.vScale.id]||void 0===e[t.vScale.id][t.index])return;delete e[t.vScale.id][t.index]}}const Ti=t=>"reset"===t||"none"===t,Ri=(t,e)=>e?t:Object.assign({},t);class Li{constructor(t,e){this.chart=t,this._ctx=t.ctx,this.index=e,this._cachedDataOpts={},this._cachedMeta=this.getMeta(),this._type=this._cachedMeta.type,this.options=void 0,this._parsing=!1,this._data=void 0,this._objectData=void 0,this._sharedOptions=void 0,this._drawStart=void 0,this._drawCount=void 0,this.enableOptionSharing=!1,this.$context=void 0,this.initialize()}initialize(){const t=this,e=t._cachedMeta;t.configure(),t.linkScales(),e._stacked=Si(e.vScale,e),t.addElements()}updateIndex(t){this.index=t}linkScales(){const t=this,e=t.chart,i=t._cachedMeta,n=t.getDataset(),o=(t,e,i,n)=>"x"===t?e:"r"===t?n:i,s=i.xAxisID=K(n.xAxisID,Oi(e,"x")),a=i.yAxisID=K(n.yAxisID,Oi(e,"y")),r=i.rAxisID=K(n.rAxisID,Oi(e,"r")),l=i.indexAxis,c=i.iAxisID=o(l,s,a,r),h=i.vAxisID=o(l,a,s,r);i.xScale=t.getScaleForId(s),i.yScale=t.getScaleForId(a),i.rScale=t.getScaleForId(r),i.iScale=t.getScaleForId(c),i.vScale=t.getScaleForId(h)}getDataset(){return this.chart.data.datasets[this.index]}getMeta(){return this.chart.getDatasetMeta(this.index)}getScaleForId(t){return this.chart.scales[t]}_getOtherScale(t){const e=this._cachedMeta;return t===e.iScale?e.vScale:e.iScale}reset(){this._update("reset")}_destroy(){const t=this._cachedMeta;this._data&&ce(this._data,this),t._stacked&&Ai(t)}_dataCheck(){const t=this,e=t.getDataset(),i=e.data||(e.data=[]);U(i)?t._data=function(t){const e=Object.keys(t),i=new Array(e.length);let n,o,s;for(n=0,o=e.length;n0&&n._parsed[t-1];if(!1===i._parsing)n._parsed=o,n._sorted=!0,h=o;else{h=Y(o[t])?i.parseArrayData(n,o,t,e):U(o[t])?i.parseObjectData(n,o,t,e):i.parsePrimitiveData(n,o,t,e);const s=()=>null===c[r]||u&&c[r]p||d=0;--u)if(!m()){i.updateRangeFromParsed(c,t,g,l);break}return c}getAllParsedValues(t){const e=this._cachedMeta._parsed,i=[];let n,o,s;for(n=0,o=e.length;n=0&&tn.getContext(i,o)),d);return g.$shared&&(g.$shared=l,s[a]=Object.freeze(Ri(g,l))),g}_resolveAnimations(t,e,i){const n=this,o=n.chart,s=n._cachedDataOpts,a="animation-"+e,r=s[a];if(r)return r;let l;if(!1!==o.options.animation){const o=n.chart.config,s=o.datasetAnimationScopeKeys(n._type,e),a=o.getOptionScopes(n.getDataset(),s);l=o.createResolver(a,n.getContext(t,i,e))}const c=new vi(o,l&&l.animations);return l&&l._cacheable&&(s[a]=Object.freeze(c)),c}getSharedOptions(t){if(t.$shared)return this._sharedOptions||(this._sharedOptions=Object.assign({},t))}includeOptions(t,e){return!e||Ti(t)||this.chart._animationsDisabled}updateElement(t,e,i,n){Ti(n)?Object.assign(t,i):this._resolveAnimations(e,n).update(t,i)}updateSharedOptions(t,e,i){t&&!Ti(e)&&this._resolveAnimations(void 0,e).update(t,i)}_setStyle(t,e,i,n){t.active=n;const o=this.getStyle(e,n);this._resolveAnimations(e,i,n).update(t,{options:!n&&this.getSharedOptions(o)||o})}removeHoverStyle(t,e,i){this._setStyle(t,i,"active",!1)}setHoverStyle(t,e,i){this._setStyle(t,i,"active",!0)}_removeDatasetHoverStyle(){const t=this._cachedMeta.dataset;t&&this._setStyle(t,void 0,"active",!1)}_setDatasetHoverStyle(){const t=this._cachedMeta.dataset;t&&this._setStyle(t,void 0,"active",!0)}_resyncElements(t){const e=this,i=e._cachedMeta.data.length,n=e._data.length;n>i?e._insertElements(i,n-i,t):n{for(t.length+=e,r=t.length-1;r>=a;r--)t[r]=t[r-e]};for(l(s),r=t;r{o[t]=n[t]&&n[t].active()?n[t]._to:i[t]})),o}}Ei.defaults={},Ei.defaultRoutes=void 0;const Ii=new Map;function zi(t,e,i){return function(t,e){e=e||{};const i=t+JSON.stringify(e);let n=Ii.get(i);return n||(n=new Intl.NumberFormat(t,e),Ii.set(i,n)),n}(e,i).format(t)}const Fi={values:t=>Y(t)?t:""+t,numeric(t,e,i){if(0===t)return"0";const n=this.chart.options.locale;let o,s=t;if(i.length>1){const e=Math.max(Math.abs(i[0].value),Math.abs(i[i.length-1].value));(e<1e-4||e>1e15)&&(o="scientific"),s=function(t,e){let i=e.length>3?e[2].value-e[1].value:e[1].value-e[0].value;Math.abs(i)>1&&t!==Math.floor(t)&&(i=t-Math.floor(t));return i}(t,i)}const a=Pt(Math.abs(s)),r=Math.max(Math.min(-1*Math.floor(a),20),0),l={notation:o,minimumFractionDigits:r,maximumFractionDigits:r};return Object.assign(l,this.options.ticks.format),zi(t,n,l)},logarithmic(t,e,i){if(0===t)return"0";const n=t/Math.pow(10,Math.floor(Pt(t)));return 1===n||2===n||5===n?Fi.numeric.call(this,t,e,i):""}};var Vi={formatters:Fi};function Bi(t,e){const i=t.options.ticks,n=i.maxTicksLimit||function(t){const e=t.options.offset,i=t._tickSize(),n=t._length/i+(e?0:1),o=t._maxLength/i;return Math.floor(Math.min(n,o))}(t),o=i.major.enabled?function(t){const e=[];let i,n;for(i=0,n=t.length;in)return function(t,e,i,n){let o,s=0,a=i[0];for(n=Math.ceil(n),o=0;oo)return e}return Math.max(o,1)}(o,e,n);if(s>0){let t,i;const n=s>1?Math.round((r-a)/(s-1)):null;for(Wi(e,l,c,$(n)?0:a-n,a),t=0,i=s-1;te.lineWidth,tickColor:(t,e)=>e.color,offset:!1,borderDash:[],borderDashOffset:0,borderWidth:1},title:{display:!1,text:"",padding:{top:4,bottom:4}},ticks:{minRotation:0,maxRotation:50,mirror:!1,textStrokeWidth:0,textStrokeColor:"",padding:3,display:!0,autoSkip:!0,autoSkipPadding:3,labelOffset:0,callback:Vi.formatters.values,minor:{},major:{},align:"center",crossAlign:"near",showLabelBackdrop:!1,backdropColor:"rgba(255, 255, 255, 0.75)",backdropPadding:2}}),xt.route("scale.ticks","color","","color"),xt.route("scale.grid","color","","borderColor"),xt.route("scale.grid","borderColor","","borderColor"),xt.route("scale.title","color","","color"),xt.describe("scale",{_fallback:!1,_scriptable:t=>!t.startsWith("before")&&!t.startsWith("after")&&"callback"!==t&&"parser"!==t,_indexable:t=>"borderDash"!==t&&"tickBorderDash"!==t}),xt.describe("scales",{_fallback:"scale"});const Hi=(t,e,i)=>"top"===e||"left"===e?t[e]+i:t[e]-i;function Ni(t,e){const i=[],n=t.length/e,o=t.length;let s=0;for(;sa+r)))return c}function $i(t){return t.drawTicks?t.tickLength:0}function Yi(t,e){if(!t.display)return 0;const i=Fe(t.font,e),n=ze(t.padding);return(Y(t.text)?t.text.length:1)*i.lineHeight+n.height}function Ui(t,e,i){let o=n(t);return(i&&"right"!==e||!i&&"right"===e)&&(o=(t=>"left"===t?"right":"right"===t?"left":t)(o)),o}class Xi extends Ei{constructor(t){super(),this.id=t.id,this.type=t.type,this.options=void 0,this.ctx=t.ctx,this.chart=t.chart,this.top=void 0,this.bottom=void 0,this.left=void 0,this.right=void 0,this.width=void 0,this.height=void 0,this._margins={left:0,right:0,top:0,bottom:0},this.maxWidth=void 0,this.maxHeight=void 0,this.paddingTop=void 0,this.paddingBottom=void 0,this.paddingLeft=void 0,this.paddingRight=void 0,this.axis=void 0,this.labelRotation=void 0,this.min=void 0,this.max=void 0,this._range=void 0,this.ticks=[],this._gridLineItems=null,this._labelItems=null,this._labelSizes=null,this._length=0,this._maxLength=0,this._longestTextCache={},this._startPixel=void 0,this._endPixel=void 0,this._reversePixels=!1,this._userMax=void 0,this._userMin=void 0,this._suggestedMax=void 0,this._suggestedMin=void 0,this._ticksLength=0,this._borderValue=0,this._cache={},this._dataLimitsCached=!1,this.$context=void 0}init(t){const e=this;e.options=t.setContext(e.getContext()),e.axis=t.axis,e._userMin=e.parse(t.min),e._userMax=e.parse(t.max),e._suggestedMin=e.parse(t.suggestedMin),e._suggestedMax=e.parse(t.suggestedMax)}parse(t,e){return t}getUserBounds(){let{_userMin:t,_userMax:e,_suggestedMin:i,_suggestedMax:n}=this;return t=q(t,Number.POSITIVE_INFINITY),e=q(e,Number.NEGATIVE_INFINITY),i=q(i,Number.POSITIVE_INFINITY),n=q(n,Number.NEGATIVE_INFINITY),{min:q(t,i),max:q(e,n),minDefined:X(t),maxDefined:X(e)}}getMinMax(t){const e=this;let i,{min:n,max:o,minDefined:s,maxDefined:a}=e.getUserBounds();if(s&&a)return{min:n,max:o};const r=e.getMatchingVisibleMetas();for(let l=0,c=r.length;l=s||n<=1||!t.isHorizontal())return void(t.labelRotation=o);const h=t._getLabelSizes(),d=h.widest.width,u=h.highest.height,f=Nt(t.chart.width-d,0,t.maxWidth);a=e.offset?t.maxWidth/n:f/(n-1),d+6>a&&(a=f/(n-(e.offset?.5:1)),r=t.maxHeight-$i(e.grid)-i.padding-Yi(e.title,t.chart.options.font),l=Math.sqrt(d*d+u*u),c=It(Math.min(Math.asin(Math.min((h.highest.height+6)/a,1)),Math.asin(Math.min(r/l,1))-Math.asin(u/l))),c=Math.max(o,Math.min(s,c))),t.labelRotation=c}afterCalculateLabelRotation(){Q(this.options.afterCalculateLabelRotation,[this])}beforeFit(){Q(this.options.beforeFit,[this])}fit(){const t=this,e={width:0,height:0},{chart:i,options:{ticks:n,title:o,grid:s}}=t,a=t._isVisible(),r=t.isHorizontal();if(a){const a=Yi(o,i.options.font);if(r?(e.width=t.maxWidth,e.height=$i(s)+a):(e.height=t.maxHeight,e.width=$i(s)+a),n.display&&t.ticks.length){const{first:i,last:o,widest:s,highest:a}=t._getLabelSizes(),l=2*n.padding,c=Et(t.labelRotation),h=Math.cos(c),d=Math.sin(c);if(r){const i=n.mirror?0:d*s.width+h*a.height;e.height=Math.min(t.maxHeight,e.height+i+l)}else{const i=n.mirror?0:h*s.width+d*a.height;e.width=Math.min(t.maxWidth,e.width+i+l)}t._calculatePadding(i,o,d,h)}}t._handleMargins(),r?(t.width=t._length=i.width-t._margins.left-t._margins.right,t.height=e.height):(t.width=e.width,t.height=t._length=i.height-t._margins.top-t._margins.bottom)}_calculatePadding(t,e,i,n){const o=this,{ticks:{align:s,padding:a},position:r}=o.options,l=0!==o.labelRotation,c="top"!==r&&"x"===o.axis;if(o.isHorizontal()){const r=o.getPixelForTick(0)-o.left,h=o.right-o.getPixelForTick(o.ticks.length-1);let d=0,u=0;l?c?(d=n*t.width,u=i*e.height):(d=i*t.height,u=n*e.width):"start"===s?u=e.width:"end"===s?d=t.width:(d=t.width/2,u=e.width/2),o.paddingLeft=Math.max((d-r+a)*o.width/(o.width-r),0),o.paddingRight=Math.max((u-h+a)*o.width/(o.width-h),0)}else{let i=e.height/2,n=t.height/2;"start"===s?(i=0,n=t.height):"end"===s&&(i=e.height,n=0),o.paddingTop=i+a,o.paddingBottom=n+a}}_handleMargins(){const t=this;t._margins&&(t._margins.left=Math.max(t.paddingLeft,t._margins.left),t._margins.top=Math.max(t.paddingTop,t._margins.top),t._margins.right=Math.max(t.paddingRight,t._margins.right),t._margins.bottom=Math.max(t.paddingBottom,t._margins.bottom))}afterFit(){Q(this.options.afterFit,[this])}isHorizontal(){const{axis:t,position:e}=this.options;return"top"===e||"bottom"===e||"x"===t}isFullSize(){return this.options.fullSize}_convertTicksToLabels(t){const e=this;e.beforeTickToLabelConversion(),e.generateTickLabels(t),e.afterTickToLabelConversion()}_getLabelSizes(){const t=this;let e=t._labelSizes;if(!e){const i=t.options.ticks.sampleSize;let n=t.ticks;i{const i=t.gc,n=i.length/2;let o;if(n>e){for(o=0;o({width:o[t]||0,height:s[t]||0});return{first:v(0),last:v(e-1),widest:v(_),highest:v(y),widths:o,heights:s}}getLabelForValue(t){return t}getPixelForValue(t,e){return NaN}getValueForPixel(t){}getPixelForTick(t){const e=this.ticks;return t<0||t>e.length-1?null:this.getPixelForValue(e[t].value)}getPixelForDecimal(t){const e=this;e._reversePixels&&(t=1-t);const i=e._startPixel+t*e._length;return jt(e._alignToPixels?Xt(e.chart,i,0):i)}getDecimalForPixel(t){const e=(t-this._startPixel)/this._length;return this._reversePixels?1-e:e}getBasePixel(){return this.getPixelForValue(this.getBaseValue())}getBaseValue(){const{min:t,max:e}=this;return t<0&&e<0?e:t>0&&e>0?t:0}getContext(t){const e=this,i=e.ticks||[];if(t>=0&&tr*o?r/n:l/o:l*o0}_computeGridLineItems(t){const e=this,i=e.axis,n=e.chart,o=e.options,{grid:s,position:a}=o,r=s.offset,l=e.isHorizontal(),c=e.ticks.length+(r?1:0),h=$i(s),d=[],u=s.setContext(e.getContext()),f=u.drawBorder?u.borderWidth:0,g=f/2,p=function(t){return Xt(n,t,f)};let m,x,b,_,y,v,w,M,k,S,P,D;if("top"===a)m=p(e.bottom),v=e.bottom-h,M=m-g,S=p(t.top)+g,D=t.bottom;else if("bottom"===a)m=p(e.top),S=t.top,D=p(t.bottom)-g,v=m+g,M=e.top+h;else if("left"===a)m=p(e.right),y=e.right-h,w=m-g,k=p(t.left)+g,P=t.right;else if("right"===a)m=p(e.left),k=t.left,P=p(t.right)-g,y=m+g,w=e.left+h;else if("x"===i){if("center"===a)m=p((t.top+t.bottom)/2+.5);else if(U(a)){const t=Object.keys(a)[0],i=a[t];m=p(e.chart.scales[t].getPixelForValue(i))}S=t.top,D=t.bottom,v=m+g,M=v+h}else if("y"===i){if("center"===a)m=p((t.left+t.right)/2);else if(U(a)){const t=Object.keys(a)[0],i=a[t];m=p(e.chart.scales[t].getPixelForValue(i))}y=m-g,w=y-h,k=t.left,P=t.right}for(x=0;xe.value===t));if(n>=0){return i.setContext(e.getContext(n)).lineWidth}return 0}drawGrid(t){const e=this,i=e.options.grid,n=e.ctx,o=e._gridLineItems||(e._gridLineItems=e._computeGridLineItems(t));let s,a;const r=(t,e,i)=>{i.width&&i.color&&(n.save(),n.lineWidth=i.width,n.strokeStyle=i.color,n.setLineDash(i.borderDash||[]),n.lineDashOffset=i.borderDashOffset,n.beginPath(),n.moveTo(t.x,t.y),n.lineTo(e.x,e.y),n.stroke(),n.restore())};if(i.display)for(s=0,a=o.length;st[0])){ht(n)||(n=an("_fallback",t));const s={[Symbol.toStringTag]:"Object",_cacheable:!0,_scopes:t,_rootScopes:i,_fallback:n,_getTarget:o,override:o=>qi([o,...t],e,i,n)};return new Proxy(s,{deleteProperty:(e,i)=>(delete e[i],delete e._keys,delete t[0][i],!0),get:(i,n)=>Ji(i,n,(()=>function(t,e,i,n){let o;for(const s of e)if(o=an(Zi(s,t),i),ht(o))return Qi(t,o)?on(i,n,t,o):o}(n,e,t,i))),getOwnPropertyDescriptor:(t,e)=>Reflect.getOwnPropertyDescriptor(t._scopes[0],e),getPrototypeOf:()=>Reflect.getPrototypeOf(t[0]),has:(t,e)=>rn(t).includes(e),ownKeys:t=>rn(t),set:(t,e,i)=>((t._storage||(t._storage=o()))[e]=i,delete t[e],delete t._keys,!0)})}function Ki(t,e,i,n){const o={_cacheable:!1,_proxy:t,_context:e,_subProxy:i,_stack:new Set,_descriptors:Gi(t,n),setContext:e=>Ki(t,e,i,n),override:o=>Ki(t.override(o),e,i,n)};return new Proxy(o,{deleteProperty:(e,i)=>(delete e[i],delete t[i],!0),get:(t,e,i)=>Ji(t,e,(()=>function(t,e,i){const{_proxy:n,_context:o,_subProxy:s,_descriptors:a}=t;let r=n[e];dt(r)&&a.isScriptable(e)&&(r=function(t,e,i,n){const{_proxy:o,_context:s,_subProxy:a,_stack:r}=i;if(r.has(t))throw new Error("Recursion detected: "+[...r].join("->")+"->"+t);r.add(t),e=e(s,a||n),r.delete(t),U(e)&&(e=on(o._scopes,o,t,e));return e}(e,r,t,i));Y(r)&&r.length&&(r=function(t,e,i,n){const{_proxy:o,_context:s,_subProxy:a,_descriptors:r}=i;if(ht(s.index)&&n(t))e=e[s.index%e.length];else if(U(e[0])){const i=e,n=o._scopes.filter((t=>t!==i));e=[];for(const l of i){const i=on(n,o,t,l);e.push(Ki(i,s,a&&a[t],r))}}return e}(e,r,t,a.isIndexable));Qi(e,r)&&(r=Ki(r,o,s&&s[e],a));return r}(t,e,i))),getOwnPropertyDescriptor:(e,i)=>e._descriptors.allKeys?Reflect.has(t,i)?{enumerable:!0,configurable:!0}:void 0:Reflect.getOwnPropertyDescriptor(t,i),getPrototypeOf:()=>Reflect.getPrototypeOf(t),has:(e,i)=>Reflect.has(t,i),ownKeys:()=>Reflect.ownKeys(t),set:(e,i,n)=>(t[i]=n,delete e[i],!0)})}function Gi(t,e={scriptable:!0,indexable:!0}){const{_scriptable:i=e.scriptable,_indexable:n=e.indexable,_allKeys:o=e.allKeys}=t;return{allKeys:o,scriptable:i,indexable:n,isScriptable:dt(i)?i:()=>i,isIndexable:dt(n)?n:()=>n}}const Zi=(t,e)=>t?t+ct(e):e,Qi=(t,e)=>U(e)&&"adapters"!==t;function Ji(t,e,i){let n=t[e];return ht(n)||(n=i(),ht(n)&&(t[e]=n)),n}function tn(t,e,i){return dt(t)?t(e,i):t}const en=(t,e)=>!0===t?e:"string"==typeof t?lt(e,t):void 0;function nn(t,e,i,n){for(const o of e){const e=en(i,o);if(e){t.add(e);const o=tn(e._fallback,i,e);if(ht(o)&&o!==i&&o!==n)return o}else if(!1===e&&ht(n)&&i!==n)return null}return!1}function on(t,e,i,n){const o=e._rootScopes,s=tn(e._fallback,i,n),a=[...t,...o],r=new Set;r.add(n);let l=sn(r,a,i,s||i);return null!==l&&((!ht(s)||s===i||(l=sn(r,a,s,l),null!==l))&&qi([...r],[""],o,s,(()=>{const t=e._getTarget();return i in t||(t[i]={}),t[i]})))}function sn(t,e,i,n){for(;i;)i=nn(t,e,i,n);return i}function an(t,e){for(const i of e){if(!i)continue;const e=i[t];if(ht(e))return e}}function rn(t){let e=t._keys;return e||(e=t._keys=function(t){const e=new Set;for(const i of t)for(const t of Object.keys(i).filter((t=>!t.startsWith("_"))))e.add(t);return[...e]}(t._scopes)),e}const ln=Number.EPSILON||1e-14,cn=(t,e)=>e!t.skip))),"monotone"===e.cubicInterpolationMode)dn(t);else{let i=n?t[t.length-1]:t[0];for(o=0,s=t.length;o0?e.y:t.y}}function mn(t,e,i,n){const o={x:t.cp2x,y:t.cp2y},s={x:e.cp1x,y:e.cp1y},a=gn(t,o,i),r=gn(o,s,i),l=gn(s,e,i),c=gn(a,r,i),h=gn(r,l,i);return gn(c,h,i)}function xn(t,e,i){return t?function(t,e){return{x:i=>t+t+e-i,setWidth(t){e=t},textAlign:t=>"center"===t?t:"right"===t?"left":"right",xPlus:(t,e)=>t-e,leftForLtr:(t,e)=>t-e}}(e,i):{x:t=>t,setWidth(t){},textAlign:t=>t,xPlus:(t,e)=>t+e,leftForLtr:(t,e)=>t}}function bn(t,e){let i,n;"ltr"!==e&&"rtl"!==e||(i=t.canvas.style,n=[i.getPropertyValue("direction"),i.getPropertyPriority("direction")],i.setProperty("direction",e,"important"),t.prevTextDirection=n)}function _n(t,e){void 0!==e&&(delete t.prevTextDirection,t.canvas.style.setProperty("direction",e[0],e[1]))}function yn(t){return"angle"===t?{between:Ht,compare:Bt,normalize:Wt}:{between:(t,e,i)=>t>=Math.min(e,i)&&t<=Math.max(i,e),compare:(t,e)=>t-e,normalize:t=>t}}function vn({start:t,end:e,count:i,loop:n,style:o}){return{start:t%i,end:e%i,loop:n&&(e-t+1)%i==0,style:o}}function wn(t,e,i){if(!i)return[t];const{property:n,start:o,end:s}=i,a=e.length,{compare:r,between:l,normalize:c}=yn(n),{start:h,end:d,loop:u,style:f}=function(t,e,i){const{property:n,start:o,end:s}=i,{between:a,normalize:r}=yn(n),l=e.length;let c,h,{start:d,end:u,loop:f}=t;if(f){for(d+=l,u+=l,c=0,h=l;cb||l(o,x,p)&&0!==r(o,x),v=()=>!b||0===r(s,p)||l(s,x,p);for(let t=h,i=h;t<=d;++t)m=e[t%a],m.skip||(p=c(m[n]),b=l(p,o,s),null===_&&y()&&(_=0===r(p,o)?t:i),null!==_&&v()&&(g.push(vn({start:_,end:t,loop:u,count:a,style:f})),_=null),i=t,x=p);return null!==_&&g.push(vn({start:_,end:d,loop:u,count:a,style:f})),g}function Mn(t,e){const i=[],n=t.segments;for(let o=0;oo&&t[s%e].skip;)s--;return s%=e,{start:o,end:s}}(i,o,s,n);if(!0===n)return Sn([{start:a,end:r,loop:s}],i,e);return Sn(function(t,e,i,n){const o=t.length,s=[];let a,r=e,l=t[e];for(a=e+1;a<=i;++a){const i=t[a%o];i.skip||i.stop?l.skip||(n=!1,s.push({start:e%o,end:(a-1)%o,loop:n}),e=r=i.stop?a:null):(r=a,l.skip&&(e=a)),l=i}return null!==r&&s.push({start:e%o,end:r%o,loop:n}),s}(i,a,r{const n=i.split("."),o=n.pop(),s=[t].concat(n).join("."),a=e[i].split("."),r=a.pop(),l=a.join(".");xt.route(s,o,l,r)}))}(e,t.defaultRoutes);t.descriptors&&xt.describe(e,t.descriptors)}(t,a,n),e.override&&xt.override(t.id,t.overrides)),a}get(t){return this.items[t]}unregister(t){const e=this.items,i=t.id,n=this.scope;i in e&&delete e[i],n&&i in xt[n]&&(delete xt[n][i],this.override&&delete ft[i])}}var An=new class{constructor(){this.controllers=new On(Li,"datasets",!0),this.elements=new On(Ei,"elements"),this.plugins=new On(Object,"plugins"),this.scales=new On(Xi,"scales"),this._typedRegistries=[this.controllers,this.scales,this.elements]}add(...t){this._each("register",t)}remove(...t){this._each("unregister",t)}addControllers(...t){this._each("register",t,this.controllers)}addElements(...t){this._each("register",t,this.elements)}addPlugins(...t){this._each("register",t,this.plugins)}addScales(...t){this._each("register",t,this.scales)}getController(t){return this._get(t,this.controllers,"controller")}getElement(t){return this._get(t,this.elements,"element")}getPlugin(t){return this._get(t,this.plugins,"plugin")}getScale(t){return this._get(t,this.scales,"scale")}removeControllers(...t){this._each("unregister",t,this.controllers)}removeElements(...t){this._each("unregister",t,this.elements)}removePlugins(...t){this._each("unregister",t,this.plugins)}removeScales(...t){this._each("unregister",t,this.scales)}_each(t,e,i){const n=this;[...e].forEach((e=>{const o=i||n._getRegistryForType(e);i||o.isForType(e)||o===n.plugins&&e.id?n._exec(t,o,e):J(e,(e=>{const o=i||n._getRegistryForType(e);n._exec(t,o,e)}))}))}_exec(t,e,i){const n=ct(t);Q(i["before"+n],[],i),e[t](i),Q(i["after"+n],[],i)}_getRegistryForType(t){for(let e=0;et.filter((t=>!e.some((e=>t.plugin.id===e.plugin.id))));this._notify(n(e,i),t,"stop"),this._notify(n(i,e),t,"start")}}function Rn(t,e){return e||!1!==t?!0===t?{}:t:null}function Ln(t,e,i,n){const o=t.pluginScopeKeys(e),s=t.getOptionScopes(i,o);return t.createResolver(s,n,[""],{scriptable:!1,indexable:!1,allKeys:!0})}function En(t,e){const i=xt.datasets[t]||{};return((e.datasets||{})[t]||{}).indexAxis||e.indexAxis||i.indexAxis||"x"}function In(t,e){return"x"===t||"y"===t?t:e.axis||("top"===(i=e.position)||"bottom"===i?"x":"left"===i||"right"===i?"y":void 0)||t.charAt(0).toLowerCase();var i}function zn(t){const e=t.options||(t.options={});e.plugins=K(e.plugins,{}),e.scales=function(t,e){const i=ft[t.type]||{scales:{}},n=e.scales||{},o=En(t.type,e),s=Object.create(null),a=Object.create(null);return Object.keys(n).forEach((t=>{const e=n[t],r=In(t,e),l=function(t,e){return t===e?"_index_":"_value_"}(r,o),c=i.scales||{};s[r]=s[r]||t,a[t]=st(Object.create(null),[{axis:r},e,c[r],c[l]])})),t.data.datasets.forEach((i=>{const o=i.type||t.type,r=i.indexAxis||En(o,e),l=(ft[o]||{}).scales||{};Object.keys(l).forEach((t=>{const e=function(t,e){let i=t;return"_index_"===t?i=e:"_value_"===t&&(i="x"===e?"y":"x"),i}(t,r),o=i[e+"AxisID"]||s[e]||e;a[o]=a[o]||Object.create(null),st(a[o],[{axis:e},n[o],l[t]])}))})),Object.keys(a).forEach((t=>{const e=a[t];st(e,[xt.scales[e.type],xt.scale])})),a}(t,e)}function Fn(t){return(t=t||{}).datasets=t.datasets||[],t.labels=t.labels||[],t}const Vn=new Map,Bn=new Set;function Wn(t,e){let i=Vn.get(t);return i||(i=e(),Vn.set(t,i),Bn.add(i)),i}const Hn=(t,e,i)=>{const n=lt(e,i);void 0!==n&&t.add(n)};class Nn{constructor(t){this._config=function(t){return(t=t||{}).data=Fn(t.data),zn(t),t}(t),this._scopeCache=new Map,this._resolverCache=new Map}get type(){return this._config.type}set type(t){this._config.type=t}get data(){return this._config.data}set data(t){this._config.data=Fn(t)}get options(){return this._config.options}set options(t){this._config.options=t}get plugins(){return this._config.plugins}update(){const t=this._config;this.clearCache(),zn(t)}clearCache(){this._scopeCache.clear(),this._resolverCache.clear()}datasetScopeKeys(t){return Wn(t,(()=>[["datasets."+t,""]]))}datasetAnimationScopeKeys(t,e){return Wn(`${t}.transition.${e}`,(()=>[[`datasets.${t}.transitions.${e}`,"transitions."+e],["datasets."+t,""]]))}datasetElementScopeKeys(t,e){return Wn(`${t}-${e}`,(()=>[[`datasets.${t}.elements.${e}`,"datasets."+t,"elements."+e,""]]))}pluginScopeKeys(t){const e=t.id;return Wn(`${this.type}-plugin-${e}`,(()=>[["plugins."+e,...t.additionalOptionScopes||[]]]))}_cachedScopes(t,e){const i=this._scopeCache;let n=i.get(t);return n&&!e||(n=new Map,i.set(t,n)),n}getOptionScopes(t,e,i){const{options:n,type:o}=this,s=this._cachedScopes(t,i),a=s.get(e);if(a)return a;const r=new Set;e.forEach((e=>{t&&(r.add(t),e.forEach((e=>Hn(r,t,e)))),e.forEach((t=>Hn(r,n,t))),e.forEach((t=>Hn(r,ft[o]||{},t))),e.forEach((t=>Hn(r,xt,t))),e.forEach((t=>Hn(r,gt,t)))}));const l=[...r];return Bn.has(e)&&s.set(e,l),l}chartOptionScopes(){const{options:t,type:e}=this;return[t,ft[e]||{},xt.datasets[e]||{},{type:e},xt,gt]}resolveNamedOptions(t,e,i,n=[""]){const o={$shared:!0},{resolver:s,subPrefixes:a}=jn(this._resolverCache,t,n);let r=s;if(function(t,e){const{isScriptable:i,isIndexable:n}=Gi(t);for(const o of e)if(i(o)&&dt(t[o])||n(o)&&Y(t[o]))return!0;return!1}(s,e)){o.$shared=!1;r=Ki(s,i=dt(i)?i():i,this.createResolver(t,i,a))}for(const t of e)o[t]=r[t];return o}createResolver(t,e,i=[""],n){const{resolver:o}=jn(this._resolverCache,t,i);return U(e)?Ki(o,e,void 0,n):o}}function jn(t,e,i){let n=t.get(e);n||(n=new Map,t.set(e,n));const o=i.join();let s=n.get(o);if(!s){s={resolver:qi(e,i),subPrefixes:i.filter((t=>!t.toLowerCase().includes("hover")))},n.set(o,s)}return s}const $n=["top","bottom","left","right","chartArea"];function Yn(t,e){return"top"===t||"bottom"===t||-1===$n.indexOf(t)&&"x"===e}function Un(t,e){return function(i,n){return i[t]===n[t]?i[e]-n[e]:i[t]-n[t]}}function Xn(t){const e=t.chart,i=e.options.animation;e.notifyPlugins("afterRender"),Q(i&&i.onComplete,[t],e)}function qn(t){const e=t.chart,i=e.options.animation;Q(i&&i.onProgress,[t],e)}function Kn(){return"undefined"!=typeof window&&"undefined"!=typeof document}function Gn(t){return Kn()&&"string"==typeof t?t=document.getElementById(t):t&&t.length&&(t=t[0]),t&&t.canvas&&(t=t.canvas),t}const Zn={},Qn=t=>{const e=Gn(t);return Object.values(Zn).filter((t=>t.canvas===e)).pop()};class Jn{constructor(t,e){const n=this;this.config=e=new Nn(e);const o=Gn(t),s=Qn(o);if(s)throw new Error("Canvas is already in use. Chart with ID '"+s.id+"' must be destroyed before the canvas can be reused.");const r=e.createResolver(e.chartOptionScopes(),n.getContext());this.platform=n._initializePlatform(o,e);const l=n.platform.acquireContext(o,r.aspectRatio),c=l&&l.canvas,h=c&&c.height,d=c&&c.width;this.id=j(),this.ctx=l,this.canvas=c,this.width=d,this.height=h,this._options=r,this._aspectRatio=this.aspectRatio,this._layers=[],this._metasets=[],this._stacks=void 0,this.boxes=[],this.currentDevicePixelRatio=void 0,this.chartArea=void 0,this._active=[],this._lastEvent=void 0,this._listeners={},this._sortedMetasets=[],this.scales={},this.scale=void 0,this._plugins=new Tn,this.$proxies={},this._hiddenIndices={},this.attached=!1,this._animationsDisabled=void 0,this.$context=void 0,this._doResize=i((()=>this.update("resize")),r.resizeDelay||0),Zn[n.id]=n,l&&c?(a.listen(n,"complete",Xn),a.listen(n,"progress",qn),n._initialize(),n.attached&&n.update()):console.error("Failed to create chart: can't acquire context from the given item")}get aspectRatio(){const{options:{aspectRatio:t,maintainAspectRatio:e},width:i,height:n,_aspectRatio:o}=this;return $(t)?e&&o?o:n?i/n:null:t}get data(){return this.config.data}set data(t){this.config.data=t}get options(){return this._options}set options(t){this.config.options=t}_initialize(){const t=this;return t.notifyPlugins("beforeInit"),t.options.responsive?t.resize():ye(t,t.options.devicePixelRatio),t.bindEvents(),t.notifyPlugins("afterInit"),t}_initializePlatform(t,e){return e.platform?new e.platform:!Kn()||"undefined"!=typeof OffscreenCanvas&&t instanceof OffscreenCanvas?new Qe:new di}clear(){return qt(this.canvas,this.ctx),this}stop(){return a.stop(this),this}resize(t,e){a.running(this)?this._resizeBeforeDraw={width:t,height:e}:this._resize(t,e)}_resize(t,e){const i=this,n=i.options,o=i.canvas,s=n.maintainAspectRatio&&i.aspectRatio,a=i.platform.getMaximumSize(o,t,e,s),r=i.currentDevicePixelRatio,l=n.devicePixelRatio||i.platform.getDevicePixelRatio();i.width===a.width&&i.height===a.height&&r===l||(i.width=a.width,i.height=a.height,i._aspectRatio=i.aspectRatio,ye(i,l,!0),i.notifyPlugins("resize",{size:a}),Q(n.onResize,[i,a],i),i.attached&&i._doResize()&&i.render())}ensureScalesHaveIDs(){J(this.options.scales||{},((t,e)=>{t.id=e}))}buildOrUpdateScales(){const t=this,e=t.options,i=e.scales,n=t.scales,o=Object.keys(n).reduce(((t,e)=>(t[e]=!1,t)),{});let s=[];i&&(s=s.concat(Object.keys(i).map((t=>{const e=i[t],n=In(t,e),o="r"===n,s="x"===n;return{options:e,dposition:o?"chartArea":s?"bottom":"left",dtype:o?"radialLinear":s?"category":"linear"}})))),J(s,(i=>{const s=i.options,a=s.id,r=In(a,s),l=K(s.type,i.dtype);void 0!==s.position&&Yn(s.position,r)===Yn(i.dposition)||(s.position=i.dposition),o[a]=!0;let c=null;if(a in n&&n[a].type===l)c=n[a];else{c=new(An.getScale(l))({id:a,type:l,ctx:t.ctx,chart:t}),n[c.id]=c}c.init(s,e)})),J(o,((t,e)=>{t||delete n[e]})),J(n,(e=>{Ge.configure(t,e,e.options),Ge.addBox(t,e)}))}_updateMetasetIndex(t,e){const i=this._metasets,n=t.index;n!==e&&(i[n]=i[e],i[e]=t,t.index=e)}_updateMetasets(){const t=this,e=t._metasets,i=t.data.datasets.length,n=e.length;if(n>i){for(let e=i;ei.length&&delete t._stacks,e.forEach(((e,n)=>{0===i.filter((t=>t===e._dataset)).length&&t._destroyDatasetMeta(n)}))}buildOrUpdateControllers(){const t=this,e=[],i=t.data.datasets;let n,o;for(t._removeUnreferencedMetasets(),n=0,o=i.length;n{t.getDatasetMeta(i).controller.reset()}),t)}reset(){this._resetElements(),this.notifyPlugins("reset")}update(t){const e=this,i=e.config;i.update(),e._options=i.createResolver(i.chartOptionScopes(),e.getContext()),J(e.scales,(t=>{Ge.removeBox(e,t)}));const n=e._animationsDisabled=!e.options.animation;e.ensureScalesHaveIDs(),e.buildOrUpdateScales();const o=new Set(Object.keys(e._listeners)),s=new Set(e.options.events);if(ut(o,s)||(e.unbindEvents(),e.bindEvents()),e._plugins.invalidate(),!1===e.notifyPlugins("beforeUpdate",{mode:t,cancelable:!0}))return;const a=e.buildOrUpdateControllers();e.notifyPlugins("beforeElementsUpdate");let r=0;for(let t=0,i=e.data.datasets.length;t{t.reset()})),e._updateDatasets(t),e.notifyPlugins("afterUpdate",{mode:t}),e._layers.sort(Un("z","_idx")),e._lastEvent&&e._eventHandler(e._lastEvent,!0),e.render()}_updateLayout(t){const e=this;if(!1===e.notifyPlugins("beforeLayout",{cancelable:!0}))return;Ge.update(e,e.width,e.height,t);const i=e.chartArea,n=i.width<=0||i.height<=0;e._layers=[],J(e.boxes,(t=>{n&&"chartArea"===t.position||(t.configure&&t.configure(),e._layers.push(...t._layers()))}),e),e._layers.forEach(((t,e)=>{t._idx=e})),e.notifyPlugins("afterLayout")}_updateDatasets(t){const e=this,i="function"==typeof t;if(!1!==e.notifyPlugins("beforeDatasetsUpdate",{mode:t,cancelable:!0})){for(let n=0,o=e.data.datasets.length;n=0;--i)t._drawDataset(e[i]);t.notifyPlugins("afterDatasetsDraw")}_drawDataset(t){const e=this,i=e.ctx,n=t._clip,o=e.chartArea,s={meta:t,index:t.index,cancelable:!0};!1!==e.notifyPlugins("beforeDatasetDraw",s)&&(Zt(i,{left:!1===n.left?0:o.left-n.left,right:!1===n.right?e.width:o.right+n.right,top:!1===n.top?0:o.top-n.top,bottom:!1===n.bottom?e.height:o.bottom+n.bottom}),t.controller.draw(),Qt(i),s.cancelable=!1,e.notifyPlugins("afterDatasetDraw",s))}getElementsAtEventForMode(t,e,i,n){const o=Oe.modes[e];return"function"==typeof o?o(this,t,i,n):[]}getDatasetMeta(t){const e=this.data.datasets[t],i=this._metasets;let n=i.filter((t=>t&&t._dataset===e)).pop();return n||(n=i[t]={type:null,data:[],dataset:null,controller:null,hidden:null,xAxisID:null,yAxisID:null,order:e&&e.order||0,index:t,_dataset:e,_parsed:[],_sorted:!1}),n}getContext(){return this.$context||(this.$context={chart:this,type:"chart"})}getVisibleDatasetCount(){return this.getSortedVisibleDatasetMetas().length}isDatasetVisible(t){const e=this.data.datasets[t];if(!e)return!1;const i=this.getDatasetMeta(t);return"boolean"==typeof i.hidden?!i.hidden:!e.hidden}setDatasetVisibility(t,e){this.getDatasetMeta(t).hidden=!e}toggleDataVisibility(t){this._hiddenIndices[t]=!this._hiddenIndices[t]}getDataVisibility(t){return!this._hiddenIndices[t]}_updateDatasetVisibility(t,e){const i=this,n=e?"show":"hide",o=i.getDatasetMeta(t),s=o.controller._resolveAnimations(void 0,n);i.setDatasetVisibility(t,e),s.update(o,{visible:e}),i.update((e=>e.datasetIndex===t?n:void 0))}hide(t){this._updateDatasetVisibility(t,!1)}show(t){this._updateDatasetVisibility(t,!0)}_destroyDatasetMeta(t){const e=this,i=e._metasets&&e._metasets[t];i&&i.controller&&(i.controller._destroy(),delete e._metasets[t])}destroy(){const t=this,{canvas:e,ctx:i}=t;let n,o;for(t.stop(),a.remove(t),n=0,o=t.data.datasets.length;n{i.addEventListener(t,n,o),e[n]=o},o=(n,o)=>{e[n]&&(i.removeEventListener(t,n,o),delete e[n])};let s=function(e,i,n){e.offsetX=i,e.offsetY=n,t._eventHandler(e)};if(J(t.options.events,(t=>n(t,s))),t.options.responsive){let e;s=(e,i)=>{t.canvas&&t.resize(e,i)};const a=()=>{o("attach",a),t.attached=!0,t.resize(),n("resize",s),n("detach",e)};e=()=>{t.attached=!1,o("resize",s),n("attach",a)},i.isAttached(t.canvas)?a():e()}else t.attached=!0}unbindEvents(){const t=this,e=t._listeners;e&&(t._listeners={},J(e,((e,i)=>{t.platform.removeEventListener(t,i,e)})))}updateHoverStyle(t,e,i){const n=i?"set":"remove";let o,s,a,r;for("dataset"===e&&(o=this.getDatasetMeta(t[0].datasetIndex),o.controller["_"+n+"DatasetHoverStyle"]()),a=0,r=t.length;a{const n=e.getDatasetMeta(t);if(!n)throw new Error("No dataset found at index "+t);return{datasetIndex:t,element:n.data[i],index:i}}));!tt(n,i)&&(e._active=n,e._updateHoverStyles(n,i))}notifyPlugins(t,e,i){return this._plugins.notify(this,t,e,i)}_updateHoverStyles(t,e,i){const n=this,o=n.options.hover,s=(t,e)=>t.filter((t=>!e.some((e=>t.datasetIndex===e.datasetIndex&&t.index===e.index)))),a=s(e,t),r=i?t:s(t,e);a.length&&n.updateHoverStyle(a,o.mode,!1),r.length&&o.mode&&n.updateHoverStyle(r,o.mode,!0)}_eventHandler(t,e){const i=this,n={event:t,replay:e,cancelable:!0},o=e=>(e.options.events||this.options.events).includes(t.type);if(!1===i.notifyPlugins("beforeEvent",n,o))return;const s=i._handleEvent(t,e);return n.cancelable=!1,i.notifyPlugins("afterEvent",n,o),(s||n.changed)&&i.render(),i}_handleEvent(t,e){const i=this,{_active:n=[],options:o}=i,s=o.hover,a=e;let r=[],l=!1,c=null;return"mouseout"!==t.type&&(r=i.getElementsAtEventForMode(t,s.mode,s,a),c="click"===t.type?i._lastEvent:t),i._lastEvent=null,Gt(t,i.chartArea,i._minPadding)&&(Q(o.onHover,[t,r,i],i),"mouseup"!==t.type&&"click"!==t.type&&"contextmenu"!==t.type||Q(o.onClick,[t,r,i],i)),l=!tt(r,n),(l||e)&&(i._active=r,i._updateHoverStyles(r,n,e)),i._lastEvent=c,l}}const to=()=>J(Jn.instances,(t=>t._plugins.invalidate())),eo=!0;function io(){throw new Error("This method is not implemented: either no adapter can be found or an incomplete integration was provided.")}Object.defineProperties(Jn,{defaults:{enumerable:eo,value:xt},instances:{enumerable:eo,value:Zn},overrides:{enumerable:eo,value:ft},registry:{enumerable:eo,value:An},version:{enumerable:eo,value:"3.2.1"},getChart:{enumerable:eo,value:Qn},register:{enumerable:eo,value:(...t)=>{An.add(...t),to()}},unregister:{enumerable:eo,value:(...t)=>{An.remove(...t),to()}}});class no{constructor(t){this.options=t||{}}formats(){return io()}parse(t,e){return io()}format(t,e){return io()}add(t,e,i){return io()}diff(t,e,i){return io()}startOf(t,e,i){return io()}endOf(t,e){return io()}}no.override=function(t){Object.assign(no.prototype,t)};var oo={_date:no};function so(t){const e=function(t){if(!t._cache.$bar){const e=t.getMatchingVisibleMetas("bar");let i=[];for(let n=0,o=e.length;nt-e)))}return t._cache.$bar}(t);let i,n,o,s,a=t._length;const r=()=>{32767!==o&&-32768!==o&&(ht(s)&&(a=Math.min(a,Math.abs(o-s)||a)),s=o)};for(i=0,n=e.length;iMath.abs(r)&&(l=r,c=a),e[i.axis]=c,e._custom={barStart:l,barEnd:c,start:o,end:s,min:a,max:r}}(t,e,i,n):e[i.axis]=i.parse(t,n),e}function ro(t,e,i,n){const o=t.iScale,s=t.vScale,a=o.getLabels(),r=o===s,l=[];let c,h,d,u;for(c=i,h=i+n;c0?(p+=t,h-=t):h<0&&(p-=t,h+=t)}return{size:h,base:p,head:c,center:c+h/2}}_calculateBarIndexPixels(t,e){const i=this,n=e.scale,o=i.options,s=o.skipNull,a=K(o.maxBarThickness,1/0);let r,l;if(e.grouped){const n=s?i._getStackCount(t):e.stackCount,c="flex"===o.barThickness?function(t,e,i,n){const o=e.pixels,s=o[t];let a=t>0?o[t-1]:null,r=t=0;--n)i=Math.max(i,t[n].size()/2,e[n]._custom);return i>0&&i}getLabelAndValue(t){const e=this._cachedMeta,{xScale:i,yScale:n}=e,o=this.getParsed(t),s=i.getLabelForValue(o.x),a=n.getLabelForValue(o.y),r=o._custom;return{label:e.label,value:"("+s+", "+a+(r?", "+r:"")+")"}}update(t){const e=this._cachedMeta.data;this.updateElements(e,0,e.length,t)}updateElements(t,e,i,n){const o=this,s="reset"===n,{xScale:a,yScale:r}=o._cachedMeta,l=o.resolveDataElementOptions(e,n),c=o.getSharedOptions(l),h=o.includeOptions(n,c);for(let l=e;l""}}}};class uo extends Li{constructor(t,e){super(t,e),this.enableOptionSharing=!0,this.innerRadius=void 0,this.outerRadius=void 0,this.offsetX=void 0,this.offsetY=void 0}linkScales(){}parse(t,e){const i=this.getDataset().data,n=this._cachedMeta;let o,s;for(o=t,s=t+e;oHt(t,r,l)?1:Math.max(e,e*i,n,n*i),g=(t,e,n)=>Ht(t,r,l)?-1:Math.min(e,e*i,n,n*i),p=f(0,c,d),m=f(Mt,h,u),x=g(bt,c,d),b=g(bt+Mt,h,u);n=(p-x)/2,o=(m-b)/2,s=-(p+x)/2,a=-(m+b)/2}return{ratioX:n,ratioY:o,offsetX:s,offsetY:a}}(d,h,l),m=(n.width-a)/u,x=(n.height-a)/f,b=Math.max(Math.min(m,x)/2,0),_=Z(e.options.radius,b),y=(_-Math.max(_*l,0))/e._getVisibleDatasetWeightTotal();e.offsetX=g*_,e.offsetY=p*_,o.total=e.calculateTotal(),e.outerRadius=_-y*e._getRingWeightOffset(e.index),e.innerRadius=Math.max(e.outerRadius-y*c,0),e.updateElements(s,0,s.length,t)}_circumference(t,e){const i=this,n=i.options,o=i._cachedMeta,s=i._getCircumference();return e&&n.animation.animateRotate||!this.chart.getDataVisibility(t)||null===o._parsed[t]?0:i.calculateCircumference(o._parsed[t]*s/_t)}updateElements(t,e,i,n){const o=this,s="reset"===n,a=o.chart,r=a.chartArea,l=a.options.animation,c=(r.left+r.right)/2,h=(r.top+r.bottom)/2,d=s&&l.animateScale,u=d?0:o.innerRadius,f=d?0:o.outerRadius,g=o.resolveDataElementOptions(e,n),p=o.getSharedOptions(g),m=o.includeOptions(n,p);let x,b=o._getRotation();for(x=0;x0&&!isNaN(t)?_t*(Math.abs(t)/e):0}getLabelAndValue(t){const e=this._cachedMeta,i=this.chart,n=i.data.labels||[],o=zi(e._parsed[t],i.options.locale);return{label:n[t]||"",value:o}}getMaxBorderWidth(t){const e=this;let i=0;const n=e.chart;let o,s,a,r,l;if(!t)for(o=0,s=n.data.datasets.length;o{const n=t.getDatasetMeta(0).controller.getStyle(i);return{text:e,fillStyle:n.backgroundColor,strokeStyle:n.borderColor,lineWidth:n.borderWidth,hidden:!t.getDataVisibility(i),index:i}})):[]}},onClick(t,e,i){i.chart.toggleDataVisibility(e.index),i.chart.update()}},tooltip:{callbacks:{title:()=>"",label(t){let e=t.label;const i=": "+t.formattedValue;return Y(e)?(e=e.slice(),e[0]+=i):e+=i,e}}}}};class fo extends Li{initialize(){this.enableOptionSharing=!0,super.initialize()}update(t){const e=this,i=e._cachedMeta,{dataset:n,data:o=[],_dataset:s}=i,a=e.chart._animationsDisabled;let{start:r,count:l}=function(t,e,i){const n=e.length;let o=0,s=n;if(t._sorted){const{iScale:a,_parsed:r}=t,l=a.axis,{min:c,max:h,minDefined:d,maxDefined:u}=a.getUserBounds();d&&(o=Nt(Math.min(oe(r,a.axis,c).lo,i?n:oe(e,l,a.getPixelForValue(c)).lo),0,n-1)),s=u?Nt(Math.max(oe(r,a.axis,h).hi+1,i?0:oe(e,l,a.getPixelForValue(h)).hi+1),o,n)-o:n-o}return{start:o,count:s}}(i,o,a);e._drawStart=r,e._drawCount=l,function(t){const{xScale:e,yScale:i,_scaleRanges:n}=t,o={xmin:e.min,xmax:e.max,ymin:i.min,ymax:i.max};if(!n)return t._scaleRanges=o,!0;const s=n.xmin!==e.min||n.xmax!==e.max||n.ymin!==i.min||n.ymax!==i.max;return Object.assign(n,o),s}(i)&&(r=0,l=o.length),n._decimated=!!s._decimated,n.points=o;const c=e.resolveDatasetElementOptions(t);e.options.showLine||(c.borderWidth=0),c.segment=e.options.segment,e.updateElement(n,void 0,{animated:!a,options:c},t),e.updateElements(o,r,l,t)}updateElements(t,e,i,n){const o=this,s="reset"===n,{xScale:a,yScale:r,_stacked:l}=o._cachedMeta,c=o.resolveDataElementOptions(e,n),h=o.getSharedOptions(c),d=o.includeOptions(n,h),u=o.options.spanGaps,f=At(u)?u:Number.POSITIVE_INFINITY,g=o.chart._animationsDisabled||s||"none"===n;let p=e>0&&o.getParsed(e-1);for(let c=e;c0&&i.x-p.x>f,u.parsed=i,d&&(u.options=h||o.resolveDataElementOptions(c,n)),g||o.updateElement(e,c,u,n),p=i}o.updateSharedOptions(h,n,c)}getMaxOverflow(){const t=this,e=t._cachedMeta,i=e.dataset,n=i.options&&i.options.borderWidth||0,o=e.data||[];if(!o.length)return n;const s=o[0].size(t.resolveDataElementOptions(0)),a=o[o.length-1].size(t.resolveDataElementOptions(o.length-1));return Math.max(n,s,a)/2}draw(){this._cachedMeta.dataset.updateControlPoints(this.chart.chartArea),super.draw()}}fo.id="line",fo.defaults={datasetElementType:"line",dataElementType:"point",showLine:!0,spanGaps:!1},fo.overrides={scales:{_index_:{type:"category"},_value_:{type:"linear"}}};class go extends Li{constructor(t,e){super(t,e),this.innerRadius=void 0,this.outerRadius=void 0}update(t){const e=this._cachedMeta.data;this._updateRadius(),this.updateElements(e,0,e.length,t)}_updateRadius(){const t=this,e=t.chart,i=e.chartArea,n=e.options,o=Math.min(i.right-i.left,i.bottom-i.top),s=Math.max(o/2,0),a=(s-Math.max(n.cutoutPercentage?s/100*n.cutoutPercentage:1,0))/e.getVisibleDatasetCount();t.outerRadius=s-a*t.index,t.innerRadius=t.outerRadius-a}updateElements(t,e,i,n){const o=this,s="reset"===n,a=o.chart,r=o.getDataset(),l=a.options.animation,c=o._cachedMeta.rScale,h=c.xCenter,d=c.yCenter,u=c.getIndexAngle(0)-.5*bt;let f,g=u;const p=360/o.countVisibleElements();for(f=0;f{!isNaN(t.data[n])&&this.chart.getDataVisibility(n)&&i++})),i}_computeAngle(t,e,i){return this.chart.getDataVisibility(t)?Et(this.resolveDataElementOptions(t,e).angle||i):0}}go.id="polarArea",go.defaults={dataElementType:"arc",animation:{animateRotate:!0,animateScale:!0},animations:{numbers:{type:"number",properties:["x","y","startAngle","endAngle","innerRadius","outerRadius"]}},indexAxis:"r",startAngle:0},go.overrides={aspectRatio:1,plugins:{legend:{labels:{generateLabels(t){const e=t.data;return e.labels.length&&e.datasets.length?e.labels.map(((e,i)=>{const n=t.getDatasetMeta(0).controller.getStyle(i);return{text:e,fillStyle:n.backgroundColor,strokeStyle:n.borderColor,lineWidth:n.borderWidth,hidden:!t.getDataVisibility(i),index:i}})):[]}},onClick(t,e,i){i.chart.toggleDataVisibility(e.index),i.chart.update()}},tooltip:{callbacks:{title:()=>"",label:t=>t.chart.data.labels[t.dataIndex]+": "+t.formattedValue}}},scales:{r:{type:"radialLinear",angleLines:{display:!1},beginAtZero:!0,grid:{circular:!0},pointLabels:{display:!1},startAngle:0}}};class po extends uo{}po.id="pie",po.defaults={cutout:0,rotation:0,circumference:360,radius:"100%"};class mo extends Li{getLabelAndValue(t){const e=this._cachedMeta.vScale,i=this.getParsed(t);return{label:e.getLabels()[t],value:""+e.getLabelForValue(i[e.axis])}}update(t){const e=this,i=e._cachedMeta,n=i.dataset,o=i.data||[],s=i.iScale.getLabels();if(n.points=o,"resize"!==t){const i=e.resolveDatasetElementOptions(t);e.options.showLine||(i.borderWidth=0);const a={_loop:!0,_fullLoop:s.length===o.length,options:i};e.updateElement(n,void 0,a,t)}e.updateElements(o,0,o.length,t)}updateElements(t,e,i,n){const o=this,s=o.getDataset(),a=o._cachedMeta.rScale,r="reset"===n;for(let l=e;l"",label:t=>"("+t.label+", "+t.formattedValue+")"}}},scales:{x:{type:"linear"},y:{type:"linear"}}};var bo=Object.freeze({__proto__:null,BarController:co,BubbleController:ho,DoughnutController:uo,LineController:fo,PolarAreaController:go,PieController:po,RadarController:mo,ScatterController:xo});function _o(t,e){const{startAngle:i,endAngle:n,pixelMargin:o,x:s,y:a,outerRadius:r,innerRadius:l}=e;let c=o/r;t.beginPath(),t.arc(s,a,r,i-c,n+c),l>o?(c=o/l,t.arc(s,a,l,n+c,i-c,!0)):t.arc(s,a,o,n+Mt,i-Mt),t.closePath(),t.clip()}function yo(t,e,i,n){const o=Le(t.options.borderRadius,["outerStart","outerEnd","innerStart","innerEnd"]);const s=(i-e)/2,a=Math.min(s,n*e/2),r=t=>{const e=(i-Math.min(s,t))*n/2;return Nt(t,0,Math.min(s,e))};return{outerStart:r(o.outerStart),outerEnd:r(o.outerEnd),innerStart:Nt(o.innerStart,0,a),innerEnd:Nt(o.innerEnd,0,a)}}function vo(t,e,i,n){return{x:i+t*Math.cos(e),y:n+t*Math.sin(e)}}function wo(t,e){const{x:i,y:n,startAngle:o,endAngle:s,pixelMargin:a}=e,r=Math.max(e.outerRadius-a,0),l=e.innerRadius+a,{outerStart:c,outerEnd:h,innerStart:d,innerEnd:u}=yo(e,l,r,s-o),f=r-c,g=r-h,p=o+c/f,m=s-h/g,x=l+d,b=l+u,_=o+d/x,y=s-u/b;if(t.beginPath(),t.arc(i,n,r,p,m),h>0){const e=vo(g,m,i,n);t.arc(e.x,e.y,h,m,s+Mt)}const v=vo(b,s,i,n);if(t.lineTo(v.x,v.y),u>0){const e=vo(b,y,i,n);t.arc(e.x,e.y,u,s+Mt,y+Math.PI)}if(t.arc(i,n,l,s-u/l,o+d/l,!0),d>0){const e=vo(x,_,i,n);t.arc(e.x,e.y,d,_+Math.PI,o-Mt)}const w=vo(f,o,i,n);if(t.lineTo(w.x,w.y),c>0){const e=vo(f,p,i,n);t.arc(e.x,e.y,c,o-Mt,p)}t.closePath()}function Mo(t,e){const{options:i}=e,n="inner"===i.borderAlign;i.borderWidth&&(n?(t.lineWidth=2*i.borderWidth,t.lineJoin="round"):(t.lineWidth=i.borderWidth,t.lineJoin="bevel"),e.fullCircles&&function(t,e,i){const{x:n,y:o,startAngle:s,endAngle:a,pixelMargin:r}=e,l=Math.max(e.outerRadius-r,0),c=e.innerRadius+r;let h;for(i&&(e.endAngle=e.startAngle+_t,_o(t,e),e.endAngle=a,e.endAngle===e.startAngle&&(e.endAngle+=_t,e.fullCircles--)),t.beginPath(),t.arc(n,o,c,s+_t,s,!0),h=0;h=_t||Ht(o,a,r))&&(s>=l&&s<=c)}getCenterPoint(t){const{x:e,y:i,startAngle:n,endAngle:o,innerRadius:s,outerRadius:a}=this.getProps(["x","y","startAngle","endAngle","innerRadius","outerRadius"],t),r=(n+o)/2,l=(s+a)/2;return{x:e+Math.cos(r)*l,y:i+Math.sin(r)*l}}tooltipPosition(t){return this.getCenterPoint(t)}draw(t){const e=this,i=e.options,n=i.offset||0;if(e.pixelMargin="inner"===i.borderAlign?.33:0,e.fullCircles=Math.floor(e.circumference/_t),!(0===e.circumference||e.innerRadius<0||e.outerRadius<0)){if(t.save(),n&&e.circumference<_t){const i=(e.startAngle+e.endAngle)/2;t.translate(Math.cos(i)*n,Math.sin(i)*n)}t.fillStyle=i.backgroundColor,t.strokeStyle=i.borderColor,function(t,e){if(e.fullCircles){e.endAngle=e.startAngle+_t,wo(t,e);for(let i=0;ir&&s>r;return{count:n,start:l,loop:e.loop,ilen:c(a+(c?r-t:t))%s,_=()=>{f!==g&&(t.lineTo(m,g),t.lineTo(m,f),t.lineTo(m,p))};for(l&&(d=o[b(0)],t.moveTo(d.x,d.y)),h=0;h<=r;++h){if(d=o[b(h)],d.skip)continue;const e=d.x,i=d.y,n=0|e;n===u?(ig&&(g=i),m=(x*m+e)/++x):(_(),t.lineTo(e,i),u=n,x=0,f=g=i),p=i}_()}function Ao(t){const e=t.options,i=e.borderDash&&e.borderDash.length;return!(t._decimated||t._loop||e.tension||"monotone"===e.cubicInterpolationMode||e.stepped||i)?Oo:Co}ko.id="arc",ko.defaults={borderAlign:"center",borderColor:"#fff",borderRadius:0,borderWidth:2,offset:0,angle:void 0},ko.defaultRoutes={backgroundColor:"backgroundColor"};const To="function"==typeof Path2D;function Ro(t,e,i,n){To&&1===e.segments.length?function(t,e,i,n){let o=e._path;o||(o=e._path=new Path2D,e.path(o,i,n)&&o.closePath()),So(t,e.options),t.stroke(o)}(t,e,i,n):function(t,e,i,n){const{segments:o,options:s}=e,a=Ao(e);for(const r of o)So(t,s,r.style),t.beginPath(),a(t,e,r,{start:i,end:i+n-1})&&t.closePath(),t.stroke()}(t,e,i,n)}class Lo extends Ei{constructor(t){super(),this.animated=!0,this.options=void 0,this._loop=void 0,this._fullLoop=void 0,this._path=void 0,this._points=void 0,this._segments=void 0,this._decimated=!1,this._pointsUpdated=!1,t&&Object.assign(this,t)}updateControlPoints(t){const e=this,i=e.options;if((i.tension||"monotone"===i.cubicInterpolationMode)&&!i.stepped&&!e._pointsUpdated){const n=i.spanGaps?e._loop:e._fullLoop;fn(e._points,i,t,n),e._pointsUpdated=!0}}set points(t){const e=this;e._points=t,delete e._segments,delete e._path,e._pointsUpdated=!1}get points(){return this._points}get segments(){return this._segments||(this._segments=kn(this,this.options.segment))}first(){const t=this.segments,e=this.points;return t.length&&e[t[0].start]}last(){const t=this.segments,e=this.points,i=t.length;return i&&e[t[i-1].end]}interpolate(t,e){const i=this,n=i.options,o=t[e],s=i.points,a=Mn(i,{property:e,start:o,end:o});if(!a.length)return;const r=[],l=function(t){return t.stepped?pn:t.tension||"monotone"===t.cubicInterpolationMode?mn:gn}(n);let c,h;for(c=0,h=a.length;c"borderDash"!==t&&"fill"!==t};class Io extends Ei{constructor(t){super(),this.options=void 0,this.parsed=void 0,this.skip=void 0,this.stop=void 0,t&&Object.assign(this,t)}inRange(t,e,i){const n=this.options,{x:o,y:s}=this.getProps(["x","y"],i);return Math.pow(t-o,2)+Math.pow(e-s,2)t.x):Vo(e,"bottom","top",t.base=a.left&&e<=a.right)&&(s||i>=a.top&&i<=a.bottom)}function jo(t,e){t.rect(e.x,e.y,e.w,e.h)}Io.id="point",Io.defaults={borderWidth:1,hitRadius:1,hoverBorderWidth:1,hoverRadius:4,pointStyle:"circle",radius:3,rotation:0},Io.defaultRoutes={backgroundColor:"backgroundColor",borderColor:"borderColor"};class $o extends Ei{constructor(t){super(),this.options=void 0,this.horizontal=void 0,this.base=void 0,this.width=void 0,this.height=void 0,t&&Object.assign(this,t)}draw(t){const e=this.options,{inner:i,outer:n}=Ho(this),o=(s=n.radius).topLeft||s.topRight||s.bottomLeft||s.bottomRight?ie:jo;var s;t.save(),n.w===i.w&&n.h===i.h||(t.beginPath(),o(t,n),t.clip(),o(t,i),t.fillStyle=e.borderColor,t.fill("evenodd")),t.beginPath(),o(t,i),t.fillStyle=e.backgroundColor,t.fill(),t.restore()}inRange(t,e,i){return No(this,t,e,i)}inXRange(t,e){return No(this,t,null,e)}inYRange(t,e){return No(this,null,t,e)}getCenterPoint(t){const{x:e,y:i,base:n,horizontal:o}=this.getProps(["x","y","base","horizontal"],t);return{x:o?(e+n)/2:e,y:o?i:(i+n)/2}}getRange(t){return"x"===t?this.width/2:this.height/2}}$o.id="bar",$o.defaults={borderSkipped:"start",borderWidth:0,borderRadius:0,enableBorderRadius:!0,pointStyle:void 0},$o.defaultRoutes={backgroundColor:"backgroundColor",borderColor:"borderColor"};var Yo=Object.freeze({__proto__:null,ArcElement:ko,LineElement:Lo,PointElement:Io,BarElement:$o});function Uo(t){if(t._decimated){const e=t._data;delete t._decimated,delete t._data,Object.defineProperty(t,"data",{value:e})}}function Xo(t){t.data.datasets.forEach((t=>{Uo(t)}))}var qo={id:"decimation",defaults:{algorithm:"min-max",enabled:!1},beforeElementsUpdate:(t,e,i)=>{if(!i.enabled)return void Xo(t);const n=t.width;t.data.datasets.forEach(((e,o)=>{const{_data:s,indexAxis:a}=e,r=t.getDatasetMeta(o),l=s||e.data;if("y"===Ve([a,t.options.indexAxis]))return;if("line"!==r.type)return;const c=t.scales[r.xAxisID];if("linear"!==c.type&&"time"!==c.type)return;if(t.options.parsing)return;let h,{start:d,count:u}=function(t,e){const i=e.length;let n,o=0;const{iScale:s}=t,{min:a,max:r,minDefined:l,maxDefined:c}=s.getUserBounds();return l&&(o=Nt(oe(e,s.axis,a).lo,0,i-1)),n=c?Nt(oe(e,s.axis,r).hi+1,o,i)-o:i-o,{start:o,count:n}}(r,l);if(u<=4*n)Uo(e);else{switch($(s)&&(e._data=l,delete e.data,Object.defineProperty(e,"data",{configurable:!0,enumerable:!0,get:function(){return this._decimated},set:function(t){this._data=t}})),i.algorithm){case"lttb":h=function(t,e,i,n,o){const s=o.samples||n;if(s>=i)return t.slice(e,e+i);const a=[],r=(i-2)/(s-2);let l=0;const c=e+i-1;let h,d,u,f,g,p=e;for(a[l++]=t[p],h=0;hu&&(u=f,d=t[n],g=n);a[l++]=d,p=g}return a[l++]=t[c],a}(l,d,u,n,i);break;case"min-max":h=function(t,e,i,n){let o,s,a,r,l,c,h,d,u,f,g=0,p=0;const m=[],x=e+i-1,b=t[e].x,_=t[x].x-b;for(o=e;of&&(f=r,h=o),g=(p*g+s.x)/++p;else{const i=o-1;if(!$(c)&&!$(h)){const e=Math.min(c,h),n=Math.max(c,h);e!==d&&e!==i&&m.push({...t[e],x:g}),n!==d&&n!==i&&m.push({...t[n],x:g})}o>0&&i!==d&&m.push(t[i]),m.push(s),l=e,p=0,u=f=r,c=h=d=o}}return m}(l,d,u,n);break;default:throw new Error(`Unsupported decimation algorithm '${i.algorithm}'`)}e._decimated=h}}))},destroy(t){Xo(t)}};function Ko(t,e,i){const n=function(t){const e=t.options,i=e.fill;let n=K(i&&i.target,i);return void 0===n&&(n=!!e.backgroundColor),!1!==n&&null!==n&&(!0===n?"origin":n)}(t);if(U(n))return!isNaN(n.value)&&n;let o=parseFloat(n);return X(o)&&Math.floor(o)===o?("-"!==n[0]&&"+"!==n[0]||(o=e+o),!(o===e||o<0||o>=i)&&o):["origin","start","end","stack"].indexOf(n)>=0&&n}class Go{constructor(t){this.x=t.x,this.y=t.y,this.radius=t.radius}pathSegment(t,e,i){const{x:n,y:o,radius:s}=this;return e=e||{start:0,end:_t},t.arc(n,o,s,e.end,e.start,!0),!i.bounds}interpolate(t){const{x:e,y:i,radius:n}=this,o=t.angle;return{x:e+Math.cos(o)*n,y:i+Math.sin(o)*n,angle:o}}}function Zo(t){return(t.scale||{}).getPointPositionForValue?function(t){const{scale:e,fill:i}=t,n=e.options,o=e.getLabels().length,s=[],a=n.reverse?e.max:e.min,r=n.reverse?e.min:e.max;let l,c,h;if(h="start"===i?a:"end"===i?r:U(i)?i.value:e.getBaseValue(),n.grid.circular)return c=e.getPointPositionForValue(0,a),new Go({x:c.x,y:c.y,radius:e.getDistanceFromCenterForValue(h)});for(l=0;l"line"===t.type&&!t.hidden;function ts(t,e,i){const n=[];for(let o=0;o=n&&o<=c){r=o===n,l=o===c;break}}return{first:r,last:l,point:n}}function is(t,e){let i=[],n=!1;return Y(t)?(n=!0,i=t):i=function(t,e){const{x:i=null,y:n=null}=t||{},o=e.points,s=[];return e.segments.forEach((t=>{const e=o[t.start],a=o[t.end];null!==n?(s.push({x:e.x,y:n}),s.push({x:a.x,y:n})):null!==i&&(s.push({x:i,y:e.y}),s.push({x:i,y:a.y}))})),s}(t,e),i.length?new Lo({points:i,options:{tension:0},_loop:n,_fullLoop:n}):null}function ns(t,e,i){let n=t[e].fill;const o=[e];let s;if(!i)return n;for(;!1!==n&&-1===o.indexOf(n);){if(!X(n))return n;if(s=t[n],!s)return!1;if(s.visible)return n;o.push(n),n=s.fill}return!1}function os(t,e,i){t.beginPath(),e.path(t),t.lineTo(e.last().x,i),t.lineTo(e.first().x,i),t.closePath(),t.clip()}function ss(t,e,i,n){if(n)return;let o=e[t],s=i[t];return"angle"===t&&(o=Wt(o),s=Wt(s)),{property:t,start:o,end:s}}function as(t,e,i,n){return t&&e?n(t[i],e[i]):t?t[i]:e?e[i]:0}function rs(t,e,i){const{top:n,bottom:o}=e.chart.chartArea,{property:s,start:a,end:r}=i||{};"x"===s&&(t.beginPath(),t.rect(a,n,r-a,o-n),t.clip())}function ls(t,e,i,n){const o=e.interpolate(i,n);o&&t.lineTo(o.x,o.y)}function cs(t,e){const{line:i,target:n,property:o,color:s,scale:a}=e,r=function(t,e,i){const n=t.segments,o=t.points,s=e.points,a=[];for(const t of n){const n=ss(i,o[t.start],o[t.end],t.loop);if(!e.segments){a.push({source:t,target:n,start:o[t.start],end:o[t.end]});continue}const r=Mn(e,n);for(const e of r){const r=ss(i,s[e.start],s[e.end],e.loop),l=wn(t,o,r);for(const t of l)a.push({source:t,target:e,start:{[i]:as(n,r,"start",Math.max)},end:{[i]:as(n,r,"end",Math.min)}})}}return a}(i,n,o);for(const{source:e,target:l,start:c,end:h}of r){const{style:{backgroundColor:r=s}={}}=e;t.save(),t.fillStyle=r,rs(t,a,ss(o,c,h)),t.beginPath();const d=!!i.pathSegment(t,e);d?t.closePath():ls(t,n,h,o);const u=!!n.pathSegment(t,l,{move:d,reverse:!0}),f=d&&u;f||ls(t,n,c,o),t.closePath(),t.fill(f?"evenodd":"nonzero"),t.restore()}}function hs(t,e,i){const n=function(t){const{chart:e,fill:i,line:n}=t;if(X(i))return function(t,e){const i=t.getDatasetMeta(e);return i&&t.isDatasetVisible(e)?i.dataset:null}(e,i);if("stack"===i)return Qo(t);const o=Zo(t);return o instanceof Go?o:is(o,n)}(e),{line:o,scale:s,axis:a}=e,r=o.options,l=r.fill,c=r.backgroundColor,{above:h=c,below:d=c}=l||{};n&&o.points.length&&(Zt(t,i),function(t,e){const{line:i,target:n,above:o,below:s,area:a,scale:r}=e,l=i._loop?"angle":e.axis;t.save(),"x"===l&&s!==o&&(os(t,n,a.top),cs(t,{line:i,target:n,color:o,scale:r,property:l}),t.restore(),t.save(),os(t,n,a.bottom)),cs(t,{line:i,target:n,color:s,scale:r,property:l}),t.restore()}(t,{line:o,target:n,above:h,below:d,area:i,scale:s,axis:a}),Qt(t))}var ds={id:"filler",afterDatasetsUpdate(t,e,i){const n=(t.data.datasets||[]).length,o=[];let s,a,r,l;for(a=0;a=0;--e){const i=o[e].$filler;i&&(i.line.updateControlPoints(s),n&&hs(t.ctx,i,s))}},beforeDatasetsDraw(t,e,i){if("beforeDatasetsDraw"!==i.drawTime)return;const n=t.getSortedVisibleDatasetMetas();for(let e=n.length-1;e>=0;--e){const i=n[e].$filler;i&&hs(t.ctx,i,t.chartArea)}},beforeDatasetDraw(t,e,i){const n=e.meta.$filler;n&&!1!==n.fill&&"beforeDatasetDraw"===i.drawTime&&hs(t.ctx,n,t.chartArea)},defaults:{propagate:!0,drawTime:"beforeDatasetDraw"}};const us=(t,e)=>{let{boxHeight:i=e,boxWidth:n=e}=t;return t.usePointStyle&&(i=Math.min(i,e),n=Math.min(n,e)),{boxWidth:n,boxHeight:i,itemHeight:Math.max(e,i)}};class fs extends Ei{constructor(t){super(),this._added=!1,this.legendHitBoxes=[],this._hoveredItem=null,this.doughnutMode=!1,this.chart=t.chart,this.options=t.options,this.ctx=t.ctx,this.legendItems=void 0,this.columnSizes=void 0,this.lineWidths=void 0,this.maxHeight=void 0,this.maxWidth=void 0,this.top=void 0,this.bottom=void 0,this.left=void 0,this.right=void 0,this.height=void 0,this.width=void 0,this._margins=void 0,this.position=void 0,this.weight=void 0,this.fullSize=void 0}update(t,e,i){const n=this;n.maxWidth=t,n.maxHeight=e,n._margins=i,n.setDimensions(),n.buildLabels(),n.fit()}setDimensions(){const t=this;t.isHorizontal()?(t.width=t.maxWidth,t.left=0,t.right=t.width):(t.height=t.maxHeight,t.top=0,t.bottom=t.height)}buildLabels(){const t=this,e=t.options.labels||{};let i=Q(e.generateLabels,[t.chart],t)||[];e.filter&&(i=i.filter((i=>e.filter(i,t.chart.data)))),e.sort&&(i=i.sort(((i,n)=>e.sort(i,n,t.chart.data)))),t.options.reverse&&i.reverse(),t.legendItems=i}fit(){const t=this,{options:e,ctx:i}=t;if(!e.display)return void(t.width=t.height=0);const n=e.labels,o=Fe(n.font),s=o.size,a=t._computeTitleHeight(),{boxWidth:r,itemHeight:l}=us(n,s);let c,h;i.font=o.string,t.isHorizontal()?(c=t.maxWidth,h=t._fitRows(a,s,r,l)+10):(h=t.maxHeight,c=t._fitCols(a,s,r,l)+10),t.width=Math.min(c,e.maxWidth||t.maxWidth),t.height=Math.min(h,e.maxHeight||t.maxHeight)}_fitRows(t,e,i,n){const o=this,{ctx:s,maxWidth:a,options:{labels:{padding:r}}}=o,l=o.legendHitBoxes=[],c=o.lineWidths=[0],h=n+r;let d=t;s.textAlign="left",s.textBaseline="middle";let u=-1,f=-h;return o.legendItems.forEach(((t,o)=>{const g=i+e/2+s.measureText(t.text).width;(0===o||c[c.length-1]+g+2*r>a)&&(d+=h,c[c.length-(o>0?0:1)]=0,f+=h,u++),l[o]={left:0,top:f,row:u,width:g,height:n},c[c.length-1]+=g+r})),d}_fitCols(t,e,i,n){const o=this,{ctx:s,maxHeight:a,options:{labels:{padding:r}}}=o,l=o.legendHitBoxes=[],c=o.columnSizes=[],h=a-t;let d=r,u=0,f=0,g=0,p=0,m=0;return o.legendItems.forEach(((t,o)=>{const a=i+e/2+s.measureText(t.text).width;o>0&&f+e+2*r>h&&(d+=u+r,c.push({width:u,height:f}),g+=u+r,m++,p=0,u=f=0),u=Math.max(u,a),f+=e+r,l[o]={left:g,top:p,col:m,width:a,height:n},p+=n+r})),d+=u,c.push({width:u,height:f}),d}adjustHitBoxes(){const t=this;if(!t.options.display)return;const e=t._computeTitleHeight(),{legendHitBoxes:i,options:{align:n,labels:{padding:s}}}=t;if(this.isHorizontal()){let a=0,r=o(n,t.left+s,t.right-t.lineWidths[a]);for(const l of i)a!==l.row&&(a=l.row,r=o(n,t.left+s,t.right-t.lineWidths[a])),l.top+=t.top+e+s,l.left=r,r+=l.width+s}else{let a=0,r=o(n,t.top+e+s,t.bottom-t.columnSizes[a].height);for(const l of i)l.col!==a&&(a=l.col,r=o(n,t.top+e+s,t.bottom-t.columnSizes[a].height)),l.top=r,l.left+=t.left+s,r+=l.height+s}}isHorizontal(){return"top"===this.options.position||"bottom"===this.options.position}draw(){const t=this;if(t.options.display){const e=t.ctx;Zt(e,t),t._draw(),Qt(e)}}_draw(){const t=this,{options:e,columnSizes:i,lineWidths:n,ctx:a}=t,{align:r,labels:l}=e,c=xt.color,h=xn(e.rtl,t.left,t.width),d=Fe(l.font),{color:u,padding:f}=l,g=d.size,p=g/2;let m;t.drawTitle(),a.textAlign=h.textAlign("left"),a.textBaseline="middle",a.lineWidth=.5,a.font=d.string;const{boxWidth:x,boxHeight:b,itemHeight:_}=us(l,g),y=t.isHorizontal(),v=this._computeTitleHeight();m=y?{x:o(r,t.left+f,t.right-n[0]),y:t.top+f+v,line:0}:{x:t.left+f,y:o(r,t.top+v+f,t.bottom-i[0].height),line:0},bn(t.ctx,e.textDirection);const w=_+f;t.legendItems.forEach(((e,M)=>{a.strokeStyle=e.fontColor||u,a.fillStyle=e.fontColor||u;const k=a.measureText(e.text).width,S=h.textAlign(e.textAlign||(e.textAlign=l.textAlign)),P=x+g/2+k;let D=m.x,C=m.y;h.setWidth(t.width),y?M>0&&D+P+f>t.right&&(C=m.y+=w,m.line++,D=m.x=o(r,t.left+f,t.right-n[m.line])):M>0&&C+w>t.bottom&&(D=m.x=D+i[m.line].width+f,m.line++,C=m.y=o(r,t.top+v+f,t.bottom-i[m.line].height));!function(t,e,i){if(isNaN(x)||x<=0||isNaN(b)||b<0)return;a.save();const n=K(i.lineWidth,1);if(a.fillStyle=K(i.fillStyle,c),a.lineCap=K(i.lineCap,"butt"),a.lineDashOffset=K(i.lineDashOffset,0),a.lineJoin=K(i.lineJoin,"miter"),a.lineWidth=n,a.strokeStyle=K(i.strokeStyle,c),a.setLineDash(K(i.lineDash,[])),l.usePointStyle){const o={radius:x*Math.SQRT2/2,pointStyle:i.pointStyle,rotation:i.rotation,borderWidth:n},s=h.xPlus(t,x/2);Kt(a,o,s,e+p)}else{const o=e+Math.max((g-b)/2,0),s=h.leftForLtr(t,x),r=Ie(i.borderRadius);a.beginPath(),Object.values(r).some((t=>0!==t))?ie(a,{x:s,y:o,w:x,h:b,radius:r}):a.rect(s,o,x,b),a.fill(),0!==n&&a.stroke()}a.restore()}(h.x(D),C,e),D=s(S,D+x+p,t.right),function(t,e,i){ee(a,i.text,t,e+_/2,d,{strikethrough:i.hidden,textAlign:i.textAlign})}(h.x(D),C,e),y?m.x+=P+f:m.y+=w})),_n(t.ctx,e.textDirection)}drawTitle(){const t=this,e=t.options,i=e.title,s=Fe(i.font),a=ze(i.padding);if(!i.display)return;const r=xn(e.rtl,t.left,t.width),l=t.ctx,c=i.position,h=s.size/2,d=a.top+h;let u,f=t.left,g=t.width;if(this.isHorizontal())g=Math.max(...t.lineWidths),u=t.top+d,f=o(e.align,f,t.right-g);else{const i=t.columnSizes.reduce(((t,e)=>Math.max(t,e.height)),0);u=d+o(e.align,t.top,t.bottom-i-e.labels.padding-t._computeTitleHeight())}const p=o(c,f,f+g);l.textAlign=r.textAlign(n(c)),l.textBaseline="middle",l.strokeStyle=i.color,l.fillStyle=i.color,l.font=s.string,ee(l,i.text,p,u,s)}_computeTitleHeight(){const t=this.options.title,e=Fe(t.font),i=ze(t.padding);return t.display?e.lineHeight+i.height:0}_getLegendItemAt(t,e){const i=this;let n,o,s;if(t>=i.left&&t<=i.right&&e>=i.top&&e<=i.bottom)for(s=i.legendHitBoxes,n=0;n=o.left&&t<=o.left+o.width&&e>=o.top&&e<=o.top+o.height)return i.legendItems[n];return null}handleEvent(t){const e=this,i=e.options;if(!function(t,e){if("mousemove"===t&&(e.onHover||e.onLeave))return!0;if(e.onClick&&("click"===t||"mouseup"===t))return!0;return!1}(t.type,i))return;const n=e._getLegendItemAt(t.x,t.y);if("mousemove"===t.type){const a=e._hoveredItem,r=(s=n,null!==(o=a)&&null!==s&&o.datasetIndex===s.datasetIndex&&o.index===s.index);a&&!r&&Q(i.onLeave,[t,a,e],e),e._hoveredItem=n,n&&!r&&Q(i.onHover,[t,n,e],e)}else n&&Q(i.onClick,[t,n,e],e);var o,s}}var gs={id:"legend",_element:fs,start(t,e,i){const n=t.legend=new fs({ctx:t.ctx,options:i,chart:t});Ge.configure(t,n,i),Ge.addBox(t,n)},stop(t){Ge.removeBox(t,t.legend),delete t.legend},beforeUpdate(t,e,i){const n=t.legend;Ge.configure(t,n,i),n.options=i},afterUpdate(t){const e=t.legend;e.buildLabels(),e.adjustHitBoxes()},afterEvent(t,e){e.replay||t.legend.handleEvent(e.event)},defaults:{display:!0,position:"top",align:"center",fullSize:!0,reverse:!1,weight:1e3,onClick(t,e,i){const n=e.datasetIndex,o=i.chart;o.isDatasetVisible(n)?(o.hide(n),e.hidden=!0):(o.show(n),e.hidden=!1)},onHover:null,onLeave:null,labels:{color:t=>t.chart.options.color,boxWidth:40,padding:10,generateLabels(t){const e=t.data.datasets,{labels:{usePointStyle:i,pointStyle:n,textAlign:o,color:s}}=t.legend.options;return t._getSortedDatasetMetas().map((t=>{const a=t.controller.getStyle(i?0:void 0),r=ze(a.borderWidth);return{text:e[t.index].label,fillStyle:a.backgroundColor,fontColor:s,hidden:!t.visible,lineCap:a.borderCapStyle,lineDash:a.borderDash,lineDashOffset:a.borderDashOffset,lineJoin:a.borderJoinStyle,lineWidth:(r.width+r.height)/4,strokeStyle:a.borderColor,pointStyle:n||a.pointStyle,rotation:a.rotation,textAlign:o||a.textAlign,borderRadius:0,datasetIndex:t.index}}),this)}},title:{color:t=>t.chart.options.color,display:!1,position:"center",text:""}},descriptors:{_scriptable:t=>!t.startsWith("on"),labels:{_scriptable:t=>!["generateLabels","filter","sort"].includes(t)}}};class ps extends Ei{constructor(t){super(),this.chart=t.chart,this.options=t.options,this.ctx=t.ctx,this._padding=void 0,this.top=void 0,this.bottom=void 0,this.left=void 0,this.right=void 0,this.width=void 0,this.height=void 0,this.position=void 0,this.weight=void 0,this.fullSize=void 0}update(t,e){const i=this,n=i.options;if(i.left=0,i.top=0,!n.display)return void(i.width=i.height=i.right=i.bottom=0);i.width=i.right=t,i.height=i.bottom=e;const o=Y(n.text)?n.text.length:1;i._padding=ze(n.padding);const s=o*Fe(n.font).lineHeight+i._padding.height;i.isHorizontal()?i.height=s:i.width=s}isHorizontal(){const t=this.options.position;return"top"===t||"bottom"===t}_drawArgs(t){const{top:e,left:i,bottom:n,right:s,options:a}=this,r=a.align;let l,c,h,d=0;return this.isHorizontal()?(c=o(r,i,s),h=e+t,l=s-i):("left"===a.position?(c=i+t,h=o(r,n,e),d=-.5*bt):(c=s-t,h=o(r,e,n),d=.5*bt),l=n-e),{titleX:c,titleY:h,maxWidth:l,rotation:d}}draw(){const t=this,e=t.ctx,i=t.options;if(!i.display)return;const o=Fe(i.font),s=o.lineHeight/2+t._padding.top,{titleX:a,titleY:r,maxWidth:l,rotation:c}=t._drawArgs(s);ee(e,i.text,0,0,o,{color:i.color,maxWidth:l,rotation:c,textAlign:n(i.align),textBaseline:"middle",translation:[a,r]})}}var ms={id:"title",_element:ps,start(t,e,i){!function(t,e){const i=new ps({ctx:t.ctx,options:e,chart:t});Ge.configure(t,i,e),Ge.addBox(t,i),t.titleBlock=i}(t,i)},stop(t){const e=t.titleBlock;Ge.removeBox(t,e),delete t.titleBlock},beforeUpdate(t,e,i){const n=t.titleBlock;Ge.configure(t,n,i),n.options=i},defaults:{align:"center",display:!1,font:{weight:"bold"},fullSize:!0,padding:10,position:"top",text:"",weight:2e3},defaultRoutes:{color:"color"},descriptors:{_scriptable:!0,_indexable:!1}};const xs={average(t){if(!t.length)return!1;let e,i,n=0,o=0,s=0;for(e=0,i=t.length;e-1?t.split("\n"):t}function ys(t,e){const{element:i,datasetIndex:n,index:o}=e,s=t.getDatasetMeta(n).controller,{label:a,value:r}=s.getLabelAndValue(o);return{chart:t,label:a,parsed:s.getParsed(o),raw:t.data.datasets[n].data[o],formattedValue:r,dataset:s.getDataset(),dataIndex:o,datasetIndex:n,element:i}}function vs(t,e){const i=t._chart.ctx,{body:n,footer:o,title:s}=t,{boxWidth:a,boxHeight:r}=e,l=Fe(e.bodyFont),c=Fe(e.titleFont),h=Fe(e.footerFont),d=s.length,u=o.length,f=n.length,g=ze(e.padding);let p=g.height,m=0,x=n.reduce(((t,e)=>t+e.before.length+e.lines.length+e.after.length),0);if(x+=t.beforeBody.length+t.afterBody.length,d&&(p+=d*c.lineHeight+(d-1)*e.titleSpacing+e.titleMarginBottom),x){p+=f*(e.displayColors?Math.max(r,l.lineHeight):l.lineHeight)+(x-f)*l.lineHeight+(x-1)*e.bodySpacing}u&&(p+=e.footerMarginTop+u*h.lineHeight+(u-1)*e.footerSpacing);let b=0;const _=function(t){m=Math.max(m,i.measureText(t).width+b)};return i.save(),i.font=c.string,J(t.title,_),i.font=l.string,J(t.beforeBody.concat(t.afterBody),_),b=e.displayColors?a+2:0,J(n,(t=>{J(t.before,_),J(t.lines,_),J(t.after,_)})),b=0,i.font=h.string,J(t.footer,_),i.restore(),m+=g.width,{width:m,height:p}}function ws(t,e,i,n){const{x:o,width:s}=i,{width:a,chartArea:{left:r,right:l}}=t;let c="center";return"center"===n?c=o<=(r+l)/2?"left":"right":o<=s/2?c="left":o>=a-s/2&&(c="right"),function(t,e,i,n){const{x:o,width:s}=n,a=i.caretSize+i.caretPadding;return"left"===t&&o+s+a>e.width||"right"===t&&o-s-a<0||void 0}(c,t,e,i)&&(c="center"),c}function Ms(t,e,i){const n=e.yAlign||function(t,e){const{y:i,height:n}=e;return it.height-n/2?"bottom":"center"}(t,i);return{xAlign:e.xAlign||ws(t,e,i,n),yAlign:n}}function ks(t,e,i,n){const{caretSize:o,caretPadding:s,cornerRadius:a}=t,{xAlign:r,yAlign:l}=i,c=o+s,h=a+s;let d=function(t,e){let{x:i,width:n}=t;return"right"===e?i-=n:"center"===e&&(i-=n/2),i}(e,r);const u=function(t,e,i){let{y:n,height:o}=t;return"top"===e?n+=i:n-="bottom"===e?o+i:o/2,n}(e,l,c);return"center"===l?"left"===r?d+=c:"right"===r&&(d-=c):"left"===r?d-=h:"right"===r&&(d+=h),{x:Nt(d,0,n.width-e.width),y:Nt(u,0,n.height-e.height)}}function Ss(t,e,i){const n=ze(i.padding);return"center"===e?t.x+t.width/2:"right"===e?t.x+t.width-n.right:t.x+n.left}function Ps(t){return bs([],_s(t))}function Ds(t,e){const i=e&&e.dataset&&e.dataset.tooltip&&e.dataset.tooltip.callbacks;return i?t.override(i):t}class Cs extends Ei{constructor(t){super(),this.opacity=0,this._active=[],this._chart=t._chart,this._eventPosition=void 0,this._size=void 0,this._cachedAnimations=void 0,this._tooltipItems=[],this.$animations=void 0,this.$context=void 0,this.options=t.options,this.dataPoints=void 0,this.title=void 0,this.beforeBody=void 0,this.body=void 0,this.afterBody=void 0,this.footer=void 0,this.xAlign=void 0,this.yAlign=void 0,this.x=void 0,this.y=void 0,this.height=void 0,this.width=void 0,this.caretX=void 0,this.caretY=void 0,this.labelColors=void 0,this.labelPointStyles=void 0,this.labelTextColors=void 0}initialize(t){this.options=t,this._cachedAnimations=void 0,this.$context=void 0}_resolveAnimations(){const t=this,e=t._cachedAnimations;if(e)return e;const i=t._chart,n=t.options.setContext(t.getContext()),o=n.enabled&&i.options.animation&&n.animations,s=new vi(t._chart,o);return o._cacheable&&(t._cachedAnimations=Object.freeze(s)),s}getContext(){const t=this;return t.$context||(t.$context=(e=t._chart.getContext(),i=t,n=t._tooltipItems,Object.assign(Object.create(e),{tooltip:i,tooltipItems:n,type:"tooltip"})));var e,i,n}getTitle(t,e){const i=this,{callbacks:n}=e,o=n.beforeTitle.apply(i,[t]),s=n.title.apply(i,[t]),a=n.afterTitle.apply(i,[t]);let r=[];return r=bs(r,_s(o)),r=bs(r,_s(s)),r=bs(r,_s(a)),r}getBeforeBody(t,e){return Ps(e.callbacks.beforeBody.apply(this,[t]))}getBody(t,e){const i=this,{callbacks:n}=e,o=[];return J(t,(t=>{const e={before:[],lines:[],after:[]},s=Ds(n,t);bs(e.before,_s(s.beforeLabel.call(i,t))),bs(e.lines,s.label.call(i,t)),bs(e.after,_s(s.afterLabel.call(i,t))),o.push(e)})),o}getAfterBody(t,e){return Ps(e.callbacks.afterBody.apply(this,[t]))}getFooter(t,e){const i=this,{callbacks:n}=e,o=n.beforeFooter.apply(i,[t]),s=n.footer.apply(i,[t]),a=n.afterFooter.apply(i,[t]);let r=[];return r=bs(r,_s(o)),r=bs(r,_s(s)),r=bs(r,_s(a)),r}_createItems(t){const e=this,i=e._active,n=e._chart.data,o=[],s=[],a=[];let r,l,c=[];for(r=0,l=i.length;rt.filter(e,i,o,n)))),t.itemSort&&(c=c.sort(((e,i)=>t.itemSort(e,i,n)))),J(c,(i=>{const n=Ds(t.callbacks,i);o.push(n.labelColor.call(e,i)),s.push(n.labelPointStyle.call(e,i)),a.push(n.labelTextColor.call(e,i))})),e.labelColors=o,e.labelPointStyles=s,e.labelTextColors=a,e.dataPoints=c,c}update(t,e){const i=this,n=i.options.setContext(i.getContext()),o=i._active;let s,a=[];if(o.length){const t=xs[n.position].call(i,o,i._eventPosition);a=i._createItems(n),i.title=i.getTitle(a,n),i.beforeBody=i.getBeforeBody(a,n),i.body=i.getBody(a,n),i.afterBody=i.getAfterBody(a,n),i.footer=i.getFooter(a,n);const e=i._size=vs(i,n),r=Object.assign({},t,e),l=Ms(i._chart,n,r),c=ks(n,r,l,i._chart);i.xAlign=l.xAlign,i.yAlign=l.yAlign,s={opacity:1,x:c.x,y:c.y,width:e.width,height:e.height,caretX:t.x,caretY:t.y}}else 0!==i.opacity&&(s={opacity:0});i._tooltipItems=a,i.$context=void 0,s&&i._resolveAnimations().update(i,s),t&&n.external&&n.external.call(i,{chart:i._chart,tooltip:i,replay:e})}drawCaret(t,e,i,n){const o=this.getCaretPosition(t,i,n);e.lineTo(o.x1,o.y1),e.lineTo(o.x2,o.y2),e.lineTo(o.x3,o.y3)}getCaretPosition(t,e,i){const{xAlign:n,yAlign:o}=this,{cornerRadius:s,caretSize:a}=i,{x:r,y:l}=t,{width:c,height:h}=e;let d,u,f,g,p,m;return"center"===o?(p=l+h/2,"left"===n?(d=r,u=d-a,g=p+a,m=p-a):(d=r+c,u=d+a,g=p-a,m=p+a),f=d):(u="left"===n?r+s+a:"right"===n?r+c-s-a:this.caretX,"top"===o?(g=l,p=g-a,d=u-a,f=u+a):(g=l+h,p=g+a,d=u+a,f=u-a),m=g),{x1:d,x2:u,x3:f,y1:g,y2:p,y3:m}}drawTitle(t,e,i){const n=this,o=n.title,s=o.length;let a,r,l;if(s){const c=xn(i.rtl,n.x,n.width);for(t.x=Ss(n,i.titleAlign,i),e.textAlign=c.textAlign(i.titleAlign),e.textBaseline="middle",a=Fe(i.titleFont),r=i.titleSpacing,e.fillStyle=i.titleColor,e.font=a.string,l=0;l0!==t))?(t.beginPath(),t.fillStyle=o.multiKeyBackground,ie(t,{x:e,y:g,w:c,h:l,radius:s}),t.fill(),t.stroke(),t.fillStyle=a.backgroundColor,t.beginPath(),ie(t,{x:i,y:g+1,w:c-2,h:l-2,radius:s}),t.fill()):(t.fillStyle=o.multiKeyBackground,t.fillRect(e,g,c,l),t.strokeRect(e,g,c,l),t.fillStyle=a.backgroundColor,t.fillRect(i,g+1,c-2,l-2))}t.fillStyle=s.labelTextColors[i]}drawBody(t,e,i){const n=this,{body:o}=n,{bodySpacing:s,bodyAlign:a,displayColors:r,boxHeight:l,boxWidth:c}=i,h=Fe(i.bodyFont);let d=h.lineHeight,u=0;const f=xn(i.rtl,n.x,n.width),g=function(i){e.fillText(i,f.x(t.x+u),t.y+d/2),t.y+=d+s},p=f.textAlign(a);let m,x,b,_,y,v,w;for(e.textAlign=a,e.textBaseline="middle",e.font=h.string,t.x=Ss(n,p,i),e.fillStyle=i.bodyColor,J(n.beforeBody,g),u=r&&"right"!==p?"center"===a?c/2+1:c+2:0,_=0,v=o.length;_0&&e.stroke()}_updateAnimationTarget(t){const e=this,i=e._chart,n=e.$animations,o=n&&n.x,s=n&&n.y;if(o||s){const n=xs[t.position].call(e,e._active,e._eventPosition);if(!n)return;const a=e._size=vs(e,t),r=Object.assign({},n,e._size),l=Ms(i,t,r),c=ks(t,r,l,i);o._to===c.x&&s._to===c.y||(e.xAlign=l.xAlign,e.yAlign=l.yAlign,e.width=a.width,e.height=a.height,e.caretX=n.x,e.caretY=n.y,e._resolveAnimations().update(e,c))}}draw(t){const e=this,i=e.options.setContext(e.getContext());let n=e.opacity;if(!n)return;e._updateAnimationTarget(i);const o={width:e.width,height:e.height},s={x:e.x,y:e.y};n=Math.abs(n)<.001?0:n;const a=ze(i.padding),r=e.title.length||e.beforeBody.length||e.body.length||e.afterBody.length||e.footer.length;i.enabled&&r&&(t.save(),t.globalAlpha=n,e.drawBackground(s,t,o,i),bn(t,i.textDirection),s.y+=a.top,e.drawTitle(s,t,i),e.drawBody(s,t,i),e.drawFooter(s,t,i),_n(t,i.textDirection),t.restore())}getActiveElements(){return this._active||[]}setActiveElements(t,e){const i=this,n=i._active,o=t.map((({datasetIndex:t,index:e})=>{const n=i._chart.getDatasetMeta(t);if(!n)throw new Error("Cannot find a dataset at index "+t);return{datasetIndex:t,element:n.data[e],index:e}})),s=!tt(n,o),a=i._positionChanged(o,e);(s||a)&&(i._active=o,i._eventPosition=e,i.update(!0))}handleEvent(t,e){const i=this,n=i.options,o=i._active||[];let s=!1,a=[];"mouseout"!==t.type&&(a=i._chart.getElementsAtEventForMode(t,n.mode,n,e),n.reverse&&a.reverse());const r=i._positionChanged(a,t);return s=e||!tt(a,o)||r,s&&(i._active=a,(n.enabled||n.external)&&(i._eventPosition={x:t.x,y:t.y},i.update(!0,e))),s}_positionChanged(t,e){const{caretX:i,caretY:n,options:o}=this,s=xs[o.position].call(this,t,e);return!1!==s&&(i!==s.x||n!==s.y)}}Cs.positioners=xs;var Os={id:"tooltip",_element:Cs,positioners:xs,afterInit(t,e,i){i&&(t.tooltip=new Cs({_chart:t,options:i}))},beforeUpdate(t,e,i){t.tooltip&&t.tooltip.initialize(i)},reset(t,e,i){t.tooltip&&t.tooltip.initialize(i)},afterDraw(t){const e=t.tooltip,i={tooltip:e};!1!==t.notifyPlugins("beforeTooltipDraw",i)&&(e&&e.draw(t.ctx),t.notifyPlugins("afterTooltipDraw",i))},afterEvent(t,e){if(t.tooltip){const i=e.replay;t.tooltip.handleEvent(e.event,i)&&(e.changed=!0)}},defaults:{enabled:!0,external:null,position:"average",backgroundColor:"rgba(0,0,0,0.8)",titleColor:"#fff",titleFont:{weight:"bold"},titleSpacing:2,titleMarginBottom:6,titleAlign:"left",bodyColor:"#fff",bodySpacing:2,bodyFont:{},bodyAlign:"left",footerColor:"#fff",footerSpacing:2,footerMarginTop:6,footerFont:{weight:"bold"},footerAlign:"left",padding:6,caretPadding:2,caretSize:5,cornerRadius:6,boxHeight:(t,e)=>e.bodyFont.size,boxWidth:(t,e)=>e.bodyFont.size,multiKeyBackground:"#fff",displayColors:!0,borderColor:"rgba(0,0,0,0)",borderWidth:0,animation:{duration:400,easing:"easeOutQuart"},animations:{numbers:{type:"number",properties:["x","y","width","height","caretX","caretY"]},opacity:{easing:"linear",duration:200}},callbacks:{beforeTitle:N,title(t){if(t.length>0){const e=t[0],i=e.chart.data.labels,n=i?i.length:0;if(this&&this.options&&"dataset"===this.options.mode)return e.dataset.label||"";if(e.label)return e.label;if(n>0&&e.dataIndex"filter"!==t&&"itemSort"!==t&&"external"!==t,_indexable:!1,callbacks:{_scriptable:!1,_indexable:!1},animation:{_fallback:!1},animations:{_fallback:"animation"}},additionalOptionScopes:["interaction"]},As=Object.freeze({__proto__:null,Decimation:qo,Filler:ds,Legend:gs,Title:ms,Tooltip:Os});function Ts(t,e,i){const n=t.indexOf(e);if(-1===n)return((t,e,i)=>"string"==typeof e?t.push(e)-1:isNaN(e)?null:i)(t,e,i);return n!==t.lastIndexOf(e)?i:n}class Rs extends Xi{constructor(t){super(t),this._startValue=void 0,this._valueRange=0}parse(t,e){if($(t))return null;const i=this.getLabels();return((t,e)=>null===t?null:Nt(Math.round(t),0,e))(e=isFinite(e)&&i[e]===t?e:Ts(i,t,K(e,t)),i.length-1)}determineDataLimits(){const t=this,{minDefined:e,maxDefined:i}=t.getUserBounds();let{min:n,max:o}=t.getMinMax(!0);"ticks"===t.options.bounds&&(e||(n=0),i||(o=t.getLabels().length-1)),t.min=n,t.max=o}buildTicks(){const t=this,e=t.min,i=t.max,n=t.options.offset,o=[];let s=t.getLabels();s=0===e&&i===s.length-1?s:s.slice(e,i+1),t._valueRange=Math.max(s.length-(n?0:1),1),t._startValue=t.min-(n?.5:0);for(let t=e;t<=i;t++)o.push({value:t});return o}getLabelForValue(t){const e=this.getLabels();return t>=0&&te.length-1?null:this.getPixelForValue(e[t].value)}getValueForPixel(t){const e=this;return Math.round(e._startValue+e.getDecimalForPixel(t)*e._valueRange)}getBasePixel(){return this.bottom}}Rs.id="category",Rs.defaults={ticks:{callback:Rs.prototype.getLabelForValue}};class Ls extends Xi{constructor(t){super(t),this.start=void 0,this.end=void 0,this._startValue=void 0,this._endValue=void 0,this._valueRange=0}parse(t,e){return $(t)||("number"==typeof t||t instanceof Number)&&!isFinite(+t)?null:+t}handleTickRangeOptions(){const t=this,{beginAtZero:e,stacked:i}=t.options,{minDefined:n,maxDefined:o}=t.getUserBounds();let{min:s,max:a}=t;const r=t=>s=n?s:t,l=t=>a=o?a:t;if(e||i){const t=Dt(s),e=Dt(a);t<0&&e<0?l(0):t>0&&e>0&&r(0)}s===a&&(l(a+1),e||r(s-1)),t.min=s,t.max=a}getTickLimit(){const t=this,e=t.options.ticks;let i,{maxTicksLimit:n,stepSize:o}=e;return o?i=Math.ceil(t.max/o)-Math.floor(t.min/o)+1:(i=t.computeTickLimit(),n=n||11),n&&(i=Math.min(n,i)),i}computeTickLimit(){return Number.POSITIVE_INFINITY}buildTicks(){const t=this,e=t.options,i=e.ticks;let n=t.getTickLimit();n=Math.max(2,n);const o=function(t,e){const i=[],{step:n,min:o,max:s,precision:a,count:r,maxTicks:l,maxDigits:c,horizontal:h}=t,d=n||1,u=l-1,{min:f,max:g}=e,p=!$(o),m=!$(s),x=!$(r),b=(g-f)/c;let _,y,v,w,M=Ct((g-f)/u/d)*d;if(M<1e-14&&!p&&!m)return[{value:f},{value:g}];w=Math.ceil(g/M)-Math.floor(f/M),w>u&&(M=Ct(w*M/u/d)*d),$(a)||(_=Math.pow(10,a),M=Math.ceil(M*_)/_),y=Math.floor(f/M)*M,v=Math.ceil(g/M)*M,p&&m&&n&&Rt((s-o)/n,M/1e3)?(w=Math.min((s-o)/M,l),M=(s-o)/w,y=o,v=s):x?(y=p?o:y,v=m?s:v,w=r-1,M=(v-y)/w):(w=(v-y)/M,w=Tt(w,Math.round(w),M/1e3)?Math.round(w):Math.ceil(w)),_=Math.pow(10,$(a)?zt(M):a),y=Math.round(y*_)/_,v=Math.round(v*_)/_;let k=0;for(p&&(i.push({value:o}),y<=o&&k++,Tt(Math.round((y+k*M)*_)/_,o,b*(h?(""+o).length:1))&&k++);k0?i:null;this._zero=!0}determineDataLimits(){const t=this,{min:e,max:i}=t.getMinMax(!0);t.min=X(e)?Math.max(0,e):null,t.max=X(i)?Math.max(0,i):null,t.options.beginAtZero&&(t._zero=!0),t.handleTickRangeOptions()}handleTickRangeOptions(){const t=this,{minDefined:e,maxDefined:i}=t.getUserBounds();let n=t.min,o=t.max;const s=t=>n=e?n:t,a=t=>o=i?o:t,r=(t,e)=>Math.pow(10,Math.floor(Pt(t))+e);n===o&&(n<=0?(s(1),a(10)):(s(r(n,-1)),a(r(o,1)))),n<=0&&s(r(o,-1)),o<=0&&a(r(n,1)),t._zero&&t.min!==t._suggestedMin&&n===r(t.min,0)&&s(r(n,-1)),t.min=n,t.max=o}buildTicks(){const t=this,e=t.options,i=function(t,e){const i=Math.floor(Pt(e.max)),n=Math.ceil(e.max/Math.pow(10,i)),o=[];let s=q(t.min,Math.pow(10,Math.floor(Pt(e.min)))),a=Math.floor(Pt(s)),r=Math.floor(s/Math.pow(10,a)),l=a<0?Math.pow(10,Math.abs(a)):1;do{o.push({value:s,major:Is(s)}),++r,10===r&&(r=1,++a,l=a>=0?1:l),s=Math.round(r*Math.pow(10,a)*l)/l}while(ao?{start:e-i,end:e}:{start:e,end:e+i}}function Bs(t){return 0===t||180===t?"center":t<180?"left":"right"}function Ws(t,e,i){90===t||270===t?i.y-=e.h/2:(t>270||t<90)&&(i.y-=e.h)}function Hs(t,e,i,n){const{ctx:o}=t;if(i)o.arc(t.xCenter,t.yCenter,e,0,_t);else{let i=t.getPointPosition(0,e);o.moveTo(i.x,i.y);for(let s=1;s{const n=Q(e.options.pointLabels.callback,[t,i],e);return n||0===n?n:""}))}fit(){const t=this,e=t.options;e.display&&e.pointLabels.display?function(t){const e={l:0,r:t.width,t:0,b:t.height-t.paddingTop},i={};let n,o,s;const a=[],r=[],l=t.getLabels().length;for(n=0;ne.r&&(e.r=p.end,i.r=f),m.starte.b&&(e.b=m.end,i.b=f)}var c,h,d;t._setReductions(t.drawingArea,e,i),t._pointLabelItems=[];const u=t.options,f=Fs(u),g=t.getDistanceFromCenterForValue(u.ticks.reverse?t.min:t.max);for(n=0;n=0;o--){const e=n.setContext(t.getContext(o)),s=Fe(e.font),{x:a,y:r,textAlign:l,left:c,top:h,right:d,bottom:u}=t._pointLabelItems[o],{backdropColor:f}=e;if(!$(f)){const t=ze(e.backdropPadding);i.fillStyle=f,i.fillRect(c-t.left,h-t.top,d-c+t.width,u-h+t.height)}ee(i,t._pointLabels[o],a,r+s.lineHeight/2,s,{color:e.color,textAlign:l,textBaseline:"middle"})}}(t,s),o.display&&t.ticks.forEach(((e,i)=>{if(0!==i){r=t.getDistanceFromCenterForValue(e.value);const n=o.setContext(t.getContext(i-1));!function(t,e,i,n){const o=t.ctx,s=e.circular,{color:a,lineWidth:r}=e;!s&&!n||!a||!r||i<0||(o.save(),o.strokeStyle=a,o.lineWidth=r,o.setLineDash(e.borderDash),o.lineDashOffset=e.borderDashOffset,o.beginPath(),Hs(t,i,s,n),o.closePath(),o.stroke(),o.restore())}(t,n,r,s)}})),n.display){for(e.save(),a=t.getLabels().length-1;a>=0;a--){const o=n.setContext(t.getContext(a)),{color:s,lineWidth:c}=o;c&&s&&(e.lineWidth=c,e.strokeStyle=s,e.setLineDash(o.borderDash),e.lineDashOffset=o.borderDashOffset,r=t.getDistanceFromCenterForValue(i.ticks.reverse?t.min:t.max),l=t.getPointPosition(a,r),e.beginPath(),e.moveTo(t.xCenter,t.yCenter),e.lineTo(l.x,l.y),e.stroke())}e.restore()}}drawBorder(){}drawLabels(){const t=this,e=t.ctx,i=t.options,n=i.ticks;if(!n.display)return;const o=t.getIndexAngle(0);let s,a;e.save(),e.translate(t.xCenter,t.yCenter),e.rotate(o),e.textAlign="center",e.textBaseline="middle",t.ticks.forEach(((o,r)=>{if(0===r&&!i.reverse)return;const l=n.setContext(t.getContext(r)),c=Fe(l.font);if(s=t.getDistanceFromCenterForValue(t.ticks[r].value),l.showLabelBackdrop){a=e.measureText(o.label).width,e.fillStyle=l.backdropColor;const t=ze(l.backdropPadding);e.fillRect(-a/2-t.left,-s-c.size/2-t.top,a+t.width,c.size+t.height)}ee(e,o.label,0,-s,c,{color:l.color})})),e.restore()}drawTitle(){}}js.id="radialLinear",js.defaults={display:!0,animate:!0,position:"chartArea",angleLines:{display:!0,lineWidth:1,borderDash:[],borderDashOffset:0},grid:{circular:!1},startAngle:0,ticks:{showLabelBackdrop:!0,callback:Vi.formatters.numeric},pointLabels:{backdropColor:void 0,backdropPadding:2,display:!0,font:{size:10},callback:t=>t,padding:5}},js.defaultRoutes={"angleLines.color":"borderColor","pointLabels.color":"color","ticks.color":"color"},js.descriptors={angleLines:{_fallback:"grid"}};const $s={millisecond:{common:!0,size:1,steps:1e3},second:{common:!0,size:1e3,steps:60},minute:{common:!0,size:6e4,steps:60},hour:{common:!0,size:36e5,steps:24},day:{common:!0,size:864e5,steps:30},week:{common:!1,size:6048e5,steps:4},month:{common:!0,size:2628e6,steps:12},quarter:{common:!1,size:7884e6,steps:4},year:{common:!0,size:3154e7}},Ys=Object.keys($s);function Us(t,e){return t-e}function Xs(t,e){if($(e))return null;const i=t._adapter,{parser:n,round:o,isoWeekday:s}=t._parseOpts;let a=e;return"function"==typeof n&&(a=n(a)),X(a)||(a="string"==typeof n?i.parse(a,n):i.parse(a)),null===a?null:(o&&(a="week"!==o||!At(s)&&!0!==s?i.startOf(a,o):i.startOf(a,"isoWeek",s)),+a)}function qs(t,e,i,n){const o=Ys.length;for(let s=Ys.indexOf(t);s=e?i[n]:i[o]]=!0}}else t[e]=!0}function Gs(t,e,i){const n=[],o={},s=e.length;let a,r;for(a=0;a=0&&(e[l].major=!0);return e}(t,n,o,i):n}class Zs extends Xi{constructor(t){super(t),this._cache={data:[],labels:[],all:[]},this._unit="day",this._majorUnit=void 0,this._offsets={},this._normalized=!1,this._parseOpts=void 0}init(t,e){const i=t.time||(t.time={}),n=this._adapter=new oo._date(t.adapters.date);st(i.displayFormats,n.formats()),this._parseOpts={parser:i.parser,round:i.round,isoWeekday:i.isoWeekday},super.init(t),this._normalized=e.normalized}parse(t,e){return void 0===t?null:Xs(this,t)}beforeLayout(){super.beforeLayout(),this._cache={data:[],labels:[],all:[]}}determineDataLimits(){const t=this,e=t.options,i=t._adapter,n=e.time.unit||"day";let{min:o,max:s,minDefined:a,maxDefined:r}=t.getUserBounds();function l(t){a||isNaN(t.min)||(o=Math.min(o,t.min)),r||isNaN(t.max)||(s=Math.max(s,t.max))}a&&r||(l(t._getLabelBounds()),"ticks"===e.bounds&&"labels"===e.ticks.source||l(t.getMinMax(!1))),o=X(o)&&!isNaN(o)?o:+i.startOf(Date.now(),n),s=X(s)&&!isNaN(s)?s:+i.endOf(Date.now(),n)+1,t.min=Math.min(o,s-1),t.max=Math.max(o+1,s)}_getLabelBounds(){const t=this.getLabelTimestamps();let e=Number.POSITIVE_INFINITY,i=Number.NEGATIVE_INFINITY;return t.length&&(e=t[0],i=t[t.length-1]),{min:e,max:i}}buildTicks(){const t=this,e=t.options,i=e.time,n=e.ticks,o="labels"===n.source?t.getLabelTimestamps():t._generate();"ticks"===e.bounds&&o.length&&(t.min=t._userMin||o[0],t.max=t._userMax||o[o.length-1]);const s=t.min,a=ae(o,s,t.max);return t._unit=i.unit||(n.autoSkip?qs(i.minUnit,t.min,t.max,t._getLabelCapacity(s)):function(t,e,i,n,o){for(let s=Ys.length-1;s>=Ys.indexOf(i);s--){const i=Ys[s];if($s[i].common&&t._adapter.diff(o,n,i)>=e-1)return i}return Ys[i?Ys.indexOf(i):0]}(t,a.length,i.minUnit,t.min,t.max)),t._majorUnit=n.major.enabled&&"year"!==t._unit?function(t){for(let e=Ys.indexOf(t)+1,i=Ys.length;e1e5*r)throw new Error(i+" and "+n+" are too far apart with stepSize of "+r+" "+a);const g="data"===o.ticks.source&&t.getDataTimestamps();for(d=f,u=0;dt-e)).map((t=>+t))}getLabelForValue(t){const e=this._adapter,i=this.options.time;return i.tooltipFormat?e.format(t,i.tooltipFormat):e.format(t,i.displayFormats.datetime)}_tickFormatFunction(t,e,i,n){const o=this,s=o.options,a=s.time.displayFormats,r=o._unit,l=o._majorUnit,c=r&&a[r],h=l&&a[l],d=i[e],u=l&&h&&d&&d.major,f=o._adapter.format(t,n||(u?h:c)),g=s.ticks.callback;return g?Q(g,[f,e,i],o):f}generateTickLabels(t){let e,i,n;for(e=0,i=t.length;e0?r:1}getDataTimestamps(){const t=this;let e,i,n=t._cache.data||[];if(n.length)return n;const o=t.getMatchingVisibleMetas();if(t._normalized&&o.length)return t._cache.data=o[0].controller.getAllParsedValues(t);for(e=0,i=o.length;ee&&a0&&!$(e)?e/i._maxIndex:i.getDecimalForValue(t);return i.getPixelForDecimal((n.start+o)*n.factor)}getDecimalForValue(t){return Qs(this._table,t)/this._maxIndex}getValueForPixel(t){const e=this,i=e._offsets,n=e.getDecimalForPixel(t)/i.factor-i.end;return Qs(e._table,n*this._maxIndex,!0)}}Js.id="timeseries",Js.defaults=Zs.defaults;var ta=Object.freeze({__proto__:null,CategoryScale:Rs,LinearScale:Es,LogarithmicScale:zs,RadialLinearScale:js,TimeScale:Zs,TimeSeriesScale:Js});return Jn.register(bo,ta,Yo,As),Jn.helpers={...Cn},Jn._adapters=oo,Jn.Animation=_i,Jn.Animations=vi,Jn.animator=a,Jn.controllers=An.controllers.items,Jn.DatasetController=Li,Jn.Element=Ei,Jn.elements=Yo,Jn.Interaction=Oe,Jn.layouts=Ge,Jn.platforms=ui,Jn.Scale=Xi,Jn.Ticks=Vi,Object.assign(Jn,bo,ta,Yo,As,ui),Jn.Chart=Jn,"undefined"!=typeof window&&(window.Chart=Jn),Jn})); diff --git a/content/www/js/jquery.js b/content/www/js/jquery.js new file mode 100644 index 0000000..c4c6022 --- /dev/null +++ b/content/www/js/jquery.js @@ -0,0 +1,2 @@ +/*! jQuery v3.6.0 | (c) OpenJS Foundation and other contributors | jquery.org/license */ +!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},x=function(e){return null!=e&&e===e.window},E=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.6.0",S=function(e,t){return new S.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ye(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace($,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e&&e.namespaceURI,n=e&&(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||v.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||v.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||v.push(".#.+[+~]"),e.querySelectorAll("\\\f"),v.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},j=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&y(p,e)?-1:t==C||t.ownerDocument==p&&y(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||D,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,D=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),y.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",y.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="",y.option=!!ce.lastChild;var ge={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;n",""]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function je(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function De(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function qe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Le(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var _t,zt=[],Ut=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=zt.pop()||S.expando+"_"+wt.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Ut.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Ut.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Ut,"$1"+r):!1!==e.jsonp&&(e.url+=(Tt.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,zt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((_t=E.implementation.createHTMLDocument("").body).innerHTML="
",2===_t.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return $(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=Fe(y.pixelPosition,function(e,t){if(t)return t=We(e,n),Pe.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return $(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0 { + dataset.data = data; + }); + + chart.update(); +} + +function queue_update() { + $.getJSON("sensor_data").done(refresh_chart); +} \ No newline at end of file diff --git a/custom_modules/mqtt_server/SCsub b/custom_modules/mqtt_server/SCsub new file mode 100644 index 0000000..59b379f --- /dev/null +++ b/custom_modules/mqtt_server/SCsub @@ -0,0 +1,13 @@ +#!/usr/bin/env python + +Import("env_mod") +Import("env") + +env_mod.core_sources = [] + +env_mod.add_source_files(env_mod.core_sources, "*.cpp") +env_mod.add_source_files(env_mod.core_sources, "./mqtt_broker/src/*.cc") + +# Build it all as a library +lib = env_mod.add_library("mqtt_server", env_mod.core_sources) +env.Prepend(LIBS=[lib]) diff --git a/custom_modules/mqtt_server/__pycache__/detect.cpython-39.pyc b/custom_modules/mqtt_server/__pycache__/detect.cpython-39.pyc new file mode 100644 index 0000000..de03581 Binary files /dev/null and b/custom_modules/mqtt_server/__pycache__/detect.cpython-39.pyc differ diff --git a/custom_modules/mqtt_server/detect.py b/custom_modules/mqtt_server/detect.py new file mode 100644 index 0000000..210c397 --- /dev/null +++ b/custom_modules/mqtt_server/detect.py @@ -0,0 +1,46 @@ +import os +import platform +import sys + + +def is_active(): + return True + + +def get_name(): + return "mqtt_server" + + +def can_build(): + if os.name == "posix" or sys.platform == "darwin": + x11_error = os.system("pkg-config --version > /dev/null") + if x11_error: + return False + + libevent_err = os.system("pkg-config libevent --modversion --silence-errors > /dev/null ") + + if libevent_err: + print("libevent not found! MQTT server will not be available!") + return False + + print("libevent found! MQTT server will be available!") + + return True + + return False + + +def get_opts(): + return [] + +def get_flags(): + + return [] + + +def configure(env): + env.ParseConfig("pkg-config libevent --cflags --libs") + + env.Append(CPPDEFINES=["MQTT_SERVER_PRESENT"]) + + diff --git a/custom_modules/mqtt_server/libmqtt_server.a b/custom_modules/mqtt_server/libmqtt_server.a new file mode 100644 index 0000000..36f84cb Binary files /dev/null and b/custom_modules/mqtt_server/libmqtt_server.a differ diff --git a/custom_modules/mqtt_server/mqtt_broker/HEAD b/custom_modules/mqtt_server/mqtt_broker/HEAD new file mode 100644 index 0000000..3028a1b --- /dev/null +++ b/custom_modules/mqtt_server/mqtt_broker/HEAD @@ -0,0 +1 @@ +bea4d892540d329cf055a61339200b76001e191d \ No newline at end of file diff --git a/custom_modules/mqtt_server/mqtt_broker/LICENSE.txt b/custom_modules/mqtt_server/mqtt_broker/LICENSE.txt new file mode 100644 index 0000000..63b4b68 --- /dev/null +++ b/custom_modules/mqtt_server/mqtt_broker/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) [year] [fullname] + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/custom_modules/mqtt_server/mqtt_broker/README.md b/custom_modules/mqtt_server/mqtt_broker/README.md new file mode 100644 index 0000000..5562f6d --- /dev/null +++ b/custom_modules/mqtt_server/mqtt_broker/README.md @@ -0,0 +1,93 @@ +# MQTT broker and clients + +## About + +The [MQTT (MQ Telemetry Transport) publish/subscribe +protocol](htts://mqtt.org) is a simple lightweight messaging protocol +for distributed network connected devices. It provides low overhead, +reliable connectivity for resource constrained devices. + +This is an open source, asynchronous, C++ implementation of the broker +(server) and connecting clients. The implementation follows the 3.1.1 +OASIS standard available +[here](http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html). + +## Installation + +### Requirements + +Asynchronous networking support requires the +[Libevent](http://libevent.org) networking library. Other than that +there are no other external run-time dependencies. + +* A C++11 conformant compiler. +* [Libevent](http://libevent.org) +* [CMake](Cmhttps://cmake.org/) + +Verified platforms. + +* Ubuntu Linux 16.04 (gcc 5.4.0) +* Mac OSX 10.11 (llvm 7.3.0) + +### Building + +1. Clone this repository. +```` + $ git clone https://github.com/inyotech/mqtt_broker.git + $ cd mqtt_broker +```` + +2. Install the google tests framework +``` + $ pushd test/lib + $ git clone https://github.com/google/googletest.git + $ popd +``` + +3. Create a build directory. +```` + $ mkdir build + $ cd build +```` + +4. Generate build files. +```` + $ cmake .. +```` + +5. Build +```` + $ make +```` + +## Example + +* Open a terminal and execute the broker. +```` + $ mqtt_broker +```` + +* In a second terminal execute a subscriber. +```` + $ mqtt_client_sub --topic 'a/b/c' +```` + +* Execute a publisher in a third terminal. +```` + $ mqtt_client_pub --topic 'a/b/c' --message 'published message' +```` + +## Documentation + +[Doxygen](http://www.stack.nl/~dimitri/doxygen/) documentation is +available [here](https://inyotech.github.io/mqtt_broker). + +## License + +This software is licensed under the MIT License. See the LICENSE.TXT file for details. + +## TODO + +* Client Will not implemented. +* Retained message publication not implemented. +* SSL support not implemented. diff --git a/custom_modules/mqtt_server/mqtt_broker/src/base_session.cc b/custom_modules/mqtt_server/mqtt_broker/src/base_session.cc new file mode 100644 index 0000000..7570a63 --- /dev/null +++ b/custom_modules/mqtt_server/mqtt_broker/src/base_session.cc @@ -0,0 +1,115 @@ +/** + * @file base_session.cc + */ + +#include "base_session.h" + +#include + +void BaseSession::packet_received(std::unique_ptr packet) { + + switch (packet->type) { + case PacketType::Connect: + handle_connect(dynamic_cast(*packet)); + break; + case PacketType::Connack: + handle_connack(dynamic_cast(*packet)); + break; + case PacketType::Publish: + handle_publish(dynamic_cast(*packet)); + break; + case PacketType::Puback: + handle_puback(dynamic_cast(*packet)); + break; + case PacketType::Pubrec: + handle_pubrec(dynamic_cast(*packet)); + break; + case PacketType::Pubrel: + handle_pubrel(dynamic_cast(*packet)); + break; + case PacketType::Pubcomp: + handle_pubcomp(dynamic_cast(*packet)); + break; + case PacketType::Subscribe: + handle_subscribe(dynamic_cast(*packet)); + break; + case PacketType::Suback: + handle_suback(dynamic_cast(*packet)); + break; + case PacketType::Unsubscribe: + handle_unsubscribe(dynamic_cast(*packet)); + break; + case PacketType::Unsuback: + handle_unsuback(dynamic_cast(*packet)); + break; + case PacketType::Pingreq: + handle_pingreq(dynamic_cast(*packet)); + break; + case PacketType::Pingresp: + handle_pingresp(dynamic_cast(*packet)); + break; + case PacketType::Disconnect: + handle_disconnect(dynamic_cast(*packet)); + break; + } + +} + +void BaseSession::packet_manager_event(PacketManager::EventType event) { + packet_manager->close_connection(); +} + +void BaseSession::handle_connect(const ConnectPacket &) { + throw std::exception(); +} + +void BaseSession::handle_connack(const ConnackPacket &) { + throw std::exception(); +} + +void BaseSession::handle_publish(const PublishPacket &) { + throw std::exception(); +} + +void BaseSession::handle_puback(const PubackPacket &) { + throw std::exception(); +} + +void BaseSession::handle_pubrec(const PubrecPacket &) { + throw std::exception(); +} + +void BaseSession::handle_pubrel(const PubrelPacket &) { + throw std::exception(); +} + +void BaseSession::handle_pubcomp(const PubcompPacket &) { + throw std::exception(); +} + +void BaseSession::handle_subscribe(const SubscribePacket &) { + throw std::exception(); +} + +void BaseSession::handle_suback(const SubackPacket &) { + throw std::exception(); +} + +void BaseSession::handle_unsubscribe(const UnsubscribePacket &) { + throw std::exception(); +} + +void BaseSession::handle_unsuback(const UnsubackPacket &) { + throw std::exception(); +} + +void BaseSession::handle_pingreq(const PingreqPacket &) { + PingrespPacket pingresp_packet; + packet_manager->send_packet(pingresp_packet); +} + +void BaseSession::handle_pingresp(const PingrespPacket &) {} + +void BaseSession::handle_disconnect(const DisconnectPacket &) { + throw std::exception(); +} diff --git a/custom_modules/mqtt_server/mqtt_broker/src/base_session.h b/custom_modules/mqtt_server/mqtt_broker/src/base_session.h new file mode 100644 index 0000000..fa17d4a --- /dev/null +++ b/custom_modules/mqtt_server/mqtt_broker/src/base_session.h @@ -0,0 +1,213 @@ +/** + * @file base_session.h + * + * Base class for MQTT sessions. This class add facilities for persistence and resumption of session state. The MQTT + * standard requires that the client and server both maintain session state while connected. The server is also + * required to resume the state when a client re-connects with the same client id. + */ + +#pragma once + +#include "packet.h" +#include "packet_manager.h" + +#include + +#include + +/** + * Base session class + * + * Maintains session attributes and provides default handler methods for received control packets. Classes derived + * from BaseSession will override control packet handlers as required. + * + * Each BaseSession composes a PacketManager instance that can be moved between BaseSession instances. + */ +class BaseSession { + +public: + + /** + * Constructor + * + * Accepts a pointer to a libevent bufferevent as the only argument. The bufferevent is forwarded to a + * newly instantiated PacketManager that use it to handle all network related functions. + * + * This BaseSession instance can persist in memory after the network connection is closed. If a connection is + * received and the Connect control packet contains the same client id as an existing session. Any currently + * active connection in the original session is closed and this PacketManager will be moved to the original + * session. This session will then be deleted. + * + * @param bev Pointer to a bufferevent. + */ + BaseSession(struct bufferevent *bev) : packet_manager(new PacketManager(bev)) { + packet_manager->set_packet_received_handler( + std::bind(&BaseSession::packet_received, this, std::placeholders::_1)); + packet_manager->set_event_handler(std::bind(&BaseSession::packet_manager_event, this, std::placeholders::_1)); + } + + /** + * Desctructor + * + * Virtual so the desctructor for derived classes will be called. + */ + virtual ~BaseSession() {} + + /** Client id. */ + std::string client_id; + + /** Clean session flag. */ + bool clean_session; + + /** + * PacketManager callback. + * + * Invoked by the installed PacketManager instance when it receives a complete control packet. The default handler + * methods will be passed a reference to the received packet. Packet memory is heap allocated on creation and + * will be freed according to standard C++ std::unique_ptr rules. It is the responsibility of subclasses to + * manage the std::unique_ptr. + * + * @param packet Pointer to a packet. + */ + virtual void packet_received(std::unique_ptr packet); + + /** + * PacketManager callback. + * + * Invoked by the installed PacketManager instance when it detects a low level protocol or network error. The + * default action is to close the network connection. + * + * @param event The type of event detected. + */ + virtual void packet_manager_event(PacketManager::EventType event); + + /** + * Handle a received ConnectPacket. + * + * The default action is to throw an exception. Subclasses should override this method. + * + * @param connect_packet A reference to the packet. + */ + virtual void handle_connect(const ConnectPacket & connect_packet); + + /** + * Handle a received ConnackPacket. + * + * The default action is to throw an exception. Subclasses should override this method. + * + * @param connack_packet A reference to the packet. + */ + virtual void handle_connack(const ConnackPacket & connack_packet); + + /** + * Handle a received PublishPacket. + * + * The default action is to throw an exception. Subclasses should override this method. + * + * @param publish_packet A reference to the packet. + */ + virtual void handle_publish(const PublishPacket & publish_packet); + + /** + * Handle a received PubackPacket. + * + * The default action is to throw an exception. Subclasses should override this method. + * + * @param puback_packet A reference to the packet. + */ + virtual void handle_puback(const PubackPacket & puback_packet); + + /** + * Handle a received PubrecPacket. + * + * The default action is to throw an exception. Subclasses should override this method. + * + * @param pubrec_packet A reference to the packet. + */ + virtual void handle_pubrec(const PubrecPacket & pubrec_packet); + + /** + * Handle a received PubrelPacket. + * + * The default action is to throw an exception. Subclasses should override this method. + * + * @param pubrel_packet A reference to the packet. + */ + virtual void handle_pubrel(const PubrelPacket & pubrel_packet); + + /** + * Handle a received PubcompPacket. + * + * The default action is to throw an exception. Subclasses should override this method. + * + * @param pubcomp_packet A reference to the packet. + */ + virtual void handle_pubcomp(const PubcompPacket & pubcomp_packet); + + /** + * Handle a received SubscribePacket. + * + * The default action is to throw an exception. Subclasses should override this method. + * + * @param subscribe_packet A reference to the packet. + */ + virtual void handle_subscribe(const SubscribePacket & subscribe_packet); + + /** + * Handle a received SubackPacket. + * + * The default action is to throw an exception. Subclasses should override this method. + * + * @param suback_packet A reference to the packet. + */ + virtual void handle_suback(const SubackPacket & suback_packet); + + /** + * Handle a received UnsubscribePacket. + * + * The default action is to throw an exception. Subclasses should override this method. + * + * @param unsubscribe_packet A reference to the packet. + */ + virtual void handle_unsubscribe(const UnsubscribePacket & unsubscribe_packet); + + /** + * Handle a received UnsubackPacket. + * + * The default action is to throw an exception. Subclasses should override this method. + * + * @param unsuback_packet A reference to the packet. + */ + virtual void handle_unsuback(const UnsubackPacket & unsuback_packet); + + /** + * Handle a received PingreqPacket. + * + * The default action is to send a Pingresp packet. Subclasses can override this method. + * + * @param pingreq_packet A reference to the packet. + */ + virtual void handle_pingreq(const PingreqPacket & pingreq_packet); + + /** + * Handle a received PingrespPacket. + * + * The default action is to do nothing. Subclasses can override this method. + * + * @param pingresp_packet A reference to the packet. + */ + virtual void handle_pingresp(const PingrespPacket & pingresp_packet); + + /** + * Handle a received DisconnectPacket. + * + * The default action is to throw an exception. Subclasses should override this method. + * + * @param disconnect_packet A reference to the packet. + */ + virtual void handle_disconnect(const DisconnectPacket & disconnect_packet); + + /** Pointer to the installed PacketManager instance. */ + std::unique_ptr packet_manager; + +}; \ No newline at end of file diff --git a/custom_modules/mqtt_server/mqtt_broker/src/broker.cc b/custom_modules/mqtt_server/mqtt_broker/src/broker.cc new file mode 100644 index 0000000..7e71e38 --- /dev/null +++ b/custom_modules/mqtt_server/mqtt_broker/src/broker.cc @@ -0,0 +1,181 @@ +/** + * @file broker.cc + * + * MQTT Broker (server) + * + * Listen for connections from clients. Accept subscribe, unsubscribe and publish commands and forward according to + * the [MQTT protocol](http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/mqtt-v3.1.1.html) + */ + +#include "session_manager.h" +#include "broker_session.h" + +#include + +#include + +#include +#include + +/** + * Manange sessions for each client. + * Sessions will persist between connections and are identified by the client id of the connecting client. + */ +SessionManager session_manager; + +/** + * Callback run when SIGINT or SIGTERM is attached, will cleanly exit. + * + * @param signal Integer value of signal. + * @param event Should be EV_SIGNAL. + * @param arg Pointer originally passed to evsignal_new. + */ +static void signal_cb(evutil_socket_t signal, short event, void * arg); + +/** + * Callback run when connection is received on the listening socket. + * + * @param listener Pointer to this listener's internal control structure. + * @param fd File descriptor of the newly accepted socket. + * @param addr Address structure for the peer. + * @param socklen Length of the address structure. + * @param arg Pointer orignally passed to evconlistener_new_bind. + */ +static void listener_cb(struct evconnlistener * listener, evutil_socket_t fd, + struct sockaddr * addr, int socklen, void * arg); + +/** + * Parse command line. + * + * Recognized command line arguments are parsed and added to the options instance. This options instance will be + * used to configure the broker instance. + * + * @param argc Command line argument count. + * @param argv Command line argument values. + */ +static void parse_arguments(int argc, char *argv[]); + +/** + * Options settable through command line arguments. + */ +struct options_t { + + /** Network interface address to bind to. */ + std::string bind_address = "0"; + + /** Port number to bind to. */ + uint16_t bind_port = 1883; + +} options; + +int main(int argc, char *argv[]) { + + struct event_base *evloop; + struct event *signal_event; + struct evconnlistener *listener; + struct sockaddr_in sin; + + unsigned short listen_port = 1883; + + parse_arguments(argc, argv); + + evloop = event_base_new(); + if (!evloop) { + std::cerr << "Could not initialize libevent\n"; + return 1; + } + + signal_event = evsignal_new(evloop, SIGINT, signal_cb, evloop); + evsignal_add(signal_event, NULL); + signal_event = evsignal_new(evloop, SIGTERM, signal_cb, evloop); + evsignal_add(signal_event, NULL); + + std::memset(&sin, 0, sizeof(sin)); + sin.sin_family = AF_INET; + evutil_inet_pton(sin.sin_family, options.bind_address.c_str(), &sin.sin_addr); + sin.sin_port = htons(listen_port); + + listener = evconnlistener_new_bind(evloop, listener_cb, (void *) evloop, + LEV_OPT_REUSEABLE | LEV_OPT_CLOSE_ON_FREE, -1, + (struct sockaddr *) &sin, sizeof(sin)); + if (!listener) { + std::cerr << "Could not create listener!\n"; + return 1; + } + + event_base_dispatch(evloop); + + event_free(signal_event); + evconnlistener_free(listener); + event_base_free(evloop); + + return 0; + +} + +void usage() { + std::cout << + R"END(usage: mqtt_broker [OPTION] + +MQTT broker server. Bind to address and listen for client connections. + +OPTIONS + +--broker-host | -b Broker host name or ip address, default localhost +--broker-port | -p Broker port, default 1883 +--help | -h Display this message and exit +)END"; + +} +void parse_arguments(int argc, char *argv[]) { + static struct option longopts[] = { + {"bind-addr", required_argument, NULL, 'b'}, + {"bind-port", required_argument, NULL, 'p'}, + {"help", no_argument, NULL, 'h'} + }; + + + int ch; + while ((ch = getopt_long(argc, argv, "b:p:h", longopts, NULL)) != -1) { + switch (ch) { + case 'b': + options.bind_address = optarg; + break; + case 'p': + options.bind_port = static_cast(atoi(optarg)); + break; + case 'h': + usage(); + std::exit(0); + default: + usage(); + std::exit(1); + } + } + +} + +static void listener_cb(struct evconnlistener *listener, evutil_socket_t fd, + struct sockaddr *sa, int socklen, void *user_data) { + struct event_base *base = static_cast(user_data); + struct bufferevent *bev; + + bev = bufferevent_socket_new(base, fd, BEV_OPT_CLOSE_ON_FREE); + if (!bev) { + std::cerr << "Error constructing bufferevent!\n"; + event_base_loopbreak(base); + return; + } + + session_manager.accept_connection(bev); +} + +static void signal_cb(evutil_socket_t fd, short event, void *arg) { + + event_base *base = static_cast(arg); + + if (event_base_loopexit(base, NULL)) { + std::cerr << "failed to exit event loop\n"; + } + +} diff --git a/custom_modules/mqtt_server/mqtt_broker/src/broker_session.cc b/custom_modules/mqtt_server/mqtt_broker/src/broker_session.cc new file mode 100644 index 0000000..72c5b5b --- /dev/null +++ b/custom_modules/mqtt_server/mqtt_broker/src/broker_session.cc @@ -0,0 +1,250 @@ +/** + * @file broker_session.cc + */ + +#include "broker_session.h" +#include "session_manager.h" + +#include + +bool BrokerSession::authorize_connection(const ConnectPacket &packet) { + return true; +} + +void BrokerSession::resume_session(std::unique_ptr &session, + std::unique_ptr packet_manager_ptr) { + + packet_manager_ptr->set_event_handler( + std::bind(&BrokerSession::packet_manager_event, session.get(), std::placeholders::_1)); + packet_manager_ptr->set_packet_received_handler( + std::bind(&BrokerSession::packet_received, session.get(), std::placeholders::_1)); + session->packet_manager = std::move(packet_manager_ptr); + + ConnackPacket connack; + + connack.session_present(true); + connack.return_code = ConnackPacket::ReturnCode::Accepted; + + session->packet_manager->send_packet(connack); +} + +void BrokerSession::forward_packet(const PublishPacket &packet) { + + if (packet.qos() == QoSType::QoS0) { + packet_manager->send_packet(packet); + } else if (packet.qos() == QoSType::QoS1) { + PublishPacket packet_to_send(packet); + packet_to_send.dup(false); + packet_to_send.retain(false); + packet_to_send.packet_id = packet_manager->next_packet_id(); + qos1_pending_puback.push_back(packet_to_send); + packet_manager->send_packet(packet_to_send); + } else if (packet.qos() == QoSType::QoS2) { + + PublishPacket packet_to_send(packet); + packet_to_send.dup(false); + packet_to_send.retain(false); + packet_to_send.packet_id = packet_manager->next_packet_id(); + + auto previous_packet = find_if(qos2_pending_pubrec.begin(), qos2_pending_pubrec.end(), + [&packet](const PublishPacket &p) { return p.packet_id == packet.packet_id; }); + if (previous_packet == qos2_pending_pubrec.end()) { + qos2_pending_pubrec.push_back(packet_to_send); + } + + packet_manager->send_packet(packet_to_send); + + } +} + +void BrokerSession::send_pending_message() { + + if (!qos1_pending_puback.empty()) { + packet_manager->send_packet(qos1_pending_puback[0]); + } else if (!qos2_pending_pubrec.empty()) { + packet_manager->send_packet(qos2_pending_pubrec[0]); + } else if (!qos2_pending_pubrel.empty()) { + PubrecPacket pubrec_packet; + pubrec_packet.packet_id = qos2_pending_pubrel[0]; + packet_manager->send_packet(pubrec_packet); + } else if (!qos2_pending_pubcomp.empty()) { + PubrelPacket pubrel_packet; + pubrel_packet.packet_id = qos2_pending_pubcomp[0]; + packet_manager->send_packet(pubrel_packet); + } + + return; +} + +void BrokerSession::packet_received(std::unique_ptr packet) { + BaseSession::packet_received(std::move(packet)); + send_pending_message(); +} + +void BrokerSession::packet_manager_event(PacketManager::EventType event) { + BaseSession::packet_manager_event(event); + if (clean_session) { + session_manager.erase_session(this); + } +} + +void BrokerSession::handle_connect(const ConnectPacket &packet) { + + if (!authorize_connection(packet)) { + session_manager.erase_session(this); + return; + } + + if (packet.clean_session()) { + session_manager.erase_session(packet.client_id); + } else { + auto previous_session_it = session_manager.find_session(packet.client_id); + if (previous_session_it != session_manager.sessions.end()) { + std::unique_ptr &previous_session_ptr = *previous_session_it; + resume_session(previous_session_ptr, std::move(packet_manager)); + session_manager.erase_session(this); + return; + } + } + + client_id = packet.client_id; + clean_session = packet.clean_session(); + + ConnackPacket connack; + + connack.session_present(false); + connack.return_code = ConnackPacket::ReturnCode::Accepted; + + packet_manager->send_packet(connack); + +} + +void BrokerSession::handle_publish(const PublishPacket &packet) { + + if (packet.qos() == QoSType::QoS0) { + + session_manager.handle_publish(packet); + + } else if (packet.qos() == QoSType::QoS1) { + + session_manager.handle_publish(packet); + PubackPacket puback; + puback.packet_id = packet.packet_id; + packet_manager->send_packet(puback); + + } else if (packet.qos() == QoSType::QoS2) { + + auto previous_packet = find_if(qos2_pending_pubrel.begin(), qos2_pending_pubrel.end(), + [& packet](uint16_t packet_id) { return packet_id == packet.packet_id; }); + if (previous_packet == qos2_pending_pubrel.end()) { + qos2_pending_pubrel.push_back(packet.packet_id); + session_manager.handle_publish(packet); + } + + } + + session_manager.handle_local_publish(client_id, packet); +} + +void BrokerSession::handle_puback(const PubackPacket &packet) { + + auto message = find_if(qos1_pending_puback.begin(), qos1_pending_puback.end(), + [&packet](const PublishPacket &p) { return p.packet_id == packet.packet_id; }); + if (message != qos1_pending_puback.end()) { + qos1_pending_puback.erase(message); + } + +} + +void BrokerSession::handle_pubrec(const PubrecPacket &packet) { + + qos2_pending_pubrec.erase( + std::remove_if(qos2_pending_pubrec.begin(), qos2_pending_pubrec.end(), + [&packet](const PublishPacket &p) { return p.packet_id == packet.packet_id; }), + qos2_pending_pubrec.end() + ); + + auto pubcomp_packet = find_if(qos2_pending_pubcomp.begin(), qos2_pending_pubcomp.end(), + [&packet](uint16_t packet_id) { return packet_id == packet.packet_id; }); + + if (pubcomp_packet == qos2_pending_pubcomp.end()) { + qos2_pending_pubcomp.push_back(packet.packet_id); + } + +} + +void BrokerSession::handle_pubrel(const PubrelPacket &packet) { + + qos2_pending_pubrel.erase( + std::remove_if(qos2_pending_pubrel.begin(), qos2_pending_pubrel.end(), + [&packet](uint16_t packet_id) { return packet_id == packet.packet_id; }), + qos2_pending_pubrel.end() + ); + + PubcompPacket pubcomp; + pubcomp.packet_id = packet.packet_id; + packet_manager->send_packet(pubcomp); +} + +void BrokerSession::handle_pubcomp(const PubcompPacket &packet) { + + qos2_pending_pubcomp.erase( + std::remove_if(qos2_pending_pubcomp.begin(), qos2_pending_pubcomp.end(), + [&packet](uint16_t packet_id) { return packet_id == packet.packet_id; }), + qos2_pending_pubcomp.end() + ); + +} + +void BrokerSession::handle_subscribe(const SubscribePacket &packet) { + + SubackPacket suback; + + suback.packet_id = packet.packet_id; + + for (auto subscription : packet.subscriptions) { + + auto previous_subscription = find_if(subscriptions.begin(), subscriptions.end(), + [&subscription](const Subscription &s) { + return topic_match(s.topic_filter, subscription.topic_filter); + }); + if (previous_subscription != subscriptions.end()) { + subscriptions.erase(previous_subscription); + } + + subscriptions.push_back(subscription); + + SubackPacket::ReturnCode return_code = SubackPacket::ReturnCode::Failure; + switch (subscription.qos) { + case QoSType::QoS0: + return_code = SubackPacket::ReturnCode::SuccessQoS0; + break; + case QoSType::QoS1: + return_code = SubackPacket::ReturnCode::SuccessQoS1; + break; + case QoSType::QoS2: + return_code = SubackPacket::ReturnCode::SuccessQoS2; + break; + } + suback.return_codes.push_back(return_code); + } + + packet_manager->send_packet(suback); + +} + +void BrokerSession::handle_unsubscribe(const UnsubscribePacket &packet) { + + UnsubackPacket unsuback; + + unsuback.packet_id = packet.packet_id; + + packet_manager->send_packet(unsuback); + +} + +void BrokerSession::handle_disconnect(const DisconnectPacket &packet) { + if (clean_session) { + session_manager.erase_session(this); + } +} diff --git a/custom_modules/mqtt_server/mqtt_broker/src/broker_session.h b/custom_modules/mqtt_server/mqtt_broker/src/broker_session.h new file mode 100644 index 0000000..5ff79dc --- /dev/null +++ b/custom_modules/mqtt_server/mqtt_broker/src/broker_session.h @@ -0,0 +1,266 @@ +/** + * @file broker_session.h + * + * This class builds on the BaseSession class adding members and methods needed for a MQTT session in the broker. + * + * Drived class for MQTT broker sessions. In addition to maintaining session state throughout the lifetime of the + * connection, the MQTT specification requires that the broker persist session state after a client closes the + * connection. When a subsequent connection is made with the same client id, the persisted session should be resumed. + * Any QoS 1 or QoS 2 messages delivered to client subscribed topics should be forwarded over the new connection. + */ + +#pragma once + +#include "base_session.h" +#include "packet_manager.h" +#include "packet.h" + +#include + +#include +#include + +class SessionManager; + +class Message; + +class Subscription; + +/** + * Broker session class + * + * In addition to maintaining session attributes and handling control packets, this subclass adds facilities for + * persisting and resuming session state according to the MQTT 3.1.1 standard. + */ +class BrokerSession : public BaseSession { + +public: + + /** + * Constructor + * + * In addition to the bufferen event pointer required by the BaseSession constructor, this constructor accepts + * a reference to a SessionManager class. + * + * @param bev Pointer to a bufferevent internal control structure. + * @param session_manager Reference to the SessionManager. + */ + BrokerSession(struct bufferevent *bev, SessionManager &session_manager) : BaseSession(bev), + session_manager(session_manager) { + } + + /** + * Handle authentication and authorization of a connecting client. + * + * This method is called from the connect packet handler. It currently always returns true. + * + * @return Authorization granted. + */ + bool authorize_connection(const ConnectPacket &); + + /** + * Resume a persisted session. + * + * The MQTT 3.1.1 standard requires that the session state be restored for clients connecting with the same client + * id. This method is used to perform that action once a persisted session is recognized. This method accepts + * a reference to the BrokerSession to be restored and PacketManager instance to be installed in the restored + * session. Once installed a the PacketManager will send a Connack packet to the connecting client with the + * Session Present flag set. + * + * @param session Reference to the session to be resumed. + * @param packet_manager PacketManager to be installed in the resumed session. + */ + void resume_session(std::unique_ptr &session, + std::unique_ptr packet_manager); + + /** + * List of topics subscribed to by this client. + */ + std::vector subscriptions; + + /** + * Forward a publshed message to the connected client. + * + * This method is called by the SessionManager when forwarding messages to subscribed clients. It will behave + * according to the QoS in the PublishPacket. QoS 0 packets will be forwarded and forgotten. In the case of QoS 1 + * or 2 messages, these will be retained until they are acknowledged according to the publish control packet + * protocol flow described in the MQTT 3.1.1 standard. + * + * @param packet Reference to the PublishPacket to forward. + */ + void forward_packet(const PublishPacket &packet); + + /** + * Send messages from the pending queues. + * + * Iterate through the pending message queues and if non-empty send a single pending message. This method should + * be called periodically. Currently it is called each time a packet is received from a client. + */ + void send_pending_message(void); + + /** + * PacketManager callback. + * + * This method will delegate to the BaseSession method and then invoke send_pending_message. Ownership of the + * Packet is transfered back to the BaseSession instance. + * + * @param packet Reference counted pointer to a packet. + */ + void packet_received(std::unique_ptr packet) override; + + /** + * PacketManager callback. + * + * This method will delegate to the BaseSession method then potentially remove this session from the SessionManager + * based on the clean_session flag. + * + * @param event The type of event detected. + */ + void packet_manager_event(PacketManager::EventType event) override; + + /** + * Handle a received ConnectPacket. + * + * This method will examine the ConnectPacket and restore or set up a new session. The authorize_connection method + * will be called to authenticate and authorize this connection. The client id will be used to lookup any + * previous session and if found the PacketManager instance installed in this session will be moved to the + * persisted session and this session instance will be destroyed. + * + * In case a persisted session is not found. A Connack packet will be sent with the session_present flag set to + * false. + * + * @param connect_packet A reference to the packet. + */ + void handle_connect(const ConnectPacket & connect_packet) override; + + /** + * Handle a received PublishPacket. + * + * Delegate forwarding of this message to the SessionManager. Additional actions will be performed based on the + * QoS value in the PublishPacket. For QoS 1 a Puback packet will be sent. For QoS 2, queue and expected Pubrel + * packet id which will initiate the sending of a Pubrec packet at the next run of the pending packets queue. + * + * @param publish_packet A reference to the packet. + */ + void handle_publish(const PublishPacket & publish_packet) override; + + /** + * Handle a received PubackPacket. + * + * This packet is expected in response to a Publish control packet with QoS 1. Publish packets with QoS 1 will be + * resent periodically until a Puback is received. QoS 1 Publish packets have an 'at least once' delivery + * guarantee. This handler will remove the Publish packet from the queue of pending messages ending the QoS 1 + * protocol flow. + * + * @param puback_packet A reference to the packet. + */ + void handle_puback(const PubackPacket & puback_packet) override; + + /** + * Handle a received PubrecPacket + * + * This packet is received in response to a Publish control packet with QoS 2. Publish packets with QoS 2 will be + * resent periodically until a PubRec is received. QoS 2 Publish packets have an 'exactly once' delivery + * guarantee. This handler will remove the Publish packet from the queue of pending messages and will continue + * the QoS 2 protocol flow by adding the packet id to the pending Pubcomp queue. This will enable the send of a + * Pubrel control packet at the next pending packet queue run. + * + * @param pubrec_packet A reference to the packet. + */ + void handle_pubrec(const PubrecPacket & pubrec_packet) override; + + /** + * Handle a received PubrelPacket. + * + * This packet is received in response to a Pubrec packet in the QoS 2 protocol flow. This handler will remove + * the Pubrec packet id from the queue of pending Pubrel packets and send a Pubcomp packet ending the QoS 2 + * protocol flow. + * + * @param pubrel_packet A reference to the packet. + */ + void handle_pubrel(const PubrelPacket & pubrel_packet) override; + + /** + * Handle a received PubcompPacket. + * + * This packet is received in response to a Pubrel control packet in the QoS 2 protocol flow. This handler will + * remove the packet id from the pending Pubcomp queue. No further processing is done. + * + * @param pubcomp_packet A reference to the packet. + */ + void handle_pubcomp(const PubcompPacket & pubcomp_packet) override; + + /** + * Handle a received SubscribePacket. + * + * Add the contained topic names to the list of subscriptions maintained in this session. Any previous matching + * subscribed topic will be replaced by the new one overriding the subscribed QoS. Send a Suback packet in + * response. + * + * @param subscribe_packet A reference to the packet. + */ + void handle_subscribe(const SubscribePacket & subscribe_packet) override; + + /** + * Handle a received UnsubscribePacket. + * + * Remove the topic name from the list of subscribed topics. + * + * //TODO This function is currently incomplete. + * + * @param unsubscribe_packet A reference to the packet. + */ + void handle_unsubscribe(const UnsubscribePacket & unsubscribe_packet) override; + + /** + * Handle a DisconnectPacket. + * + * Remove this session from the pool of persistent sessions and destroy it, depending on the clean_session + * attribute of this session. + * + * @param disconnect_packet A reference to the packet. + */ + void handle_disconnect(const DisconnectPacket & disconnect_packet) override; + + /** + * List of QoS 1 messages waiting for Puback. + * + * These messages have been forwarded to subscribed clients. This list will be persisted between connections as + * part of the BrokerSession state. + */ + std::vector qos1_pending_puback; + + /** + * List of QoS 2 messages waiting for Pubrec. + * + * These messages have been forwarded to subscribed clients. This list will be persisted between connections as + * part of the BrokerSession state. + */ + std::vector qos2_pending_pubrec; + + /** + * List of QoS 2 messages waiting for Pubrel. + * + * These messages have been received in Publish control packets from a client connected to this session and + * potentially forwarded on by the SessionManager to other subscribed clients. Packet ids are added to this + * list when a Pubrec control packet has been sent. The list will be persisted between connections as part of the + * BrokerSession state. + */ + std::vector qos2_pending_pubrel; + + /** + * List of QoS 2 messages waiting for Pubcomp. + * + * These messages have been forwarded to subscribed clients. Packet ids are added to this list when a Pubrel + * control packet has been received. This list will be persisted between connections as part of the BrokerSession + * state. + */ + std::vector qos2_pending_pubcomp; + + /** + * A reference to the SessionManager instance. + */ + SessionManager &session_manager; + +}; + diff --git a/custom_modules/mqtt_server/mqtt_broker/src/client_id.cc b/custom_modules/mqtt_server/mqtt_broker/src/client_id.cc new file mode 100644 index 0000000..215f833 --- /dev/null +++ b/custom_modules/mqtt_server/mqtt_broker/src/client_id.cc @@ -0,0 +1,26 @@ +/** + * @file client_id.cc + */ + +#include "client_id.h" + +#include +#include +#include + +static const std::string characters = "abcdefghijklmnopqrstuvwxyz0123456789"; +static std::once_flag init_rnd; + +std::string generate_client_id(size_t len) { + + std::call_once(init_rnd, [](){ std::srand(std::time(nullptr)); }); + + std::string random_string; + + for (size_t i=0; i +#include + +/** + * Generate a unique client id. + * + * The client id is a random character sequence drawn from the character set [a-z0-9]. + * + * @param len Length of the sequence to generate + * @return Random character sequence + */ +std::string generate_client_id(size_t len=32); \ No newline at end of file diff --git a/custom_modules/mqtt_server/mqtt_broker/src/client_pub.cc b/custom_modules/mqtt_server/mqtt_broker/src/client_pub.cc new file mode 100644 index 0000000..bfb141e --- /dev/null +++ b/custom_modules/mqtt_server/mqtt_broker/src/client_pub.cc @@ -0,0 +1,362 @@ +/** + * @file client_pub.cc + * + * MQTT Publisher (client) + * + * Connect to a listening broker and publish a message on a topic. Topic and message are passed as command line + * arguments to this program. + * See [the MQTT specification](http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/mqtt-v3.1.1.html) + */ + +#include "packet.h" +#include "base_session.h" + +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +/** + * Display usage message + * + * Displays help on command line arguments. + */ +static void usage(void); + +/** + * Parse command line. + * + * Recognized command line arguments are parsed and added to the options instance. This options instance will be + * passed to the session instance. + * + * @param argc Command line argument count. + * @param argv Command line argument values. + */ +static void parse_arguments(int argc, char *argv[]); + +/** + * On broker connection callback. + * + * @param bev Pointer to bufferevent internal control structure. + * @param event The bufferevent event code, on success this will be EV_EVENT_CONNECTED, it can also be one of + * EV_EVENT_EOF, EV_EVENT_ERROR, or EV_EVENT_TIMEOUT. + * @param arg Pointer to user data passed in when callback is set. Here this should be a pointer to the event_base + * object. + */ +static void connect_event_cb(struct bufferevent *bev, short event, void *arg); + +/** + * Socket close handler. + * + * When the session is closing this function is set to be called when the write buffer is empty. When called it will + * close the connection and exit the event loop. + * + * @param bev Pointer to the bufferevent internal control structure. + * @param arg Pointer to user data passed in when callback is set. Here this should be a pointer to the event_base + * object. + */ +static void close_cb(struct bufferevent *bev, void *arg); + +/** + * Options settable through command line arguments. + */ +static struct options_t { + + /** Broker host to connect to. DNS name is allowed. */ + std::string broker_host = "localhost"; + + /** Broker port. */ + uint16_t broker_port = 1883; + + /** Client id. If empty no client id will be sent. The broker will generate a client id automatically. */ + std::string client_id; + + /** Topic to publish to. */ + std::string topic; + + /** Message text to publish. */ + std::vector message; + + /** Quality of service specifier for message. */ + QoSType qos = QoSType::QoS0; + + /** + * Drop this session on exit if true. If false this session will persist in the broker after disconnection + * and will be continued at the next connection with the same client id. This feature isn't useful when a client + * id is not provided. + */ + bool clean_session = false; + +} options; + +/** + * Session class specialized for this publishing client. + * + * Override base class control packet handlers used in message publishing. Any other control packets received will be + * handled by default methods, in most cases that will result in a thrown exception. + */ +class ClientSession : public BaseSession { + +public: + + /** + * Constructor + * + * @param bev Pointer to the buffer event structure for the broker connection. + * @param options Options structure. + */ + ClientSession(bufferevent *bev, const options_t &options) : BaseSession(bev), options(options) {} + + /** Reference to the options structure with members updated from command line arguments. */ + const options_t &options; + + /** + * Store the packet id of the publish packet we send for comparisison with any puback or pubcomp control packets + * received from the broker. + */ + uint16_t published_packet_id = 0; + + /** + * Handle a connack control packet from the broker. + * + * This packet will be sent in response to the connect packet that this client will send. Check the return code + * and disconnect on any error. Otherwise construct a publish packet from the options and send this to the broker. + * + * @param connack_packet The connack control packet received. + */ + void handle_connack(const ConnackPacket &connack_packet) override { + + if (connack_packet.return_code != ConnackPacket::ReturnCode::Accepted) { + std::cerr << "connection not accepted by broker\n"; + disconnect(); + } + + PublishPacket publish_packet; + publish_packet.qos(options.qos); + publish_packet.topic_name = options.topic; + publish_packet.packet_id = packet_manager->next_packet_id(); + published_packet_id = publish_packet.packet_id; + publish_packet.message_data = std::vector(options.message.begin(), options.message.end()); + packet_manager->send_packet(publish_packet); + + if (options.qos == QoSType::QoS0) { + disconnect(); + } + } + + /** + * Handle a puback control packet from the broker. + * + * This packet will be sent in response to a publish message with QoS 1. Compare the packet id with the packet + * id sent in a previous publish message. Notify in case of a mismatch. + * + * @param puback_packet The received puback control packet. + */ + void handle_puback(const PubackPacket &puback_packet) override { + + if (puback_packet.packet_id != published_packet_id) { + std::cout << "puback packet id mismatch: sent " << published_packet_id << " received " + << puback_packet.packet_id << "\n"; + } + disconnect(); + } + + /** + * Handle a pubrec control packet from the broker. + * + * This packet will be sent in repsponse to a publish message with QoS 2. Compare the packet id with the packet + * id sent in a previous publish message. Notify in case of mismatch. Send a pubrel packet containing the received + * packet id in response. Wait for pubcomp control packet response. + * + * @param pubrec_packet The received pubrec control packet. + */ + void handle_pubrec(const PubrecPacket &pubrec_packet) override { + + if (pubrec_packet.packet_id != published_packet_id) { + std::cout << "pubrec packet id mismatch: sent " << published_packet_id << " received " + << pubrec_packet.packet_id << "\n"; + } + + PubrelPacket pubrel_packet; + pubrel_packet.packet_id = pubrec_packet.packet_id; + packet_manager->send_packet(pubrel_packet); + + } + + /** + * Handle a pubcomp control packet from the broker. + * + * This packet will be sent in response to a pubrel packet confirmation of a QoS 2 publish message. Compare the + * packet id sent in a previous publish message. Notify in case of mismatch. On receipt of this message the QoS 2 + * confirmation exchange is complete. If the packet id matches the the packet id of the original message + * disconnect from the broker, otherwise stay connected waiting for the pubcomp with the required packet id. + * + * @param puback_packet the received pubcomp packet + */ + void handle_pubcomp(const PubcompPacket &pubcomp_packet) override { + + if (pubcomp_packet.packet_id != published_packet_id) { + std::cout << "pubcomp packet id mismatch: sent " << published_packet_id << " received " + << pubcomp_packet.packet_id << "\n"; + + } else { + disconnect(); + } + } + + /** + * Disconnect from the broker. + * + * Send a disconnect control packet. Set up libevent to wait for the disconnect packet to be written then close + * the network connection. + */ + void disconnect() { + DisconnectPacket disconnect_packet; + packet_manager->send_packet(disconnect_packet); + bufferevent_enable(packet_manager->bev, EV_WRITE); + bufferevent_setcb(packet_manager->bev, packet_manager->bev->readcb, close_cb, NULL, + packet_manager->bev->ev_base); + } +}; + +/** + * The session instance. + * + * MQTT requires that both the client and server maintain a session state. + */ +static std::unique_ptr session; + +int main(int argc, char *argv[]) { + + struct event_base *evloop; + struct evdns_base *dns_base; + struct bufferevent *bev; + + parse_arguments(argc, argv); + + evloop = event_base_new(); + if (!evloop) { + std::cerr << "Could not initialize libevent\n"; + std::exit(1); + } + + dns_base = evdns_base_new(evloop, 1); + + bev = bufferevent_socket_new(evloop, -1, BEV_OPT_CLOSE_ON_FREE); + + bufferevent_setcb(bev, NULL, NULL, connect_event_cb, evloop); + + bufferevent_socket_connect_hostname(bev, dns_base, AF_UNSPEC, options.broker_host.c_str(), options.broker_port); + + evdns_base_free(dns_base, 0); + + event_base_dispatch(evloop); + event_base_free(evloop); + +} + +static void connect_event_cb(struct bufferevent *bev, short events, void *arg) { + + if (events & BEV_EVENT_CONNECTED) { + + session = std::unique_ptr(new ClientSession(bev, options)); + + ConnectPacket connect_packet; + connect_packet.client_id = options.client_id; + session->packet_manager->send_packet(connect_packet); + + } else if (events & (BEV_EVENT_ERROR | BEV_EVENT_EOF)) { + std::cerr << "error connecting to broker\n"; + struct event_base *base = static_cast(arg); + if (events & BEV_EVENT_ERROR) { + int err = bufferevent_socket_get_dns_error(bev); + if (err) + std::cerr << "DNS error: " << evutil_gai_strerror(err) << "\n"; + } + + bufferevent_free(bev); + event_base_loopexit(base, NULL); + } + +} + +static void usage() { + std::cout << +R"END(usage: mqtt_client_pub [OPTIONS] + +Connect to an MQTT broker and publish a single message to a single topic. + +OPTIONS + +--broker-host | -b Broker host name or ip address, default localhost +--broker-port | -p Broker port, default 1883 +--client-id | -i Client id, default none +--topic | -t Topic string, default none +--message | -m Message data, default none +--qos | -q QoS (Quality of Service), should be 0, 1, or 2, default 0 +--clean-session | -c Disable session persistence, default false +--help | -h Display this message and exit +)END"; + +} + +static void parse_arguments(int argc, char *argv[]) { + static struct option longopts[] = { + {"broker-host", required_argument, NULL, 'b'}, + {"broker-port", required_argument, NULL, 'p'}, + {"client-id", required_argument, NULL, 'i'}, + {"topic", required_argument, NULL, 't'}, + {"message", required_argument, NULL, 'm'}, + {"qos", required_argument, NULL, 'q'}, + {"clean-session", no_argument, NULL, 'c'}, + {"help", no_argument, NULL, 'h'} + }; + + + int ch; + while ((ch = getopt_long(argc, argv, "b:p:i:t:m:q:ch", longopts, NULL)) != -1) { + switch (ch) { + case 'b': + options.broker_host = optarg; + break; + case 'p': + options.broker_port = static_cast(atoi(optarg)); + break; + case 'i': + options.client_id = optarg; + break; + case 't': + options.topic = optarg; + break; + case 'm': + options.message = std::vector(optarg, optarg + strlen(optarg)); + case 'q': + options.qos = static_cast(atoi(optarg)); + break; + case 'c': + options.clean_session = true; + break; + case 'h': + usage(); + std::exit(0); + + default: + usage(); + std::exit(1); + } + } + +} + +static void close_cb(struct bufferevent *bev, void *arg) { + session->packet_manager->close_connection(); + event_base *base = static_cast(arg); + event_base_loopexit(base, NULL);; +} diff --git a/custom_modules/mqtt_server/mqtt_broker/src/client_sub.cc b/custom_modules/mqtt_server/mqtt_broker/src/client_sub.cc new file mode 100644 index 0000000..1de4241 --- /dev/null +++ b/custom_modules/mqtt_server/mqtt_broker/src/client_sub.cc @@ -0,0 +1,369 @@ +/** + * @file client_sub.cc + * + * MQTT Subscriber (client) + * + * Connect to a listening broker and add a subscription a topic then listen for pubished messages from the broker. + * Topic strings can follow the MQTT 3.1.1 specification including wildcards. + * See [the MQTT specification](http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/mqtt-v3.1.1.html) + */ + +#include "packet.h" +#include "packet_manager.h" +#include "base_session.h" + +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +/** + * Display usage message + * + * Displays help on command line arguments. + */ +static void usage(void); + +/** + * Parse command line. + * + * Recognized command line arguments are parsed and added to the options instance. This options instance will be + * passed to the session instance. + * + * @param argc Command line argument count. + * @param argv Command line argument values. + */ +static void parse_arguments(int argc, char *argv[]); + +/** + * On broker connection callback. + * + * @param bev Pointer to bufferevent internal control structure. + * @param event The bufferevent event code, on success this will be EV_EVENT_CONNECTED, it can also be one of + * EV_EVENT_EOF, EV_EVENT_ERROR, or EV_EVENT_TIMEOUT. + * @param arg Pointer to user data passed in when callback is set. Here this should be a pointer to the event_base + * object. + */ +static void connect_event_cb(struct bufferevent *bev, short event, void *arg); + +/** + * Socket close handler. + * + * When the session is closing this function is set to be called when the write buffer is empty. When called it will + * close the connection and exit the event loop. + * + * @param bev Pointer to the bufferevent internal control structure. + * @param arg Pointer to user data passed in when callback is set. Here this should be a pointer to the event_base + * object. + */ +static void close_cb(struct bufferevent *bev, void *arg); + +/** + * Callback run when SIGINT or SIGTERM is attached, will cleanly exit. + * + * @param signal Integer value of signal. + * @param event Should be EV_SIGNAL. + * @param arg Pointer originally passed to evsignal_new. + */ +static void signal_cb(evutil_socket_t, short event, void *); + +/** + * Options settable through command line arguments. + */ +struct options_t { + + /** Broker host to connect to. DNS name is allowed. */ + std::string broker_host = "localhost"; + + /** Broker port. */ + uint16_t broker_port = 1883; + + /** Client id. If empty no client id will be sent. The broker will generate a client id automatically. */ + std::string client_id; + + /** Topics to subscribe to. This option can be specified more than once to subscribe to multiple topics. */ + std::vector topics; + + /** Quality of service specifier for message. */ + QoSType qos = QoSType::QoS0; + + /** + * Drop this session on exit if true. If false this session will persist in the broker after disconnection + * and will be continued at the next connection with the same client id. This feature isn't useful when a client + * id is not provided. + */ + bool clean_session = false; + +} options; + +/** + * Session class specialized for this publishing client. + * + * Override base class control packet handlers used to subscribe to topics and to receive messages forwarded from the + * broker. Any other control packets received will be handled by default methods, in most cases that will result in a + * thrown exception. + */ +class ClientSession : public BaseSession { +public: + + /** + * Constructor + * + * @param bev Pointer to the buffer event structure for the broker connection. + * @param options Options structure. + */ + ClientSession(bufferevent *bev, const options_t &options) : BaseSession(bev), options(options) {} + + /** Reference to the options structure with members updated from command line arguments. */ + const options_t &options; + + /** + * Handle a connack control packet from the broker. + * + * This packet will be sent in response to the connect packet that this client will send. Check the return code + * and disconnect on any error. Otherwise construct a subscribe packet from the options and send it to the broker. + * + * @param connack_packet The connack control packet received. + */ + void handle_connack(const ConnackPacket &connack_packet) override { + + SubscribePacket subscribe_packet; + subscribe_packet.packet_id = packet_manager->next_packet_id(); + + for (auto topic : options.topics) { + subscribe_packet.subscriptions.push_back(Subscription{topic, options.qos}); + } + packet_manager->send_packet(subscribe_packet); + } + + /** + * Handle a suback control packet from the broker. + * + * This packet will be sent in response to a subscribe message. Check the error code in the packet and if there is + * an error output an error message but continue to listen for messages. Also check the qos field in the suback + * packet and compare with the requested qos in the original subscribe. If there is a mismatch output a message. + * + * @param suback_packet The received suback control packet. + */ + void handle_suback(const SubackPacket &suback_packet) override { + + for (size_t i = 0; i < suback_packet.return_codes.size(); i++) { + SubackPacket::ReturnCode code = suback_packet.return_codes[i]; + if (code == SubackPacket::ReturnCode::Failure) { + std::cout << "Subscription to topic " << options.topics[i] << "failed\n"; + } else if (static_cast(code) != options.qos) { + std::cout << "Topic " << options.topics[i] << " requested qos " << static_cast(options.qos) + << " subscribed " + << static_cast(code) << "\n"; + } + } + } + + /** + * Handle a publish control packet from the broker. + * + * This packet will contain forwarded messages from other clients published to the topic. Output the message and + * acknowledge based on the QoS in the packet. In the case of a QoS 0 message there is no acknowledegment. + * A Qos 1 message will require a Puback packet in response. A QoS 2 message requires a Pubrec packet from the + * subscriber followed by a Pubrel packet from the broker and finally a Pubcomp packet is sent from the subscriber. + * + * @param publish_packet the received publish packet + */ + void handle_publish(const PublishPacket &publish_packet) override { + + std::cout << std::string(publish_packet.message_data.begin(), publish_packet.message_data.end()) << "\n"; + + if (publish_packet.qos() == QoSType::QoS1) { + PubackPacket puback_packet; + puback_packet.packet_id = publish_packet.packet_id; + packet_manager->send_packet(puback_packet); + } else if (publish_packet.qos() == QoSType::QoS2) { + PubrecPacket pubrec_packet; + pubrec_packet.packet_id = publish_packet.packet_id; + packet_manager->send_packet(pubrec_packet); + } + } + + + /** + * Handle a pubrel control packet from the broker. + * + * This packet will be sent in repsponse to a Pubrel packet in the QoS 2 protocol flow. Send a Pubcomp packet + * in response. This completes the QoS 2 flow. + * + * @param pubrel_packet The received pubrel control packet. + */ + void handle_pubrel(const PubrelPacket &pubrel_packet) override { + PubcompPacket pubcomp_packet; + pubcomp_packet.packet_id = pubrel_packet.packet_id; + packet_manager->send_packet(pubcomp_packet); + } + + /** + * Handle protocol events in the packet manager. + * + * This callback will be called by the packet manager in case of protocol or network error. In most cases the + * response is to close the connection to the broker. + * + * @param event The specific type of the event reported by the packet manager. + */ + void packet_manager_event(PacketManager::EventType event) override { + event_base_loopexit(packet_manager->bev->ev_base, NULL); + BaseSession::packet_manager_event(event); + } +}; + +/** + * The session instance. + * + * MQTT requires that both the client and server maintain a session state. + */ +std::unique_ptr session; + +int main(int argc, char *argv[]) { + + struct event_base *evloop; + struct event *signal_event; + struct evdns_base *dns_base; + struct bufferevent *bev; + + parse_arguments(argc, argv); + + evloop = event_base_new(); + if (!evloop) { + std::cerr << "Could not initialize libevent\n"; + std::exit(1); + } + + signal_event = evsignal_new(evloop, SIGINT, signal_cb, evloop); + evsignal_add(signal_event, NULL); + signal_event = evsignal_new(evloop, SIGTERM, signal_cb, evloop); + evsignal_add(signal_event, NULL); + + dns_base = evdns_base_new(evloop, 1); + + bev = bufferevent_socket_new(evloop, -1, BEV_OPT_CLOSE_ON_FREE); + + bufferevent_setcb(bev, NULL, NULL, connect_event_cb, evloop); + + bufferevent_socket_connect_hostname(bev, dns_base, AF_UNSPEC, options.broker_host.c_str(), options.broker_port); + evdns_base_free(dns_base, 0); + + event_base_dispatch(evloop); + + event_free(signal_event); + event_base_free(evloop); + +} + +static void connect_event_cb(struct bufferevent *bev, short events, void *arg) { + + if (events & BEV_EVENT_CONNECTED) { + + session = std::unique_ptr(new ClientSession(bev, options)); + + ConnectPacket connect_packet; + connect_packet.client_id = options.client_id; + connect_packet.clean_session(options.clean_session); + session->packet_manager->send_packet(connect_packet); + + } else if (events & (BEV_EVENT_ERROR | BEV_EVENT_EOF)) { + struct event_base *base = static_cast(arg); + if (events & BEV_EVENT_ERROR) { + int err = bufferevent_socket_get_dns_error(bev); + if (err) + std::cerr << "DNS error: " << evutil_gai_strerror(err) << "\n"; + } + + bufferevent_free(bev); + event_base_loopexit(base, NULL); + } + +} + +void usage() { + std::cout << +R"END(usage: mqtt_client_sub [OPTIONS] + +Connect to an MQTT broker and publish a single message to a single topic. + +OPTIONS + +--broker-host | -b Broker host name or ip address, default localhost +--broker-port | -p Broker port, default 1883 +--client-id | -i Client id, default none +--topic | -t Topic string to subscribe to, this option can be provided more that once to subscribe + to multiple topics, default none +--qos | -q QoS (Quality of Service), should be 0, 1, or 2, default 0 +--clean-session | -c Disable session persistence, default false +--help | -h Display this message and exit +)END"; + +} + +void parse_arguments(int argc, char *argv[]) { + static struct option longopts[] = { + {"broker-host", required_argument, NULL, 'b'}, + {"broker-port", required_argument, NULL, 'p'}, + {"client-id", required_argument, NULL, 'i'}, + {"topic", required_argument, NULL, 't'}, + {"qos", required_argument, NULL, 'q'}, + {"clean-session", no_argument, NULL, 'c'}, + {"help", no_argument, NULL, 'h'} + }; + + + int ch; + while ((ch = getopt_long(argc, argv, "b:p:i:t:q:ch", longopts, NULL)) != -1) { + switch (ch) { + case 'b': + options.broker_host = optarg; + break; + case 'p': + options.broker_port = static_cast(atoi(optarg)); + break; + case 'i': + options.client_id = optarg; + break; + case 't': + options.topics.push_back(optarg); + break; + case 'q': + options.qos = static_cast(atoi(optarg)); + break; + case 'c': + options.clean_session = true; + break; + case 'h': + usage(); + std::exit(0); + + default: + usage(); + std::exit(1); + } + } + +} + +static void close_cb(struct bufferevent *bev, void *arg) { + + event_base *base = static_cast(arg); + event_base_loopexit(base, NULL); +} + +static void signal_cb(evutil_socket_t fd, short event, void *arg) { + + DisconnectPacket disconnect_packet; + session->packet_manager->send_packet(disconnect_packet); + + bufferevent_disable(session->packet_manager->bev, EV_READ); + bufferevent_setcb(session->packet_manager->bev, NULL, close_cb, NULL, arg); +} diff --git a/custom_modules/mqtt_server/mqtt_broker/src/packet.cc b/custom_modules/mqtt_server/mqtt_broker/src/packet.cc new file mode 100644 index 0000000..01deed8 --- /dev/null +++ b/custom_modules/mqtt_server/mqtt_broker/src/packet.cc @@ -0,0 +1,509 @@ +/** + * @file packet.cc + */ + +#include "packet.h" +#include "client_id.h" + +#include + +void Packet::read_fixed_header(PacketDataReader &reader) { + + uint8_t command_header = reader.read_byte(); + type = static_cast(command_header >> 4); + header_flags = command_header & 0x0F; + + size_t remaining_length = reader.read_remaining_length(); + if (remaining_length != reader.get_packet_data().size() - reader.get_offset()) { + throw std::exception(); + } +} + +ConnectPacket::ConnectPacket(const packet_data_t &packet_data) { + PacketDataReader reader(packet_data); + + read_fixed_header(reader); + + if (type != PacketType::Connect) { + throw std::exception(); + } + + if (header_flags != 0) { + throw std::exception(); + } + + protocol_name = reader.read_string(); + protocol_level = reader.read_byte(); + connect_flags = reader.read_byte(); + keep_alive = reader.read_uint16(); + client_id = reader.read_string(); + + if (client_id.empty()) { + client_id = generate_client_id(); + } + + if (will_flag()) { + will_topic = reader.read_string(); + will_message = reader.read_bytes(); + } + + if (username_flag()) { + username = reader.read_string(); + } + + if (password_flag()) { + password = reader.read_bytes(); + } + +} + +packet_data_t ConnectPacket::serialize() const { + packet_data_t packet_data; + PacketDataWriter writer(packet_data); + writer.write_byte((static_cast(type) << 4) | (header_flags & 0x0F)); + + size_t remaining_length = 2 + protocol_name.size(); + remaining_length += 1; // protocol_level + remaining_length += 1; // connect_flags + remaining_length += 2; // keep_alive + remaining_length += 2 + client_id.size(); + + if (will_flag()) { + remaining_length += 2 + will_topic.size(); + remaining_length += 2 + will_message.size(); + } + + if (username_flag()) { + remaining_length += 2 + username.size(); + } + + if (password_flag()) { + remaining_length += 2 + password.size(); + } + + writer.write_remaining_length(remaining_length); + writer.write_string(protocol_name); + writer.write_byte(protocol_level); + writer.write_byte(connect_flags); + writer.write_uint16(keep_alive); + writer.write_string(client_id); + + if (will_flag()) { + writer.write_string(will_topic); + writer.write_bytes(will_message); + } + + if (username_flag()) { + writer.write_string(username); + } + + if (password_flag()) { + writer.write_bytes(password); + } + + return packet_data; +} + +ConnackPacket::ConnackPacket(const packet_data_t &packet_data) { + + PacketDataReader reader(packet_data); + + read_fixed_header(reader); + + if (type != PacketType::Connack) { + throw std::exception(); + } + + if (header_flags != 0) { + throw std::exception(); + } + + acknowledge_flags = reader.read_byte(); + return_code = static_cast(reader.read_byte()); +} + +packet_data_t ConnackPacket::serialize() const { + + packet_data_t packet_data; + PacketDataWriter writer(packet_data); + + writer.write_byte((static_cast(type) << 4) | (header_flags & 0x0F)); + writer.write_remaining_length(2); + writer.write_byte(acknowledge_flags); + writer.write_byte(static_cast(return_code)); + return packet_data; +} + +PublishPacket::PublishPacket(const packet_data_t &packet_data) { + + PacketDataReader reader(packet_data); + + read_fixed_header(reader); + + if (type != PacketType::Publish) { + throw std::exception(); + } + + topic_name = reader.read_string(); + + if (qos() != QoSType::QoS0) { + packet_id = reader.read_uint16(); + } + + size_t payload_len = packet_data.size() - reader.get_offset(); + + message_data = reader.read_bytes(payload_len); + +} + +packet_data_t PublishPacket::serialize() const { + packet_data_t packet_data; + PacketDataWriter writer(packet_data); + writer.write_byte((static_cast(type) << 4) | (header_flags & 0x0F)); + uint16_t remaining_length = 2 + topic_name.size() + message_data.size(); + if (qos() != QoSType::QoS0) { + remaining_length += 2; + } + writer.write_remaining_length(remaining_length); + writer.write_string(topic_name); + if (qos() != QoSType::QoS0) { + writer.write_uint16(packet_id); + } + for (size_t i = 0; i < message_data.size(); i++) { + writer.write_byte(message_data[i]); + } + + return packet_data; +} + +PubackPacket::PubackPacket(const packet_data_t &packet_data) { + + PacketDataReader reader(packet_data); + + read_fixed_header(reader); + + if (type != PacketType::Puback) { + throw std::exception(); + } + + if (header_flags != 0) { + throw std::exception(); + } + + packet_id = reader.read_uint16(); + +} + +packet_data_t PubackPacket::serialize() const { + + packet_data_t packet_data; + PacketDataWriter writer(packet_data); + writer.write_byte((static_cast(type) << 4) | (header_flags & 0x0F)); + writer.write_remaining_length(2); + writer.write_uint16(packet_id); + return packet_data; +} + +PubrecPacket::PubrecPacket(const packet_data_t &packet_data) { + + PacketDataReader reader(packet_data); + + read_fixed_header(reader); + + if (type != PacketType::Pubrec) { + throw std::exception(); + } + + if (header_flags != 0) { + throw std::exception(); + } + + packet_id = reader.read_uint16(); + +} + +packet_data_t PubrecPacket::serialize() const { + + packet_data_t packet_data; + PacketDataWriter writer(packet_data); + writer.write_byte((static_cast(type) << 4) | (header_flags & 0x0F)); + writer.write_remaining_length(2); + writer.write_uint16(packet_id); + return packet_data; +} + +PubrelPacket::PubrelPacket(const packet_data_t &packet_data) { + + PacketDataReader reader(packet_data); + + read_fixed_header(reader); + + if (type != PacketType::Pubrel) { + throw std::exception(); + } + + if (header_flags != 0x02) { + throw std::exception(); + } + + packet_id = reader.read_uint16(); + +} + +packet_data_t PubrelPacket::serialize() const { + + packet_data_t packet_data; + PacketDataWriter writer(packet_data); + writer.write_byte((static_cast(type) << 4) | (header_flags & 0x0F)); + writer.write_remaining_length(2); + writer.write_uint16(packet_id); + return packet_data; +} + +PubcompPacket::PubcompPacket(const packet_data_t &packet_data) { + + PacketDataReader reader(packet_data); + + read_fixed_header(reader); + + if (type != PacketType::Pubcomp) { + throw std::exception(); + } + + if (header_flags != 0) { + throw std::exception(); + } + + packet_id = reader.read_uint16(); + +} + +packet_data_t PubcompPacket::serialize() const { + + packet_data_t packet_data; + PacketDataWriter writer(packet_data); + writer.write_byte((static_cast(type) << 4) | (header_flags & 0x0F)); + writer.write_remaining_length(2); + writer.write_uint16(packet_id); + return packet_data; +} + +SubscribePacket::SubscribePacket(const packet_data_t &packet_data) { + + PacketDataReader reader(packet_data); + + read_fixed_header(reader); + + if (type != PacketType::Subscribe) { + throw std::exception(); + } + + if (header_flags != 0x02) { + throw std::exception(); + } + + packet_id = reader.read_uint16(); + + do { + std::string topic = reader.read_string(); + QoSType qos = static_cast(reader.read_byte()); + // TODO use emplace_back + subscriptions.push_back(Subscription{topic, qos}); + } while (!reader.empty()); +} + +packet_data_t SubscribePacket::serialize() const { + + packet_data_t packet_data; + PacketDataWriter writer(packet_data); + + writer.write_byte((static_cast(type) << 4) | (header_flags & 0x0F)); + + size_t remaining_length = 2; + for (auto subscription : subscriptions) { + remaining_length += 1 + 2 + std::string(subscription.topic_filter).size(); + } + + writer.write_remaining_length(remaining_length); + writer.write_uint16(packet_id); + + for (auto subscription : subscriptions) { + writer.write_string(std::string(subscription.topic_filter)); + writer.write_byte(static_cast(subscription.qos)); + } + + return packet_data; + +} + +SubackPacket::SubackPacket(const packet_data_t &packet_data) { + + PacketDataReader reader(packet_data); + + read_fixed_header(reader); + + if (type != PacketType::Suback) { + throw std::exception(); + } + + if (header_flags != 0) { + throw std::exception(); + } + + packet_id = reader.read_uint16(); + + do { + ReturnCode return_code = static_cast(reader.read_byte()); + return_codes.push_back(return_code); + } while (!reader.empty()); +} + +packet_data_t SubackPacket::serialize() const { + + packet_data_t packet_data; + PacketDataWriter writer(packet_data); + writer.write_byte((static_cast(type) << 4) | (header_flags & 0x0F)); + writer.write_remaining_length(2 + return_codes.size()); + writer.write_uint16(packet_id); + for (auto return_code : return_codes) { + writer.write_byte(static_cast(return_code)); + } + return packet_data; +} + +UnsubscribePacket::UnsubscribePacket(const packet_data_t &packet_data) { + + PacketDataReader reader(packet_data); + + read_fixed_header(reader); + + if (type != PacketType::Unsubscribe) { + throw std::exception(); + } + + if (header_flags != 0x02) { + throw std::exception(); + } + + packet_id = reader.read_uint16(); + + do { + topics.push_back(reader.read_string()); + } while (!reader.empty()); +} + +packet_data_t UnsubscribePacket::serialize() const { + + packet_data_t packet_data; + PacketDataWriter writer(packet_data); + writer.write_byte((static_cast(type) << 4) | (header_flags & 0x0F)); + size_t topics_size = 0; + for (auto topic : topics) { + // string data + 2 byte length + topics_size += topic.size() + 2; + } + writer.write_remaining_length(2 + topics_size); + writer.write_uint16(packet_id); + for (auto topic : topics) { + writer.write_string(topic); + } + return packet_data; +} + +UnsubackPacket::UnsubackPacket(const packet_data_t &packet_data) { + + PacketDataReader reader(packet_data); + + read_fixed_header(reader); + + if (type != PacketType::Unsuback) { + throw std::exception(); + } + + if (header_flags != 0) { + throw std::exception(); + } + + packet_id = reader.read_uint16(); +} + +packet_data_t UnsubackPacket::serialize() const { + + packet_data_t packet_data; + PacketDataWriter writer(packet_data); + writer.write_byte((static_cast(type) << 4) | (header_flags & 0x0F)); + writer.write_remaining_length(2); + writer.write_uint16(packet_id); + return packet_data; +} + +PingreqPacket::PingreqPacket(const packet_data_t &packet_data) { + + PacketDataReader reader(packet_data); + + read_fixed_header(reader); + + if (type != PacketType::Pingreq) { + throw std::exception(); + } + + if (header_flags != 0) { + throw std::exception(); + } +} + +packet_data_t PingreqPacket::serialize() const { + + packet_data_t packet_data; + PacketDataWriter writer(packet_data); + writer.write_byte((static_cast(type) << 4) | (header_flags & 0x0F)); + writer.write_remaining_length(0); + return packet_data; +} + +PingrespPacket::PingrespPacket(const packet_data_t &packet_data) { + + PacketDataReader reader(packet_data); + + read_fixed_header(reader); + + if (type != PacketType::Pingresp) { + throw std::exception(); + } + + if (header_flags != 0) { + throw std::exception(); + } +} + +packet_data_t PingrespPacket::serialize() const { + packet_data_t packet_data; + PacketDataWriter writer(packet_data); + writer.write_byte((static_cast(type) << 4) | (header_flags & 0x0F)); + writer.write_remaining_length(0); + return packet_data; +} + +DisconnectPacket::DisconnectPacket(const packet_data_t &packet_data) { + + PacketDataReader reader(packet_data); + + read_fixed_header(reader); + + if (type != PacketType::Disconnect) { + throw std::exception(); + } + + if (header_flags != 0) { + throw std::exception(); + } + +} + +packet_data_t DisconnectPacket::serialize() const { + packet_data_t packet_data; + PacketDataWriter writer(packet_data); + writer.write_byte((static_cast(type) << 4) | (header_flags & 0x0F)); + writer.write_remaining_length(0); + return packet_data; +} diff --git a/custom_modules/mqtt_server/mqtt_broker/src/packet.h b/custom_modules/mqtt_server/mqtt_broker/src/packet.h new file mode 100644 index 0000000..ab9335c --- /dev/null +++ b/custom_modules/mqtt_server/mqtt_broker/src/packet.h @@ -0,0 +1,503 @@ +/** + * @file packet.h + * + * Standard control packet classes. + * + * The MQTT 3.1.1 standard specifies the wire-level structure and operational behavior protocol control packets. This + * structure and some low level behavior is implemented here. + * + * Serialization of a control packet instance to wire format is accomplisted through instance serialization methods. + * + * Deserialization from the wire level is handled by a control packet constructor that accepts a octect sequence. + * + * Control packet instances also provide a default constructor that will create an instance using default values. + */ + +#pragma once + +#include "packet_data.h" +#include "topic.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +/** + * Enumeration constants for packet types. + * + * The interger values correspond to control packet type values as defined in the MQTT 3.1.1 standard. + */ +enum class PacketType { + Connect = 1, + Connack = 2, + Publish = 3, + Puback = 4, + Pubrec = 5, + Pubrel = 6, + Pubcomp = 7, + Subscribe = 8, + Suback = 9, + Unsubscribe = 10, + Unsuback = 11, + Pingreq = 12, + Pingresp = 13, + Disconnect = 14, +}; + +/** + * Enumeration constants for QoS values. + */ +enum class QoSType : uint8_t { + QoS0 = 0, + QoS1 = 1, + QoS2 = 2, +}; + +/** + * Subscription Class + * + * A subscription is composed of a TopicFilter and a QoS. Matching rules for topic filters differ from topic + * names. + */ +class Subscription { +public: + TopicFilter topic_filter; + QoSType qos; +}; + +/** + * Abstract base control packet class. + * + * Packet classes inherit this and extend as necessary. The serialize method implementation is required. + */ +class Packet { +public: + PacketType type; + uint8_t header_flags; + + virtual ~Packet() {} + + void read_fixed_header(PacketDataReader &); + + // static Packet unserialize(const packet_data_t &); + virtual packet_data_t serialize(void) const = 0; + +}; + +/** + * Connect control packet class + */ +class ConnectPacket : public Packet { +public: + + ConnectPacket() { + type = PacketType::Connect; + header_flags = 0; + protocol_name = "MQIsdp"; + protocol_level = 4; + } + + ConnectPacket(const packet_data_t &packet_data); + + packet_data_t serialize() const; + + std::string protocol_name; + uint8_t protocol_level; + uint8_t connect_flags; + uint16_t keep_alive; + std::string client_id; + std::string will_topic; + std::vector will_message; + std::string username; + std::vector password; + + bool clean_session() const { + return connect_flags & 0x02; + } + + void clean_session(bool set) { + if (set) { + connect_flags |= 0x02; + } else { + connect_flags &= ~0x02; + } + } + + bool will_flag() const { + return connect_flags & 0x04; + } + + void will_flag(bool set) { + if (set) { + connect_flags |= 0x04; + } else { + connect_flags &= ~0x04; + } + } + + QoSType qos() const { + return static_cast((connect_flags >> 3) & 0x03); + } + + void qos(QoSType qos) { + connect_flags |= (static_cast(qos) << 3); + } + + bool will_retain() const { + return connect_flags & 0x20; + } + + void will_retain(bool set) { + if (set) { + connect_flags |= 0x20; + } else { + connect_flags &= ~0x20; + } + } + + bool password_flag() const { + return connect_flags & 0x40; + } + + void password_flag(bool set) { + if (set) { + connect_flags |= 0x40; + } else { + connect_flags &= ~0x40; + } + } + + bool username_flag() const { + return connect_flags & 0x80; + } + + void username_flag(bool set) { + if (set) { + connect_flags |= 0x80; + } else { + connect_flags &= ~0x80; + } + } + +}; + +/** + * Connack control packet class. + */ +class ConnackPacket : public Packet { +public: + + ConnackPacket() { + type = PacketType::Connack; + header_flags = 0; + acknowledge_flags = 0; + } + + ConnackPacket(const packet_data_t &packet_data); + + enum class ReturnCode : uint8_t { + Accepted = 0x00, + UnacceptableProtocolVersion = 0x01, + IdentifierRejected = 0x02, + ServerUnavailable = 0x03, + BadUsernameOrPassword = 0x04, + NotAuthorized = 0x05 + }; + + packet_data_t serialize() const; + + uint8_t acknowledge_flags; + ReturnCode return_code; + + bool session_present() const { + return acknowledge_flags & 0x01; + } + + void session_present(bool set) { + if (set) { + acknowledge_flags |= 0x01; + } else { + acknowledge_flags &= ~0x01; + } + } + +}; + +/** + * Publish control packet class. + */ +class PublishPacket : public Packet { +public: + + PublishPacket() { + type = PacketType::Publish; + header_flags = 0; + } + + PublishPacket(const packet_data_t &packet_data); + + packet_data_t serialize() const; + + std::string topic_name; + std::vector message_data; + uint16_t packet_id; + + bool dup() const { + return header_flags & 0x08; + } + + void dup(bool set) { + if (set) { + header_flags |= 0x08; + } else { + header_flags &= ~0x08; + } + } + + QoSType qos() const { + return static_cast((header_flags >> 1) & 0x03); + } + + void qos(QoSType qos) { + header_flags |= static_cast(qos) << 1; + } + + bool retain() const { + return header_flags & 0x01; + } + + void retain(bool set) { + if (set) { + header_flags |= 0x01; + } else { + header_flags &= ~0x01; + } + } +}; + +/** + * Puback control packet class. + */ +class PubackPacket : public Packet { + +public: + + PubackPacket() { + type = PacketType::Puback; + header_flags = 0; + } + + PubackPacket(const packet_data_t &packet_data); + + packet_data_t serialize() const; + + uint16_t packet_id; +}; + +/** + * Pubrec control packet class. + */ +class PubrecPacket : public Packet { + +public: + + PubrecPacket() { + type = PacketType::Pubrec; + header_flags = 0; + } + + PubrecPacket(const packet_data_t &packet_data); + + packet_data_t serialize() const; + + uint16_t packet_id; +}; + +/** + * Pubrel control packet class. + */ +class PubrelPacket : public Packet { + +public: + + PubrelPacket() { + type = PacketType::Pubrel; + header_flags = 0x02; + } + + PubrelPacket(const packet_data_t &packet_data); + + packet_data_t serialize() const; + + uint16_t packet_id; +}; + +/** + * Pubcomp control packet class. + */ +class PubcompPacket : public Packet { + +public: + + PubcompPacket() { + type = PacketType::Pubcomp; + header_flags = 0; + } + + PubcompPacket(const packet_data_t &packet_data); + + packet_data_t serialize() const; + + uint16_t packet_id; +}; + +/** + * Subscribe control packet class. + */ +class SubscribePacket : public Packet { + +public: + + SubscribePacket() { + type = PacketType::Subscribe; + header_flags = 0x02; + } + + SubscribePacket(const packet_data_t &packet_data); + + packet_data_t serialize() const; + + uint16_t packet_id; + + std::vector subscriptions; + +}; + +/** + * Suback control packet class. + */ +class SubackPacket : public Packet { + +public: + + SubackPacket() { + type = PacketType::Suback; + header_flags = 0; + } + + SubackPacket(const packet_data_t &packet_data); + + packet_data_t serialize() const; + + enum class ReturnCode : uint8_t { + SuccessQoS0 = 0x00, + SuccessQoS1 = 0x01, + SuccessQoS2 = 0x02, + Failure = 0x80 + }; + + uint16_t packet_id; + std::vector return_codes; + +}; + +/** + * Unsubscribe control packet class. + */ +class UnsubscribePacket : public Packet { + +public: + + UnsubscribePacket() { + type = PacketType::Unsubscribe; + header_flags = 0x02; + } + + UnsubscribePacket(const packet_data_t &packet_data); + + packet_data_t serialize() const; + + uint16_t packet_id; + + std::vector topics; + +}; + +/** + * Unsuback control packet class. + */ +class UnsubackPacket : public Packet { + +public: + + UnsubackPacket() { + type = PacketType::Unsuback; + header_flags = 0; + } + + UnsubackPacket(const packet_data_t &packet_data); + + packet_data_t serialize() const; + + uint16_t packet_id; + +}; + +/** + * Pingreq control packet class. + */ +class PingreqPacket : public Packet { + +public: + + PingreqPacket() { + type = PacketType::Pingreq; + header_flags = 0; + } + + PingreqPacket(const packet_data_t &packet_data); + + packet_data_t serialize() const; +}; + +/** + * Pingresp control packet class. + */ +class PingrespPacket : public Packet { + +public: + + PingrespPacket() { + type = PacketType::Pingresp; + header_flags = 0; + } + + PingrespPacket(const packet_data_t &packet_data); + + packet_data_t serialize() const; +}; + +/** + * Disconnect control packet class. + */ +class DisconnectPacket : public Packet { + +public: + + DisconnectPacket() { + type = PacketType::Disconnect; + header_flags = 0; + } + + DisconnectPacket(const packet_data_t &packet_data); + + packet_data_t serialize() const; +}; + diff --git a/custom_modules/mqtt_server/mqtt_broker/src/packet_data.cc b/custom_modules/mqtt_server/mqtt_broker/src/packet_data.cc new file mode 100644 index 0000000..0c9d738 --- /dev/null +++ b/custom_modules/mqtt_server/mqtt_broker/src/packet_data.cc @@ -0,0 +1,130 @@ +/** + * @file packet_data.cc + */ + +#include "packet_data.h" + +#include + +void PacketDataWriter::write_remaining_length(size_t length) { + + if (length > 127 + 128*127 + 128*128*127 + 128*128*128*127) { + throw std::exception(); + } + + do { + uint8_t encoded_byte = length % 0x80; + length >>= 7; + if (length > 0) { + encoded_byte |= 0x80; + } + packet_data.push_back(encoded_byte); + } while (length > 0); +} + +void PacketDataWriter::write_byte(uint8_t byte) { + packet_data.push_back(byte); +} + +void PacketDataWriter::write_uint16(uint16_t word) { + packet_data.push_back((word >> 8) & 0xFF); + packet_data.push_back(word & 0xFF); +} + +void PacketDataWriter::write_string(const std::string &s) { + write_uint16(s.size()); + std::copy(s.begin(), s.end(), std::back_inserter(packet_data)); +} + +void PacketDataWriter::write_bytes(const packet_data_t & b) { + write_uint16(b.size()); + std::copy(b.begin(), b.end(), std::back_inserter(packet_data)); +} + +bool PacketDataReader::has_remaining_length() { + + size_t remaining = std::min(packet_data.size() - offset, 4); + + for (size_t i=offset; i packet_data.size()) { + throw std::exception(); + } + return packet_data[offset++]; +} + + +uint16_t PacketDataReader::read_uint16() { + if (offset + 2 > packet_data.size()) { + throw std::exception(); + } + uint8_t msb = packet_data[offset++]; + uint8_t lsb = packet_data[offset++]; + return (msb << 8) + lsb; +} + +std::string PacketDataReader::read_string() { + uint16_t len = read_uint16(); + if (offset + len > packet_data.size()) { + throw std::exception(); + } + std::string s(&packet_data[offset], &packet_data[offset + len]); + offset += len; + return s; +} + +std::vector PacketDataReader::read_bytes() { + uint16_t len = read_uint16(); + if (offset + len > packet_data.size()) { + throw std::exception(); + } + std::vector v(&packet_data[offset], &packet_data[offset + len]); + offset += len; + return v; +} + +std::vector PacketDataReader::read_bytes(size_t len) { + if (offset + len > packet_data.size()) { + throw std::exception(); + } + std::vector v(&packet_data[offset], &packet_data[offset + len]); + offset += len; + return v; +} + +bool PacketDataReader::empty() { + return offset == packet_data.size(); +} diff --git a/custom_modules/mqtt_server/mqtt_broker/src/packet_data.h b/custom_modules/mqtt_server/mqtt_broker/src/packet_data.h new file mode 100644 index 0000000..c98f218 --- /dev/null +++ b/custom_modules/mqtt_server/mqtt_broker/src/packet_data.h @@ -0,0 +1,171 @@ +/** + * @file packet_data.h + * + * Utility classes supporting serialization/deserialization of MQTT control packets. + */ + +#pragma once + +#include +#include +#include +#include + +/** Typedef for packet data container. */ +typedef std::vector packet_data_t; + +/** + * Serialization class. + * + * Methods are provided to write native types to the MQTT 3.1.1 standard wire format. + */ +class PacketDataWriter +{ +public: + + /** + * Constructor + * + * Accepts a reference to a packet_data_t container. This container will be filled with serialized data and + * can be sent directly through the network connection. + * + * @param packet_data A reference to a packet_data_t container. + */ + PacketDataWriter(packet_data_t & packet_data) : packet_data(packet_data) { + packet_data.resize(0); + } + + /** + * Write the integer length to the container using the MQTT 3.1.1 remaining length encoding scheme. + * + * @param length The value to encode. + */ + void write_remaining_length(size_t length); + + /** Write a byte to the packet_data_t container. */ + void write_byte(uint8_t byte); + + /** Write a 16 bit value to the packet_data_t container. */ + void write_uint16(uint16_t word); + + /** Write a UTF-8 character string to the packet_data_t container. */ + void write_string(const std::string & s); + + /** Copy the contents of a packet_data_t container into this instance's container. */ + void write_bytes(const packet_data_t & b); + +private: + + /** A reference to the packet_data_t container. */ + packet_data_t & packet_data; +}; + +/** + * Deserialization class. + * + * Methods are provided to read native types from the wire encoded control packets received over the network + * connection. MQTT 3.1.1 standard decoding methods are implemented. + */ +class PacketDataReader +{ + +public: + + /** + * Constructor + * + * Accepts a reference to a packet_data_t container that contains data received directly over a network connection. + * The class also contains a current offset pointer that is initialized to point to the beginning of the container. + * Each data read will advance the offset pointer forward to the next item in the container. + * + * @param packet_data A reference to a packet_data_t container. + */ + PacketDataReader(const packet_data_t & packet_data) : offset(0), packet_data(packet_data) {} + + /** + * Is a remaing lenght value present. + * + * Primitive function indicating a valid remaining_length value can be read from the current position in the + * packet_data_t container. The remaining length value is encoded in a variable sequence from 1 to 4 bytes. + * + * @return valid remaining length. + */ + bool has_remaining_length(); + + /** + * Read the remaining length value from the packet_data_t container. + * + * @return integer. + */ + size_t read_remaining_length(); + + /** + * Read a single byte from the packet_data_t container. + * + * @return byte. + */ + uint8_t read_byte(); + + /** + * Read a 16 bit value from the packet_data_t container. + * + * @return 16 bit integer. + */ + uint16_t read_uint16(); + + /** + * Read a UTF-8 encoded string from the packet_data_t container. + * + * @return character string. + */ + std::string read_string(); + + /** + * Read a byte sequence from the packet_data_t container. + * + * The sequence is assumed to be encoded according to the MQTT 3.1.1 standard wire format for a byte sequence. + * The sequence length is encoded along with the data. + * + * @return byte seqence. + */ + std::vector read_bytes(); + + /** + * Read a byte sequence from the packet_data_t contianer. + * + * The number of bytes read is passed as an argument to the method. + * + * @param len Lenght of the sequence to read. + * @return Byte sequence + */ + std::vector read_bytes(size_t len); + + /** + * Is the packet_data_t container empty. + * + * @return empty. + */ + bool empty(); + + /** + * Return the current packet_data_t container offset pointer. + * + * @return integer. + */ + size_t get_offset() { return offset; } + + /** + * Get a reference to the packet_data_t container. + * + * @return Reference to packet_data_t container. + */ + const packet_data_t & get_packet_data() { return packet_data; } + +private: + + /** Current packet_data_t container offset pointer */ + size_t offset; + + /** Packet data container. */ + const packet_data_t & packet_data; +}; diff --git a/custom_modules/mqtt_server/mqtt_broker/src/packet_manager.cc b/custom_modules/mqtt_server/mqtt_broker/src/packet_manager.cc new file mode 100644 index 0000000..5003a6a --- /dev/null +++ b/custom_modules/mqtt_server/mqtt_broker/src/packet_manager.cc @@ -0,0 +1,161 @@ +/** + * @file packet_manager.cc + */ + +#include "packet_manager.h" +#include "packet.h" + +#include +#include + +#include + +void PacketManager::receive_packet_data() { + + struct evbuffer *input = bufferevent_get_input(bev); + + while (evbuffer_get_length(input) != 0) { + + size_t available = evbuffer_get_length(input); + + if (available < 2) { + return; + } + + if (fixed_header_length == 0) { + + size_t peek_size = std::min(static_cast(available), 5); + + std::vector peek_buffer(peek_size); + evbuffer_copyout(input, &peek_buffer[0], peek_size); + + PacketDataReader reader(peek_buffer); + reader.read_byte(); + if (!reader.has_remaining_length()) { + if (peek_size == 5) { + if (event_handler) { + event_handler(EventType::ProtocolError); + } + evbuffer_drain(input, peek_size); + } + return; + } + + remaining_length = reader.read_remaining_length(); + fixed_header_length = reader.get_offset(); + + } + + size_t packet_size = fixed_header_length + remaining_length; + + if (available < packet_size) { + return; + } + + packet_data_t packet_data(packet_size); + evbuffer_remove(input, &packet_data[0], packet_size); + + fixed_header_length = 0; + remaining_length = 0; + + std::unique_ptr packet = parse_packet_data(packet_data); + + if (packet && packet_received_handler) { + packet_received_handler(std::move(packet)); + } + } +} + +std::unique_ptr PacketManager::parse_packet_data(const std::vector &packet_data) { + + PacketType type = static_cast(packet_data[0] >> 4); + + std::unique_ptr packet; + + try { + switch (type) { + case PacketType::Connect: + packet = std::unique_ptr(new ConnectPacket(packet_data)); + break; + case PacketType::Connack: + packet = std::unique_ptr(new ConnackPacket(packet_data)); + break; + case PacketType::Publish: + packet = std::unique_ptr(new PublishPacket(packet_data)); + break; + case PacketType::Puback: + packet = std::unique_ptr(new PubackPacket(packet_data)); + break; + case PacketType::Pubrec: + packet = std::unique_ptr(new PubrecPacket(packet_data)); + break; + case PacketType::Pubrel: + packet = std::unique_ptr(new PubrelPacket(packet_data)); + break; + case PacketType::Pubcomp: + packet = std::unique_ptr(new PubcompPacket(packet_data)); + break; + case PacketType::Subscribe: + packet = std::unique_ptr(new SubscribePacket(packet_data)); + break; + case PacketType::Suback: + packet = std::unique_ptr(new SubackPacket(packet_data)); + break; + case PacketType::Unsubscribe: + packet = std::unique_ptr(new UnsubscribePacket(packet_data)); + break; + case PacketType::Unsuback: + packet = std::unique_ptr(new UnsubackPacket(packet_data)); + break; + case PacketType::Pingreq: + packet = std::unique_ptr(new PingreqPacket(packet_data)); + break; + case PacketType::Pingresp: + packet = std::unique_ptr(new PingrespPacket(packet_data)); + break; + case PacketType::Disconnect: + packet = std::unique_ptr(new DisconnectPacket(packet_data)); + break; + } + } catch (std::exception &e) { + if (event_handler) { + event_handler(EventType::ProtocolError); + } + } + return packet; +} + +void PacketManager::send_packet(const Packet &packet) { + std::vector packet_data = packet.serialize(); + if (bev) { + bufferevent_write(bev, &packet_data[0], packet_data.size()); + } else { + std::cout << "not writing to closed bev\n"; + } +} + +void PacketManager::close_connection() { + if (bev) { + evutil_socket_t fd = bufferevent_getfd(bev); + evutil_closesocket(fd); + bufferevent_free(bev); + bev = nullptr; + } +} + +void PacketManager::handle_events(short events) { + + if (events & BEV_EVENT_EOF) { + if (event_handler) { + event_handler(EventType::ConnectionClosed); + } + } else if (events & BEV_EVENT_ERROR) { + if (event_handler) { + event_handler(EventType::NetworkError); + } + } else if (events & BEV_EVENT_TIMEOUT) { + if (event_handler) { + event_handler(EventType::Timeout); + } + } +} diff --git a/custom_modules/mqtt_server/mqtt_broker/src/packet_manager.h b/custom_modules/mqtt_server/mqtt_broker/src/packet_manager.h new file mode 100644 index 0000000..eb6a482 --- /dev/null +++ b/custom_modules/mqtt_server/mqtt_broker/src/packet_manager.h @@ -0,0 +1,225 @@ +/** + * @file packet_manager.h + * + * Manage low level network communications. + * + * The PacketManager is responsible for sending and receiving MQTT control packets across the network connection. A + * PacketManager instance is installed into every BaseSession and can be moved between sessions to implement + * session persistance. + * + * MQTT control packets received by the PacketManager and network events are forwared to containing sessions through + * callbacks. Session instances control the packet manager by invoking its methods directly. + */ + +#pragma once + +#include "packet.h" + +#include +#include + +#include +#include +#include +#include + +/** + * PacketManager class. + * + * Manage low level network operations and invoke callbacks on MQTT control packet reception or network event. + */ +class PacketManager { +public: + + /** + * Enumeration constants for PacketManager events. + * + * Events are low level network events or unrecoverable protocol errors. + * + */ + enum class EventType { + NetworkError, + ProtocolError, + ConnectionClosed, + Timeout, + }; + + /** + * Constructor + * + * The PacketManager constructor accepts a pointer to a libevent bufferevent internal structure. Callbacks are + * installed for network data received and network events. A pointer to this PacketManager instance is passed + * as the user data argument. It will be used to invoke instance methods from the static callback wrapper. + * + * @param bev Pointer to a bufferevent control structure. + */ + PacketManager(struct bufferevent *bev) : bev(bev) { + bufferevent_setcb(bev, input_ready, NULL, network_event, this); + bufferevent_enable(bev, EV_READ); + } + + /** + * Destructor + * + * Will free the bufferevent pointer. This call should also close any underlying socket connection provided + * the libevent flag LEV_OPT_CLOSE_ON_FREE was used to create the bufferevent. + */ + ~PacketManager() { + if (bev) { + bufferevent_free(bev); + bev = nullptr; + } + } + + /** + * Send a control packet through the network connection. + * + * This method is invoked by containing session instances when they want to send a control packet. The packet + * will be serialized and transmitted provided the underlying socket connection is not closed. + */ + void send_packet(const Packet &); + + /** + * Close the network connection. + * + * Explicitly close the network connection maintained by the bufferevent. This connection should also be closed + * when the destructor for this instance is run provided the bufferevent was created with the LEV_OPT_CLOSE_ON_FREE + * flag. + */ + void close_connection(); + + /** + * Set the packet received callback. + * + * This callback will be invoked when a packet is received from the network and deserialized. The callback will + * assume ownership of the packet memory. A pointer to the base packet type is passed and the actual packet can + * be recovered through a dynamic_cast<>(). + * + * @param handler Callback function. + */ + void set_packet_received_handler(std::function)> handler) { + packet_received_handler = handler; + } + + /** + * Set the network event callback. + * + * This callback will be invoked when a low level network or protocol error is detected. The type of event is + * indicated by the an EventType enumeration constant passed as an argument to the callback. + * + * @param handler Callback function. + */ + void set_event_handler(std::function handler) { + event_handler = handler; + } + + /** + * Return the next available packet id in sequence. + * + * @return Next packet id. + */ + uint16_t next_packet_id() { + + if (++packet_id == 0) { + ++packet_id; + } + return packet_id; + } + + /** + * Pointer to the contained libevent bufferevent internal control structure. + */ + struct bufferevent *bev; + +private: + + /** + * Data receiving method. + * + * This instance method is invoked from the static input_ready callback wrapper. It is run asynchronously + * whenever data is received from the network connection. The data will be buffered inside the bufferevent control + * structure until a complete control packet is received. At that point the packet will be deserialized and + * passed to any installed packet_received_handler callback. + */ + void receive_packet_data(); + + /** + * Libevent callback wrapper. + * + * Static method invoked by libevent C library. The callback method will be passed a pointer to a bufferevent + * control structure. This should be the same pointer that was originally passed to the PacketManager constructor. + * The method will also receive a pointer to the containing PacketManager instance which will be used to invoke the + * receive_packet_data instance method. + * + * @param bev Pointer to a bufferevent + * @param arg Pointer to user data installed with the callback. In this case it is a pointer to the containing + * PacketManager instance. + */ + static void input_ready(struct bufferevent *bev, void *arg) { + PacketManager *_this = static_cast(arg); + _this->receive_packet_data(); + } + + /** + * Network event callback. + * + * This instance method is invoked from the static network_event callback wrapper. It will be passed a set of + * flags indicating the type of network event that caused the invocation. The method will then deletegate to any + * installed event_handler callback passing the event type as the an EventType argument. + * + * @param events + */ + void handle_events(short events); + + /** + * Libevent callback wrapper. + * + * The callback method will be passed a pointer to a bufferevent control structure. This should be the same + * pointer that was originally passed to the PacketManager constructor. The method will also receive a pointer + * to the containing PacketManager instance which will be used to invoke the handle_events instance method. + * + * @param bev Pointer to a bufferevent control structure. + * @param events Flags indicating the type of event that caused the callback to be invoked. + * @param arg Pointer to the containing PacketManager instance, installed along with the callback wrapper. + */ + static void network_event(struct bufferevent *bev, short events, void *arg) { + PacketManager *_this = static_cast(arg); + _this->handle_events(events); + } + + /** + * Packet deserialization method. + * + * This method will receive a packet_data_t container when the receive_packet_data method has determined + * that data for a complete packet has been received. The packet will be deserialized and a reference counted + * pointer to the instantiated packet will be returned. + * + * @param packet_data Reference to a packet_data_t container. + * @return Reference counted pointer to Packet. + */ + std::unique_ptr parse_packet_data(const packet_data_t &packet_data); + + /** + * Packet received callback. + * + * Callback installed to be invoked by this PacketManager when an MQTT control packet is received from the network. + */ + std::function)> packet_received_handler; + + /** + * Network event callback. + * + * Callback installed to be invoked by this PacketManager when a low level network or protocol error is detected. + */ + std::function event_handler; + + /** Packet id counter. */ + uint16_t packet_id = 0; + + /** State variable used to determine when data for a complete control packet is available. */ + size_t fixed_header_length = 0; + + /** State variable used to determine when data for a complete control packet is available. */ + size_t remaining_length = 0; + +}; \ No newline at end of file diff --git a/custom_modules/mqtt_server/mqtt_broker/src/session_manager.cc b/custom_modules/mqtt_server/mqtt_broker/src/session_manager.cc new file mode 100644 index 0000000..7853328 --- /dev/null +++ b/custom_modules/mqtt_server/mqtt_broker/src/session_manager.cc @@ -0,0 +1,57 @@ +/** + * @file session_manager.cc + */ + +#include "session_manager.h" +#include "broker_session.h" +#include "topic.h" + +#include +#include + +void SessionManager::accept_connection(struct bufferevent *bev) { + + auto session = std::unique_ptr(new BrokerSession(bev, *this)); + sessions.push_back(std::move(session)); +} + +std::list >::iterator SessionManager::find_session(const std::string &client_id) { + + return find_if(sessions.begin(), sessions.end(), [&client_id](const std::unique_ptr &s) { + return (!s->client_id.empty() and (s->client_id == client_id)); + }); +} + +void SessionManager::erase_session(const std::string &client_id) { + sessions.erase(std::remove_if(sessions.begin(), sessions.end(), [&client_id](std::unique_ptr &s) { + return (!s->client_id.empty() and (s->client_id == client_id)); + }), + sessions.end()); +} + +void SessionManager::erase_session(const BrokerSession *session) { + sessions.erase(std::remove_if(sessions.begin(), sessions.end(), [session](std::unique_ptr &s) { + return s.get() == session; + }), + sessions.end()); +} + +void SessionManager::handle_publish(const PublishPacket &packet) { + for (auto &session : sessions) { + for (auto &subscription : session->subscriptions) { + if (topic_match(subscription.topic_filter, TopicName(packet.topic_name))) { + session->forward_packet(packet); + } + } + } +} + +void SessionManager::handle_local_publish(const std::string &client_id, const PublishPacket &packet) { + for (size_t i = 0; i < local_sessions.size(); ++i) { + LocalSession &l = local_sessions[i]; + + if (topic_match(l.filter, TopicName(packet.topic_name))) { + l.func(client_id, packet.message_data, l.obj); + } + } +} \ No newline at end of file diff --git a/custom_modules/mqtt_server/mqtt_broker/src/session_manager.h b/custom_modules/mqtt_server/mqtt_broker/src/session_manager.h new file mode 100644 index 0000000..5cc1653 --- /dev/null +++ b/custom_modules/mqtt_server/mqtt_broker/src/session_manager.h @@ -0,0 +1,104 @@ +/** + * @file session_manager.h + * + * Manage BrokerSessions. + * + * The SessionManager maintains a contaier of all sessions in a broker. BrokerConnects are created in the session + * manager when a network session is accepted the new session is added to the sessions container. The session is then + * responsible for managing the MQTT protocol. The MQTT 3.1.1 standard requires that sessions + * can persist after a client disconnects and that any subscribe QoS 1 and Qo2 messages published while disconnected be + * delivered on reconnection. + * + * The SessionManager is responsible for forwarding published messages to all subscribing clients. + */ + +#pragma once + +#include +#include +#include +#include + +struct bufferevent; + +class BrokerSession; +class PublishPacket; + +/** + * SessionManager class + * + * Composes a container of broker sessions and methods to manage them. + */ +class SessionManager { +public: + /** + * Accept a new network connection. + * + * Creates a new BrokerSession instance and adds it to the container of sessions. The session instance will manage + * the MQTT protocol. + * + * @param bev Pointer to a bufferevent + */ + void accept_connection(struct bufferevent *bev); + + /** + * Find a session in the session container. + * + * @param client_id Unique client id to find. + * @return Iterator to BrokerSession. + */ + std::list >::iterator find_session(const std::string &client_id); + + /** + * Delete a session + * + * Given a pointer to a BrokerSession, finds that session in the session container and removes it from the + * container. The session instance will be deleted. + * + * @param session Pointer to a BrokerSession; + */ + void erase_session(const BrokerSession *session); + + /** + * Finds a session in the session container with the given client id. If found the session is removed from the + * container. The session instance will be deleted. + * + * @param client_id A Client id. + */ + void erase_session(const std::string &client_id); + + /** + * Forward a message to subsribed clients. + * + * Searches through each session and their subscriptions and invokes the forward_packet method on each session + * instance with a matching subscribed TopicFilter. The session will be responsible for Managing the MQTT publish + * protocol and correctly delivering the message to its subscribed client + * + * @param publish_packet Reference to a PublishPacket; + */ + void handle_publish(const PublishPacket &publish_packet); + + void handle_local_publish(const std::string &client_id, const PublishPacket &packet); + + /** Container of BrokerSessions. */ + std::list > sessions; + +public: + void add_local_session(const std::string &filter, void (*func)(const std::string &client_id, const std::vector &data, void *obj), void *obj) { + LocalSession l; + + l.filter = filter; + l.func = func; + l.obj = obj; + + local_sessions.push_back(l); + } + + struct LocalSession { + std::string filter; + void (*func)(const std::string &client_id, const std::vector &data, void *obj); + void *obj; + }; + + std::vector local_sessions; +}; \ No newline at end of file diff --git a/custom_modules/mqtt_server/mqtt_broker/src/topic.cc b/custom_modules/mqtt_server/mqtt_broker/src/topic.cc new file mode 100644 index 0000000..ec7c718 --- /dev/null +++ b/custom_modules/mqtt_server/mqtt_broker/src/topic.cc @@ -0,0 +1,131 @@ +/** + * @file topic.cc + */ + +#include "topic.h" + +#include + +TopicName::TopicName(const std::string & s) { + if (!is_valid(s)) { + throw std::exception(); + } + + name = s; +} + +bool TopicName::is_valid(const std::string &s) const { + if (s.size() > MaxNameSize) { + return false; + } + + if ((s.find('+') == std::string::npos) and (s.find('#') == std::string::npos)) { + return true; + } + + return false; +} + +TopicFilter::TopicFilter(const std::string &s) { + if (!is_valid(s)) { + throw std::exception(); + } + + filter = s; +} + +bool TopicFilter::is_valid(const std::string &s) const { + + if (s.size() > MaxFilterSize) { + return false; + } + + size_t pos = 0; + + for (char c : s) { + if (c == '+') { + if ((pos != 0 and s[pos - 1] != '/') or (pos + 1 != s.size() and s[pos + 1] != '/')) { + return false; + } + } else if (c == '#') { + if ((pos != 0 and s[pos - 1] != '/') or (pos + 1 != s.size())) { + return false; + } + } + pos++; + } + + return true; +} + +bool topic_match(const TopicFilter &filter, const TopicName &name) { + + const std::string &f = filter.filter; + const std::string &n = name.name; + + // empty strings don't match + if (f.empty() or n.empty()) { + return false; + } + + size_t fpos = 0; + size_t npos = 0; + + // Cannot match $ with wildcard + if ((f[fpos] == '$' and n[npos] != '$') or (f[fpos] != '$' and n[npos] == '$')) { + return false; + } + + while (fpos < f.size() and npos < n.size()) { + + // filter and topic name match at the current position + if (f[fpos] == n[npos]) { + + // last character in the topic name + if (npos == n.size() - 1) { + + // at the end of the topic name and the filter has a separator followed by a multi-level wildcard, + // causing a parent level match. + if ((fpos == f.size() - 3) and (f[fpos + 1] == '/') and (f[fpos + 2] == '#')) { + return true; + } + } + + fpos++; + npos++; + + // at the end of both the filter and topic name, match + if ((fpos == f.size()) && (npos == n.size())) { + return true; + + // at the end of the topic name and the next character in the filter is wildcard. + } else if ((npos == n.size()) and (fpos == f.size() - 1) and (f[fpos] == '+')) { + return true; + } + + } else { + + if (f[fpos] == '+') { + fpos++; + while ((npos < n.size()) and (n[npos] != '/')) { + npos++; + } + if ((npos == n.size()) and (fpos == f.size())) { + return true; + } + } else if (f[fpos] == '#') { + return true; + } else { + return false; + } + } + } + + return false; +} + +bool topic_match(const TopicFilter &filter1, const TopicFilter &filter2) { + + return filter1.filter == filter2.filter; + +} \ No newline at end of file diff --git a/custom_modules/mqtt_server/mqtt_broker/src/topic.h b/custom_modules/mqtt_server/mqtt_broker/src/topic.h new file mode 100644 index 0000000..601e1ba --- /dev/null +++ b/custom_modules/mqtt_server/mqtt_broker/src/topic.h @@ -0,0 +1,141 @@ +/** + * @file topic.h + * + * Classes for managing topic names and topic filters. + * + * The MQTT 3.1.1 standard allows structured topic names. It also defines rules for matching these names and provides + * wildcard characters to enhance matching rules. + */ + +#pragma once + +#include + +class TopicFilter; + +/** + * Topic Name + * + * Topic names are UTF-8 encoded character strings. The have a structure imposed by the MQTT 3.1.1 standard. This + * class enforces that structure. Topic names differe from topic filters in that topic filters allow wildcard + * characters. + * + * This class friends the topic_match function. + */ +class TopicName { +public: + + /** Maximum lenght of a topic name according the the MQTT 3.1.1 standard. */ + const static size_t MaxNameSize = 65535; + + /** + * Constructor + * + * The string will be validated against the MQTT topic name rules. An exception will be thrown if the name is + * invalid. + * + * @param name A reference to a the topic name string. + */ + TopicName(const std::string & name); + + /** + * Validate the topic name against the MQTT 3.1.1 standard rules. + * + * @param name A name string. + * @return Topic name is valid. + */ + bool is_valid(const std::string & name) const; + + /** + * Cast an instance of this class to a std::string. + * + * @return std::string + */ + operator std::string() const {return name;} + + /** Matching friend function. */ + friend bool topic_match(const TopicFilter &, const TopicName &); + +private: + + /** The name character string. */ + std::string name; +}; + +/** + * Topic Filter + * + * Topic filters are composed of UTF-8 encoded character strings. The have a structure imposed by the MQTT 3.1.1 + * standard including wildcard characters. This class enforces that structure. + * + * This class friends the topic_match function. + */ +class TopicFilter { +public: + + /** Maximum lenght of a topic filter according the the MQTT 3.1.1 standard. */ + const static size_t MaxFilterSize = 65535; + + /** + * Constructor + * + * The string will be validated against the MQTT topic filter rules. An exception will be thrown if the filter is + * invalid. + * + * @param filter A reference to a the topic filter string. + */ + TopicFilter(const std::string & filter); + + /** + * Validate the topic filter against the MQTT 3.1.1 standard rules. + * + * @param filter A filter string. + * @return Topic filter is valid. + */ + bool is_valid(const std::string & filter) const; + + /** + * Cast an instance of this class to a std::string. + * + * @return std::string + */ + operator std::string() const {return filter;} + + /** Matching friend function. */ + friend bool topic_match(const TopicFilter &, const TopicName &); + + /** Matching friend function. */ + friend bool topic_match(const TopicFilter &, const TopicFilter &); + +private: + + /** The filter character string. */ + std::string filter; +}; + +/** + * Match a TopicFilter against a TopicName. + * + * The MQTT 3.1.1 standard topic filter matching rules will be applied including wildcard characters. + * + * @param topic_filter A reference to a TopicFilter. + * @param topic_name A reference to a TopicName + * @return match + */ +bool topic_match(const TopicFilter & topic_filter, const TopicName & topic_name); + +/** + * Match a TopicFilter against another TopicFilter. + * + * The MQTT 3.1.1 standard topic filter matching rules are applied. These are a direct character by character match. + * This function can be used when finding an existing subscription filter, in that case wildcard character matching + * does not apply. + * + * For example, the topic filters 'a/+/c' and 'a/#' both match the topic name 'a/b/c' but the two topic filters do not + * match. + * + * @param topic_filter A reference to a TopicFilter. + * @param topic_name A reference to a TopicName + * @return match + */ +bool topic_match(const TopicFilter & topic_filter, const TopicFilter & topic_name); diff --git a/custom_modules/mqtt_server/mqtt_server.cpp b/custom_modules/mqtt_server/mqtt_server.cpp new file mode 100644 index 0000000..b5c8f46 --- /dev/null +++ b/custom_modules/mqtt_server/mqtt_server.cpp @@ -0,0 +1,85 @@ +#include "mqtt_server.h" + +void MQTTServer::add_local_session(const std::string &filter, void (*func)(const std::string &client_id, const std::vector &data, void *obj), void* obj) { + session_manager->add_local_session(filter, func, obj); +} + +void MQTTServer::initialize() { + evloop = event_base_new(); + + if (!evloop) { + printf("Could not initialize libevent\n"); + return; + } + + std::memset(&sin, 0, sizeof(sin)); + sin.sin_family = AF_INET; + evutil_inet_pton(sin.sin_family, bind_address.c_str(), &sin.sin_addr); + sin.sin_port = htons(port); + + listener = evconnlistener_new_bind(evloop, MQTTServer::listener_cb, + (void *)this, + LEV_OPT_REUSEABLE | LEV_OPT_CLOSE_ON_FREE, -1, + (struct sockaddr *)&sin, sizeof(sin)); + + if (!listener) { + std::cerr << "Could not create listener!\n"; + return; + } +} + +void MQTTServer::listener_cb(struct evconnlistener *listener, evutil_socket_t fd, struct sockaddr *sa, int socklen, void *user_data) { + + MQTTServer *server = static_cast(user_data); + + struct bufferevent *bev; + + bev = bufferevent_socket_new(server->evloop, fd, BEV_OPT_CLOSE_ON_FREE); + if (!bev) { + std::cerr << "Error constructing bufferevent!\n"; + event_base_loopbreak(server->evloop); + return; + } + + server->session_manager->accept_connection(bev); +} + +void MQTTServer::run_async() { + if (_thread) { + printf("MQTTServer::run_async Error! A thread is already runnig!\n"); + return; + } + + _thread = new std::thread([this]() { event_base_dispatch(this->evloop); }); +} + +MQTTServer::MQTTServer() { + bind_address = "0"; + port = 1883; + _thread = nullptr; + + session_manager = new SessionManager(); + + evloop = nullptr; + listener = nullptr; +} + +MQTTServer::~MQTTServer() { + //this first, as evloop runs in _thread + if (evloop && event_base_loopexit(evloop, NULL)) { + std::cout << "failed to exit event loop\n"; + } + + if (_thread) { + _thread->join(); + delete _thread; + } + + if (listener) + evconnlistener_free(listener); + + if (evloop) + event_base_free(evloop); + + delete session_manager; +} diff --git a/custom_modules/mqtt_server/mqtt_server.h b/custom_modules/mqtt_server/mqtt_server.h new file mode 100644 index 0000000..e817730 --- /dev/null +++ b/custom_modules/mqtt_server/mqtt_server.h @@ -0,0 +1,43 @@ +#ifndef MQTT_SERVER_H +#define MQTT_SERVER_H + +#include +#include +#include +#include + +#include "./mqtt_broker/src/broker_session.h" +#include "./mqtt_broker/src/session_manager.h" + +#include + +#include + +#include +#include + +class MQTTServer { + +public: + static void listener_cb(struct evconnlistener *listener, evutil_socket_t fd, struct sockaddr *addr, int socklen, void *arg); + + void add_local_session(const std::string &filter, void (*func)(const std::string &client_id, const std::vector &data, void *obj), void* obj); + + void initialize(); + void run_async(); + + MQTTServer(); + ~MQTTServer(); + + std::thread *_thread; + + SessionManager *session_manager; + std::string bind_address; + uint16_t port; + + struct event_base *evloop; + struct evconnlistener *listener; + struct sockaddr_in sin; +}; + +#endif \ No newline at end of file diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..e06b787 --- /dev/null +++ b/main.cpp @@ -0,0 +1,109 @@ +#include +#include +#include + +#include "core/http/web_application.h" +#include "core/file_cache.h" +#include "core/bry_http/http_server.h" + +#include "app/ic_application.h" + +#include "core/database/database_manager.h" + +#include "database/db_init.h" + +#include "core/settings.h" + +#include "custom_modules/mqtt_server/mqtt_server.h" + +#define MAIN_CLASS ICApplication + +void create_databases() { + + //Settings *settings = Settings::get_singleton(); + + //if (!settings) { + // printf("create_databases: Settings singleton is null!"); + // return; + //} + + /* + rapidjson::Value dbs = settings->settings["databases"]; + + if (!dbs.IsArray()) { + printf("create_databases: dbs !dbs.IsArray()!"); + return; + } +*/ + + DatabaseManager *dbm = DatabaseManager::get_singleton(); + + //uint32_t index = dbm->create_database("mysql"); + //Database *db = dbm->databases[0]; + //db->connect(""); + + uint32_t index = dbm->create_database("sqlite"); + Database *db = dbm->databases[index]; + db->connect("database.sqlite"); +} + +int main(int argc, char **argv) { + bool migrate = false; + + for (int i = 1; i < argc; ++i) { + const char *a = argv[i]; + + if (a[0] == 'm') { + migrate = true; + } + } + + initialize_database_backends(); + + Settings *settings = new Settings(true); + + settings->parse_file("settings.json"); + + FileCache *file_cache = new FileCache(true); + file_cache->wwwroot = "./content/www"; + file_cache->wwwroot_refresh_cache(); + + DatabaseManager *dbm = new DatabaseManager(); + + create_databases(); + + WebApplication *app = new MAIN_CLASS(); + + app->load_settings(); + app->setup_routes(); + app->setup_middleware(); + + HTTPServer *server = new HTTPServer(); + server->application = app; + + server->port = 8080; + server->initialize(); + + MQTTServer *mqtt_server = new MQTTServer(); + mqtt_server->initialize(); + mqtt_server->add_local_session("a/b", [](const std::string &client_id, const std::vector &data, void* obj){ reinterpret_cast(obj)->mqtt_sensor_callback(client_id, data); }, app); + + if (!migrate) { + printf("Initialized!\n"); + + mqtt_server->run_async(); + server->main_loop(); + } else { + printf("Running migrations.\n"); + app->migrate(); + } + + delete mqtt_server; + delete server; + delete app; + delete dbm; + delete file_cache; + delete settings; + + return 0; +} \ No newline at end of file diff --git a/migrate.sh b/migrate.sh new file mode 100755 index 0000000..5ae21d3 --- /dev/null +++ b/migrate.sh @@ -0,0 +1 @@ +./engine/bin/server m \ No newline at end of file diff --git a/publish_data.sh b/publish_data.sh new file mode 100755 index 0000000..03a6650 --- /dev/null +++ b/publish_data.sh @@ -0,0 +1 @@ +python publish_random_data.py \ No newline at end of file diff --git a/publish_random_data.py b/publish_random_data.py new file mode 100644 index 0000000..e6ae903 --- /dev/null +++ b/publish_random_data.py @@ -0,0 +1,17 @@ +import time +import random +import sys +import os +import subprocess + +initial_val = random.uniform(-10, 60) + + +while True: + initial_val += random.uniform(-2.3, 2.3); + + subprocess.call('mosquitto_pub -t "a/b" -m "' + str(initial_val) + '" -i 1', shell=True) + + print("Sending: " + str(initial_val)) + + time.sleep(1) diff --git a/run.sh b/run.sh new file mode 100755 index 0000000..633e0e5 --- /dev/null +++ b/run.sh @@ -0,0 +1 @@ +./engine/bin/server \ No newline at end of file